/** * Checks the JSON bodies that `routes/dumps.ts` takes to post a link and to * edit a dump. A file dump arrives as a multipart form instead, and * `dump-service.ts` checks its title and comment. * * @module */ import type { CreateUrlDumpRequest, UpdateDumpRequest, } from "../../shared/dump.ts"; import { VALIDATION } from "../../shared/validation.ts"; function isStringArray(value: unknown): value is string[] { return Array.isArray(value) && value.every((v) => typeof v === "string"); } /** * Checks whether `obj` is a {@link CreateUrlDumpRequest} whose title and * comment fit the `VALIDATION` limits. It leaves the URL to `createUrlDump`, * which rejects anything but a public http(s) URL. */ export function isCreateUrlDumpRequest( obj: unknown, ): obj is CreateUrlDumpRequest { if ( !obj || typeof obj !== "object" || !("url" in obj) || typeof obj.url !== "string" ) return false; const o = obj as Record; if ("comment" in o && typeof o.comment !== "string") return false; if ( typeof o.comment === "string" && (o.comment as string).length > VALIDATION.DUMP_COMMENT_MAX ) return false; if ("title" in o && typeof o.title !== "string") return false; if ( typeof o.title === "string" && (o.title as string).length > VALIDATION.DUMP_TITLE_MAX ) return false; if ("isPrivate" in o && typeof o.isPrivate !== "boolean") return false; if ("categoryIds" in o && !isStringArray(o.categoryIds)) return false; return true; } /** * Checks whether `obj` is an {@link UpdateDumpRequest} whose title and comment * fit the `VALIDATION` limits. Unlike on creation, a title that is given must * not be blank. */ export function isUpdateDumpRequest(obj: unknown): obj is UpdateDumpRequest { if (!obj || typeof obj !== "object") return false; const o = obj as Record; if ("url" in o && typeof o.url !== "string") return false; // Its format is changeSlug's check, which says what is wrong. if ("slug" in o && typeof o.slug !== "string") return false; if ("title" in o) { if (typeof o.title !== "string") return false; const trimmed = (o.title as string).trim(); if (!trimmed || trimmed.length > VALIDATION.DUMP_TITLE_MAX) return false; } if ( "comment" in o && typeof o.comment !== "string" && o.comment !== null ) return false; if ( typeof o.comment === "string" && (o.comment as string).length > VALIDATION.DUMP_COMMENT_MAX ) return false; if ("isPrivate" in o && typeof o.isPrivate !== "boolean") return false; if ("categoryIds" in o && !isStringArray(o.categoryIds)) return false; return true; } /** * Parses a multipart form's `categoryIds`, a JSON array sent as a string. A * missing or malformed value counts as no category. The dumps and files * routes read the forms that post and replace a file with it. */ export function parseCategoryIds(value: FormDataEntryValue | null): string[] { if (typeof value !== "string" || !value) return []; try { const parsed: unknown = JSON.parse(value); return Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === "string") : []; } catch { return []; } }