1import { z } from "zod"; 2import { BLOCK_TYPES } from "../../constants.js"; 3 4/** 5 * DTOs for the internal block endpoints (TASK-0092 slice 2). 6 * 7 * The realtime gateway hydrates a cold room and persists (flushes) a CRDT 8 * doc against these endpoints. They are the **internal twin** of the public 9 * block endpoints: same tables, service-token auth, no permission re-check 10 * (the room's join matrix already gated the edits). 11 * 12 * Block shape = the codec/DTO convention (PRODUCT-SPEC §4): lowercase 13 * snake_case `type`, fractional `position`, JSON `content`. The DB enum is 14 * SCREAMING_SNAKE; the translation happens in `internal.service.ts` (the 15 * one boundary where the codec doc meets Postgres — same resolution as the 16 * realtime `PrismaDocStore` seam, recorded in the phase-07 task file). 17 */ 18 19/** The codec block types (lowercase) — the DB `BLOCK_TYPES` case-folded. */ 20const CODEC_BLOCK_TYPES = BLOCK_TYPES.map((t) => t.toLowerCase()) as [string, ...string[]]; 21 22export const InternalBlockDto = z.object({ 23 id: z.string().min(1), 24 parentId: z.string().min(1).nullable().optional(), 25 type: z.enum(CODEC_BLOCK_TYPES), 26 position: z.string().min(1), 27 content: z.record(z.unknown()).default({}), 28 properties: z.record(z.unknown()).optional() 29}); 30export type InternalBlock = z.infer<typeof InternalBlockDto>; 31 32/** 33 * `GET /internal/blocks/:pageId` query. Cursor = the `position` of the last 34 * block of the previous page (exclusive lower bound) — position-ordered 35 * paging for the 100k-block hydration (doc §4.2, streamed per page). 36 */ 37export const InternalReadQuery = z.object({ 38 cursor: z.string().min(1).optional(), 39 pageSize: z.coerce.number().int().min(1).max(500).optional() 40}); 41export type InternalReadQueryInput = z.infer<typeof InternalReadQuery>; 42 43/** 44 * `POST /internal/blocks/flush` body. The full block set of the page (the 45 * CRDT doc re-encoded), persisted under the `pages.version` CAS: 46 * `baseVersion` must equal the current version or the flush is a no-op and 47 * the response carries `conflict.externalBlocks` (the concurrent writer's 48 * state) for the room to rebase on (doc §4.3, the "flush wins" rule). 49 */ 50export const InternalFlushDto = z.object({ 51 pageId: z.string().min(1), 52 baseVersion: z.number().int().min(1), 53 blocks: z.array(InternalBlockDto).max(5000) 54}); 55export type InternalFlushInput = z.infer<typeof InternalFlushDto>; 56