/** * @factory/shared — page DTOs (PRODUCT-SPEC §3; prisma `Page`). * * Pages are nested via `parentId` and ordered by fractional `position` * (see `@factory/shared/positions`). Sibling positioning in move * operations is expressed with neighbor ids (`before`/`after`), never * indexes. */ import { z } from "zod"; import { LIMITS } from "../constants.js"; import { generatePosition } from "../positions.js"; const idString = z.string().min(1); export const createPageSchema = z .object({ title: z.string().max(LIMITS.MAX_PAGE_TITLE).default(""), icon: z.string().max(32).optional().nullable(), coverUrl: z.string().url().optional().nullable(), description: z.string().max(2000).optional().nullable(), /** Teamspaces (teams). Omitted → private page. */ teamId: idString.optional(), parentId: idString.optional(), /** Explicit fractional position; defaults to a generated `"a0"`. */ position: z.string().min(1).optional(), }) .strict() .transform((v) => ({ title: v.title, icon: v.icon ?? null, coverUrl: v.coverUrl ?? null, description: v.description ?? null, teamId: v.teamId, parentId: v.parentId, position: v.position ?? generatePosition(), })); export type CreatePage = z.infer; export const updatePageSchema = z .object({ title: z.string().max(LIMITS.MAX_PAGE_TITLE).optional(), icon: z.string().max(32).optional().nullable(), coverUrl: z.string().url().optional().nullable(), description: z.string().max(2000).optional().nullable(), isPrivate: z.boolean().optional(), isFavorite: z.boolean().optional(), archived: z.boolean().optional(), trashed: z.boolean().optional(), }) .strict() .refine((v) => Object.keys(v).length > 0, { message: "No fields to update" }); export type UpdatePage = z.infer; /** * Move a page (or re-parent it). `before`/`after` are sibling page ids in * the *target* parent: the moved page sorts between them. At most one of * before/after may be given (the API picks the side); neither means append. */ export const movePageSchema = z .object({ /** New parent; omitted keeps the current parent (reorder in place). */ parentId: idString.optional(), before: idString.optional(), after: idString.optional(), }) .strict() .refine((v) => !(v.before && v.after), { message: "Specify at most one of before/after", }) .refine((v) => v.parentId || v.before || v.after, { message: "Nothing to move: provide parentId, before, or after", }); export type MovePage = z.infer;