emdash 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{apply-UsrFuO7l.mjs → apply-Ded_1vng.mjs} +36 -25
- package/dist/{apply-UsrFuO7l.mjs.map → apply-Ded_1vng.mjs.map} +1 -1
- package/dist/astro/index.d.mts +5 -5
- package/dist/astro/index.mjs +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +83 -33
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +9 -7
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-C3vnhIpU.mjs → byline-gFn1r0vA.mjs} +2 -2
- package/dist/{byline-C3vnhIpU.mjs.map → byline-gFn1r0vA.mjs.map} +1 -1
- package/dist/{bylines-esI7ioa9.mjs → bylines-DTFI8nDM.mjs} +4 -4
- package/dist/{bylines-esI7ioa9.mjs.map → bylines-DTFI8nDM.mjs.map} +1 -1
- package/dist/{cache-fTzxgMFJ.mjs → cache-BAJbeoZ8.mjs} +2 -2
- package/dist/{cache-fTzxgMFJ.mjs.map → cache-BAJbeoZ8.mjs.map} +1 -1
- package/dist/{chunks-Da2-b-oA.mjs → chunks-BK1oZS-l.mjs} +2 -2
- package/dist/{chunks-Da2-b-oA.mjs.map → chunks-BK1oZS-l.mjs.map} +1 -1
- package/dist/cli/index.mjs +102 -27
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{content-C7G4QXkK.mjs → content-CERxPUN0.mjs} +2 -2
- package/dist/{content-C7G4QXkK.mjs.map → content-CERxPUN0.mjs.map} +1 -1
- package/dist/database/instrumentation.d.mts +6 -4
- package/dist/database/instrumentation.d.mts.map +1 -1
- package/dist/database/instrumentation.mjs +19 -7
- package/dist/database/instrumentation.mjs.map +1 -1
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +1 -1
- package/dist/{index-DjPMOfO0.d.mts → index-Cg-rC4Gj.d.mts} +32 -24
- package/dist/index-Cg-rC4Gj.d.mts.map +1 -0
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +19 -19
- package/dist/{load-sXRuM7Us.mjs → load-DR1VwFXR.mjs} +2 -2
- package/dist/{load-sXRuM7Us.mjs.map → load-DR1VwFXR.mjs.map} +1 -1
- package/dist/{loader-Bx2_9-5e.mjs → loader-ou_PXAjg.mjs} +2 -2
- package/dist/{loader-Bx2_9-5e.mjs.map → loader-ou_PXAjg.mjs.map} +1 -1
- package/dist/media/local-runtime.d.mts +5 -5
- package/dist/media/local-runtime.mjs +1 -1
- package/dist/{media-D8FbNsl0.mjs → media-1fFhub9c.mjs} +21 -9
- package/dist/media-1fFhub9c.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-Bo-msrmu.mjs → query-8c_meo_K.mjs} +10 -10
- package/dist/{query-Bo-msrmu.mjs.map → query-8c_meo_K.mjs.map} +1 -1
- package/dist/{registry-Beb7wxFc.mjs → registry-Do34mz_P.mjs} +6 -5
- package/dist/registry-Do34mz_P.mjs.map +1 -0
- package/dist/{request-cache-C-tIpYIw.mjs → request-cache-D4I69LeL.mjs} +6 -2
- package/dist/request-cache-D4I69LeL.mjs.map +1 -0
- package/dist/request-context.d.mts +27 -1
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs +16 -3
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DMnlIkh4.mjs → runner-DIcU2UCC.mjs} +174 -152
- package/dist/runner-DIcU2UCC.mjs.map +1 -0
- package/dist/{runner-Clwe4Mme.d.mts → runner-Iu3IZSDM.d.mts} +2 -2
- package/dist/{runner-Clwe4Mme.d.mts.map → runner-Iu3IZSDM.d.mts.map} +1 -1
- package/dist/runtime.d.mts +5 -5
- package/dist/runtime.mjs +1 -1
- package/dist/{search-DkN-BqsS.mjs → search-DuWhx4NG.mjs} +172 -30
- package/dist/search-DuWhx4NG.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -10
- package/dist/{taxonomies-CTtewrSQ.mjs → taxonomies-Bw76xAxo.mjs} +6 -6
- package/dist/{taxonomies-CTtewrSQ.mjs.map → taxonomies-Bw76xAxo.mjs.map} +1 -1
- package/dist/{taxonomy-DSxx2K2L.mjs → taxonomy-D6NvlKo8.mjs} +3 -3
- package/dist/{taxonomy-DSxx2K2L.mjs.map → taxonomy-D6NvlKo8.mjs.map} +1 -1
- package/dist/{types-Eg829jj9.mjs → types-56BKbld_.mjs} +1 -1
- package/dist/types-56BKbld_.mjs.map +1 -0
- package/dist/{types-Dtx1mSMX.d.mts → types-BQx6ZXpR.d.mts} +2 -1
- package/dist/types-BQx6ZXpR.d.mts.map +1 -0
- package/dist/types-DiI8NOG_.mjs +16 -0
- package/dist/types-DiI8NOG_.mjs.map +1 -0
- package/dist/{types-D19uBYWn.d.mts → types-IN5z_S3P.d.mts} +19 -98
- package/dist/types-IN5z_S3P.d.mts.map +1 -0
- package/dist/{types-Dl1fgFjn.d.mts → types-IZSZfEwv.d.mts} +4 -3
- package/dist/types-IZSZfEwv.d.mts.map +1 -0
- package/dist/{validate-DHGwADqO.d.mts → validate-CO3JjFV5.d.mts} +7 -3
- package/dist/validate-CO3JjFV5.d.mts.map +1 -0
- package/dist/{validate-CBIbxM3L.mjs → validate-UK4Ja1uo.mjs} +3 -3
- package/dist/{validate-CBIbxM3L.mjs.map → validate-UK4Ja1uo.mjs.map} +1 -1
- package/dist/{validation-B1NYiEos.mjs → validation-Vc5DQkJa.mjs} +4 -4
- package/dist/{validation-B1NYiEos.mjs.map → validation-Vc5DQkJa.mjs.map} +1 -1
- package/dist/version-Bg31I_Ff.mjs +7 -0
- package/dist/{version-CMD42IRC.mjs.map → version-Bg31I_Ff.mjs.map} +1 -1
- package/dist/{zod-generator-BNJDQBSZ.mjs → zod-generator-CHnJUP2l.mjs} +1 -1
- package/dist/{zod-generator-BNJDQBSZ.mjs.map → zod-generator-CHnJUP2l.mjs.map} +1 -1
- package/package.json +9 -8
- package/src/api/errors.ts +5 -0
- package/src/api/handlers/content.ts +9 -0
- package/src/api/handlers/media-allowlist.ts +40 -0
- package/src/api/handlers/media.ts +1 -1
- package/src/api/handlers/menus.ts +158 -28
- package/src/api/handlers/validate-media-fields.ts +125 -0
- package/src/api/schemas/media.ts +23 -3
- package/src/api/schemas/schema.ts +11 -2
- package/src/astro/middleware.ts +46 -11
- package/src/astro/routes/api/media/upload-url.ts +10 -4
- package/src/astro/routes/api/media.ts +12 -4
- package/src/astro/types.ts +5 -1
- package/src/auth/rate-limit.ts +3 -3
- package/src/cli/commands/bundle-utils.ts +81 -6
- package/src/cli/commands/bundle.ts +18 -15
- package/src/cli/commands/export-seed.ts +57 -3
- package/src/database/instrumentation.ts +22 -8
- package/src/database/migrations/016_api_tokens.ts +18 -3
- package/src/database/migrations/037_credential_algorithm.ts +18 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/media.ts +40 -10
- package/src/database/types.ts +2 -1
- package/src/emdash-runtime.ts +16 -3
- package/src/fields/file.ts +7 -6
- package/src/fields/image.ts +12 -11
- package/src/fields/types.ts +3 -0
- package/src/index.ts +1 -1
- package/src/mcp/server.ts +37 -8
- package/src/media/mime.ts +75 -0
- package/src/plugins/types.ts +81 -191
- package/src/request-cache.ts +6 -2
- package/src/request-context.ts +42 -2
- package/src/schema/registry.ts +5 -5
- package/src/schema/types.ts +3 -2
- package/src/seed/apply.ts +25 -8
- package/src/seed/types.ts +4 -0
- package/dist/index-DjPMOfO0.d.mts.map +0 -1
- package/dist/media-D8FbNsl0.mjs.map +0 -1
- package/dist/registry-Beb7wxFc.mjs.map +0 -1
- package/dist/request-cache-C-tIpYIw.mjs.map +0 -1
- package/dist/runner-DMnlIkh4.mjs.map +0 -1
- package/dist/search-DkN-BqsS.mjs.map +0 -1
- package/dist/types-CoO6mpV3.mjs +0 -68
- package/dist/types-CoO6mpV3.mjs.map +0 -1
- package/dist/types-D19uBYWn.d.mts.map +0 -1
- package/dist/types-Dl1fgFjn.d.mts.map +0 -1
- package/dist/types-Dtx1mSMX.d.mts.map +0 -1
- package/dist/types-Eg829jj9.mjs.map +0 -1
- package/dist/validate-DHGwADqO.d.mts.map +0 -1
- package/dist/version-CMD42IRC.mjs +0 -7
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-1fFhub9c.mjs","names":[],"sources":["../src/database/repositories/media.ts"],"sourcesContent":["import { sql, type ExpressionBuilder, type Kysely, type SqlBool } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database, MediaRow } from \"../types.js\";\nimport type { FindManyResult } from \"./types.js\";\nimport { encodeCursor, decodeCursor } from \"./types.js\";\n\n/** Escape LIKE wildcard characters and the escape char itself in user-supplied values */\nfunction escapeLike(value: string): string {\n\treturn value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"%\", \"\\\\%\").replaceAll(\"_\", \"\\\\_\");\n}\n\n/**\n * Normalize a mimeType filter (string or array) into a clean string[].\n * Entries that are empty strings are dropped.\n */\nfunction normalizeMimeFilter(input?: string | readonly string[]): string[] {\n\tif (!input) return [];\n\tconst arr = Array.isArray(input) ? input : [input];\n\treturn arr\n\t\t.filter((entry): entry is string => typeof entry === \"string\" && entry.length > 0)\n\t\t.map((entry) =>\n\t\t\tentry.endsWith(\"/\") ? entry.toLowerCase() : entry.split(\";\")[0]!.trim().toLowerCase(),\n\t\t);\n}\n\n/**\n * Build a WHERE clause that matches `mime_type` against any of the given\n * filter entries — exact equality for full MIMEs, LIKE prefix for entries\n * ending in \"/\".\n */\nfunction mimeMatchExpr(eb: ExpressionBuilder<Database, \"media\">, filters: string[]) {\n\treturn eb.or(\n\t\tfilters.map((entry) =>\n\t\t\tentry.endsWith(\"/\")\n\t\t\t\t? sql<SqlBool>`mime_type LIKE ${`${escapeLike(entry)}%`} ESCAPE '\\\\'`\n\t\t\t\t: eb(\"mime_type\", \"=\", entry),\n\t\t),\n\t);\n}\n\nexport type MediaStatus = \"pending\" | \"ready\" | \"failed\";\n\nexport interface MediaItem {\n\tid: string;\n\tfilename: string;\n\tmimeType: string;\n\tsize: number | null;\n\twidth: number | null;\n\theight: number | null;\n\talt: string | null;\n\tcaption: string | null;\n\tstorageKey: string;\n\tstatus: MediaStatus;\n\tcontentHash: string | null;\n\tblurhash: string | null;\n\tdominantColor: string | null;\n\tcreatedAt: string;\n\tauthorId: string | null;\n}\n\nexport interface CreateMediaInput {\n\tfilename: string;\n\tmimeType: string;\n\tsize?: number;\n\twidth?: number;\n\theight?: number;\n\talt?: string;\n\tcaption?: string;\n\tstorageKey: string;\n\tcontentHash?: string;\n\tblurhash?: string;\n\tdominantColor?: string;\n\tstatus?: MediaStatus;\n\tauthorId?: string;\n}\n\nexport interface FindManyMediaOptions {\n\tlimit?: number;\n\tcursor?: string;\n\t/** Filter by MIME type. Pass a string for a single prefix/exact, or an array to match any. Strings ending with \"/\" are treated as LIKE prefix matches; others are exact equality. */\n\tmimeType?: string | readonly string[];\n\tstatus?: MediaStatus | \"all\"; // Filter by status, defaults to \"ready\"\n}\n\n/**\n * Media repository for database operations\n */\nexport class MediaRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new media item\n\t */\n\tasync create(input: CreateMediaInput): Promise<MediaItem> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tconst row: Omit<MediaRow, \"rowid\"> = {\n\t\t\tid,\n\t\t\tfilename: input.filename,\n\t\t\tmime_type: input.mimeType,\n\t\t\tsize: input.size ?? null,\n\t\t\twidth: input.width ?? null,\n\t\t\theight: input.height ?? null,\n\t\t\talt: input.alt ?? null,\n\t\t\tcaption: input.caption ?? null,\n\t\t\tstorage_key: input.storageKey,\n\t\t\tcontent_hash: input.contentHash ?? null,\n\t\t\tblurhash: input.blurhash ?? null,\n\t\t\tdominant_color: input.dominantColor ?? null,\n\t\t\tstatus: input.status ?? \"ready\",\n\t\t\tcreated_at: now,\n\t\t\tauthor_id: input.authorId ?? null,\n\t\t};\n\n\t\tawait this.db.insertInto(\"media\").values(row).execute();\n\n\t\treturn this.rowToItem(row as MediaRow);\n\t}\n\n\t/**\n\t * Create a pending media item (for signed URL upload flow)\n\t */\n\tasync createPending(input: {\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize?: number;\n\t\tstorageKey: string;\n\t\tcontentHash?: string;\n\t\tauthorId?: string;\n\t}): Promise<MediaItem> {\n\t\treturn this.create({\n\t\t\t...input,\n\t\t\tstatus: \"pending\",\n\t\t});\n\t}\n\n\t/**\n\t * Confirm upload (mark as ready)\n\t */\n\tasync confirmUpload(\n\t\tid: string,\n\t\tmetadata?: { width?: number; height?: number; size?: number },\n\t): Promise<MediaItem | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst updates: Partial<MediaRow> = {\n\t\t\tstatus: \"ready\",\n\t\t};\n\t\tif (metadata?.width !== undefined) updates.width = metadata.width;\n\t\tif (metadata?.height !== undefined) updates.height = metadata.height;\n\t\tif (metadata?.size !== undefined) updates.size = metadata.size;\n\n\t\tawait this.db.updateTable(\"media\").set(updates).where(\"id\", \"=\", id).execute();\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Mark upload as failed\n\t */\n\tasync markFailed(id: string): Promise<MediaItem | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) {\n\t\t\treturn null;\n\t\t}\n\n\t\tawait this.db.updateTable(\"media\").set({ status: \"failed\" }).where(\"id\", \"=\", id).execute();\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Find media by ID\n\t */\n\tasync findById(id: string): Promise<MediaItem | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find media by filename\n\t * Useful for idempotent imports\n\t */\n\tasync findByFilename(filename: string): Promise<MediaItem | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.where(\"filename\", \"=\", filename)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find media by content hash\n\t * Used for deduplication - same content = same hash\n\t */\n\tasync findByContentHash(contentHash: string): Promise<MediaItem | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.where(\"content_hash\", \"=\", contentHash)\n\t\t\t.where(\"status\", \"=\", \"ready\")\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToItem(row) : null;\n\t}\n\n\t/**\n\t * Find many media items with cursor pagination\n\t *\n\t * Uses keyset pagination (cursor-based) for consistent results.\n\t * The cursor encodes the created_at and id of the last item.\n\t */\n\tasync findMany(options: FindManyMediaOptions = {}): Promise<FindManyResult<MediaItem>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\t// Handle cursor-based pagination — throws on invalid cursor.\n\t\tif (options.cursor) {\n\t\t\tconst { orderValue: createdAt, id: cursorId } = decodeCursor(options.cursor);\n\n\t\t\t// Keyset pagination: get items where (created_at, id) < cursor\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", createdAt),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", createdAt), eb(\"id\", \"<\", cursorId)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tconst mimeFilters = normalizeMimeFilter(options.mimeType);\n\t\tif (mimeFilters.length > 0) {\n\t\t\tquery = query.where((eb) => mimeMatchExpr(eb, mimeFilters));\n\t\t}\n\n\t\t// Default to only showing ready items\n\t\tif (options.status !== \"all\") {\n\t\t\tquery = query.where(\"status\", \"=\", options.status ?? \"ready\");\n\t\t}\n\n\t\tconst rows = await query.execute();\n\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((row) => this.rowToItem(row));\n\n\t\tlet nextCursor: string | undefined;\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst lastItem = items.at(-1)!;\n\t\t\tnextCursor = encodeCursor(lastItem.createdAt, lastItem.id);\n\t\t}\n\n\t\treturn { items, nextCursor };\n\t}\n\n\t/**\n\t * Update media metadata\n\t */\n\tasync update(\n\t\tid: string,\n\t\tinput: Partial<Pick<CreateMediaInput, \"alt\" | \"caption\" | \"width\" | \"height\">>,\n\t): Promise<MediaItem | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst updates: Partial<MediaRow> = {};\n\t\tif (input.alt !== undefined) updates.alt = input.alt;\n\t\tif (input.caption !== undefined) updates.caption = input.caption;\n\t\tif (input.width !== undefined) updates.width = input.width;\n\t\tif (input.height !== undefined) updates.height = input.height;\n\n\t\tif (Object.keys(updates).length > 0) {\n\t\t\tawait this.db.updateTable(\"media\").set(updates).where(\"id\", \"=\", id).execute();\n\t\t}\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Delete media item\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db.deleteFrom(\"media\").where(\"id\", \"=\", id).executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Count media items\n\t */\n\tasync count(mimeType?: string | readonly string[]): Promise<number> {\n\t\tconst filters = normalizeMimeFilter(mimeType);\n\t\tlet query = this.db.selectFrom(\"media\").select((eb) => eb.fn.count<number>(\"id\").as(\"count\"));\n\n\t\tif (filters.length > 0) {\n\t\t\tquery = query.where((eb) => mimeMatchExpr(eb, filters));\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Delete pending uploads older than the given age.\n\t * Pending uploads that were never confirmed indicate abandoned upload flows.\n\t *\n\t * Returns the storage keys of deleted rows so callers can remove the\n\t * corresponding files from object storage.\n\t */\n\tasync cleanupPendingUploads(maxAgeMs: number = 60 * 60 * 1000): Promise<string[]> {\n\t\tconst cutoff = new Date(Date.now() - maxAgeMs).toISOString();\n\n\t\t// Select the storage keys first -- SQLite doesn't support RETURNING\n\t\t// on DELETE in all drivers, and Kysely's RETURNING isn't universal.\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.select(\"storage_key\")\n\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t.where(\"created_at\", \"<\", cutoff)\n\t\t\t.execute();\n\n\t\tif (rows.length === 0) return [];\n\n\t\tawait this.db\n\t\t\t.deleteFrom(\"media\")\n\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t.where(\"created_at\", \"<\", cutoff)\n\t\t\t.execute();\n\n\t\treturn rows.map((r) => r.storage_key);\n\t}\n\n\t/**\n\t * Convert database row to MediaItem\n\t */\n\tprivate rowToItem(row: MediaRow): MediaItem {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tfilename: row.filename,\n\t\t\tmimeType: row.mime_type,\n\t\t\tsize: row.size,\n\t\t\twidth: row.width,\n\t\t\theight: row.height,\n\t\t\talt: row.alt,\n\t\t\tcaption: row.caption,\n\t\t\tstorageKey: row.storage_key,\n\t\t\tcontentHash: row.content_hash,\n\t\t\tblurhash: row.blurhash,\n\t\t\tdominantColor: row.dominant_color,\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- DB stores string; validated at insert but linter can't follow\n\t\t\tstatus: row.status as MediaStatus,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tauthorId: row.author_id,\n\t\t};\n\t}\n}\n"],"mappings":"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"}
|
package/dist/page/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as PageFragmentContribution, I as PageMetadataLinkRel, L as PagePlacement, N as PageMetadataContribution, X as PublicPageContext, t as BreadcrumbItem } from "../types-
|
|
2
|
-
import { n as SeoSettings } from "../types-
|
|
1
|
+
import { A as PageFragmentContribution, I as PageMetadataLinkRel, L as PagePlacement, N as PageMetadataContribution, X as PublicPageContext, t as BreadcrumbItem } from "../types-IN5z_S3P.mjs";
|
|
2
|
+
import { n as SeoSettings } from "../types-IZSZfEwv.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/page/context.d.ts
|
|
5
5
|
/** Fields shared by both input forms */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import "../types-
|
|
2
|
-
import { mn as PluginDescriptor } from "../index-
|
|
3
|
-
import "../runner-
|
|
4
|
-
import { $ as ResolvedPlugin, it as StandardPluginDefinition } from "../types-
|
|
5
|
-
import "../validate-
|
|
1
|
+
import "../types-BQx6ZXpR.mjs";
|
|
2
|
+
import { mn as PluginDescriptor } from "../index-Cg-rC4Gj.mjs";
|
|
3
|
+
import "../runner-Iu3IZSDM.mjs";
|
|
4
|
+
import { $ as ResolvedPlugin, it as StandardPluginDefinition } from "../types-IN5z_S3P.mjs";
|
|
5
|
+
import "../validate-CO3JjFV5.mjs";
|
|
6
6
|
|
|
7
7
|
//#region src/plugins/adapt-sandbox-entry.d.ts
|
|
8
8
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as PLUGIN_CAPABILITIES, t as HOOK_NAMES } from "../manifest-schema-CXAbd1vH.mjs";
|
|
2
|
-
import { i as normalizeCapabilities } from "../types-
|
|
2
|
+
import { i as normalizeCapabilities } from "../types-DiI8NOG_.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/plugins/adapt-sandbox-entry.ts
|
|
5
5
|
/**
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { i as __exportAll } from "./runner-
|
|
1
|
+
import { i as __exportAll } from "./runner-DIcU2UCC.mjs";
|
|
2
2
|
import { getRequestContext } from "./request-context.mjs";
|
|
3
3
|
import { n as getI18nConfig, r as isI18nEnabled, t as getFallbackChain } from "./config-CVssduLe.mjs";
|
|
4
4
|
import { i as encodeCursor } from "./types-BIgulNsW.mjs";
|
|
5
5
|
import { t as isMissingTableError } from "./db-errors-B7P2pSCn.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { n as requestCached } from "./request-cache-D4I69LeL.mjs";
|
|
7
|
+
import { t as CURSOR_RAW_VALUES } from "./loader-ou_PXAjg.mjs";
|
|
8
8
|
|
|
9
9
|
//#region src/visual-editing/editable.ts
|
|
10
10
|
/**
|
|
@@ -472,7 +472,7 @@ async function getEmDashEntry(type, id, options) {
|
|
|
472
472
|
async function hydrateEntryBylines(type, entries) {
|
|
473
473
|
if (entries.length === 0) return;
|
|
474
474
|
try {
|
|
475
|
-
const { getBylinesForEntries } = await import("./bylines-
|
|
475
|
+
const { getBylinesForEntries } = await import("./bylines-DTFI8nDM.mjs").then((n) => n.t);
|
|
476
476
|
const refs = entries.map((e) => {
|
|
477
477
|
const data = entryData(e);
|
|
478
478
|
const id = dataStr(data, "id");
|
|
@@ -515,7 +515,7 @@ async function hydrateEntryBylines(type, entries) {
|
|
|
515
515
|
async function hydrateEntryTerms(type, entries) {
|
|
516
516
|
if (entries.length === 0) return;
|
|
517
517
|
try {
|
|
518
|
-
const { getAllTermsForEntries } = await import("./taxonomies-
|
|
518
|
+
const { getAllTermsForEntries } = await import("./taxonomies-Bw76xAxo.mjs").then((n) => n.u);
|
|
519
519
|
const ids = entries.map((e) => dataStr(entryData(e), "id")).filter(Boolean);
|
|
520
520
|
if (ids.length === 0) return;
|
|
521
521
|
const termsMap = await getAllTermsForEntries(type, ids);
|
|
@@ -549,9 +549,9 @@ async function hydrateEntryTerms(type, entries) {
|
|
|
549
549
|
*/
|
|
550
550
|
async function getTranslations(type, id) {
|
|
551
551
|
try {
|
|
552
|
-
const db = (await import("./loader-
|
|
552
|
+
const db = (await import("./loader-ou_PXAjg.mjs").then((n) => n.i)).getDb;
|
|
553
553
|
const dbInstance = await db();
|
|
554
|
-
const { ContentRepository } = await import("./content-
|
|
554
|
+
const { ContentRepository } = await import("./content-CERxPUN0.mjs").then((n) => n.n);
|
|
555
555
|
const repo = new ContentRepository(dbInstance);
|
|
556
556
|
const item = await repo.findByIdOrSlug(type, id);
|
|
557
557
|
if (!item) return {
|
|
@@ -620,8 +620,8 @@ function invalidateUrlPatternCache() {
|
|
|
620
620
|
*/
|
|
621
621
|
async function resolveEmDashPath(path) {
|
|
622
622
|
if (!cachedUrlPatterns) {
|
|
623
|
-
const { getDb } = await import("./loader-
|
|
624
|
-
const { SchemaRegistry } = await import("./registry-
|
|
623
|
+
const { getDb } = await import("./loader-ou_PXAjg.mjs").then((n) => n.i);
|
|
624
|
+
const { SchemaRegistry } = await import("./registry-Do34mz_P.mjs").then((n) => n.r);
|
|
625
625
|
const collections = await new SchemaRegistry(await getDb()).listCollections();
|
|
626
626
|
cachedUrlPatterns = [];
|
|
627
627
|
for (const collection of collections) {
|
|
@@ -653,4 +653,4 @@ async function resolveEmDashPath(path) {
|
|
|
653
653
|
|
|
654
654
|
//#endregion
|
|
655
655
|
export { invalidateUrlPatternCache as a, createEditable as c, getTranslations as i, createNoop as l, getEmDashCollection as n, query_exports as o, getEmDashEntry as r, resolveEmDashPath as s, getEditMeta as t };
|
|
656
|
-
//# sourceMappingURL=query-
|
|
656
|
+
//# sourceMappingURL=query-8c_meo_K.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-Bo-msrmu.mjs","names":[],"sources":["../src/visual-editing/editable.ts","../src/query.ts"],"sourcesContent":["/**\n * Visual editing annotation system\n *\n * Creates Proxy objects that emit data-emdash-ref attributes when spread onto elements.\n */\n\nexport interface CMSAnnotation {\n\tcollection: string;\n\tid: string;\n\tfield?: string;\n\t/** Entry status — only present on entry-level annotations (not field-level) */\n\tstatus?: string;\n\t/** Whether the entry has unpublished draft changes */\n\thasDraft?: boolean;\n}\n\n/** The shape returned when spreading an edit annotation onto an element */\nexport interface FieldAnnotation {\n\t\"data-emdash-ref\": string;\n}\n\nexport interface EditableOptions {\n\t/** Entry status: \"draft\", \"published\", \"scheduled\" */\n\tstatus?: string;\n\t/** true when draftRevisionId exists and differs from liveRevisionId */\n\thasDraft?: boolean;\n}\n\n/**\n * Create an editable proxy for an entry.\n *\n * Usage:\n * - `{...entry.edit}` - entry-level annotation (includes status/hasDraft)\n * - `{...entry.edit.title}` - field-level annotation\n * - `{...entry.edit['nested.field']}` - nested field (bracket notation)\n */\nexport function createEditable(\n\tcollection: string,\n\tid: string,\n\toptions?: EditableOptions,\n): EditProxy {\n\tconst base: CMSAnnotation = {\n\t\tcollection,\n\t\tid,\n\t\t...(options?.status && { status: options.status }),\n\t\t...(options?.hasDraft && { hasDraft: true }),\n\t};\n\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (prop === \"toJSON\") return () => ({ \"data-emdash-ref\": JSON.stringify(base) });\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\n\t\t\t// data-emdash-ref access returns the entry-level string\n\t\t\tif (prop === \"data-emdash-ref\") return JSON.stringify(base);\n\n\t\t\t// Field-level: return a FieldAnnotation for the specific field\n\t\t\treturn {\n\t\t\t\t\"data-emdash-ref\": JSON.stringify({ ...base, field: String(prop) }),\n\t\t\t} satisfies FieldAnnotation;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [\"data-emdash-ref\"];\n\t\t},\n\t\tgetOwnPropertyDescriptor(_, prop) {\n\t\t\tif (prop === \"data-emdash-ref\") {\n\t\t\t\treturn {\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tvalue: JSON.stringify(base),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Create a noop proxy for production mode.\n * Spreading this produces no attributes.\n */\nexport function createNoop(): EditProxy {\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\t\t\t// All property access returns undefined in noop mode\n\t\t\treturn undefined;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [];\n\t\t},\n\t\tgetOwnPropertyDescriptor() {\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Visual editing proxy type.\n *\n * Spread directly onto elements for entry-level annotations: `{...entry.edit}`\n * Access a field for field-level annotations: `{...entry.edit.title}`\n *\n * In production, spreading produces no attributes (noop).\n */\nexport type EditProxy = {\n\treadonly [field: string]: Partial<FieldAnnotation>;\n};\n","/**\n * Query functions for EmDash content\n *\n * These wrap Astro's getLiveCollection/getLiveEntry with type filtering.\n * Use these instead of calling Astro's functions directly.\n *\n * Error handling follows Astro's pattern - returns { entries/entry, error }\n * so callers can gracefully handle errors (including 404s).\n *\n * Preview mode is handled implicitly via ALS request context —\n * no parameters needed. The middleware verifies the preview token\n * and sets the context; query functions read it automatically.\n */\n\nimport { encodeCursor } from \"./database/repositories/types.js\";\nimport { getFallbackChain, getI18nConfig, isI18nEnabled } from \"./i18n/config.js\";\nimport { CURSOR_RAW_VALUES } from \"./loader.js\";\nimport { requestCached } from \"./request-cache.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { isMissingTableError } from \"./utils/db-errors.js\";\nimport {\n\tcreateEditable,\n\tcreateNoop,\n\ttype EditProxy,\n\ttype EditableOptions,\n} from \"./visual-editing/editable.js\";\n\n/**\n * Collection type registry for type-safe queries.\n *\n * This interface is extended by the generated emdash-env.d.ts file\n * to provide type inference for collection names and their data shapes.\n *\n * @example\n * ```ts\n * // In emdash-env.d.ts (generated):\n * declare module \"emdash\" {\n * interface EmDashCollections {\n * posts: { title: string; content: PortableTextBlock[]; };\n * pages: { title: string; body: PortableTextBlock[]; };\n * }\n * }\n *\n * // Then in your code:\n * const { entries } = await getEmDashCollection(\"posts\");\n * // entries[0].data.title is typed as string\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface EmDashCollections {}\n\n/**\n * Helper type to infer the data type for a collection.\n * Returns the registered type if known, otherwise falls back to Record<string, unknown>.\n */\nexport type InferCollectionData<T extends string> = T extends keyof EmDashCollections\n\t? EmDashCollections[T]\n\t: Record<string, unknown>;\n\n/**\n * Sort direction\n */\nexport type SortDirection = \"asc\" | \"desc\";\n\n/**\n * Order by specification - field name to direction\n * @example { created_at: \"desc\" } - Sort by created_at descending\n * @example { title: \"asc\" } - Sort by title ascending\n * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n */\nexport type OrderBySpec = Record<string, SortDirection>;\n\nexport interface CollectionFilter {\n\tstatus?: \"draft\" | \"published\" | \"archived\";\n\tlimit?: number;\n\t/**\n\t * Opaque cursor for keyset pagination.\n\t * Pass the `nextCursor` value from a previous result to fetch the next page.\n\t * @example\n\t * ```ts\n\t * const cursor = Astro.url.searchParams.get(\"cursor\") ?? undefined;\n\t * const { entries, nextCursor } = await getEmDashCollection(\"posts\", {\n\t * limit: 10,\n\t * cursor,\n\t * });\n\t * ```\n\t */\n\tcursor?: string;\n\t/**\n\t * Filter by field values or taxonomy terms\n\t * @example { category: 'news' } - Filter by taxonomy term\n\t * @example { category: ['news', 'featured'] } - Filter by multiple terms (OR)\n\t */\n\twhere?: Record<string, string | string[]>;\n\t/**\n\t * Order results by field(s)\n\t * @default { created_at: \"desc\" }\n\t * @example { created_at: \"desc\" } - Sort by created_at descending (default)\n\t * @example { title: \"asc\" } - Sort by title ascending\n\t * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n\t */\n\torderBy?: OrderBySpec;\n\t/**\n\t * Filter by locale. When set, only returns entries in this locale.\n\t * Only relevant when i18n is configured.\n\t * @example \"en\" — English entries only\n\t * @example \"fr\" — French entries only\n\t */\n\tlocale?: string;\n}\n\nexport interface ContentEntry<T = Record<string, unknown>> {\n\tid: string;\n\tdata: T;\n\t/** Visual editing annotations. Spread onto elements: {...entry.edit.title} */\n\tedit: EditProxy;\n}\n\n/** Cache hint returned by the content loader for route caching */\nexport interface CacheHint {\n\ttags?: string[];\n\tlastModified?: Date;\n}\n\n/**\n * Result from getEmDashCollection\n */\nexport interface CollectionResult<T> {\n\t/** The entries (empty array if error or none found) */\n\tentries: ContentEntry<T>[];\n\t/** Error if the query failed */\n\terror?: Error;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n\t/**\n\t * Opaque cursor for the next page.\n\t * Undefined when there are no more results.\n\t * Pass this as `cursor` in the next query to get the next page.\n\t */\n\tnextCursor?: string;\n}\n\n/**\n * Result from getEmDashEntry\n */\nexport interface EntryResult<T> {\n\t/** The entry, or null if not found */\n\tentry: ContentEntry<T> | null;\n\t/** Error if the query failed (not set for \"not found\", only for actual errors) */\n\terror?: Error;\n\t/** Whether we're in preview mode (valid token was provided) */\n\tisPreview: boolean;\n\t/** Set when a fallback locale was used instead of the requested locale */\n\tfallbackLocale?: string;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n}\n\nconst COLLECTION_NAME = \"_emdash\";\n\n/** Symbol key for edit metadata on PT arrays — avoids collision with user data */\nconst EMDASH_EDIT = Symbol.for(\"__emdash\");\n\n/** Edit metadata attached to PT arrays in edit mode */\nexport interface EditFieldMeta {\n\tcollection: string;\n\tid: string;\n\tfield: string;\n}\n\n/** Type guard for EditFieldMeta */\nfunction isEditFieldMeta(value: unknown): value is EditFieldMeta {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tif (!(\"collection\" in value) || !(\"id\" in value) || !(\"field\" in value)) return false;\n\t// After `in` checks, TS narrows to Record<\"collection\" | \"id\" | \"field\", unknown>\n\tconst { collection, id, field } = value;\n\treturn typeof collection === \"string\" && typeof id === \"string\" && typeof field === \"string\";\n}\n\n/**\n * Read edit metadata from a value (returns undefined if not tagged).\n * Uses Object.getOwnPropertyDescriptor to access Symbol-keyed property\n * without an unsafe type assertion.\n */\nexport function getEditMeta(value: unknown): EditFieldMeta | undefined {\n\tif (value && typeof value === \"object\") {\n\t\tconst desc = Object.getOwnPropertyDescriptor(value, EMDASH_EDIT);\n\t\tconst meta: unknown = desc?.value;\n\t\tif (isEditFieldMeta(meta)) {\n\t\t\treturn meta;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Tag PT-like arrays in entry data with edit metadata (non-enumerable).\n * A PT array is identified by: is an array, first element has _type property.\n */\nfunction tagEditableFields(data: Record<string, unknown>, collection: string, id: string): void {\n\tfor (const [field, value] of Object.entries(data)) {\n\t\tif (\n\t\t\tArray.isArray(value) &&\n\t\t\tvalue.length > 0 &&\n\t\t\tvalue[0] &&\n\t\t\ttypeof value[0] === \"object\" &&\n\t\t\t\"_type\" in value[0]\n\t\t) {\n\t\t\tObject.defineProperty(value, EMDASH_EDIT, {\n\t\t\t\tvalue: { collection, id, field } satisfies EditFieldMeta,\n\t\t\t\tenumerable: false,\n\t\t\t\tconfigurable: true,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/** Safely read a string field from a Record, with optional fallback */\nfunction dataStr(data: Record<string, unknown>, key: string, fallback = \"\"): string {\n\tconst val = data[key];\n\treturn typeof val === \"string\" ? val : fallback;\n}\n\n/** Type guard for Record<string, unknown> */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Extract data as Record from an Astro entry (which is any-typed) */\nfunction entryData(entry: { data?: unknown }): Record<string, unknown> {\n\treturn isRecord(entry.data) ? entry.data : {};\n}\n\n/** Extract the database ID from entry data (data.id is the ULID, entry.id is the slug) */\nfunction entryDatabaseId(entry: { id: string; data?: unknown }): string {\n\tconst d = entryData(entry);\n\treturn dataStr(d, \"id\") || entry.id;\n}\n\n/** Extract edit options from entry data for the proxy */\nfunction entryEditOptions(entry: { data?: unknown }): EditableOptions {\n\tconst data = entryData(entry);\n\tconst status = dataStr(data, \"status\", \"draft\");\n\tconst draftRevisionId = dataStr(data, \"draftRevisionId\") || undefined;\n\tconst liveRevisionId = dataStr(data, \"liveRevisionId\") || undefined;\n\tconst hasDraft = !!draftRevisionId && draftRevisionId !== liveRevisionId;\n\treturn { status, hasDraft };\n}\n\n/**\n * Get all entries of a content type\n *\n * Returns { entries, error } for graceful error handling.\n *\n * When emdash-env.d.ts is generated, the collection name will be\n * type-checked and the return type will be inferred automatically.\n *\n * @example\n * ```ts\n * import { getEmDashCollection } from \"emdash\";\n *\n * const { entries: posts, error } = await getEmDashCollection(\"posts\");\n * if (error) {\n * console.error(\"Failed to load posts:\", error);\n * return;\n * }\n * // posts[0].data.title is typed (if emdash-env.d.ts exists)\n *\n * // With filters\n * const { entries: drafts } = await getEmDashCollection(\"posts\", { status: \"draft\" });\n * ```\n */\nexport async function getEmDashCollection<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Cache per (type, filter) within a single request. Edit mode and\n\t// preview are request-scoped and stable, so they don't need to be\n\t// part of the key. Widgets and layouts frequently request the same\n\t// collection shape as the page itself (e.g. a \"recent posts\" list\n\t// appears on the home page AND in the sidebar) — caching collapses\n\t// those duplicate queries, along with the bylines and taxonomy-term\n\t// hydration each call would otherwise re-do.\n\t//\n\t// Bucket small limits to a shared minimum so a page with several\n\t// \"recent N posts\" widgets at slightly different limits (e.g. a\n\t// post-detail page asking for 4 in the body and 5 in the sidebar)\n\t// shares one fetch + hydration round-trip rather than running two.\n\t// Cursor-paginated calls are exempt: their limit is part of the\n\t// pagination contract.\n\tconst bucketed = bucketFilter(filter);\n\tconst cached = await requestCached(collectionCacheKey(type, bucketed.fetchFilter), () =>\n\t\tgetEmDashCollectionUncached<T, D>(type, bucketed.fetchFilter),\n\t);\n\treturn bucketed.requestedLimit === undefined\n\t\t? cached\n\t\t: sliceCollectionResult(cached, bucketed.requestedLimit, filter?.orderBy);\n}\n\n/**\n * Threshold for limit bucketing. Page templates routinely render small\n * \"recent posts\" widgets at limits 3-8; rounding those up to a single\n * shared bucket lets one fetch satisfy several widgets within a request.\n * Above this, the requested limit is honoured exactly — bucketing limit:50\n * to limit:64 would waste hydration work for callers fetching real pages.\n */\nconst BUCKET_LIMIT_THRESHOLD = 10;\n\ninterface BucketedFilter {\n\t/** Filter to pass to the loader (with limit possibly raised). */\n\tfetchFilter: CollectionFilter | undefined;\n\t/** Original limit; defined only when bucketing was applied. */\n\trequestedLimit: number | undefined;\n}\n\n/** @internal exported for unit tests; not part of the public API. */\nexport function bucketFilter(filter: CollectionFilter | undefined): BucketedFilter {\n\tconst limit = filter?.limit;\n\tif (\n\t\tlimit === undefined ||\n\t\tlimit >= BUCKET_LIMIT_THRESHOLD ||\n\t\tlimit <= 0 ||\n\t\tfilter?.cursor !== undefined\n\t) {\n\t\treturn { fetchFilter: filter, requestedLimit: undefined };\n\t}\n\treturn {\n\t\tfetchFilter: { ...filter, limit: BUCKET_LIMIT_THRESHOLD },\n\t\trequestedLimit: limit,\n\t};\n}\n\n/**\n * Slice a cached bucketed result down to the originally-requested limit\n * and recompute `nextCursor` from the row that would have been the\n * over-fetch detector for that limit. When truncation is needed, returns\n * a shallow-copied result with a new `entries` array; otherwise returns\n * the cached result unchanged (including error results and results\n * already within the requested limit).\n */\n/** @internal exported for unit tests; not part of the public API. */\nexport function sliceCollectionResult<D>(\n\tcached: CollectionResult<D>,\n\tlimit: number,\n\torderBy: OrderBySpec | undefined,\n): CollectionResult<D> {\n\tif (cached.error) return cached;\n\tif (cached.entries.length <= limit) return cached;\n\tconst sliced = cached.entries.slice(0, limit);\n\t// Mirror the loader's encoding: cursor points at the last returned row,\n\t// so \"next page\" picks up at the row immediately after it. See\n\t// buildCursorCondition in loader.ts — it filters strictly past this row.\n\tconst lastEntry = sliced.at(-1);\n\tconst nextCursor = lastEntry ? encodeEntryCursor(lastEntry, orderBy) : undefined;\n\treturn { ...cached, entries: sliced, nextCursor };\n}\n\n/** Map of database column names to camelCase keys present on entry.data. */\nconst ENTRY_DATA_KEY_MAP: Record<string, string> = {\n\tcreated_at: \"createdAt\",\n\tupdated_at: \"updatedAt\",\n\tpublished_at: \"publishedAt\",\n\tscheduled_at: \"scheduledAt\",\n\tauthor_id: \"authorId\",\n\tprimary_byline_id: \"primaryBylineId\",\n};\n\n// Mirror loader.ts FIELD_NAME_PATTERN. Kept in sync intentionally — diverging\n// would let the encoder accept a field name the loader's getPrimarySort then\n// rejected, producing a cursor that paginates against a different column.\nconst FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Encode a `nextCursor` from a content entry, mirroring the loader's\n * encoding scheme: `(orderValue, id)` where `orderValue` is the primary\n * sort field's stringified value. For date columns, reads the raw DB\n * string the loader stashed via CURSOR_RAW_VALUES — round-tripping the\n * parsed Date through `toISOString()` would lose precision for stored\n * values that aren't already ISO-with-milliseconds.\n */\nfunction encodeEntryCursor<D>(\n\tentry: ContentEntry<D>,\n\torderBy: OrderBySpec | undefined,\n): string | undefined {\n\tconst data = entryData(entry);\n\tconst id = dataStr(data, \"id\");\n\tif (!id) return undefined;\n\n\t// Match loader.ts getPrimarySort: take the first valid field, default to created_at.\n\tlet dbField = \"created_at\";\n\tif (orderBy) {\n\t\tfor (const field of Object.keys(orderBy)) {\n\t\t\tif (FIELD_NAME_PATTERN.test(field)) {\n\t\t\t\tdbField = field;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Date columns: prefer the raw stored string captured by the loader so\n\t// the cursor matches what a direct loader fetch would emit, regardless\n\t// of how the DB stored the timestamp.\n\tconst rawDateValuesRaw = Reflect.get(data, CURSOR_RAW_VALUES);\n\tif (rawDateValuesRaw !== null && typeof rawDateValuesRaw === \"object\") {\n\t\tconst raw = Reflect.get(rawDateValuesRaw, dbField);\n\t\tif (typeof raw === \"string\") return encodeCursor(raw, id);\n\t}\n\n\tconst dataKey = ENTRY_DATA_KEY_MAP[dbField] ?? dbField;\n\tconst value = data[dataKey];\n\tlet orderValue: string;\n\tif (value instanceof Date) {\n\t\torderValue = value.toISOString();\n\t} else if (typeof value === \"string\" || typeof value === \"number\") {\n\t\torderValue = String(value);\n\t} else {\n\t\t// Match the loader's empty-string fallback for null/undefined order\n\t\t// values so cursor decoding stays valid even at the boundary.\n\t\torderValue = \"\";\n\t}\n\treturn encodeCursor(orderValue, id);\n}\n\n/**\n * Build a canonical cache key for `getEmDashCollection`.\n *\n * `JSON.stringify` is insertion-order-sensitive, so two callers passing\n * semantically identical filters with different key orders would miss\n * the cache. We fix the top-level field order and sort `where` keys\n * (order there is irrelevant), while preserving `orderBy` key order\n * because that's the sort priority.\n */\nfunction collectionCacheKey(type: string, filter?: CollectionFilter): string {\n\tif (!filter) return `collection:${type}:`;\n\tconst parts = [\n\t\tfilter.status ?? \"\",\n\t\tfilter.limit ?? \"\",\n\t\tfilter.cursor ?? \"\",\n\t\tfilter.where ? stableStringify(filter.where) : \"\",\n\t\tfilter.orderBy ? JSON.stringify(filter.orderBy) : \"\",\n\t\tfilter.locale ?? \"\",\n\t];\n\treturn `collection:${type}:${parts.join(\"|\")}`;\n}\n\nfunction stableStringify(value: Record<string, unknown>): string {\n\tconst keys = Object.keys(value).toSorted();\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const k of keys) ordered[k] = value[k];\n\treturn JSON.stringify(ordered);\n}\n\nasync function getEmDashCollectionUncached<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveCollection } = await import(\"astro:content\");\n\n\t// Resolve locale: explicit filter > ALS context > defaultLocale (when i18n enabled)\n\t// Without this, queries return all locale rows, producing broken IDs\n\tconst ctx = getRequestContext();\n\tconst i18nConfig = getI18nConfig();\n\tconst resolvedLocale =\n\t\tfilter?.locale ?? ctx?.locale ?? (isI18nEnabled() ? i18nConfig!.defaultLocale : undefined);\n\n\tconst result = await getLiveCollection(COLLECTION_NAME, {\n\t\ttype,\n\t\tstatus: filter?.status,\n\t\tlimit: filter?.limit,\n\t\tcursor: filter?.cursor,\n\t\twhere: filter?.where,\n\t\torderBy: filter?.orderBy,\n\t\tlocale: resolvedLocale,\n\t});\n\n\tconst { entries, error, cacheHint } = result;\n\t// nextCursor is returned by the emdash loader but not part of Astro's base\n\t// LiveLoader return type. Extract it safely via property descriptor to avoid\n\t// an unsafe type assertion on the `any`-typed result object.\n\tconst rawCursor = Object.getOwnPropertyDescriptor(result, \"nextCursor\")?.value;\n\tconst nextCursor: string | undefined = typeof rawCursor === \"string\" ? rawCursor : undefined;\n\n\tif (error) {\n\t\treturn { entries: [], error, cacheHint: {} };\n\t}\n\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst entriesWithEdit = entries.map((entry: ContentEntry<D>) => {\n\t\tconst dbId = entryDatabaseId(entry);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(entry), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...entry,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(entry)) : createNoop(),\n\t\t};\n\t});\n\n\t// Eagerly hydrate bylines and taxonomy terms for all entries in parallel.\n\t// Both are independent queries, so running them concurrently halves the\n\t// round-trip cost on remote databases (D1 replicas, etc.).\n\tawait Promise.all([\n\t\thydrateEntryBylines(type, entriesWithEdit),\n\t\thydrateEntryTerms(type, entriesWithEdit),\n\t]);\n\n\treturn { entries: entriesWithEdit, nextCursor, cacheHint: cacheHint ?? {} };\n}\n\n/**\n * Get a single entry by type and ID/slug\n *\n * Returns { entry, error, isPreview } for graceful error handling.\n * - entry is null if not found (not an error)\n * - error is set only for actual errors (db issues, etc.)\n *\n * Preview mode is detected automatically from request context (ALS).\n * When the URL has a valid `_preview` token, the middleware sets preview\n * context and this function serves draft revision data if available.\n *\n * @example\n * ```ts\n * import { getEmDashEntry } from \"emdash\";\n *\n * // Simple usage — preview just works via middleware\n * const { entry: post, isPreview, error } = await getEmDashEntry(\"posts\", \"my-slug\");\n * if (!post) return Astro.redirect(\"/404\");\n * ```\n */\nexport async function getEmDashEntry<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tid: string,\n\toptions?: { locale?: string },\n): Promise<EntryResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveEntry } = await import(\"astro:content\");\n\n\t// Check ALS for preview and edit mode context\n\tconst ctx = getRequestContext();\n\tconst preview = ctx?.preview;\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst isPreviewMode = !!preview && preview.collection === type;\n\t// Edit mode implies preview — editors should see draft content\n\tconst serveDrafts = isPreviewMode || isEditMode;\n\n\t// Resolve locale: explicit option > ALS context > undefined (no filter)\n\tconst requestedLocale = options?.locale ?? ctx?.locale;\n\n\t/** Wrap a raw Astro entry with edit proxy, tagging editable fields if needed */\n\tfunction wrapEntry(raw: ContentEntry<D>): ContentEntry<D> {\n\t\tconst dbId = entryDatabaseId(raw);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(raw), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...raw,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(raw)) : createNoop(),\n\t\t};\n\t}\n\n\t/** Check if an entry is publicly visible (published or scheduled past its time) */\n\tfunction isVisible(entry: ContentEntry<D>): boolean {\n\t\tconst data = entryData(entry);\n\t\tconst status = dataStr(data, \"status\");\n\t\tconst scheduledAt = dataStr(data, \"scheduledAt\") || undefined;\n\t\tconst isPublished = status === \"published\";\n\t\tconst isScheduledAndReady =\n\t\t\tstatus === \"scheduled\" && scheduledAt && new Date(scheduledAt) <= new Date();\n\t\treturn isPublished || !!isScheduledAndReady;\n\t}\n\n\t// Build the fallback chain: [requestedLocale, fallback1, ..., defaultLocale]\n\t// When i18n is disabled or no locale requested, just use a single-element chain\n\tconst localeChain =\n\t\trequestedLocale && isI18nEnabled() ? getFallbackChain(requestedLocale) : [requestedLocale];\n\n\t/** Return a successful EntryResult with bylines and taxonomy terms hydrated */\n\tasync function successResult(\n\t\twrapped: ContentEntry<D>,\n\t\topts: { isPreview: boolean; fallbackLocale?: string; cacheHint: CacheHint },\n\t): Promise<EntryResult<D>> {\n\t\tawait Promise.all([hydrateEntryBylines(type, [wrapped]), hydrateEntryTerms(type, [wrapped])]);\n\t\treturn {\n\t\t\tentry: wrapped,\n\t\t\tisPreview: opts.isPreview,\n\t\t\tfallbackLocale: opts.fallbackLocale,\n\t\t\tcacheHint: opts.cacheHint,\n\t\t};\n\t}\n\n\tif (serveDrafts) {\n\t\t// Draft mode: try each locale in the fallback chain\n\t\tfor (let i = 0; i < localeChain.length; i++) {\n\t\t\tconst locale = localeChain[i];\n\t\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\t\tconst {\n\t\t\t\tentry: baseEntry,\n\t\t\t\terror: baseError,\n\t\t\t\tcacheHint,\n\t\t\t} = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\ttype,\n\t\t\t\tid,\n\t\t\t\tlocale,\n\t\t\t});\n\n\t\t\tif (baseError) {\n\t\t\t\treturn { entry: null, error: baseError, isPreview: serveDrafts, cacheHint: {} };\n\t\t\t}\n\n\t\t\tif (!baseEntry) continue; // Try next locale in chain\n\n\t\t\t// Preview tokens are item-scoped: verify the resolved entry matches.\n\t\t\t// Edit mode (authenticated editors) has collection-wide draft access.\n\t\t\tif (isPreviewMode && !isEditMode) {\n\t\t\t\tconst dbId = entryDatabaseId(baseEntry);\n\t\t\t\tif (preview.id !== dbId && preview.id !== id) {\n\t\t\t\t\t// Token doesn't match — serve only if publicly visible, without draft access\n\t\t\t\t\tif (isVisible(baseEntry)) {\n\t\t\t\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\t\t\t\tisPreview: false,\n\t\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\t// Not visible — try next locale in fallback chain\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check if entry has a draft revision — if so, re-fetch with revision data\n\t\t\tconst baseData = entryData(baseEntry);\n\t\t\tconst draftRevisionId = dataStr(baseData, \"draftRevisionId\") || undefined;\n\n\t\t\tif (draftRevisionId) {\n\t\t\t\tconst { entry: draftEntry, error: draftError } = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\t\ttype,\n\t\t\t\t\tid,\n\t\t\t\t\trevisionId: draftRevisionId,\n\t\t\t\t\tlocale,\n\t\t\t\t});\n\n\t\t\t\tif (!draftError && draftEntry) {\n\t\t\t\t\treturn successResult(wrapEntry(draftEntry), {\n\t\t\t\t\t\tisPreview: serveDrafts,\n\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\tisPreview: serveDrafts,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\n\t\t// No entry found in any locale\n\t\treturn { entry: null, isPreview: serveDrafts, cacheHint: {} };\n\t}\n\n\t// Normal mode: try each locale in the fallback chain, only return published content\n\tfor (let i = 0; i < localeChain.length; i++) {\n\t\tconst locale = localeChain[i];\n\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\tconst { entry, error, cacheHint } = await getLiveEntry(COLLECTION_NAME, { type, id, locale });\n\t\tif (error) {\n\t\t\treturn { entry: null, error, isPreview: false, cacheHint: {} };\n\t\t}\n\n\t\tif (entry && isVisible(entry)) {\n\t\t\treturn successResult(wrapEntry(entry), {\n\t\t\t\tisPreview: false,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\t\t// Entry not found or not visible in this locale — try next\n\t}\n\n\treturn { entry: null, isPreview: false, cacheHint: {} };\n}\n\n/**\n * Eagerly hydrate byline data onto entry.data for one or more entries.\n *\n * Attaches `bylines` (array of ContentBylineCredit) and `byline`\n * (primary BylineSummary or null) to each entry's data object.\n * Uses batch queries to avoid N+1.\n *\n * Fails silently if the byline tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryBylines<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getBylinesForEntries } = await import(\"./bylines/index.js\");\n\n\t\tconst refs = entries\n\t\t\t.map((e) => {\n\t\t\t\tconst data = entryData(e);\n\t\t\t\tconst id = dataStr(data, \"id\");\n\t\t\t\treturn id ? { id, authorId: dataStr(data, \"authorId\") || null } : null;\n\t\t\t})\n\t\t\t.filter((r): r is { id: string; authorId: string | null } => r !== null);\n\t\tif (refs.length === 0) return;\n\n\t\tconst bylinesMap = await getBylinesForEntries(type, refs);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tconst credits = bylinesMap.get(dbId) ?? [];\n\t\t\tdata.bylines = credits;\n\t\t\tdata.byline = credits[0]?.byline ?? null;\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate bylines:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Eagerly hydrate taxonomy term data onto entry.data for one or more entries.\n *\n * Attaches `terms` (Record keyed by taxonomy name with an array of TaxonomyTerm\n * values) to each entry's data object. Uses a single batched JOIN query across\n * all taxonomies so the cost is O(1) regardless of the number of entries or\n * taxonomies on the site.\n *\n * This eliminates the common N+1 pattern where templates loop over list\n * results and call getEntryTerms() per entry. With hydration, the list page\n * stays at a single round-trip for term data.\n *\n * Fails silently if the taxonomy tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryTerms<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getAllTermsForEntries } = await import(\"./taxonomies/index.js\");\n\n\t\tconst ids = entries.map((e) => dataStr(entryData(e), \"id\")).filter(Boolean);\n\t\tif (ids.length === 0) return;\n\n\t\tconst termsMap = await getAllTermsForEntries(type, ids);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tdata.terms = termsMap.get(dbId) ?? {};\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate terms:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Translation summary for a single locale variant\n */\nexport interface TranslationSummary {\n\t/** Content item ID */\n\tid: string;\n\t/** Locale code (e.g. \"en\", \"fr\") */\n\tlocale: string;\n\t/** URL slug */\n\tslug: string | null;\n\t/** Current status */\n\tstatus: string;\n}\n\n/**\n * Result from getTranslations\n */\nexport interface TranslationsResult {\n\t/** The translation group ID (shared across locales) */\n\ttranslationGroup: string;\n\t/** All locale variants in this group */\n\ttranslations: TranslationSummary[];\n\t/** Error if the query failed */\n\terror?: Error;\n}\n\n/**\n * Get all translations of a content item.\n *\n * Given a content entry, returns all locale variants that share the same\n * translation group. This is useful for building language switcher UI.\n *\n * @example\n * ```ts\n * import { getEmDashEntry, getTranslations } from \"emdash\";\n *\n * const { entry: post } = await getEmDashEntry(\"posts\", \"hello-world\", { locale: \"en\" });\n * const { translations } = await getTranslations(\"posts\", post.data.id);\n * // translations = [{ id: \"...\", locale: \"en\", slug: \"hello-world\", status: \"published\" }, ...]\n * ```\n */\nexport async function getTranslations(type: string, id: string): Promise<TranslationsResult> {\n\ttry {\n\t\tconst db = (await import(\"./loader.js\")).getDb;\n\t\tconst dbInstance = await db();\n\t\tconst { ContentRepository } = await import(\"./database/repositories/content.js\");\n\t\tconst repo = new ContentRepository(dbInstance);\n\n\t\t// Find the item to get its translation group\n\t\tconst item = await repo.findByIdOrSlug(type, id);\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\ttranslationGroup: \"\",\n\t\t\t\ttranslations: [],\n\t\t\t\terror: new Error(`Content item not found: ${id}`),\n\t\t\t};\n\t\t}\n\n\t\tconst group = item.translationGroup || item.id;\n\t\tconst translations = await repo.findTranslations(type, group);\n\n\t\treturn {\n\t\t\ttranslationGroup: group,\n\t\t\ttranslations: translations.map((t) => ({\n\t\t\t\tid: t.id,\n\t\t\t\tlocale: t.locale || \"en\",\n\t\t\t\tslug: t.slug,\n\t\t\t\tstatus: t.status,\n\t\t\t})),\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\ttranslationGroup: \"\",\n\t\t\ttranslations: [],\n\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t};\n\t}\n}\n\n/**\n * Result from resolveEmDashPath\n */\nexport interface ResolvePathResult<T = Record<string, unknown>> {\n\t/** The matched entry */\n\tentry: ContentEntry<T>;\n\t/** The collection slug that matched */\n\tcollection: string;\n\t/** Extracted parameters from the URL pattern (e.g. { slug: \"my-post\" }) */\n\tparams: Record<string, string>;\n}\n\n/** Matches `{paramName}` placeholders in URL patterns */\nconst URL_PARAM_PATTERN = /\\{(\\w+)\\}/g;\n\n/** Convert a URL pattern like \"/blog/{slug}\" to a regex and param name list */\nfunction patternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } {\n\tconst paramNames: string[] = [];\n\tconst regexStr = pattern.replace(URL_PARAM_PATTERN, (_match, name: string) => {\n\t\tparamNames.push(name);\n\t\treturn \"([^/]+)\";\n\t});\n\treturn { regex: new RegExp(`^${regexStr}$`), paramNames };\n}\n\n/** Cached compiled URL patterns for resolveEmDashPath */\ninterface CachedPattern {\n\tslug: string;\n\tregex: RegExp;\n\tparamNames: string[];\n}\nlet cachedUrlPatterns: CachedPattern[] | null = null;\n\n/**\n * Invalidate the cached URL patterns used by resolveEmDashPath.\n * Call when collection URL patterns change (schema updates).\n */\nexport function invalidateUrlPatternCache(): void {\n\tcachedUrlPatterns = null;\n}\n\n/**\n * Resolve a URL path to a content entry by matching against collection URL patterns.\n *\n * Loads all collections with a `urlPattern` set, converts each pattern to a regex,\n * and tests the given path. On match, extracts the slug and fetches the entry.\n *\n * @example\n * ```ts\n * import { resolveEmDashPath } from \"emdash\";\n *\n * // Given pages with urlPattern \"/{slug}\" and posts with \"/blog/{slug}\":\n * const result = await resolveEmDashPath(\"/blog/hello-world\");\n * if (result) {\n * console.log(result.collection); // \"posts\"\n * console.log(result.params.slug); // \"hello-world\"\n * console.log(result.entry.data); // post data\n * }\n * ```\n */\nexport async function resolveEmDashPath<T = Record<string, unknown>>(\n\tpath: string,\n): Promise<ResolvePathResult<T> | null> {\n\t// Build and cache compiled patterns on first call\n\tif (!cachedUrlPatterns) {\n\t\tconst { getDb } = await import(\"./loader.js\");\n\t\tconst { SchemaRegistry } = await import(\"./schema/registry.js\");\n\t\tconst db = await getDb();\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst collections = await registry.listCollections();\n\n\t\tcachedUrlPatterns = [];\n\t\tfor (const collection of collections) {\n\t\t\tif (!collection.urlPattern) continue;\n\t\t\tconst { regex, paramNames } = patternToRegex(collection.urlPattern);\n\t\t\tcachedUrlPatterns.push({ slug: collection.slug, regex, paramNames });\n\t\t}\n\t}\n\n\tfor (const pattern of cachedUrlPatterns) {\n\t\tconst match = path.match(pattern.regex);\n\t\tif (!match) continue;\n\n\t\t// Extract params\n\t\tconst params: Record<string, string> = {};\n\t\tfor (let i = 0; i < pattern.paramNames.length; i++) {\n\t\t\tparams[pattern.paramNames[i]] = match[i + 1];\n\t\t}\n\n\t\t// Look up entry by slug (most common pattern)\n\t\tconst slug = params.slug;\n\t\tif (!slug) continue;\n\n\t\tconst { entry } = await getEmDashEntry<string, T>(pattern.slug, slug);\n\t\tif (entry) {\n\t\t\treturn { entry, collection: pattern.slug, params };\n\t\t}\n\t}\n\n\treturn null;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"query-8c_meo_K.mjs","names":[],"sources":["../src/visual-editing/editable.ts","../src/query.ts"],"sourcesContent":["/**\n * Visual editing annotation system\n *\n * Creates Proxy objects that emit data-emdash-ref attributes when spread onto elements.\n */\n\nexport interface CMSAnnotation {\n\tcollection: string;\n\tid: string;\n\tfield?: string;\n\t/** Entry status — only present on entry-level annotations (not field-level) */\n\tstatus?: string;\n\t/** Whether the entry has unpublished draft changes */\n\thasDraft?: boolean;\n}\n\n/** The shape returned when spreading an edit annotation onto an element */\nexport interface FieldAnnotation {\n\t\"data-emdash-ref\": string;\n}\n\nexport interface EditableOptions {\n\t/** Entry status: \"draft\", \"published\", \"scheduled\" */\n\tstatus?: string;\n\t/** true when draftRevisionId exists and differs from liveRevisionId */\n\thasDraft?: boolean;\n}\n\n/**\n * Create an editable proxy for an entry.\n *\n * Usage:\n * - `{...entry.edit}` - entry-level annotation (includes status/hasDraft)\n * - `{...entry.edit.title}` - field-level annotation\n * - `{...entry.edit['nested.field']}` - nested field (bracket notation)\n */\nexport function createEditable(\n\tcollection: string,\n\tid: string,\n\toptions?: EditableOptions,\n): EditProxy {\n\tconst base: CMSAnnotation = {\n\t\tcollection,\n\t\tid,\n\t\t...(options?.status && { status: options.status }),\n\t\t...(options?.hasDraft && { hasDraft: true }),\n\t};\n\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (prop === \"toJSON\") return () => ({ \"data-emdash-ref\": JSON.stringify(base) });\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\n\t\t\t// data-emdash-ref access returns the entry-level string\n\t\t\tif (prop === \"data-emdash-ref\") return JSON.stringify(base);\n\n\t\t\t// Field-level: return a FieldAnnotation for the specific field\n\t\t\treturn {\n\t\t\t\t\"data-emdash-ref\": JSON.stringify({ ...base, field: String(prop) }),\n\t\t\t} satisfies FieldAnnotation;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [\"data-emdash-ref\"];\n\t\t},\n\t\tgetOwnPropertyDescriptor(_, prop) {\n\t\t\tif (prop === \"data-emdash-ref\") {\n\t\t\t\treturn {\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tvalue: JSON.stringify(base),\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Create a noop proxy for production mode.\n * Spreading this produces no attributes.\n */\nexport function createNoop(): EditProxy {\n\treturn new Proxy({} as EditProxy, {\n\t\tget(_, prop) {\n\t\t\tif (typeof prop === \"symbol\") return undefined;\n\t\t\t// All property access returns undefined in noop mode\n\t\t\treturn undefined;\n\t\t},\n\t\townKeys() {\n\t\t\treturn [];\n\t\t},\n\t\tgetOwnPropertyDescriptor() {\n\t\t\treturn undefined;\n\t\t},\n\t});\n}\n\n/**\n * Visual editing proxy type.\n *\n * Spread directly onto elements for entry-level annotations: `{...entry.edit}`\n * Access a field for field-level annotations: `{...entry.edit.title}`\n *\n * In production, spreading produces no attributes (noop).\n */\nexport type EditProxy = {\n\treadonly [field: string]: Partial<FieldAnnotation>;\n};\n","/**\n * Query functions for EmDash content\n *\n * These wrap Astro's getLiveCollection/getLiveEntry with type filtering.\n * Use these instead of calling Astro's functions directly.\n *\n * Error handling follows Astro's pattern - returns { entries/entry, error }\n * so callers can gracefully handle errors (including 404s).\n *\n * Preview mode is handled implicitly via ALS request context —\n * no parameters needed. The middleware verifies the preview token\n * and sets the context; query functions read it automatically.\n */\n\nimport { encodeCursor } from \"./database/repositories/types.js\";\nimport { getFallbackChain, getI18nConfig, isI18nEnabled } from \"./i18n/config.js\";\nimport { CURSOR_RAW_VALUES } from \"./loader.js\";\nimport { requestCached } from \"./request-cache.js\";\nimport { getRequestContext } from \"./request-context.js\";\nimport { isMissingTableError } from \"./utils/db-errors.js\";\nimport {\n\tcreateEditable,\n\tcreateNoop,\n\ttype EditProxy,\n\ttype EditableOptions,\n} from \"./visual-editing/editable.js\";\n\n/**\n * Collection type registry for type-safe queries.\n *\n * This interface is extended by the generated emdash-env.d.ts file\n * to provide type inference for collection names and their data shapes.\n *\n * @example\n * ```ts\n * // In emdash-env.d.ts (generated):\n * declare module \"emdash\" {\n * interface EmDashCollections {\n * posts: { title: string; content: PortableTextBlock[]; };\n * pages: { title: string; body: PortableTextBlock[]; };\n * }\n * }\n *\n * // Then in your code:\n * const { entries } = await getEmDashCollection(\"posts\");\n * // entries[0].data.title is typed as string\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface EmDashCollections {}\n\n/**\n * Helper type to infer the data type for a collection.\n * Returns the registered type if known, otherwise falls back to Record<string, unknown>.\n */\nexport type InferCollectionData<T extends string> = T extends keyof EmDashCollections\n\t? EmDashCollections[T]\n\t: Record<string, unknown>;\n\n/**\n * Sort direction\n */\nexport type SortDirection = \"asc\" | \"desc\";\n\n/**\n * Order by specification - field name to direction\n * @example { created_at: \"desc\" } - Sort by created_at descending\n * @example { title: \"asc\" } - Sort by title ascending\n * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n */\nexport type OrderBySpec = Record<string, SortDirection>;\n\nexport interface CollectionFilter {\n\tstatus?: \"draft\" | \"published\" | \"archived\";\n\tlimit?: number;\n\t/**\n\t * Opaque cursor for keyset pagination.\n\t * Pass the `nextCursor` value from a previous result to fetch the next page.\n\t * @example\n\t * ```ts\n\t * const cursor = Astro.url.searchParams.get(\"cursor\") ?? undefined;\n\t * const { entries, nextCursor } = await getEmDashCollection(\"posts\", {\n\t * limit: 10,\n\t * cursor,\n\t * });\n\t * ```\n\t */\n\tcursor?: string;\n\t/**\n\t * Filter by field values or taxonomy terms\n\t * @example { category: 'news' } - Filter by taxonomy term\n\t * @example { category: ['news', 'featured'] } - Filter by multiple terms (OR)\n\t */\n\twhere?: Record<string, string | string[]>;\n\t/**\n\t * Order results by field(s)\n\t * @default { created_at: \"desc\" }\n\t * @example { created_at: \"desc\" } - Sort by created_at descending (default)\n\t * @example { title: \"asc\" } - Sort by title ascending\n\t * @example { published_at: \"desc\", title: \"asc\" } - Multi-field sort\n\t */\n\torderBy?: OrderBySpec;\n\t/**\n\t * Filter by locale. When set, only returns entries in this locale.\n\t * Only relevant when i18n is configured.\n\t * @example \"en\" — English entries only\n\t * @example \"fr\" — French entries only\n\t */\n\tlocale?: string;\n}\n\nexport interface ContentEntry<T = Record<string, unknown>> {\n\tid: string;\n\tdata: T;\n\t/** Visual editing annotations. Spread onto elements: {...entry.edit.title} */\n\tedit: EditProxy;\n}\n\n/** Cache hint returned by the content loader for route caching */\nexport interface CacheHint {\n\ttags?: string[];\n\tlastModified?: Date;\n}\n\n/**\n * Result from getEmDashCollection\n */\nexport interface CollectionResult<T> {\n\t/** The entries (empty array if error or none found) */\n\tentries: ContentEntry<T>[];\n\t/** Error if the query failed */\n\terror?: Error;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n\t/**\n\t * Opaque cursor for the next page.\n\t * Undefined when there are no more results.\n\t * Pass this as `cursor` in the next query to get the next page.\n\t */\n\tnextCursor?: string;\n}\n\n/**\n * Result from getEmDashEntry\n */\nexport interface EntryResult<T> {\n\t/** The entry, or null if not found */\n\tentry: ContentEntry<T> | null;\n\t/** Error if the query failed (not set for \"not found\", only for actual errors) */\n\terror?: Error;\n\t/** Whether we're in preview mode (valid token was provided) */\n\tisPreview: boolean;\n\t/** Set when a fallback locale was used instead of the requested locale */\n\tfallbackLocale?: string;\n\t/** Cache hint for route caching (pass to Astro.cache.set()) */\n\tcacheHint: CacheHint;\n}\n\nconst COLLECTION_NAME = \"_emdash\";\n\n/** Symbol key for edit metadata on PT arrays — avoids collision with user data */\nconst EMDASH_EDIT = Symbol.for(\"__emdash\");\n\n/** Edit metadata attached to PT arrays in edit mode */\nexport interface EditFieldMeta {\n\tcollection: string;\n\tid: string;\n\tfield: string;\n}\n\n/** Type guard for EditFieldMeta */\nfunction isEditFieldMeta(value: unknown): value is EditFieldMeta {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tif (!(\"collection\" in value) || !(\"id\" in value) || !(\"field\" in value)) return false;\n\t// After `in` checks, TS narrows to Record<\"collection\" | \"id\" | \"field\", unknown>\n\tconst { collection, id, field } = value;\n\treturn typeof collection === \"string\" && typeof id === \"string\" && typeof field === \"string\";\n}\n\n/**\n * Read edit metadata from a value (returns undefined if not tagged).\n * Uses Object.getOwnPropertyDescriptor to access Symbol-keyed property\n * without an unsafe type assertion.\n */\nexport function getEditMeta(value: unknown): EditFieldMeta | undefined {\n\tif (value && typeof value === \"object\") {\n\t\tconst desc = Object.getOwnPropertyDescriptor(value, EMDASH_EDIT);\n\t\tconst meta: unknown = desc?.value;\n\t\tif (isEditFieldMeta(meta)) {\n\t\t\treturn meta;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * Tag PT-like arrays in entry data with edit metadata (non-enumerable).\n * A PT array is identified by: is an array, first element has _type property.\n */\nfunction tagEditableFields(data: Record<string, unknown>, collection: string, id: string): void {\n\tfor (const [field, value] of Object.entries(data)) {\n\t\tif (\n\t\t\tArray.isArray(value) &&\n\t\t\tvalue.length > 0 &&\n\t\t\tvalue[0] &&\n\t\t\ttypeof value[0] === \"object\" &&\n\t\t\t\"_type\" in value[0]\n\t\t) {\n\t\t\tObject.defineProperty(value, EMDASH_EDIT, {\n\t\t\t\tvalue: { collection, id, field } satisfies EditFieldMeta,\n\t\t\t\tenumerable: false,\n\t\t\t\tconfigurable: true,\n\t\t\t});\n\t\t}\n\t}\n}\n\n/** Safely read a string field from a Record, with optional fallback */\nfunction dataStr(data: Record<string, unknown>, key: string, fallback = \"\"): string {\n\tconst val = data[key];\n\treturn typeof val === \"string\" ? val : fallback;\n}\n\n/** Type guard for Record<string, unknown> */\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Extract data as Record from an Astro entry (which is any-typed) */\nfunction entryData(entry: { data?: unknown }): Record<string, unknown> {\n\treturn isRecord(entry.data) ? entry.data : {};\n}\n\n/** Extract the database ID from entry data (data.id is the ULID, entry.id is the slug) */\nfunction entryDatabaseId(entry: { id: string; data?: unknown }): string {\n\tconst d = entryData(entry);\n\treturn dataStr(d, \"id\") || entry.id;\n}\n\n/** Extract edit options from entry data for the proxy */\nfunction entryEditOptions(entry: { data?: unknown }): EditableOptions {\n\tconst data = entryData(entry);\n\tconst status = dataStr(data, \"status\", \"draft\");\n\tconst draftRevisionId = dataStr(data, \"draftRevisionId\") || undefined;\n\tconst liveRevisionId = dataStr(data, \"liveRevisionId\") || undefined;\n\tconst hasDraft = !!draftRevisionId && draftRevisionId !== liveRevisionId;\n\treturn { status, hasDraft };\n}\n\n/**\n * Get all entries of a content type\n *\n * Returns { entries, error } for graceful error handling.\n *\n * When emdash-env.d.ts is generated, the collection name will be\n * type-checked and the return type will be inferred automatically.\n *\n * @example\n * ```ts\n * import { getEmDashCollection } from \"emdash\";\n *\n * const { entries: posts, error } = await getEmDashCollection(\"posts\");\n * if (error) {\n * console.error(\"Failed to load posts:\", error);\n * return;\n * }\n * // posts[0].data.title is typed (if emdash-env.d.ts exists)\n *\n * // With filters\n * const { entries: drafts } = await getEmDashCollection(\"posts\", { status: \"draft\" });\n * ```\n */\nexport async function getEmDashCollection<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Cache per (type, filter) within a single request. Edit mode and\n\t// preview are request-scoped and stable, so they don't need to be\n\t// part of the key. Widgets and layouts frequently request the same\n\t// collection shape as the page itself (e.g. a \"recent posts\" list\n\t// appears on the home page AND in the sidebar) — caching collapses\n\t// those duplicate queries, along with the bylines and taxonomy-term\n\t// hydration each call would otherwise re-do.\n\t//\n\t// Bucket small limits to a shared minimum so a page with several\n\t// \"recent N posts\" widgets at slightly different limits (e.g. a\n\t// post-detail page asking for 4 in the body and 5 in the sidebar)\n\t// shares one fetch + hydration round-trip rather than running two.\n\t// Cursor-paginated calls are exempt: their limit is part of the\n\t// pagination contract.\n\tconst bucketed = bucketFilter(filter);\n\tconst cached = await requestCached(collectionCacheKey(type, bucketed.fetchFilter), () =>\n\t\tgetEmDashCollectionUncached<T, D>(type, bucketed.fetchFilter),\n\t);\n\treturn bucketed.requestedLimit === undefined\n\t\t? cached\n\t\t: sliceCollectionResult(cached, bucketed.requestedLimit, filter?.orderBy);\n}\n\n/**\n * Threshold for limit bucketing. Page templates routinely render small\n * \"recent posts\" widgets at limits 3-8; rounding those up to a single\n * shared bucket lets one fetch satisfy several widgets within a request.\n * Above this, the requested limit is honoured exactly — bucketing limit:50\n * to limit:64 would waste hydration work for callers fetching real pages.\n */\nconst BUCKET_LIMIT_THRESHOLD = 10;\n\ninterface BucketedFilter {\n\t/** Filter to pass to the loader (with limit possibly raised). */\n\tfetchFilter: CollectionFilter | undefined;\n\t/** Original limit; defined only when bucketing was applied. */\n\trequestedLimit: number | undefined;\n}\n\n/** @internal exported for unit tests; not part of the public API. */\nexport function bucketFilter(filter: CollectionFilter | undefined): BucketedFilter {\n\tconst limit = filter?.limit;\n\tif (\n\t\tlimit === undefined ||\n\t\tlimit >= BUCKET_LIMIT_THRESHOLD ||\n\t\tlimit <= 0 ||\n\t\tfilter?.cursor !== undefined\n\t) {\n\t\treturn { fetchFilter: filter, requestedLimit: undefined };\n\t}\n\treturn {\n\t\tfetchFilter: { ...filter, limit: BUCKET_LIMIT_THRESHOLD },\n\t\trequestedLimit: limit,\n\t};\n}\n\n/**\n * Slice a cached bucketed result down to the originally-requested limit\n * and recompute `nextCursor` from the row that would have been the\n * over-fetch detector for that limit. When truncation is needed, returns\n * a shallow-copied result with a new `entries` array; otherwise returns\n * the cached result unchanged (including error results and results\n * already within the requested limit).\n */\n/** @internal exported for unit tests; not part of the public API. */\nexport function sliceCollectionResult<D>(\n\tcached: CollectionResult<D>,\n\tlimit: number,\n\torderBy: OrderBySpec | undefined,\n): CollectionResult<D> {\n\tif (cached.error) return cached;\n\tif (cached.entries.length <= limit) return cached;\n\tconst sliced = cached.entries.slice(0, limit);\n\t// Mirror the loader's encoding: cursor points at the last returned row,\n\t// so \"next page\" picks up at the row immediately after it. See\n\t// buildCursorCondition in loader.ts — it filters strictly past this row.\n\tconst lastEntry = sliced.at(-1);\n\tconst nextCursor = lastEntry ? encodeEntryCursor(lastEntry, orderBy) : undefined;\n\treturn { ...cached, entries: sliced, nextCursor };\n}\n\n/** Map of database column names to camelCase keys present on entry.data. */\nconst ENTRY_DATA_KEY_MAP: Record<string, string> = {\n\tcreated_at: \"createdAt\",\n\tupdated_at: \"updatedAt\",\n\tpublished_at: \"publishedAt\",\n\tscheduled_at: \"scheduledAt\",\n\tauthor_id: \"authorId\",\n\tprimary_byline_id: \"primaryBylineId\",\n};\n\n// Mirror loader.ts FIELD_NAME_PATTERN. Kept in sync intentionally — diverging\n// would let the encoder accept a field name the loader's getPrimarySort then\n// rejected, producing a cursor that paginates against a different column.\nconst FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Encode a `nextCursor` from a content entry, mirroring the loader's\n * encoding scheme: `(orderValue, id)` where `orderValue` is the primary\n * sort field's stringified value. For date columns, reads the raw DB\n * string the loader stashed via CURSOR_RAW_VALUES — round-tripping the\n * parsed Date through `toISOString()` would lose precision for stored\n * values that aren't already ISO-with-milliseconds.\n */\nfunction encodeEntryCursor<D>(\n\tentry: ContentEntry<D>,\n\torderBy: OrderBySpec | undefined,\n): string | undefined {\n\tconst data = entryData(entry);\n\tconst id = dataStr(data, \"id\");\n\tif (!id) return undefined;\n\n\t// Match loader.ts getPrimarySort: take the first valid field, default to created_at.\n\tlet dbField = \"created_at\";\n\tif (orderBy) {\n\t\tfor (const field of Object.keys(orderBy)) {\n\t\t\tif (FIELD_NAME_PATTERN.test(field)) {\n\t\t\t\tdbField = field;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Date columns: prefer the raw stored string captured by the loader so\n\t// the cursor matches what a direct loader fetch would emit, regardless\n\t// of how the DB stored the timestamp.\n\tconst rawDateValuesRaw = Reflect.get(data, CURSOR_RAW_VALUES);\n\tif (rawDateValuesRaw !== null && typeof rawDateValuesRaw === \"object\") {\n\t\tconst raw = Reflect.get(rawDateValuesRaw, dbField);\n\t\tif (typeof raw === \"string\") return encodeCursor(raw, id);\n\t}\n\n\tconst dataKey = ENTRY_DATA_KEY_MAP[dbField] ?? dbField;\n\tconst value = data[dataKey];\n\tlet orderValue: string;\n\tif (value instanceof Date) {\n\t\torderValue = value.toISOString();\n\t} else if (typeof value === \"string\" || typeof value === \"number\") {\n\t\torderValue = String(value);\n\t} else {\n\t\t// Match the loader's empty-string fallback for null/undefined order\n\t\t// values so cursor decoding stays valid even at the boundary.\n\t\torderValue = \"\";\n\t}\n\treturn encodeCursor(orderValue, id);\n}\n\n/**\n * Build a canonical cache key for `getEmDashCollection`.\n *\n * `JSON.stringify` is insertion-order-sensitive, so two callers passing\n * semantically identical filters with different key orders would miss\n * the cache. We fix the top-level field order and sort `where` keys\n * (order there is irrelevant), while preserving `orderBy` key order\n * because that's the sort priority.\n */\nfunction collectionCacheKey(type: string, filter?: CollectionFilter): string {\n\tif (!filter) return `collection:${type}:`;\n\tconst parts = [\n\t\tfilter.status ?? \"\",\n\t\tfilter.limit ?? \"\",\n\t\tfilter.cursor ?? \"\",\n\t\tfilter.where ? stableStringify(filter.where) : \"\",\n\t\tfilter.orderBy ? JSON.stringify(filter.orderBy) : \"\",\n\t\tfilter.locale ?? \"\",\n\t];\n\treturn `collection:${type}:${parts.join(\"|\")}`;\n}\n\nfunction stableStringify(value: Record<string, unknown>): string {\n\tconst keys = Object.keys(value).toSorted();\n\tconst ordered: Record<string, unknown> = {};\n\tfor (const k of keys) ordered[k] = value[k];\n\treturn JSON.stringify(ordered);\n}\n\nasync function getEmDashCollectionUncached<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tfilter?: CollectionFilter,\n): Promise<CollectionResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveCollection } = await import(\"astro:content\");\n\n\t// Resolve locale: explicit filter > ALS context > defaultLocale (when i18n enabled)\n\t// Without this, queries return all locale rows, producing broken IDs\n\tconst ctx = getRequestContext();\n\tconst i18nConfig = getI18nConfig();\n\tconst resolvedLocale =\n\t\tfilter?.locale ?? ctx?.locale ?? (isI18nEnabled() ? i18nConfig!.defaultLocale : undefined);\n\n\tconst result = await getLiveCollection(COLLECTION_NAME, {\n\t\ttype,\n\t\tstatus: filter?.status,\n\t\tlimit: filter?.limit,\n\t\tcursor: filter?.cursor,\n\t\twhere: filter?.where,\n\t\torderBy: filter?.orderBy,\n\t\tlocale: resolvedLocale,\n\t});\n\n\tconst { entries, error, cacheHint } = result;\n\t// nextCursor is returned by the emdash loader but not part of Astro's base\n\t// LiveLoader return type. Extract it safely via property descriptor to avoid\n\t// an unsafe type assertion on the `any`-typed result object.\n\tconst rawCursor = Object.getOwnPropertyDescriptor(result, \"nextCursor\")?.value;\n\tconst nextCursor: string | undefined = typeof rawCursor === \"string\" ? rawCursor : undefined;\n\n\tif (error) {\n\t\treturn { entries: [], error, cacheHint: {} };\n\t}\n\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst entriesWithEdit = entries.map((entry: ContentEntry<D>) => {\n\t\tconst dbId = entryDatabaseId(entry);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(entry), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...entry,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(entry)) : createNoop(),\n\t\t};\n\t});\n\n\t// Eagerly hydrate bylines and taxonomy terms for all entries in parallel.\n\t// Both are independent queries, so running them concurrently halves the\n\t// round-trip cost on remote databases (D1 replicas, etc.).\n\tawait Promise.all([\n\t\thydrateEntryBylines(type, entriesWithEdit),\n\t\thydrateEntryTerms(type, entriesWithEdit),\n\t]);\n\n\treturn { entries: entriesWithEdit, nextCursor, cacheHint: cacheHint ?? {} };\n}\n\n/**\n * Get a single entry by type and ID/slug\n *\n * Returns { entry, error, isPreview } for graceful error handling.\n * - entry is null if not found (not an error)\n * - error is set only for actual errors (db issues, etc.)\n *\n * Preview mode is detected automatically from request context (ALS).\n * When the URL has a valid `_preview` token, the middleware sets preview\n * context and this function serves draft revision data if available.\n *\n * @example\n * ```ts\n * import { getEmDashEntry } from \"emdash\";\n *\n * // Simple usage — preview just works via middleware\n * const { entry: post, isPreview, error } = await getEmDashEntry(\"posts\", \"my-slug\");\n * if (!post) return Astro.redirect(\"/404\");\n * ```\n */\nexport async function getEmDashEntry<T extends string, D = InferCollectionData<T>>(\n\ttype: T,\n\tid: string,\n\toptions?: { locale?: string },\n): Promise<EntryResult<D>> {\n\t// Dynamic import to avoid build-time issues\n\tconst { getLiveEntry } = await import(\"astro:content\");\n\n\t// Check ALS for preview and edit mode context\n\tconst ctx = getRequestContext();\n\tconst preview = ctx?.preview;\n\tconst isEditMode = ctx?.editMode ?? false;\n\tconst isPreviewMode = !!preview && preview.collection === type;\n\t// Edit mode implies preview — editors should see draft content\n\tconst serveDrafts = isPreviewMode || isEditMode;\n\n\t// Resolve locale: explicit option > ALS context > undefined (no filter)\n\tconst requestedLocale = options?.locale ?? ctx?.locale;\n\n\t/** Wrap a raw Astro entry with edit proxy, tagging editable fields if needed */\n\tfunction wrapEntry(raw: ContentEntry<D>): ContentEntry<D> {\n\t\tconst dbId = entryDatabaseId(raw);\n\t\tif (isEditMode) {\n\t\t\ttagEditableFields(entryData(raw), type, dbId);\n\t\t}\n\t\treturn {\n\t\t\t...raw,\n\t\t\tedit: isEditMode ? createEditable(type, dbId, entryEditOptions(raw)) : createNoop(),\n\t\t};\n\t}\n\n\t/** Check if an entry is publicly visible (published or scheduled past its time) */\n\tfunction isVisible(entry: ContentEntry<D>): boolean {\n\t\tconst data = entryData(entry);\n\t\tconst status = dataStr(data, \"status\");\n\t\tconst scheduledAt = dataStr(data, \"scheduledAt\") || undefined;\n\t\tconst isPublished = status === \"published\";\n\t\tconst isScheduledAndReady =\n\t\t\tstatus === \"scheduled\" && scheduledAt && new Date(scheduledAt) <= new Date();\n\t\treturn isPublished || !!isScheduledAndReady;\n\t}\n\n\t// Build the fallback chain: [requestedLocale, fallback1, ..., defaultLocale]\n\t// When i18n is disabled or no locale requested, just use a single-element chain\n\tconst localeChain =\n\t\trequestedLocale && isI18nEnabled() ? getFallbackChain(requestedLocale) : [requestedLocale];\n\n\t/** Return a successful EntryResult with bylines and taxonomy terms hydrated */\n\tasync function successResult(\n\t\twrapped: ContentEntry<D>,\n\t\topts: { isPreview: boolean; fallbackLocale?: string; cacheHint: CacheHint },\n\t): Promise<EntryResult<D>> {\n\t\tawait Promise.all([hydrateEntryBylines(type, [wrapped]), hydrateEntryTerms(type, [wrapped])]);\n\t\treturn {\n\t\t\tentry: wrapped,\n\t\t\tisPreview: opts.isPreview,\n\t\t\tfallbackLocale: opts.fallbackLocale,\n\t\t\tcacheHint: opts.cacheHint,\n\t\t};\n\t}\n\n\tif (serveDrafts) {\n\t\t// Draft mode: try each locale in the fallback chain\n\t\tfor (let i = 0; i < localeChain.length; i++) {\n\t\t\tconst locale = localeChain[i];\n\t\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\t\tconst {\n\t\t\t\tentry: baseEntry,\n\t\t\t\terror: baseError,\n\t\t\t\tcacheHint,\n\t\t\t} = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\ttype,\n\t\t\t\tid,\n\t\t\t\tlocale,\n\t\t\t});\n\n\t\t\tif (baseError) {\n\t\t\t\treturn { entry: null, error: baseError, isPreview: serveDrafts, cacheHint: {} };\n\t\t\t}\n\n\t\t\tif (!baseEntry) continue; // Try next locale in chain\n\n\t\t\t// Preview tokens are item-scoped: verify the resolved entry matches.\n\t\t\t// Edit mode (authenticated editors) has collection-wide draft access.\n\t\t\tif (isPreviewMode && !isEditMode) {\n\t\t\t\tconst dbId = entryDatabaseId(baseEntry);\n\t\t\t\tif (preview.id !== dbId && preview.id !== id) {\n\t\t\t\t\t// Token doesn't match — serve only if publicly visible, without draft access\n\t\t\t\t\tif (isVisible(baseEntry)) {\n\t\t\t\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\t\t\t\tisPreview: false,\n\t\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\t// Not visible — try next locale in fallback chain\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Check if entry has a draft revision — if so, re-fetch with revision data\n\t\t\tconst baseData = entryData(baseEntry);\n\t\t\tconst draftRevisionId = dataStr(baseData, \"draftRevisionId\") || undefined;\n\n\t\t\tif (draftRevisionId) {\n\t\t\t\tconst { entry: draftEntry, error: draftError } = await getLiveEntry(COLLECTION_NAME, {\n\t\t\t\t\ttype,\n\t\t\t\t\tid,\n\t\t\t\t\trevisionId: draftRevisionId,\n\t\t\t\t\tlocale,\n\t\t\t\t});\n\n\t\t\t\tif (!draftError && draftEntry) {\n\t\t\t\t\treturn successResult(wrapEntry(draftEntry), {\n\t\t\t\t\t\tisPreview: serveDrafts,\n\t\t\t\t\t\tfallbackLocale,\n\t\t\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn successResult(wrapEntry(baseEntry), {\n\t\t\t\tisPreview: serveDrafts,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\n\t\t// No entry found in any locale\n\t\treturn { entry: null, isPreview: serveDrafts, cacheHint: {} };\n\t}\n\n\t// Normal mode: try each locale in the fallback chain, only return published content\n\tfor (let i = 0; i < localeChain.length; i++) {\n\t\tconst locale = localeChain[i];\n\t\tconst fallbackLocale = i > 0 ? locale : undefined;\n\n\t\tconst { entry, error, cacheHint } = await getLiveEntry(COLLECTION_NAME, { type, id, locale });\n\t\tif (error) {\n\t\t\treturn { entry: null, error, isPreview: false, cacheHint: {} };\n\t\t}\n\n\t\tif (entry && isVisible(entry)) {\n\t\t\treturn successResult(wrapEntry(entry), {\n\t\t\t\tisPreview: false,\n\t\t\t\tfallbackLocale,\n\t\t\t\tcacheHint: cacheHint ?? {},\n\t\t\t});\n\t\t}\n\t\t// Entry not found or not visible in this locale — try next\n\t}\n\n\treturn { entry: null, isPreview: false, cacheHint: {} };\n}\n\n/**\n * Eagerly hydrate byline data onto entry.data for one or more entries.\n *\n * Attaches `bylines` (array of ContentBylineCredit) and `byline`\n * (primary BylineSummary or null) to each entry's data object.\n * Uses batch queries to avoid N+1.\n *\n * Fails silently if the byline tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryBylines<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getBylinesForEntries } = await import(\"./bylines/index.js\");\n\n\t\tconst refs = entries\n\t\t\t.map((e) => {\n\t\t\t\tconst data = entryData(e);\n\t\t\t\tconst id = dataStr(data, \"id\");\n\t\t\t\treturn id ? { id, authorId: dataStr(data, \"authorId\") || null } : null;\n\t\t\t})\n\t\t\t.filter((r): r is { id: string; authorId: string | null } => r !== null);\n\t\tif (refs.length === 0) return;\n\n\t\tconst bylinesMap = await getBylinesForEntries(type, refs);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tconst credits = bylinesMap.get(dbId) ?? [];\n\t\t\tdata.bylines = credits;\n\t\t\tdata.byline = credits[0]?.byline ?? null;\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate bylines:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Eagerly hydrate taxonomy term data onto entry.data for one or more entries.\n *\n * Attaches `terms` (Record keyed by taxonomy name with an array of TaxonomyTerm\n * values) to each entry's data object. Uses a single batched JOIN query across\n * all taxonomies so the cost is O(1) regardless of the number of entries or\n * taxonomies on the site.\n *\n * This eliminates the common N+1 pattern where templates loop over list\n * results and call getEntryTerms() per entry. With hydration, the list page\n * stays at a single round-trip for term data.\n *\n * Fails silently if the taxonomy tables don't exist yet (pre-migration).\n */\nasync function hydrateEntryTerms<D>(type: string, entries: ContentEntry<D>[]): Promise<void> {\n\tif (entries.length === 0) return;\n\n\ttry {\n\t\tconst { getAllTermsForEntries } = await import(\"./taxonomies/index.js\");\n\n\t\tconst ids = entries.map((e) => dataStr(entryData(e), \"id\")).filter(Boolean);\n\t\tif (ids.length === 0) return;\n\n\t\tconst termsMap = await getAllTermsForEntries(type, ids);\n\n\t\tfor (const entry of entries) {\n\t\t\tconst data = entryData(entry);\n\t\t\tconst dbId = dataStr(data, \"id\");\n\t\t\tif (!dbId) continue;\n\n\t\t\tdata.terms = termsMap.get(dbId) ?? {};\n\t\t}\n\t} catch (err) {\n\t\t// Only swallow \"table not found\" errors from pre-migration databases.\n\t\t// Matches SQLite/D1 (\"no such table\") and PostgreSQL (\"relation/table\n\t\t// ... does not exist\") via the shared helper.\n\t\tif (!isMissingTableError(err)) {\n\t\t\tconst msg = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(\"[emdash] Failed to hydrate terms:\", msg);\n\t\t}\n\t}\n}\n\n/**\n * Translation summary for a single locale variant\n */\nexport interface TranslationSummary {\n\t/** Content item ID */\n\tid: string;\n\t/** Locale code (e.g. \"en\", \"fr\") */\n\tlocale: string;\n\t/** URL slug */\n\tslug: string | null;\n\t/** Current status */\n\tstatus: string;\n}\n\n/**\n * Result from getTranslations\n */\nexport interface TranslationsResult {\n\t/** The translation group ID (shared across locales) */\n\ttranslationGroup: string;\n\t/** All locale variants in this group */\n\ttranslations: TranslationSummary[];\n\t/** Error if the query failed */\n\terror?: Error;\n}\n\n/**\n * Get all translations of a content item.\n *\n * Given a content entry, returns all locale variants that share the same\n * translation group. This is useful for building language switcher UI.\n *\n * @example\n * ```ts\n * import { getEmDashEntry, getTranslations } from \"emdash\";\n *\n * const { entry: post } = await getEmDashEntry(\"posts\", \"hello-world\", { locale: \"en\" });\n * const { translations } = await getTranslations(\"posts\", post.data.id);\n * // translations = [{ id: \"...\", locale: \"en\", slug: \"hello-world\", status: \"published\" }, ...]\n * ```\n */\nexport async function getTranslations(type: string, id: string): Promise<TranslationsResult> {\n\ttry {\n\t\tconst db = (await import(\"./loader.js\")).getDb;\n\t\tconst dbInstance = await db();\n\t\tconst { ContentRepository } = await import(\"./database/repositories/content.js\");\n\t\tconst repo = new ContentRepository(dbInstance);\n\n\t\t// Find the item to get its translation group\n\t\tconst item = await repo.findByIdOrSlug(type, id);\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\ttranslationGroup: \"\",\n\t\t\t\ttranslations: [],\n\t\t\t\terror: new Error(`Content item not found: ${id}`),\n\t\t\t};\n\t\t}\n\n\t\tconst group = item.translationGroup || item.id;\n\t\tconst translations = await repo.findTranslations(type, group);\n\n\t\treturn {\n\t\t\ttranslationGroup: group,\n\t\t\ttranslations: translations.map((t) => ({\n\t\t\t\tid: t.id,\n\t\t\t\tlocale: t.locale || \"en\",\n\t\t\t\tslug: t.slug,\n\t\t\t\tstatus: t.status,\n\t\t\t})),\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\ttranslationGroup: \"\",\n\t\t\ttranslations: [],\n\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t};\n\t}\n}\n\n/**\n * Result from resolveEmDashPath\n */\nexport interface ResolvePathResult<T = Record<string, unknown>> {\n\t/** The matched entry */\n\tentry: ContentEntry<T>;\n\t/** The collection slug that matched */\n\tcollection: string;\n\t/** Extracted parameters from the URL pattern (e.g. { slug: \"my-post\" }) */\n\tparams: Record<string, string>;\n}\n\n/** Matches `{paramName}` placeholders in URL patterns */\nconst URL_PARAM_PATTERN = /\\{(\\w+)\\}/g;\n\n/** Convert a URL pattern like \"/blog/{slug}\" to a regex and param name list */\nfunction patternToRegex(pattern: string): { regex: RegExp; paramNames: string[] } {\n\tconst paramNames: string[] = [];\n\tconst regexStr = pattern.replace(URL_PARAM_PATTERN, (_match, name: string) => {\n\t\tparamNames.push(name);\n\t\treturn \"([^/]+)\";\n\t});\n\treturn { regex: new RegExp(`^${regexStr}$`), paramNames };\n}\n\n/** Cached compiled URL patterns for resolveEmDashPath */\ninterface CachedPattern {\n\tslug: string;\n\tregex: RegExp;\n\tparamNames: string[];\n}\nlet cachedUrlPatterns: CachedPattern[] | null = null;\n\n/**\n * Invalidate the cached URL patterns used by resolveEmDashPath.\n * Call when collection URL patterns change (schema updates).\n */\nexport function invalidateUrlPatternCache(): void {\n\tcachedUrlPatterns = null;\n}\n\n/**\n * Resolve a URL path to a content entry by matching against collection URL patterns.\n *\n * Loads all collections with a `urlPattern` set, converts each pattern to a regex,\n * and tests the given path. On match, extracts the slug and fetches the entry.\n *\n * @example\n * ```ts\n * import { resolveEmDashPath } from \"emdash\";\n *\n * // Given pages with urlPattern \"/{slug}\" and posts with \"/blog/{slug}\":\n * const result = await resolveEmDashPath(\"/blog/hello-world\");\n * if (result) {\n * console.log(result.collection); // \"posts\"\n * console.log(result.params.slug); // \"hello-world\"\n * console.log(result.entry.data); // post data\n * }\n * ```\n */\nexport async function resolveEmDashPath<T = Record<string, unknown>>(\n\tpath: string,\n): Promise<ResolvePathResult<T> | null> {\n\t// Build and cache compiled patterns on first call\n\tif (!cachedUrlPatterns) {\n\t\tconst { getDb } = await import(\"./loader.js\");\n\t\tconst { SchemaRegistry } = await import(\"./schema/registry.js\");\n\t\tconst db = await getDb();\n\t\tconst registry = new SchemaRegistry(db);\n\t\tconst collections = await registry.listCollections();\n\n\t\tcachedUrlPatterns = [];\n\t\tfor (const collection of collections) {\n\t\t\tif (!collection.urlPattern) continue;\n\t\t\tconst { regex, paramNames } = patternToRegex(collection.urlPattern);\n\t\t\tcachedUrlPatterns.push({ slug: collection.slug, regex, paramNames });\n\t\t}\n\t}\n\n\tfor (const pattern of cachedUrlPatterns) {\n\t\tconst match = path.match(pattern.regex);\n\t\tif (!match) continue;\n\n\t\t// Extract params\n\t\tconst params: Record<string, string> = {};\n\t\tfor (let i = 0; i < pattern.paramNames.length; i++) {\n\t\t\tparams[pattern.paramNames[i]] = match[i + 1];\n\t\t}\n\n\t\t// Look up entry by slug (most common pattern)\n\t\tconst slug = params.slug;\n\t\tif (!slug) continue;\n\n\t\tconst { entry } = await getEmDashEntry<string, T>(pattern.slug, slug);\n\t\tif (entry) {\n\t\t\treturn { entry, collection: pattern.slug, params };\n\t\t}\n\t}\n\n\treturn null;\n}\n"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { i as __exportAll } from "./runner-
|
|
1
|
+
import { i as __exportAll } from "./runner-DIcU2UCC.mjs";
|
|
2
2
|
import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
|
|
3
3
|
import { c as listTablesLike, l as tableExists, o as isSqlite, r as currentTimestamp } from "./dialect-helpers-BKCvISIQ.mjs";
|
|
4
4
|
import { t as withTransaction } from "./transaction-D44LBXvU.mjs";
|
|
5
|
-
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-
|
|
6
|
-
import { i as RESERVED_FIELD_SLUGS, n as FIELD_TYPE_TO_COLUMN, r as RESERVED_COLLECTION_SLUGS } from "./types-
|
|
5
|
+
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-BK1oZS-l.mjs";
|
|
6
|
+
import { i as RESERVED_FIELD_SLUGS, n as FIELD_TYPE_TO_COLUMN, r as RESERVED_COLLECTION_SLUGS } from "./types-56BKbld_.mjs";
|
|
7
7
|
import { sql } from "kysely";
|
|
8
8
|
import { ulid } from "ulidx";
|
|
9
9
|
|
|
@@ -577,6 +577,7 @@ var SchemaRegistry = class {
|
|
|
577
577
|
async updateField(collectionSlug, fieldSlug, input) {
|
|
578
578
|
const field = await this.getField(collectionSlug, fieldSlug);
|
|
579
579
|
if (!field) throw new SchemaError(`Field "${fieldSlug}" not found in collection "${collectionSlug}"`, "FIELD_NOT_FOUND");
|
|
580
|
+
const nextValidation = input.validation === void 0 ? field.validation : input.validation;
|
|
580
581
|
return withTransaction(this.db, async (trx) => {
|
|
581
582
|
await trx.updateTable("_emdash_fields").set({
|
|
582
583
|
label: input.label ?? field.label,
|
|
@@ -585,7 +586,7 @@ var SchemaRegistry = class {
|
|
|
585
586
|
searchable: input.searchable !== void 0 ? input.searchable ? 1 : 0 : field.searchable ? 1 : 0,
|
|
586
587
|
translatable: input.translatable !== void 0 ? input.translatable ? 1 : 0 : field.translatable ? 1 : 0,
|
|
587
588
|
default_value: input.defaultValue !== void 0 ? JSON.stringify(input.defaultValue) : field.defaultValue !== void 0 ? JSON.stringify(field.defaultValue) : null,
|
|
588
|
-
validation:
|
|
589
|
+
validation: nextValidation ? JSON.stringify(nextValidation) : null,
|
|
589
590
|
widget: input.widget ?? field.widget ?? null,
|
|
590
591
|
options: input.options ? JSON.stringify(input.options) : field.options ? JSON.stringify(field.options) : null,
|
|
591
592
|
sort_order: input.sortOrder ?? field.sortOrder
|
|
@@ -933,4 +934,4 @@ var SchemaRegistry = class {
|
|
|
933
934
|
|
|
934
935
|
//#endregion
|
|
935
936
|
export { FTSManager as i, SchemaRegistry as n, registry_exports as r, SchemaError as t };
|
|
936
|
-
//# sourceMappingURL=registry-
|
|
937
|
+
//# sourceMappingURL=registry-Do34mz_P.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-Do34mz_P.mjs","names":["dialectTableExists"],"sources":["../src/search/fts-manager.ts","../src/schema/registry.ts"],"sourcesContent":["/**\n * FTS5 Manager\n *\n * Manages FTS5 virtual tables and triggers for search indexing.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { isSqlite, tableExists as dialectTableExists } from \"../database/dialect-helpers.js\";\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport type { SearchConfig } from \"./types.js\";\n\n/**\n * FTS5 Manager\n *\n * Handles creation, deletion, and management of FTS5 virtual tables\n * for full-text search on content collections.\n */\nexport class FTSManager {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Validate a collection slug and its searchable field names.\n\t * Must be called before any raw SQL interpolation.\n\t */\n\tprivate validateInputs(collectionSlug: string, searchableFields?: string[]): void {\n\t\tvalidateIdentifier(collectionSlug, \"collection slug\");\n\t\tif (searchableFields) {\n\t\t\tfor (const field of searchableFields) {\n\t\t\t\tvalidateIdentifier(field, \"searchable field name\");\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the FTS table name for a collection\n\t * Uses _emdash_ prefix to clearly mark as internal/system table\n\t */\n\tgetFtsTableName(collectionSlug: string): string {\n\t\tvalidateIdentifier(collectionSlug, \"collection slug\");\n\t\treturn `_emdash_fts_${collectionSlug}`;\n\t}\n\n\t/**\n\t * Get the content table name for a collection\n\t */\n\tgetContentTableName(collectionSlug: string): string {\n\t\tvalidateIdentifier(collectionSlug, \"collection slug\");\n\t\treturn `ec_${collectionSlug}`;\n\t}\n\n\t/**\n\t * Check if an FTS table exists for a collection\n\t */\n\tasync ftsTableExists(collectionSlug: string): Promise<boolean> {\n\t\tconst ftsTable = this.getFtsTableName(collectionSlug);\n\t\treturn dialectTableExists(this.db, ftsTable);\n\t}\n\n\t/**\n\t * Create an FTS5 virtual table for a collection.\n\t * FTS5 is SQLite-only; on other dialects this is a no-op.\n\t *\n\t * @param collectionSlug - The collection slug\n\t * @param searchableFields - Array of field names to index\n\t * @param weights - Optional field weights for ranking\n\t */\n\tasync createFtsTable(\n\t\tcollectionSlug: string,\n\t\tsearchableFields: string[],\n\t\t_weights?: Record<string, number>,\n\t): Promise<void> {\n\t\tif (!isSqlite(this.db)) return;\n\t\tthis.validateInputs(collectionSlug, searchableFields);\n\t\tconst ftsTable = this.getFtsTableName(collectionSlug);\n\t\tconst contentTable = this.getContentTableName(collectionSlug);\n\n\t\t// Build the column list for FTS5\n\t\t// id and locale are UNINDEXED (used for joining/filtering, not searched)\n\t\tconst columns = [\"id UNINDEXED\", \"locale UNINDEXED\", ...searchableFields].join(\", \");\n\n\t\t// Create the FTS5 virtual table\n\t\t// Using content= to make it a contentless FTS table (we manage sync ourselves)\n\t\t// tokenize='porter unicode61' enables stemming (run matches running, ran, etc.)\n\t\tawait sql\n\t\t\t.raw(`\n\t\t\tCREATE VIRTUAL TABLE IF NOT EXISTS \"${ftsTable}\" USING fts5(\n\t\t\t\t${columns},\n\t\t\t\tcontent='${contentTable}',\n\t\t\t\tcontent_rowid='rowid',\n\t\t\t\ttokenize='porter unicode61'\n\t\t\t)\n\t\t`)\n\t\t\t.execute(this.db);\n\n\t\t// Create triggers for automatic sync\n\t\tawait this.createTriggers(collectionSlug, searchableFields);\n\t}\n\n\t/**\n\t * Create triggers to keep FTS table in sync with content table.\n\t *\n\t * The insert and update triggers only add rows to the FTS index when\n\t * `deleted_at IS NULL`. This keeps soft-deleted content out of the\n\t * search index and ensures the FTS row count matches the non-deleted\n\t * content count (which `verifyAndRepairIndex` relies on).\n\t */\n\tprivate async createTriggers(collectionSlug: string, searchableFields: string[]): Promise<void> {\n\t\tthis.validateInputs(collectionSlug, searchableFields);\n\t\tconst ftsTable = this.getFtsTableName(collectionSlug);\n\t\tconst contentTable = this.getContentTableName(collectionSlug);\n\t\tconst fieldList = searchableFields.join(\", \");\n\t\tconst newFieldList = searchableFields.map((f) => `NEW.${f}`).join(\", \");\n\t\t// Insert trigger - only index non-deleted content\n\t\tawait sql\n\t\t\t.raw(`\n\t\t\tCREATE TRIGGER IF NOT EXISTS \"${ftsTable}_insert\" \n\t\t\tAFTER INSERT ON \"${contentTable}\" \n\t\t\tWHEN NEW.deleted_at IS NULL\n\t\t\tBEGIN\n\t\t\t\tINSERT INTO \"${ftsTable}\"(rowid, id, locale, ${fieldList})\n\t\t\t\tVALUES (NEW.rowid, NEW.id, NEW.locale, ${newFieldList});\n\t\t\tEND\n\t\t`)\n\t\t\t.execute(this.db);\n\n\t\t// Update trigger - always remove the old FTS row, only re-insert\n\t\t// if the row is not soft-deleted. This handles both content edits\n\t\t// and soft-delete operations (UPDATE SET deleted_at = ...).\n\t\tawait sql\n\t\t\t.raw(`\n\t\t\tCREATE TRIGGER IF NOT EXISTS \"${ftsTable}_update\" \n\t\t\tAFTER UPDATE ON \"${contentTable}\" \n\t\t\tBEGIN\n\t\t\t\tDELETE FROM \"${ftsTable}\" WHERE rowid = OLD.rowid;\n\t\t\t\tINSERT INTO \"${ftsTable}\"(rowid, id, locale, ${fieldList})\n\t\t\t\tSELECT NEW.rowid, NEW.id, NEW.locale, ${newFieldList}\n\t\t\t\tWHERE NEW.deleted_at IS NULL;\n\t\t\tEND\n\t\t`)\n\t\t\t.execute(this.db);\n\n\t\t// Delete trigger\n\t\tawait sql\n\t\t\t.raw(`\n\t\t\tCREATE TRIGGER IF NOT EXISTS \"${ftsTable}_delete\" \n\t\t\tAFTER DELETE ON \"${contentTable}\" \n\t\t\tBEGIN\n\t\t\t\tDELETE FROM \"${ftsTable}\" WHERE rowid = OLD.rowid;\n\t\t\tEND\n\t\t`)\n\t\t\t.execute(this.db);\n\t}\n\n\t/**\n\t * Drop triggers for a collection\n\t */\n\tprivate async dropTriggers(collectionSlug: string): Promise<void> {\n\t\tthis.validateInputs(collectionSlug);\n\t\tconst ftsTable = this.getFtsTableName(collectionSlug);\n\n\t\tawait sql.raw(`DROP TRIGGER IF EXISTS \"${ftsTable}_insert\"`).execute(this.db);\n\t\tawait sql.raw(`DROP TRIGGER IF EXISTS \"${ftsTable}_update\"`).execute(this.db);\n\t\tawait sql.raw(`DROP TRIGGER IF EXISTS \"${ftsTable}_delete\"`).execute(this.db);\n\t}\n\n\t/**\n\t * Drop the FTS table and triggers for a collection\n\t */\n\tasync dropFtsTable(collectionSlug: string): Promise<void> {\n\t\tif (!isSqlite(this.db)) return;\n\t\tthis.validateInputs(collectionSlug);\n\t\tconst ftsTable = this.getFtsTableName(collectionSlug);\n\n\t\t// Drop triggers first\n\t\tawait this.dropTriggers(collectionSlug);\n\n\t\t// Drop the FTS table\n\t\tawait sql.raw(`DROP TABLE IF EXISTS \"${ftsTable}\"`).execute(this.db);\n\t}\n\n\t/**\n\t * Rebuild the FTS index for a collection\n\t *\n\t * This is useful after bulk imports or if the index gets out of sync.\n\t */\n\tasync rebuildIndex(\n\t\tcollectionSlug: string,\n\t\tsearchableFields: string[],\n\t\tweights?: Record<string, number>,\n\t): Promise<void> {\n\t\tif (!isSqlite(this.db)) return;\n\t\t// Drop existing table and triggers\n\t\tawait this.dropFtsTable(collectionSlug);\n\n\t\t// Recreate table and triggers\n\t\tawait this.createFtsTable(collectionSlug, searchableFields, weights);\n\n\t\t// Populate from existing content\n\t\tawait this.populateFromContent(collectionSlug, searchableFields);\n\t}\n\n\t/**\n\t * Populate the FTS table from existing content\n\t */\n\tasync populateFromContent(collectionSlug: string, searchableFields: string[]): Promise<void> {\n\t\tif (!isSqlite(this.db)) return;\n\t\tthis.validateInputs(collectionSlug, searchableFields);\n\t\tconst ftsTable = this.getFtsTableName(collectionSlug);\n\t\tconst contentTable = this.getContentTableName(collectionSlug);\n\t\tconst fieldList = searchableFields.join(\", \");\n\n\t\t// Insert all existing content into FTS table\n\t\tawait sql\n\t\t\t.raw(`\n\t\t\tINSERT INTO \"${ftsTable}\"(rowid, id, locale, ${fieldList})\n\t\t\tSELECT rowid, id, locale, ${fieldList} FROM \"${contentTable}\"\n\t\t\tWHERE deleted_at IS NULL\n\t\t`)\n\t\t\t.execute(this.db);\n\t}\n\n\t/**\n\t * Get the search configuration for a collection\n\t */\n\tasync getSearchConfig(collectionSlug: string): Promise<SearchConfig | null> {\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select(\"search_config\")\n\t\t\t.where(\"slug\", \"=\", collectionSlug)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!result?.search_config) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(result.search_config);\n\t\t\tif (\n\t\t\t\ttypeof parsed !== \"object\" ||\n\t\t\t\tparsed === null ||\n\t\t\t\t!(\"enabled\" in parsed) ||\n\t\t\t\ttypeof parsed.enabled !== \"boolean\"\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst config: SearchConfig = { enabled: parsed.enabled };\n\t\t\tif (\"weights\" in parsed && typeof parsed.weights === \"object\" && parsed.weights !== null) {\n\t\t\t\t// weights is a JSON-parsed object — safe to treat as Record<string, number>\n\t\t\t\tconst weights: Record<string, number> = {};\n\t\t\t\tfor (const [k, v] of Object.entries(parsed.weights)) {\n\t\t\t\t\tif (typeof v === \"number\") {\n\t\t\t\t\t\tweights[k] = v;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconfig.weights = weights;\n\t\t\t}\n\t\t\treturn config;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\t/**\n\t * Update the search configuration for a collection\n\t */\n\tasync setSearchConfig(collectionSlug: string, config: SearchConfig): Promise<void> {\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_collections\")\n\t\t\t.set({ search_config: JSON.stringify(config) })\n\t\t\t.where(\"slug\", \"=\", collectionSlug)\n\t\t\t.execute();\n\t}\n\n\t/**\n\t * Get searchable fields for a collection\n\t */\n\tasync getSearchableFields(collectionSlug: string): Promise<string[]> {\n\t\tconst collection = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"slug\", \"=\", collectionSlug)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!collection) {\n\t\t\treturn [];\n\t\t}\n\n\t\tconst fields = await this.db\n\t\t\t.selectFrom(\"_emdash_fields\")\n\t\t\t.select(\"slug\")\n\t\t\t.where(\"collection_id\", \"=\", collection.id)\n\t\t\t.where(\"searchable\", \"=\", 1)\n\t\t\t.execute();\n\n\t\treturn fields.map((f) => f.slug);\n\t}\n\n\t/**\n\t * Enable search for a collection.\n\t *\n\t * Uses rebuildIndex to ensure a clean state -- drop any existing FTS\n\t * table/triggers, recreate them, and populate from content. This avoids\n\t * duplicate rows when triggers have already populated the index (e.g.\n\t * during seeding where content is inserted before search is enabled).\n\t */\n\tasync enableSearch(\n\t\tcollectionSlug: string,\n\t\toptions?: { weights?: Record<string, number> },\n\t): Promise<void> {\n\t\tif (!isSqlite(this.db)) {\n\t\t\tthrow new Error(\"Full-text search is only available with SQLite databases\");\n\t\t}\n\t\t// Get searchable fields\n\t\tconst searchableFields = await this.getSearchableFields(collectionSlug);\n\n\t\tif (searchableFields.length === 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`No searchable fields defined for collection \"${collectionSlug}\". ` +\n\t\t\t\t\t`Mark at least one field as searchable before enabling search.`,\n\t\t\t);\n\t\t}\n\n\t\t// Rebuild from scratch to ensure clean state (no duplicate rows)\n\t\tawait this.rebuildIndex(collectionSlug, searchableFields, options?.weights);\n\n\t\t// Update search config\n\t\tawait this.setSearchConfig(collectionSlug, {\n\t\t\tenabled: true,\n\t\t\tweights: options?.weights,\n\t\t});\n\t}\n\n\t/**\n\t * Disable search for a collection\n\t *\n\t * Drops the FTS table and triggers.\n\t */\n\tasync disableSearch(collectionSlug: string): Promise<void> {\n\t\tif (!isSqlite(this.db)) return;\n\t\tawait this.dropFtsTable(collectionSlug);\n\t\tconst existing = await this.getSearchConfig(collectionSlug);\n\t\tawait this.setSearchConfig(collectionSlug, { enabled: false, weights: existing?.weights });\n\t}\n\n\t/**\n\t * Get index statistics for a collection\n\t */\n\tasync getIndexStats(\n\t\tcollectionSlug: string,\n\t): Promise<{ indexed: number; lastRebuilt?: string } | null> {\n\t\tif (!isSqlite(this.db)) return null;\n\t\tthis.validateInputs(collectionSlug);\n\t\tconst ftsTable = this.getFtsTableName(collectionSlug);\n\t\tconst ftsDocsizeTable = `${ftsTable}_docsize`;\n\n\t\t// Check if table exists\n\t\tif (!(await this.ftsTableExists(collectionSlug))) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Count indexed rows\n\t\tconst result = await sql<{ count: number }>`\n\t\t\tSELECT COUNT(*) as count FROM \"${sql.raw(ftsDocsizeTable)}\"\n\t\t`.execute(this.db);\n\n\t\treturn {\n\t\t\tindexed: result.rows[0]?.count ?? 0,\n\t\t};\n\t}\n\n\t/**\n\t * Verify FTS index integrity and rebuild if corrupted.\n\t *\n\t * Checks for row count mismatch between content table and FTS table.\n\t *\n\t * Returns true if the index was rebuilt, false if it was healthy.\n\t */\n\tasync verifyAndRepairIndex(collectionSlug: string): Promise<boolean> {\n\t\tif (!isSqlite(this.db)) return false;\n\t\tthis.validateInputs(collectionSlug);\n\t\tconst ftsTable = this.getFtsTableName(collectionSlug);\n\t\tconst ftsDocsizeTable = `${ftsTable}_docsize`;\n\t\tconst contentTable = this.getContentTableName(collectionSlug);\n\t\tconst fields = await this.getSearchableFields(collectionSlug);\n\t\tconst config = await this.getSearchConfig(collectionSlug);\n\n\t\tif (!(await this.ftsTableExists(collectionSlug))) {\n\t\t\tif (!config?.enabled || fields.length === 0) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconsole.warn(`FTS index for \"${collectionSlug}\" is missing. Rebuilding.`);\n\t\t\tawait this.rebuildIndex(collectionSlug, fields, config.weights);\n\t\t\treturn true;\n\t\t}\n\n\t\t// Check 1: Row count mismatch\n\t\tconst contentCount = await sql<{ count: number }>`\n\t\t\tSELECT COUNT(*) as count FROM ${sql.ref(contentTable)}\n\t\t\tWHERE deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\t// For external-content FTS tables, COUNT(*) on the virtual table is\n\t\t// answered from the backing content table, including soft-deleted rows.\n\t\t// The docsize shadow table tracks the rows actually present in the\n\t\t// full-text index, which is what we need for repair decisions.\n\t\tconst ftsCount = await sql<{ count: number }>`\n\t\t\tSELECT COUNT(*) as count FROM \"${sql.raw(ftsDocsizeTable)}\"\n\t\t`.execute(this.db);\n\n\t\tconst contentRows = contentCount.rows[0]?.count ?? 0;\n\t\tconst ftsRows = ftsCount.rows[0]?.count ?? 0;\n\n\t\tif (contentRows !== ftsRows) {\n\t\t\tconsole.warn(\n\t\t\t\t`FTS index for \"${collectionSlug}\" has ${ftsRows} rows but content table has ${contentRows}. Rebuilding.`,\n\t\t\t);\n\t\t\tif (fields.length > 0) {\n\t\t\t\tawait this.rebuildIndex(collectionSlug, fields, config?.weights);\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/**\n\t * Verify and repair FTS indexes for all search-enabled collections.\n\t *\n\t * Intended to run at startup to auto-heal any corruption from\n\t * previous process crashes.\n\t */\n\tasync verifyAndRepairAll(): Promise<number> {\n\t\tif (!isSqlite(this.db)) return 0;\n\n\t\tconst collections = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select(\"slug\")\n\t\t\t.where(\"search_config\", \"is not\", null)\n\t\t\t.execute();\n\n\t\tlet repaired = 0;\n\t\tfor (const { slug } of collections) {\n\t\t\tconst config = await this.getSearchConfig(slug);\n\t\t\tif (!config?.enabled) continue;\n\n\t\t\ttry {\n\t\t\t\tconst wasRepaired = await this.verifyAndRepairIndex(slug);\n\t\t\t\tif (wasRepaired) repaired++;\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(`Failed to verify/repair FTS index for \"${slug}\":`, error);\n\t\t\t}\n\t\t}\n\n\t\treturn repaired;\n\t}\n}\n","import type { Kysely } from \"kysely\";\nimport type { Selectable } from \"kysely\";\nimport { sql } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { currentTimestamp, listTablesLike, tableExists } from \"../database/dialect-helpers.js\";\nimport { withTransaction } from \"../database/transaction.js\";\nimport type { CollectionTable, Database, FieldTable } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { FTSManager } from \"../search/fts-manager.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../utils/chunks.js\";\nimport {\n\ttype Collection,\n\ttype CollectionSource,\n\ttype CollectionSupport,\n\ttype ColumnType,\n\ttype Field,\n\ttype CreateCollectionInput,\n\ttype UpdateCollectionInput,\n\ttype CreateFieldInput,\n\ttype UpdateFieldInput,\n\ttype CollectionWithFields,\n\ttype FieldType,\n\tFIELD_TYPE_TO_COLUMN,\n\tRESERVED_FIELD_SLUGS,\n\tRESERVED_COLLECTION_SLUGS,\n} from \"./types.js\";\n\n// Regex patterns for schema registry\nconst SLUG_VALIDATION_PATTERN = /^[a-z][a-z0-9_]*$/;\nconst EC_PREFIX_PATTERN = /^ec_/;\nconst SINGLE_QUOTE_PATTERN = /'/g;\nconst UNDERSCORE_PATTERN = /_/g;\nconst WORD_BOUNDARY_PATTERN = /\\b\\w/g;\n\n/** Valid column types for runtime validation */\nconst COLUMN_TYPES: ReadonlySet<string> = new Set([\"TEXT\", \"REAL\", \"INTEGER\", \"JSON\"]);\n\n/** Valid collection source prefixes/values */\nconst VALID_SOURCES: ReadonlySet<string> = new Set([\"manual\", \"discovered\", \"seed\"]);\n\nfunction isCollectionSource(value: string): value is CollectionSource {\n\treturn VALID_SOURCES.has(value) || value.startsWith(\"template:\") || value.startsWith(\"import:\");\n}\n\nfunction isFieldType(value: string): value is FieldType {\n\treturn value in FIELD_TYPE_TO_COLUMN;\n}\n\nfunction isColumnType(value: string): value is ColumnType {\n\treturn COLUMN_TYPES.has(value);\n}\n\nconst VALID_COLLECTION_SUPPORTS: ReadonlySet<string> = new Set<CollectionSupport>([\n\t\"drafts\",\n\t\"revisions\",\n\t\"preview\",\n\t\"scheduling\",\n\t\"search\",\n\t\"seo\",\n]);\n\nfunction isCollectionSupport(value: unknown): value is CollectionSupport {\n\treturn typeof value === \"string\" && VALID_COLLECTION_SUPPORTS.has(value);\n}\n\n/**\n * Parse a collection's `supports` column (stored as a JSON array of\n * CollectionSupport keys). Unknown/invalid entries are filtered out so the\n * runtime value matches the declared `CollectionSupport[]` type.\n *\n * Throws on malformed JSON so corruption surfaces loudly; returns an empty\n * array only for explicitly null/empty values or non-array JSON.\n */\nfunction parseSupports(raw: string | null | undefined): CollectionSupport[] {\n\tif (!raw) return [];\n\tconst parsed: unknown = JSON.parse(raw);\n\tif (!Array.isArray(parsed)) return [];\n\treturn parsed.filter(isCollectionSupport);\n}\n\n/**\n * Error thrown when a schema operation fails\n */\nexport class SchemaError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic code: string,\n\t\tpublic details?: Record<string, unknown>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"SchemaError\";\n\t}\n}\n\n/**\n * Schema Registry\n *\n * Manages collection and field definitions stored in D1.\n * Handles runtime DDL operations (CREATE TABLE, ALTER TABLE).\n */\nexport class SchemaRegistry {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t// ============================================\n\t// Collection Operations\n\t// ============================================\n\n\t/**\n\t * List all collections\n\t */\n\tasync listCollections(): Promise<Collection[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"slug\", \"asc\")\n\t\t\t.execute();\n\n\t\treturn rows.map(this.mapCollectionRow);\n\t}\n\n\t/**\n\t * Get a collection by slug\n\t */\n\tasync getCollection(slug: string): Promise<Collection | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t.selectAll()\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.mapCollectionRow(row) : null;\n\t}\n\n\t/**\n\t * Get a collection with all its fields\n\t */\n\tasync getCollectionWithFields(slug: string): Promise<CollectionWithFields | null> {\n\t\tconst collection = await this.getCollection(slug);\n\t\tif (!collection) return null;\n\n\t\tconst fields = await this.listFields(collection.id);\n\n\t\treturn { ...collection, fields };\n\t}\n\n\t/**\n\t * List every collection together with its fields in O(1) query shapes\n\t * — one for collections, then one batched query for the fields of every\n\t * returned collection — instead of the N+1 pattern of `listCollections`\n\t * + per-collection `listFields`. The fields query is chunked at\n\t * `SQL_BATCH_SIZE` to stay under D1's bound-parameter limit, so on\n\t * sites with more than `SQL_BATCH_SIZE` collections the field fetch\n\t * becomes `ceil(collectionCount / SQL_BATCH_SIZE)` queries — still\n\t * a constant factor, not N+1. Typical sites have well under\n\t * `SQL_BATCH_SIZE` collections, so this is two queries in practice.\n\t *\n\t * Used by the manifest build, which previously paid N+1 round-trips on\n\t * every admin request. Each round-trip costs ~80–150ms against the D1\n\t * primary on a busy link, so a 10-collection site spent ~1 s rebuilding\n\t * a manifest that is now built fresh per admin request (no cache).\n\t */\n\tasync listCollectionsWithFields(): Promise<CollectionWithFields[]> {\n\t\tconst collectionRows = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"slug\", \"asc\")\n\t\t\t.execute();\n\n\t\tif (collectionRows.length === 0) return [];\n\n\t\tconst fieldsByCollection = new Map<string, Field[]>();\n\t\t// Chunk to stay under D1's bound-parameter limit. Typical sites have\n\t\t// well under SQL_BATCH_SIZE collections, so this is a single query\n\t\t// in practice; on larger sites it becomes a small constant number\n\t\t// of queries, never N+1.\n\t\tfor (const idChunk of chunks(\n\t\t\tcollectionRows.map((c) => c.id),\n\t\t\tSQL_BATCH_SIZE,\n\t\t)) {\n\t\t\tconst fieldRows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_fields\")\n\t\t\t\t.where(\"collection_id\", \"in\", idChunk)\n\t\t\t\t.selectAll()\n\t\t\t\t.orderBy(\"collection_id\", \"asc\")\n\t\t\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t\t\t.orderBy(\"created_at\", \"asc\")\n\t\t\t\t.execute();\n\t\t\tfor (const row of fieldRows) {\n\t\t\t\tconst list = fieldsByCollection.get(row.collection_id) ?? [];\n\t\t\t\tlist.push(this.mapFieldRow(row));\n\t\t\t\tfieldsByCollection.set(row.collection_id, list);\n\t\t\t}\n\t\t}\n\n\t\treturn collectionRows.map((c) => ({\n\t\t\t...this.mapCollectionRow(c),\n\t\t\tfields: fieldsByCollection.get(c.id) ?? [],\n\t\t}));\n\t}\n\n\t/**\n\t * Create a new collection\n\t */\n\tasync createCollection(input: CreateCollectionInput): Promise<Collection> {\n\t\t// Validate slug\n\t\tthis.validateSlug(input.slug, \"collection\");\n\t\tif (RESERVED_COLLECTION_SLUGS.includes(input.slug)) {\n\t\t\tthrow new SchemaError(`Collection slug \"${input.slug}\" is reserved`, \"RESERVED_SLUG\");\n\t\t}\n\n\t\t// Check if collection already exists\n\t\tconst existing = await this.getCollection(input.slug);\n\t\tif (existing) {\n\t\t\tthrow new SchemaError(`Collection \"${input.slug}\" already exists`, \"COLLECTION_EXISTS\");\n\t\t}\n\n\t\tconst id = ulid();\n\n\t\t// Default `supports` to drafts + revisions when the caller didn't\n\t\t// specify it. Explicit empty array (`[]`) is preserved as an opt-out\n\t\t// — only `undefined` triggers the default. This is the canonical\n\t\t// default for new collections; the MCP and admin UI layers used to\n\t\t// duplicate this default but now defer to the registry.\n\t\tconst supports = input.supports ?? [\"drafts\", \"revisions\"];\n\n\t\t// Insert collection record and create content table in a transaction\n\t\t// so a failure in table creation doesn't leave an orphaned row.\n\t\t// Uses withTransaction for D1 compatibility (no transaction support).\n\t\t// Derive hasSeo from supports array if not explicitly set\n\t\tconst hasSeo = input.hasSeo ?? supports.includes(\"seo\") ?? false;\n\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.insertInto(\"_emdash_collections\")\n\t\t\t\t.values({\n\t\t\t\t\tid,\n\t\t\t\t\tslug: input.slug,\n\t\t\t\t\tlabel: input.label,\n\t\t\t\t\tlabel_singular: input.labelSingular ?? null,\n\t\t\t\t\tdescription: input.description ?? null,\n\t\t\t\t\ticon: input.icon ?? null,\n\t\t\t\t\tsupports: JSON.stringify(supports),\n\t\t\t\t\tsource: input.source ?? \"manual\",\n\t\t\t\t\thas_seo: hasSeo ? 1 : 0,\n\t\t\t\t\tcomments_enabled: input.commentsEnabled ? 1 : 0,\n\t\t\t\t\turl_pattern: input.urlPattern ?? null,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\t// Create the content table for this collection\n\t\t\tawait this.createContentTable(input.slug, trx);\n\t\t});\n\n\t\tconst collection = await this.getCollection(input.slug);\n\t\tif (!collection) {\n\t\t\tthrow new SchemaError(\"Failed to create collection\", \"CREATE_FAILED\");\n\t\t}\n\n\t\treturn collection;\n\t}\n\n\t/**\n\t * Update a collection\n\t */\n\tasync updateCollection(slug: string, input: UpdateCollectionInput): Promise<Collection> {\n\t\tconst existing = await this.getCollection(slug);\n\t\tif (!existing) {\n\t\t\tthrow new SchemaError(`Collection \"${slug}\" not found`, \"COLLECTION_NOT_FOUND\");\n\t\t}\n\n\t\tconst now = new Date().toISOString();\n\n\t\t// Derive hasSeo from supports array if supports is being updated and hasSeo not explicitly set\n\t\tconst supportsArray = input.supports ?? existing.supports;\n\t\tconst hasSeo =\n\t\t\tinput.hasSeo !== undefined\n\t\t\t\t? input.hasSeo\n\t\t\t\t: input.supports !== undefined\n\t\t\t\t\t? supportsArray.includes(\"seo\")\n\t\t\t\t\t: existing.hasSeo;\n\n\t\treturn withTransaction(this.db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.updateTable(\"_emdash_collections\")\n\t\t\t\t.set({\n\t\t\t\t\tlabel: input.label ?? existing.label,\n\t\t\t\t\tlabel_singular: input.labelSingular ?? existing.labelSingular ?? null,\n\t\t\t\t\tdescription: input.description ?? existing.description ?? null,\n\t\t\t\t\ticon: input.icon ?? existing.icon ?? null,\n\t\t\t\t\tsupports: input.supports\n\t\t\t\t\t\t? JSON.stringify(input.supports)\n\t\t\t\t\t\t: JSON.stringify(existing.supports),\n\t\t\t\t\turl_pattern:\n\t\t\t\t\t\tinput.urlPattern !== undefined\n\t\t\t\t\t\t\t? (input.urlPattern ?? null)\n\t\t\t\t\t\t\t: (existing.urlPattern ?? null),\n\t\t\t\t\thas_seo: hasSeo ? 1 : 0,\n\t\t\t\t\tcomments_enabled:\n\t\t\t\t\t\tinput.commentsEnabled !== undefined\n\t\t\t\t\t\t\t? input.commentsEnabled\n\t\t\t\t\t\t\t\t? 1\n\t\t\t\t\t\t\t\t: 0\n\t\t\t\t\t\t\t: existing.commentsEnabled\n\t\t\t\t\t\t\t\t? 1\n\t\t\t\t\t\t\t\t: 0,\n\t\t\t\t\tcomments_moderation: input.commentsModeration ?? existing.commentsModeration,\n\t\t\t\t\tcomments_closed_after_days:\n\t\t\t\t\t\tinput.commentsClosedAfterDays !== undefined\n\t\t\t\t\t\t\t? input.commentsClosedAfterDays\n\t\t\t\t\t\t\t: existing.commentsClosedAfterDays,\n\t\t\t\t\tcomments_auto_approve_users:\n\t\t\t\t\t\tinput.commentsAutoApproveUsers !== undefined\n\t\t\t\t\t\t\t? input.commentsAutoApproveUsers\n\t\t\t\t\t\t\t\t? 1\n\t\t\t\t\t\t\t\t: 0\n\t\t\t\t\t\t\t: existing.commentsAutoApproveUsers\n\t\t\t\t\t\t\t\t? 1\n\t\t\t\t\t\t\t\t: 0,\n\t\t\t\t\tupdated_at: now,\n\t\t\t\t})\n\t\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t\t.execute();\n\n\t\t\tconst row = await trx\n\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t\t.selectAll()\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!row) {\n\t\t\t\tthrow new SchemaError(\"Failed to update collection\", \"UPDATE_FAILED\");\n\t\t\t}\n\n\t\t\t// Sync FTS state when the supports array changes (e.g. search toggled on/off)\n\t\t\tif (input.supports !== undefined) {\n\t\t\t\tconst hadSearch = existing.supports.includes(\"search\");\n\t\t\t\tconst hasSearch = parseSupports(row.supports).includes(\"search\");\n\t\t\t\tif (hadSearch !== hasSearch) {\n\t\t\t\t\tawait this.syncSearchState(slug, trx);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn this.mapCollectionRow(row);\n\t\t});\n\t}\n\n\t/**\n\t * Delete a collection\n\t */\n\tasync deleteCollection(slug: string, options?: { force?: boolean }): Promise<void> {\n\t\tconst existing = await this.getCollection(slug);\n\t\tif (!existing) {\n\t\t\tthrow new SchemaError(`Collection \"${slug}\" not found`, \"COLLECTION_NOT_FOUND\");\n\t\t}\n\n\t\t// Check if collection has content\n\t\tif (!options?.force) {\n\t\t\tconst hasContent = await this.collectionHasContent(slug);\n\t\t\tif (hasContent) {\n\t\t\t\tthrow new SchemaError(\n\t\t\t\t\t`Collection \"${slug}\" has content. Use force: true to delete.`,\n\t\t\t\t\t\"COLLECTION_HAS_CONTENT\",\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\t// Drop FTS table and triggers before dropping the content table\n\t\t\tconst ftsManager = new FTSManager(trx);\n\t\t\tawait ftsManager.dropFtsTable(slug);\n\n\t\t\t// Drop the content table\n\t\t\tconst tableName = this.getTableName(slug);\n\t\t\tawait sql`DROP TABLE IF EXISTS ${sql.ref(tableName)}`.execute(trx);\n\n\t\t\t// Delete the collection record (fields will cascade)\n\t\t\tawait trx.deleteFrom(\"_emdash_collections\").where(\"id\", \"=\", existing.id).execute();\n\t\t});\n\t}\n\n\t// ============================================\n\t// Field Operations\n\t// ============================================\n\n\t/**\n\t * List fields for a collection\n\t */\n\tasync listFields(collectionId: string): Promise<Field[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_fields\")\n\t\t\t.where(\"collection_id\", \"=\", collectionId)\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t\t.orderBy(\"created_at\", \"asc\")\n\t\t\t.execute();\n\n\t\treturn rows.map(this.mapFieldRow);\n\t}\n\n\t/**\n\t * Get a field by slug within a collection\n\t */\n\tasync getField(collectionSlug: string, fieldSlug: string): Promise<Field | null> {\n\t\tconst collection = await this.getCollection(collectionSlug);\n\t\tif (!collection) return null;\n\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_fields\")\n\t\t\t.where(\"collection_id\", \"=\", collection.id)\n\t\t\t.where(\"slug\", \"=\", fieldSlug)\n\t\t\t.selectAll()\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.mapFieldRow(row) : null;\n\t}\n\n\t/**\n\t * Create a new field\n\t */\n\tasync createField(collectionSlug: string, input: CreateFieldInput): Promise<Field> {\n\t\tconst collection = await this.getCollection(collectionSlug);\n\t\tif (!collection) {\n\t\t\tthrow new SchemaError(`Collection \"${collectionSlug}\" not found`, \"COLLECTION_NOT_FOUND\");\n\t\t}\n\n\t\t// Validate slug\n\t\tthis.validateSlug(input.slug, \"field\");\n\t\tif (RESERVED_FIELD_SLUGS.includes(input.slug)) {\n\t\t\tthrow new SchemaError(`Field slug \"${input.slug}\" is reserved`, \"RESERVED_SLUG\");\n\t\t}\n\n\t\t// Check if field already exists\n\t\tconst existing = await this.getField(collectionSlug, input.slug);\n\t\tif (existing) {\n\t\t\tthrow new SchemaError(\n\t\t\t\t`Field \"${input.slug}\" already exists in collection \"${collectionSlug}\"`,\n\t\t\t\t\"FIELD_EXISTS\",\n\t\t\t);\n\t\t}\n\n\t\tconst id = ulid();\n\t\tconst columnType = FIELD_TYPE_TO_COLUMN[input.type];\n\n\t\t// Get max sort order\n\t\tconst maxSort = await this.db\n\t\t\t.selectFrom(\"_emdash_fields\")\n\t\t\t.where(\"collection_id\", \"=\", collection.id)\n\t\t\t.select((eb) => eb.fn.max<number>(\"sort_order\").as(\"max\"))\n\t\t\t.executeTakeFirst();\n\n\t\tconst sortOrder = input.sortOrder ?? (maxSort?.max ?? -1) + 1;\n\n\t\treturn withTransaction(this.db, async (trx) => {\n\t\t\t// Insert field record\n\t\t\tawait trx\n\t\t\t\t.insertInto(\"_emdash_fields\")\n\t\t\t\t.values({\n\t\t\t\t\tid,\n\t\t\t\t\tcollection_id: collection.id,\n\t\t\t\t\tslug: input.slug,\n\t\t\t\t\tlabel: input.label,\n\t\t\t\t\ttype: input.type,\n\t\t\t\t\tcolumn_type: columnType,\n\t\t\t\t\trequired: input.required ? 1 : 0,\n\t\t\t\t\tunique: input.unique ? 1 : 0,\n\t\t\t\t\tdefault_value:\n\t\t\t\t\t\tinput.defaultValue !== undefined ? JSON.stringify(input.defaultValue) : null,\n\t\t\t\t\tvalidation: input.validation ? JSON.stringify(input.validation) : null,\n\t\t\t\t\twidget: input.widget ?? null,\n\t\t\t\t\toptions: input.options ? JSON.stringify(input.options) : null,\n\t\t\t\t\tsort_order: sortOrder,\n\t\t\t\t\tsearchable: input.searchable ? 1 : 0,\n\t\t\t\t\ttranslatable: input.translatable === false ? 0 : 1,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\t// Add column to content table — pass trx to stay on the same connection\n\t\t\tawait this.addColumn(\n\t\t\t\tcollectionSlug,\n\t\t\t\tinput.slug,\n\t\t\t\tinput.type,\n\t\t\t\t{\n\t\t\t\t\trequired: input.required,\n\t\t\t\t\tdefaultValue: input.defaultValue,\n\t\t\t\t},\n\t\t\t\ttrx,\n\t\t\t);\n\n\t\t\t// Read the created field via trx (not this.db) to avoid connection mutex deadlock\n\t\t\tconst fieldRow = await trx\n\t\t\t\t.selectFrom(\"_emdash_fields\")\n\t\t\t\t.where(\"collection_id\", \"=\", collection.id)\n\t\t\t\t.where(\"slug\", \"=\", input.slug)\n\t\t\t\t.selectAll()\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!fieldRow) {\n\t\t\t\tthrow new SchemaError(\"Failed to create field\", \"CREATE_FAILED\");\n\t\t\t}\n\n\t\t\tconst field = this.mapFieldRow(fieldRow);\n\n\t\t\t// Sync search state if this field is searchable; support checks are handled by syncSearchState()\n\t\t\tif (input.searchable) {\n\t\t\t\tawait this.syncSearchState(collectionSlug, trx);\n\t\t\t}\n\n\t\t\treturn field;\n\t\t});\n\t}\n\n\t/**\n\t * Update a field\n\t */\n\tasync updateField(\n\t\tcollectionSlug: string,\n\t\tfieldSlug: string,\n\t\tinput: UpdateFieldInput,\n\t): Promise<Field> {\n\t\tconst field = await this.getField(collectionSlug, fieldSlug);\n\t\tif (!field) {\n\t\t\tthrow new SchemaError(\n\t\t\t\t`Field \"${fieldSlug}\" not found in collection \"${collectionSlug}\"`,\n\t\t\t\t\"FIELD_NOT_FOUND\",\n\t\t\t);\n\t\t}\n\n\t\t// `input.validation === undefined` means \"no change\" (keep existing);\n\t\t// an explicit `null` clears the column.\n\t\tconst nextValidation = input.validation === undefined ? field.validation : input.validation;\n\n\t\treturn withTransaction(this.db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.updateTable(\"_emdash_fields\")\n\t\t\t\t.set({\n\t\t\t\t\tlabel: input.label ?? field.label,\n\t\t\t\t\trequired:\n\t\t\t\t\t\tinput.required !== undefined ? (input.required ? 1 : 0) : field.required ? 1 : 0,\n\t\t\t\t\tunique: input.unique !== undefined ? (input.unique ? 1 : 0) : field.unique ? 1 : 0,\n\t\t\t\t\tsearchable:\n\t\t\t\t\t\tinput.searchable !== undefined ? (input.searchable ? 1 : 0) : field.searchable ? 1 : 0,\n\t\t\t\t\ttranslatable:\n\t\t\t\t\t\tinput.translatable !== undefined\n\t\t\t\t\t\t\t? input.translatable\n\t\t\t\t\t\t\t\t? 1\n\t\t\t\t\t\t\t\t: 0\n\t\t\t\t\t\t\t: field.translatable\n\t\t\t\t\t\t\t\t? 1\n\t\t\t\t\t\t\t\t: 0,\n\t\t\t\t\tdefault_value:\n\t\t\t\t\t\tinput.defaultValue !== undefined\n\t\t\t\t\t\t\t? JSON.stringify(input.defaultValue)\n\t\t\t\t\t\t\t: field.defaultValue !== undefined\n\t\t\t\t\t\t\t\t? JSON.stringify(field.defaultValue)\n\t\t\t\t\t\t\t\t: null,\n\t\t\t\t\tvalidation: nextValidation ? JSON.stringify(nextValidation) : null,\n\t\t\t\t\twidget: input.widget ?? field.widget ?? null,\n\t\t\t\t\toptions: input.options\n\t\t\t\t\t\t? JSON.stringify(input.options)\n\t\t\t\t\t\t: field.options\n\t\t\t\t\t\t\t? JSON.stringify(field.options)\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\tsort_order: input.sortOrder ?? field.sortOrder,\n\t\t\t\t})\n\t\t\t\t.where(\"id\", \"=\", field.id)\n\t\t\t\t.execute();\n\n\t\t\t// Read the updated field via trx (not this.db) to avoid connection mutex deadlock\n\t\t\tconst updatedRow = await trx\n\t\t\t\t.selectFrom(\"_emdash_fields\")\n\t\t\t\t.where(\"collection_id\", \"=\", field.collectionId)\n\t\t\t\t.where(\"slug\", \"=\", fieldSlug)\n\t\t\t\t.selectAll()\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!updatedRow) {\n\t\t\t\tthrow new SchemaError(\"Failed to update field\", \"UPDATE_FAILED\");\n\t\t\t}\n\n\t\t\tconst updated = this.mapFieldRow(updatedRow);\n\n\t\t\t// If searchable changed, sync FTS state for this collection\n\t\t\tconst searchableChanged =\n\t\t\t\tinput.searchable !== undefined && input.searchable !== field.searchable;\n\t\t\tif (searchableChanged) {\n\t\t\t\tawait this.syncSearchState(collectionSlug, trx);\n\t\t\t}\n\n\t\t\treturn updated;\n\t\t});\n\t}\n\n\t/**\n\t * Synchronize an existing FTS index with the collection's current state.\n\t *\n\t * Only rebuilds or disables — never first-time enables. First-time FTS\n\t * enablement is handled by the seed's explicit enableSearch call (which\n\t * is try-caught) or the admin UI toggle.\n\t *\n\t * - FTS active + still has search support and searchable fields → rebuild\n\t * - FTS active + lost search support or no searchable fields → disable\n\t * - FTS not active → no-op\n\t *\n\t * Pass `db` when calling from within a transaction so FTS operations\n\t * participate in the same transaction and are rolled back on failure.\n\t */\n\tprivate async syncSearchState(collectionSlug: string, db?: Kysely<Database>): Promise<void> {\n\t\tconst conn = db ?? this.db;\n\t\tconst ftsManager = new FTSManager(conn);\n\n\t\t// Query via conn (not this.db) to avoid connection mutex deadlock when called inside a transaction\n\t\tconst row = await conn\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.where(\"slug\", \"=\", collectionSlug)\n\t\t\t.select(\"supports\")\n\t\t\t.executeTakeFirst();\n\t\tif (!row) return;\n\n\t\tconst wantsSearch = parseSupports(row.supports).includes(\"search\");\n\t\tconst searchableFields = await ftsManager.getSearchableFields(collectionSlug);\n\t\tconst config = await ftsManager.getSearchConfig(collectionSlug);\n\t\tconst ftsActive = config?.enabled === true;\n\n\t\tif (wantsSearch && searchableFields.length > 0 && ftsActive) {\n\t\t\tawait ftsManager.rebuildIndex(collectionSlug, searchableFields, config?.weights);\n\t\t} else if (ftsActive && (!wantsSearch || searchableFields.length === 0)) {\n\t\t\tawait ftsManager.disableSearch(collectionSlug);\n\t\t}\n\t}\n\n\t/**\n\t * Delete a field\n\t */\n\tasync deleteField(collectionSlug: string, fieldSlug: string): Promise<void> {\n\t\tconst field = await this.getField(collectionSlug, fieldSlug);\n\t\tif (!field) {\n\t\t\tthrow new SchemaError(\n\t\t\t\t`Field \"${fieldSlug}\" not found in collection \"${collectionSlug}\"`,\n\t\t\t\t\"FIELD_NOT_FOUND\",\n\t\t\t);\n\t\t}\n\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\t// Delete the field record first so syncSearchState sees the updated field list.\n\t\t\t// This ordering matters for searchable fields: SQLite prevents dropping a column\n\t\t\t// that is still referenced by a trigger. syncSearchState drops and recreates the\n\t\t\t// FTS triggers based on the remaining searchable fields, clearing the dependency\n\t\t\t// before we attempt the ALTER TABLE DROP COLUMN below.\n\t\t\tawait trx.deleteFrom(\"_emdash_fields\").where(\"id\", \"=\", field.id).execute();\n\n\t\t\t// If the deleted field was searchable, sync FTS state (removes old triggers)\n\t\t\tif (field.searchable) {\n\t\t\t\tawait this.syncSearchState(collectionSlug, trx);\n\t\t\t}\n\n\t\t\t// Drop column from content table — safe now because FTS triggers are gone\n\t\t\tawait this.dropColumn(collectionSlug, fieldSlug, trx);\n\t\t});\n\t}\n\n\t/**\n\t * Reorder fields\n\t */\n\tasync reorderFields(collectionSlug: string, fieldSlugs: string[]): Promise<void> {\n\t\tconst collection = await this.getCollection(collectionSlug);\n\t\tif (!collection) {\n\t\t\tthrow new SchemaError(`Collection \"${collectionSlug}\" not found`, \"COLLECTION_NOT_FOUND\");\n\t\t}\n\n\t\t// Update sort_order for each field\n\t\tfor (let i = 0; i < fieldSlugs.length; i++) {\n\t\t\tawait this.db\n\t\t\t\t.updateTable(\"_emdash_fields\")\n\t\t\t\t.set({ sort_order: i })\n\t\t\t\t.where(\"collection_id\", \"=\", collection.id)\n\t\t\t\t.where(\"slug\", \"=\", fieldSlugs[i])\n\t\t\t\t.execute();\n\t\t}\n\t}\n\n\t// ============================================\n\t// DDL Operations\n\t// ============================================\n\n\t/**\n\t * Create a content table for a collection\n\t */\n\tprivate async createContentTable(slug: string, db?: Kysely<Database>): Promise<void> {\n\t\tconst conn = db ?? this.db;\n\t\tconst tableName = this.getTableName(slug);\n\n\t\tawait conn.schema\n\t\t\t.createTable(tableName)\n\t\t\t.addColumn(\"id\", \"text\", (col) => col.primaryKey())\n\t\t\t.addColumn(\"slug\", \"text\")\n\t\t\t.addColumn(\"status\", \"text\", (col) => col.defaultTo(\"draft\"))\n\t\t\t.addColumn(\"author_id\", \"text\")\n\t\t\t.addColumn(\"primary_byline_id\", \"text\")\n\t\t\t.addColumn(\"created_at\", \"text\", (col) => col.defaultTo(currentTimestamp(conn)))\n\t\t\t.addColumn(\"updated_at\", \"text\", (col) => col.defaultTo(currentTimestamp(conn)))\n\t\t\t.addColumn(\"published_at\", \"text\")\n\t\t\t.addColumn(\"scheduled_at\", \"text\")\n\t\t\t.addColumn(\"deleted_at\", \"text\")\n\t\t\t.addColumn(\"version\", \"integer\", (col) => col.defaultTo(1))\n\t\t\t.addColumn(\"live_revision_id\", \"text\", (col) => col.references(\"revisions.id\"))\n\t\t\t.addColumn(\"draft_revision_id\", \"text\", (col) => col.references(\"revisions.id\"))\n\t\t\t.addColumn(\"locale\", \"text\", (col) => col.notNull().defaultTo(\"en\"))\n\t\t\t.addColumn(\"translation_group\", \"text\")\n\t\t\t.addUniqueConstraint(`${tableName}_slug_locale_unique`, [\"slug\", \"locale\"])\n\t\t\t.execute();\n\n\t\t// Create standard indexes\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_slug`)}\n\t\t\tON ${sql.ref(tableName)} (slug)\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_scheduled`)}\n\t\t\tON ${sql.ref(tableName)} (scheduled_at)\n\t\t\tWHERE scheduled_at IS NOT NULL\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_live_revision`)}\n\t\t\tON ${sql.ref(tableName)} (live_revision_id)\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_draft_revision`)}\n\t\t\tON ${sql.ref(tableName)} (draft_revision_id)\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_author`)}\n\t\t\tON ${sql.ref(tableName)} (author_id)\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_primary_byline`)}\n\t\t\tON ${sql.ref(tableName)} (primary_byline_id)\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_locale`)}\n\t\t\tON ${sql.ref(tableName)} (locale)\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_translation_group`)}\n\t\t\tON ${sql.ref(tableName)} (translation_group)\n\t\t`.execute(conn);\n\n\t\t// Composite indexes for optimized query performance (see migration 033)\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_deleted_updated_id`)}\n\t\t\tON ${sql.ref(tableName)} (deleted_at, updated_at DESC, id DESC)\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_deleted_status`)}\n\t\t\tON ${sql.ref(tableName)} (deleted_at, status)\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_deleted_created_id`)}\n\t\t\tON ${sql.ref(tableName)} (deleted_at, created_at DESC, id DESC)\n\t\t`.execute(conn);\n\n\t\tawait sql`\n\t\t\tCREATE INDEX ${sql.ref(`idx_${tableName}_deleted_published_id`)}\n\t\t\tON ${sql.ref(tableName)} (deleted_at, published_at DESC, id DESC)\n\t\t`.execute(conn);\n\t}\n\n\t/**\n\t * Add a column to a content table\n\t */\n\tprivate async addColumn(\n\t\tcollectionSlug: string,\n\t\tfieldSlug: string,\n\t\tfieldType: FieldType,\n\t\toptions?: { required?: boolean; defaultValue?: unknown },\n\t\tdb?: Kysely<Database>,\n\t): Promise<void> {\n\t\tconst conn = db ?? this.db;\n\t\tconst tableName = this.getTableName(collectionSlug);\n\t\tconst columnType = FIELD_TYPE_TO_COLUMN[fieldType];\n\t\tconst columnName = this.getColumnName(fieldSlug);\n\n\t\t// Build ALTER TABLE statement\n\t\t// Note: SQLite requires DEFAULT for NOT NULL columns in ALTER TABLE\n\t\tif (options?.required && options?.defaultValue !== undefined) {\n\t\t\tconst defaultVal = this.formatDefaultValue(options.defaultValue, fieldType);\n\t\t\tawait sql`\n\t\t\t\tALTER TABLE ${sql.ref(tableName)}\n\t\t\t\tADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}\n\t\t\t`.execute(conn);\n\t\t} else if (options?.required) {\n\t\t\t// For required fields without default, use empty string/0 as default\n\t\t\tconst defaultVal = this.getEmptyDefault(fieldType);\n\t\t\tawait sql`\n\t\t\t\tALTER TABLE ${sql.ref(tableName)}\n\t\t\t\tADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)} NOT NULL DEFAULT ${sql.raw(defaultVal)}\n\t\t\t`.execute(conn);\n\t\t} else {\n\t\t\tawait sql`\n\t\t\t\tALTER TABLE ${sql.ref(tableName)}\n\t\t\t\tADD COLUMN ${sql.ref(columnName)} ${sql.raw(columnType)}\n\t\t\t`.execute(conn);\n\t\t}\n\t}\n\n\t/**\n\t * Drop a column from a content table\n\t */\n\tprivate async dropColumn(\n\t\tcollectionSlug: string,\n\t\tfieldSlug: string,\n\t\tdb?: Kysely<Database>,\n\t): Promise<void> {\n\t\tconst tableName = this.getTableName(collectionSlug);\n\t\tconst columnName = this.getColumnName(fieldSlug);\n\n\t\tawait sql`\n\t\t\tALTER TABLE ${sql.ref(tableName)}\n\t\t\tDROP COLUMN ${sql.ref(columnName)}\n\t\t`.execute(db ?? this.db);\n\t}\n\n\t// ============================================\n\t// Helpers\n\t// ============================================\n\n\t/**\n\t * Check if a collection has any content\n\t */\n\tprivate async collectionHasContent(slug: string): Promise<boolean> {\n\t\tconst tableName = this.getTableName(slug);\n\t\ttry {\n\t\t\tconst result = await sql<{ count: number }>`\n\t\t\t\tSELECT COUNT(*) as count FROM ${sql.ref(tableName)}\n\t\t\t\tWHERE deleted_at IS NULL\n\t\t\t`.execute(this.db);\n\t\t\treturn (result.rows[0]?.count ?? 0) > 0;\n\t\t} catch {\n\t\t\t// Table might not exist\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Get table name for a collection\n\t */\n\tprivate getTableName(slug: string): string {\n\t\tvalidateIdentifier(slug, \"collection slug\");\n\t\treturn `ec_${slug}`;\n\t}\n\n\t/**\n\t * Get column name for a field\n\t */\n\tprivate getColumnName(slug: string): string {\n\t\tvalidateIdentifier(slug, \"field slug\");\n\t\treturn slug;\n\t}\n\n\t/**\n\t * Validate a slug\n\t */\n\tprivate validateSlug(slug: string, type: \"collection\" | \"field\"): void {\n\t\tif (!slug || typeof slug !== \"string\") {\n\t\t\tthrow new SchemaError(`${type} slug is required`, \"INVALID_SLUG\");\n\t\t}\n\n\t\tif (!SLUG_VALIDATION_PATTERN.test(slug)) {\n\t\t\tthrow new SchemaError(\n\t\t\t\t`${type} slug must start with a letter and contain only lowercase letters, numbers, and underscores`,\n\t\t\t\t\"INVALID_SLUG\",\n\t\t\t);\n\t\t}\n\n\t\tif (slug.length > 63) {\n\t\t\tthrow new SchemaError(`${type} slug must be 63 characters or less`, \"INVALID_SLUG\");\n\t\t}\n\t}\n\n\t/**\n\t * Format a default value for SQL.\n\t *\n\t * SQLite `ALTER TABLE ADD COLUMN ... DEFAULT` requires a literal constant\n\t * expression — parameterized values cannot be used here. We manually escape\n\t * single quotes and coerce types to ensure the output is safe.\n\t *\n\t * INTEGER/REAL values are coerced through `Number()` which can only produce\n\t * digits, `.`, `-`, `e`, `Infinity`, or `NaN` — all safe in SQL.\n\t * TEXT/JSON values have single quotes escaped via SQL standard doubling (`''`).\n\t */\n\tprivate formatDefaultValue(value: unknown, fieldType: FieldType): string {\n\t\tif (value === null || value === undefined) {\n\t\t\treturn \"NULL\";\n\t\t}\n\n\t\tconst columnType = FIELD_TYPE_TO_COLUMN[fieldType];\n\n\t\tif (columnType === \"JSON\") {\n\t\t\t// JSON.stringify produces valid JSON; escape single quotes for SQL literal\n\t\t\tconst json = JSON.stringify(value);\n\t\t\treturn `'${json.replace(SINGLE_QUOTE_PATTERN, \"''\")}'`;\n\t\t}\n\n\t\tif (columnType === \"INTEGER\") {\n\t\t\tif (typeof value === \"boolean\") {\n\t\t\t\treturn value ? \"1\" : \"0\";\n\t\t\t}\n\t\t\tconst num = Number(value);\n\t\t\tif (!Number.isFinite(num)) {\n\t\t\t\treturn \"0\";\n\t\t\t}\n\t\t\treturn String(Math.trunc(num));\n\t\t}\n\n\t\tif (columnType === \"REAL\") {\n\t\t\tconst num = Number(value);\n\t\t\tif (!Number.isFinite(num)) {\n\t\t\t\treturn \"0\";\n\t\t\t}\n\t\t\treturn String(num);\n\t\t}\n\n\t\t// TEXT — escape single quotes via SQL standard doubling\n\t\tlet text: string;\n\t\tif (typeof value === \"string\") {\n\t\t\ttext = value;\n\t\t} else if (typeof value === \"number\" || typeof value === \"boolean\") {\n\t\t\ttext = String(value);\n\t\t} else if (typeof value === \"object\" && value !== null) {\n\t\t\ttext = JSON.stringify(value);\n\t\t} else {\n\t\t\ttext = \"\";\n\t\t}\n\t\treturn `'${text.replace(SINGLE_QUOTE_PATTERN, \"''\")}'`;\n\t}\n\n\t/**\n\t * Get empty default for a field type\n\t */\n\tprivate getEmptyDefault(fieldType: FieldType): string {\n\t\tconst columnType = FIELD_TYPE_TO_COLUMN[fieldType];\n\n\t\tswitch (columnType) {\n\t\t\tcase \"INTEGER\":\n\t\t\t\treturn \"0\";\n\t\t\tcase \"REAL\":\n\t\t\t\treturn \"0.0\";\n\t\t\tcase \"JSON\":\n\t\t\t\treturn \"'null'\";\n\t\t\tdefault:\n\t\t\t\treturn \"''\";\n\t\t}\n\t}\n\n\t/**\n\t * Map a collection row to a Collection object\n\t */\n\tprivate mapCollectionRow = (row: Selectable<CollectionTable>): Collection => {\n\t\tconst moderation = row.comments_moderation;\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tslug: row.slug,\n\t\t\tlabel: row.label,\n\t\t\tlabelSingular: row.label_singular ?? undefined,\n\t\t\tdescription: row.description ?? undefined,\n\t\t\ticon: row.icon ?? undefined,\n\t\t\tsupports: parseSupports(row.supports),\n\t\t\tsource: row.source && isCollectionSource(row.source) ? row.source : undefined,\n\t\t\thasSeo: row.has_seo === 1,\n\t\t\turlPattern: row.url_pattern ?? undefined,\n\t\t\tcommentsEnabled: row.comments_enabled === 1,\n\t\t\tcommentsModeration:\n\t\t\t\tmoderation === \"all\" || moderation === \"first_time\" || moderation === \"none\"\n\t\t\t\t\t? moderation\n\t\t\t\t\t: \"first_time\",\n\t\t\tcommentsClosedAfterDays: row.comments_closed_after_days ?? 90,\n\t\t\tcommentsAutoApproveUsers: row.comments_auto_approve_users === 1,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tupdatedAt: row.updated_at,\n\t\t};\n\t};\n\n\t/**\n\t * Map a field row to a Field object\n\t */\n\tprivate mapFieldRow = (row: Selectable<FieldTable>): Field => {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tcollectionId: row.collection_id,\n\t\t\tslug: row.slug,\n\t\t\tlabel: row.label,\n\t\t\ttype: isFieldType(row.type) ? row.type : \"string\",\n\t\t\tcolumnType: isColumnType(row.column_type) ? row.column_type : \"TEXT\",\n\t\t\trequired: row.required === 1,\n\t\t\tunique: row.unique === 1,\n\t\t\tdefaultValue: row.default_value ? JSON.parse(row.default_value) : undefined,\n\t\t\tvalidation: row.validation ? JSON.parse(row.validation) : undefined,\n\t\t\twidget: row.widget ?? undefined,\n\t\t\toptions: row.options ? JSON.parse(row.options) : undefined,\n\t\t\tsortOrder: row.sort_order,\n\t\t\tsearchable: row.searchable === 1,\n\t\t\ttranslatable: row.translatable !== 0,\n\t\t\tcreatedAt: row.created_at,\n\t\t};\n\t};\n\n\t// ============================================\n\t// Discovery\n\t// ============================================\n\n\t/**\n\t * Discover orphaned content tables\n\t *\n\t * Finds ec_* tables that exist in the database but don't have a\n\t * corresponding entry in _emdash_collections.\n\t */\n\tasync discoverOrphanedTables(): Promise<\n\t\tArray<{ slug: string; tableName: string; rowCount: number }>\n\t> {\n\t\t// Get all ec_* tables\n\t\t// Content tables are ec_* (e.g., ec_posts, ec_pages)\n\t\t// Internal tables are _emdash_* (e.g., _emdash_collections, _emdash_fts_posts)\n\t\tconst allTables = await listTablesLike(this.db, \"ec_%\");\n\n\t\t// Get registered collections\n\t\tconst registered = await this.listCollections();\n\t\tconst registeredSlugs = new Set(registered.map((c) => c.slug));\n\n\t\t// Find orphans\n\t\tconst orphans: Array<{\n\t\t\tslug: string;\n\t\t\ttableName: string;\n\t\t\trowCount: number;\n\t\t}> = [];\n\n\t\tfor (const tableName of allTables) {\n\t\t\tconst slug = tableName.replace(EC_PREFIX_PATTERN, \"\");\n\n\t\t\tif (!registeredSlugs.has(slug)) {\n\t\t\t\t// Count rows in the orphaned table\n\t\t\t\ttry {\n\t\t\t\t\tconst countResult = await sql<{ count: number }>`\n\t\t\t\t\t\tSELECT COUNT(*) as count FROM ${sql.ref(tableName)}\n\t\t\t\t\t\tWHERE deleted_at IS NULL\n\t\t\t\t\t`.execute(this.db);\n\n\t\t\t\t\torphans.push({\n\t\t\t\t\t\tslug,\n\t\t\t\t\t\ttableName,\n\t\t\t\t\t\trowCount: countResult.rows[0]?.count ?? 0,\n\t\t\t\t\t});\n\t\t\t\t} catch {\n\t\t\t\t\t// Table might have unexpected schema, still report it\n\t\t\t\t\torphans.push({\n\t\t\t\t\t\tslug,\n\t\t\t\t\t\ttableName,\n\t\t\t\t\t\trowCount: 0,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn orphans;\n\t}\n\n\t/**\n\t * Register an orphaned table as a collection\n\t *\n\t * Creates a _emdash_collections entry for an existing ec_* table.\n\t */\n\tasync registerOrphanedTable(\n\t\tslug: string,\n\t\toptions?: {\n\t\t\tlabel?: string;\n\t\t\tlabelSingular?: string;\n\t\t\tdescription?: string;\n\t\t},\n\t): Promise<Collection> {\n\t\t// Verify table exists\n\t\tconst tableName = this.getTableName(slug);\n\t\tconst exists = await tableExists(this.db, tableName);\n\n\t\tif (!exists) {\n\t\t\tthrow new SchemaError(`Table \"${tableName}\" does not exist`, \"TABLE_NOT_FOUND\");\n\t\t}\n\n\t\t// Check if already registered\n\t\tconst existing = await this.getCollection(slug);\n\t\tif (existing) {\n\t\t\tthrow new SchemaError(`Collection \"${slug}\" is already registered`, \"COLLECTION_EXISTS\");\n\t\t}\n\n\t\t// Create collection entry\n\t\tconst id = ulid();\n\t\tconst label = options?.label || this.slugToLabel(slug);\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_collections\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tslug,\n\t\t\t\tlabel,\n\t\t\t\tlabel_singular: options?.labelSingular ?? null,\n\t\t\t\tdescription: options?.description ?? null,\n\t\t\t\ticon: null,\n\t\t\t\tsupports: JSON.stringify([]),\n\t\t\t\tsource: \"discovered\",\n\t\t\t\thas_seo: 0,\n\t\t\t\turl_pattern: null,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tconst collection = await this.getCollection(slug);\n\t\tif (!collection) {\n\t\t\tthrow new SchemaError(\"Failed to register orphaned table\", \"REGISTER_FAILED\");\n\t\t}\n\n\t\treturn collection;\n\t}\n\n\t/**\n\t * Convert slug to human-readable label\n\t */\n\tprivate slugToLabel(slug: string): string {\n\t\treturn slug\n\t\t\t.replace(UNDERSCORE_PATTERN, \" \")\n\t\t\t.replace(WORD_BOUNDARY_PATTERN, (c) => c.toUpperCase());\n\t}\n}\n"],"mappings":"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"}
|