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
|
@@ -1,4 +1,4 @@
|
|
|
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 { i as encodeCursor, r as decodeCursor, t as EmDashValidationError } from "./types-BIgulNsW.mjs";
|
|
4
4
|
import { sql } from "kysely";
|
|
@@ -903,4 +903,4 @@ var ContentRepository = class {
|
|
|
903
903
|
|
|
904
904
|
//#endregion
|
|
905
905
|
export { slugify as a, decodeSlug as i, content_exports as n, RevisionRepository as r, ContentRepository as t };
|
|
906
|
-
//# sourceMappingURL=content-
|
|
906
|
+
//# sourceMappingURL=content-CERxPUN0.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-C7G4QXkK.mjs","names":[],"sources":["../src/utils/slugify.ts","../src/database/repositories/revision.ts","../src/database/repositories/content.ts"],"sourcesContent":["// Regex patterns for slug normalization\nconst DIACRITICS_PATTERN = /[\\u0300-\\u036f]/g;\nconst WHITESPACE_UNDERSCORE_PATTERN = /[\\s_]+/g;\nconst NON_ALPHANUMERIC_HYPHEN_PATTERN = /[^a-z0-9-]/g;\nconst MULTIPLE_HYPHENS_PATTERN = /-+/g;\nconst LEADING_TRAILING_HYPHEN_PATTERN = /^-|-$/g;\nconst TRAILING_HYPHEN_PATTERN = /-$/;\n\n/**\n * Convert a string to a URL-friendly slug.\n *\n * Handles unicode by normalizing to NFD and stripping diacritics,\n * so \"café\" becomes \"cafe\", \"naïve\" becomes \"naive\", etc.\n */\n/**\n * Decode a URI-encoded slug parameter.\n *\n * Browsers percent-encode non-ASCII characters in URLs, so a slug like\n * \"మేష-రాసి\" arrives as \"%e0%b0%ae%e0%b1%87%e0%b0%b7-%e0%b0%b0%e0%b0%be%e0%b0%b8%e0%b0%bf\".\n * Call this on `Astro.params.slug` before using it in database lookups.\n */\nexport function decodeSlug(raw: string | undefined): string | undefined {\n\treturn raw ? decodeURIComponent(raw) : undefined;\n}\n\nexport function slugify(text: string, maxLength: number = 80): string {\n\treturn (\n\t\ttext\n\t\t\t.toLowerCase()\n\t\t\t.normalize(\"NFD\")\n\t\t\t.replace(DIACRITICS_PATTERN, \"\")\n\t\t\t.replace(WHITESPACE_UNDERSCORE_PATTERN, \"-\")\n\t\t\t.replace(NON_ALPHANUMERIC_HYPHEN_PATTERN, \"\")\n\t\t\t.replace(MULTIPLE_HYPHENS_PATTERN, \"-\")\n\t\t\t.replace(LEADING_TRAILING_HYPHEN_PATTERN, \"\")\n\t\t\t.slice(0, maxLength)\n\t\t\t// Clean trailing hyphen from truncation\n\t\t\t.replace(TRAILING_HYPHEN_PATTERN, \"\")\n\t);\n}\n","import type { Kysely } from \"kysely\";\nimport { monotonicFactory } from \"ulidx\";\n\nimport type { Database, RevisionTable } from \"../types.js\";\n\nconst monotonic = monotonicFactory();\n\nexport interface Revision {\n\tid: string;\n\tcollection: string;\n\tentryId: string;\n\tdata: Record<string, unknown>;\n\tauthorId: string | null;\n\tcreatedAt: string;\n}\n\nexport interface CreateRevisionInput {\n\tcollection: string;\n\tentryId: string;\n\tdata: Record<string, unknown>;\n\tauthorId?: string;\n}\n\n/**\n * Revision repository for version history\n *\n * Each revision stores a JSON snapshot of the content at a point in time.\n * Used when collection has `supports: [\"revisions\"]` enabled.\n */\nexport class RevisionRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new revision\n\t */\n\tasync create(input: CreateRevisionInput): Promise<Revision> {\n\t\tconst id = monotonic();\n\n\t\tconst row: Omit<RevisionTable, \"created_at\"> = {\n\t\t\tid,\n\t\t\tcollection: input.collection,\n\t\t\tentry_id: input.entryId,\n\t\t\tdata: JSON.stringify(input.data),\n\t\t\tauthor_id: input.authorId ?? null,\n\t\t};\n\n\t\tawait this.db.insertInto(\"revisions\").values(row).execute();\n\n\t\tconst revision = await this.findById(id);\n\t\tif (!revision) {\n\t\t\tthrow new Error(\"Failed to create revision\");\n\t\t}\n\t\treturn revision;\n\t}\n\n\t/**\n\t * Find revision by ID\n\t */\n\tasync findById(id: string): Promise<Revision | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToRevision(row) : null;\n\t}\n\n\t/**\n\t * Get all revisions for an entry (newest first)\n\t *\n\t * Orders by monotonic ULID (descending). The monotonic factory\n\t * guarantees strictly increasing IDs even within the same millisecond.\n\t */\n\tasync findByEntry(\n\t\tcollection: string,\n\t\tentryId: string,\n\t\toptions: { limit?: number } = {},\n\t): Promise<Revision[]> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.orderBy(\"id\", \"desc\");\n\n\t\tif (options.limit) {\n\t\t\tquery = query.limit(options.limit);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row) => this.rowToRevision(row));\n\t}\n\n\t/**\n\t * Get the most recent revision for an entry\n\t */\n\tasync findLatest(collection: string, entryId: string): Promise<Revision | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(1)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToRevision(row) : null;\n\t}\n\n\t/**\n\t * Count revisions for an entry\n\t */\n\tasync countByEntry(collection: string, entryId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Delete all revisions for an entry (use when entry is deleted)\n\t */\n\tasync deleteByEntry(collection: string, entryId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"revisions\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Delete old revisions, keeping the most recent N\n\t */\n\tasync pruneOldRevisions(collection: string, entryId: string, keepCount: number): Promise<number> {\n\t\t// Get IDs of revisions to keep\n\t\tconst keep = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\") // ULID tiebreaker\n\t\t\t.limit(keepCount)\n\t\t\t.execute();\n\n\t\tconst keepIds = keep.map((r) => r.id);\n\n\t\tif (keepIds.length === 0) return 0;\n\n\t\t// Delete everything else for this entry\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"revisions\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.where(\"id\", \"not in\", keepIds)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Update revision data in place\n\t * Used for autosave to avoid creating many small revisions.\n\t */\n\tasync updateData(id: string, data: Record<string, unknown>): Promise<void> {\n\t\tawait this.db\n\t\t\t.updateTable(\"revisions\")\n\t\t\t.set({ data: JSON.stringify(data) })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\t}\n\n\t/**\n\t * Convert database row to Revision object\n\t */\n\tprivate rowToRevision(row: {\n\t\tid: string;\n\t\tcollection: string;\n\t\tentry_id: string;\n\t\tdata: string;\n\t\tauthor_id: string | null;\n\t\tcreated_at: string;\n\t}): Revision {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tcollection: row.collection,\n\t\t\tentryId: row.entry_id,\n\t\t\tdata: JSON.parse(row.data),\n\t\t\tauthorId: row.author_id,\n\t\t\tcreatedAt: row.created_at,\n\t\t};\n\t}\n}\n","import { sql, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { slugify } from \"../../utils/slugify.js\";\nimport type { Database } from \"../types.js\";\nimport { validateIdentifier } from \"../validate.js\";\nimport { RevisionRepository } from \"./revision.js\";\nimport type {\n\tCreateContentInput,\n\tUpdateContentInput,\n\tFindManyOptions,\n\tFindManyResult,\n\tContentItem,\n} from \"./types.js\";\nimport { EmDashValidationError, encodeCursor, decodeCursor } from \"./types.js\";\n\n// Regex pattern for ULID validation\nconst ULID_PATTERN = /^[0-9A-Z]{26}$/;\n\n/**\n * System columns that exist in every ec_* table\n */\nconst SYSTEM_COLUMNS = new Set([\n\t\"id\",\n\t\"slug\",\n\t\"status\",\n\t\"author_id\",\n\t\"primary_byline_id\",\n\t\"created_at\",\n\t\"updated_at\",\n\t\"published_at\",\n\t\"scheduled_at\",\n\t\"deleted_at\",\n\t\"version\",\n\t\"live_revision_id\",\n\t\"draft_revision_id\",\n\t\"locale\",\n\t\"translation_group\",\n]);\n\n/**\n * Get the table name for a collection type\n */\nfunction getTableName(type: string): string {\n\tvalidateIdentifier(type, \"collection type\");\n\treturn `ec_${type}`;\n}\n\n/**\n * Serialize a value for database storage\n * Objects/arrays are JSON-stringified\n * Booleans are converted to 0/1 for SQLite\n */\nfunction serializeValue(value: unknown): unknown {\n\tif (value === null || value === undefined) {\n\t\treturn null;\n\t}\n\tif (typeof value === \"boolean\") {\n\t\treturn value ? 1 : 0;\n\t}\n\tif (typeof value === \"object\") {\n\t\treturn JSON.stringify(value);\n\t}\n\treturn value;\n}\n\n/**\n * Deserialize a value from database storage\n * Attempts to parse JSON strings that look like objects/arrays\n */\nfunction deserializeValue(value: unknown): unknown {\n\tif (typeof value === \"string\") {\n\t\t// Try to parse if it looks like JSON\n\t\tif (value.startsWith(\"{\") || value.startsWith(\"[\")) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(value);\n\t\t\t} catch {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t}\n\treturn value;\n}\n\n/** Pattern for escaping special regex characters */\nconst REGEX_ESCAPE_PATTERN = /[.*+?^${}()|[\\]\\\\]/g;\n\n/**\n * Escape special regex characters in a string for use in `new RegExp()`\n */\nfunction escapeRegExp(s: string): string {\n\treturn s.replace(REGEX_ESCAPE_PATTERN, \"\\\\$&\");\n}\n\n/**\n * Repository for content CRUD operations\n *\n * Content is stored in per-collection tables (ec_posts, ec_pages, etc.)\n * Each field becomes a real column in the table.\n */\nexport class ContentRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new content item\n\t */\n\tasync create(input: CreateContentInput): Promise<ContentItem> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tconst {\n\t\t\ttype,\n\t\t\tslug,\n\t\t\tdata,\n\t\t\tstatus = \"draft\",\n\t\t\tauthorId,\n\t\t\tprimaryBylineId,\n\t\t\tlocale,\n\t\t\ttranslationOf,\n\t\t\tpublishedAt,\n\t\t\tcreatedAt,\n\t\t} = input;\n\n\t\t// Validate required fields\n\t\tif (!type) {\n\t\t\tthrow new EmDashValidationError(\"Content type is required\");\n\t\t}\n\n\t\tconst tableName = getTableName(type);\n\n\t\t// Resolve translation_group: if translationOf is set, look up the source item's group\n\t\tlet translationGroup: string = id; // default: self-reference\n\t\tif (translationOf) {\n\t\t\tconst source = await this.findById(type, translationOf);\n\t\t\tif (!source) {\n\t\t\t\tthrow new EmDashValidationError(\"Translation source content not found\");\n\t\t\t}\n\t\t\ttranslationGroup = source.translationGroup || source.id;\n\t\t}\n\n\t\t// Build column names and values\n\t\tconst columns: string[] = [\n\t\t\t\"id\",\n\t\t\t\"slug\",\n\t\t\t\"status\",\n\t\t\t\"author_id\",\n\t\t\t\"primary_byline_id\",\n\t\t\t\"created_at\",\n\t\t\t\"updated_at\",\n\t\t\t\"published_at\",\n\t\t\t\"version\",\n\t\t\t\"locale\",\n\t\t\t\"translation_group\",\n\t\t];\n\t\tconst values: unknown[] = [\n\t\t\tid,\n\t\t\tslug || null,\n\t\t\tstatus,\n\t\t\tauthorId || null,\n\t\t\tprimaryBylineId ?? null,\n\t\t\tcreatedAt || now,\n\t\t\tnow,\n\t\t\tpublishedAt || null,\n\t\t\t1,\n\t\t\tlocale || \"en\",\n\t\t\ttranslationGroup,\n\t\t];\n\n\t\t// Add data fields as columns (skip system columns to prevent injection via data)\n\t\tif (data && typeof data === \"object\") {\n\t\t\tfor (const [key, value] of Object.entries(data)) {\n\t\t\t\tif (!SYSTEM_COLUMNS.has(key)) {\n\t\t\t\t\tvalidateIdentifier(key, \"content field name\");\n\t\t\t\t\tcolumns.push(key);\n\t\t\t\t\tvalues.push(serializeValue(value));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Build dynamic INSERT using raw SQL\n\t\tconst columnRefs = columns.map((c) => sql.ref(c));\n\t\tconst valuePlaceholders = values.map((v) => (v === null ? sql`NULL` : sql`${v}`));\n\n\t\tawait sql`\n\t\t\tINSERT INTO ${sql.ref(tableName)} (${sql.join(columnRefs, sql`, `)})\n\t\t\tVALUES (${sql.join(valuePlaceholders, sql`, `)})\n\t\t`.execute(this.db);\n\n\t\t// Fetch and return the created item\n\t\tconst item = await this.findById(type, id);\n\t\tif (!item) {\n\t\t\tthrow new Error(\"Failed to create content\");\n\t\t}\n\t\treturn item;\n\t}\n\n\t/**\n\t * Generate a unique slug for a content item within a collection.\n\t *\n\t * Checks the collection table for existing slugs that match `baseSlug`\n\t * (optionally scoped to a locale) and appends a numeric suffix (`-1`,\n\t * `-2`, etc.) on collision to guarantee uniqueness.\n\t *\n\t * Returns `null` if `baseSlug` is empty after slugification.\n\t */\n\tasync generateUniqueSlug(type: string, text: string, locale?: string): Promise<string | null> {\n\t\tconst baseSlug = slugify(text);\n\t\tif (!baseSlug) return null;\n\n\t\tconst tableName = getTableName(type);\n\n\t\t// Check if the base slug is available\n\t\tconst existing = locale\n\t\t\t? await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${baseSlug}\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${baseSlug}\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db);\n\n\t\tif (existing.rows.length === 0) {\n\t\t\treturn baseSlug;\n\t\t}\n\n\t\t// Find all slugs matching the pattern `baseSlug` or `baseSlug-N`\n\t\tconst pattern = `${baseSlug}-%`;\n\t\tconst candidates = locale\n\t\t\t? await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE (slug = ${baseSlug} OR slug LIKE ${pattern})\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${baseSlug} OR slug LIKE ${pattern}\n\t\t\t\t`.execute(this.db);\n\n\t\t// Find the highest numeric suffix in use\n\t\tlet maxSuffix = 0;\n\t\tconst suffixPattern = new RegExp(`^${escapeRegExp(baseSlug)}-(\\\\d+)$`);\n\t\tfor (const row of candidates.rows) {\n\t\t\tconst match = suffixPattern.exec(row.slug);\n\t\t\tif (match) {\n\t\t\t\tconst n = parseInt(match[1], 10);\n\t\t\t\tif (n > maxSuffix) maxSuffix = n;\n\t\t\t}\n\t\t}\n\n\t\treturn `${baseSlug}-${maxSuffix + 1}`;\n\t}\n\n\t/**\n\t * Duplicate a content item\n\t * Creates a new draft copy with \"(Copy)\" appended to the title.\n\t * A slug is auto-generated from the new title by the handler layer.\n\t */\n\tasync duplicate(type: string, id: string, authorId?: string): Promise<ContentItem> {\n\t\t// Fetch the original item\n\t\tconst original = await this.findById(type, id);\n\t\tif (!original) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// Prepare the new data\n\t\tconst newData = { ...original.data };\n\n\t\t// Append \"(Copy)\" to title if present\n\t\tif (typeof newData.title === \"string\") {\n\t\t\tnewData.title = `${newData.title} (Copy)`;\n\t\t} else if (typeof newData.name === \"string\") {\n\t\t\tnewData.name = `${newData.name} (Copy)`;\n\t\t}\n\n\t\t// Auto-generate a unique slug from the new title/name\n\t\tconst slugSource =\n\t\t\ttypeof newData.title === \"string\"\n\t\t\t\t? newData.title\n\t\t\t\t: typeof newData.name === \"string\"\n\t\t\t\t\t? newData.name\n\t\t\t\t\t: null;\n\n\t\tconst slug = slugSource\n\t\t\t? await this.generateUniqueSlug(type, slugSource, original.locale ?? undefined)\n\t\t\t: null;\n\n\t\t// Create the duplicate as a draft — use override authorId if provided (caller owns the copy)\n\t\treturn this.create({\n\t\t\ttype,\n\t\t\tslug,\n\t\t\tdata: newData,\n\t\t\tstatus: \"draft\",\n\t\t\tauthorId: authorId || original.authorId || undefined,\n\t\t});\n\t}\n\n\t/**\n\t * Find content by ID\n\t */\n\tasync findById(type: string, id: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find content by id, including trashed (soft-deleted) items.\n\t * Used by restore endpoint for ownership checks.\n\t */\n\tasync findByIdIncludingTrashed(type: string, id: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE id = ${id}\n\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find content by ID or slug. Tries ID first if it looks like a ULID,\n\t * otherwise tries slug. Falls back to the other if the first lookup misses.\n\t */\n\tasync findByIdOrSlug(\n\t\ttype: string,\n\t\tidentifier: string,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\treturn this._findByIdOrSlug(type, identifier, false, locale);\n\t}\n\n\t/**\n\t * Find content by ID or slug, including trashed (soft-deleted) items.\n\t * Used by restore/permanent-delete endpoints.\n\t */\n\tasync findByIdOrSlugIncludingTrashed(\n\t\ttype: string,\n\t\tidentifier: string,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\treturn this._findByIdOrSlug(type, identifier, true, locale);\n\t}\n\n\tprivate async _findByIdOrSlug(\n\t\ttype: string,\n\t\tidentifier: string,\n\t\tincludeTrashed: boolean,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\t// ULIDs are 26 uppercase alphanumeric chars\n\t\tconst looksLikeUlid = ULID_PATTERN.test(identifier);\n\n\t\tconst findById = includeTrashed\n\t\t\t? (t: string, id: string) => this.findByIdIncludingTrashed(t, id)\n\t\t\t: (t: string, id: string) => this.findById(t, id);\n\t\tconst findBySlug = includeTrashed\n\t\t\t? (t: string, s: string) => this.findBySlugIncludingTrashed(t, s, locale)\n\t\t\t: (t: string, s: string) => this.findBySlug(t, s, locale);\n\n\t\tif (looksLikeUlid) {\n\t\t\t// Try ID first, fall back to slug\n\t\t\tconst byId = await findById(type, identifier);\n\t\t\tif (byId) return byId;\n\t\t\treturn findBySlug(type, identifier);\n\t\t}\n\t\t// Try slug first, fall back to ID\n\t\tconst bySlug = await findBySlug(type, identifier);\n\t\tif (bySlug) return bySlug;\n\t\treturn findById(type, identifier);\n\t}\n\n\t/**\n\t * Find content by slug\n\t */\n\tasync findBySlug(type: string, slug: string, locale?: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = locale\n\t\t\t? await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t\tORDER BY locale ASC\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find content by slug, including trashed (soft-deleted) items.\n\t * Used by restore/permanent-delete endpoints.\n\t */\n\tasync findBySlugIncludingTrashed(\n\t\ttype: string,\n\t\tslug: string,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = locale\n\t\t\t? await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tORDER BY locale ASC\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find many content items with filtering and pagination\n\t */\n\tasync findMany(\n\t\ttype: string,\n\t\toptions: FindManyOptions = {},\n\t): Promise<FindManyResult<ContentItem>> {\n\t\tconst tableName = getTableName(type);\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\t// Determine ordering\n\t\tconst orderField = options.orderBy?.field || \"createdAt\";\n\t\tconst orderDirection = options.orderBy?.direction || \"desc\";\n\t\tconst dbField = this.mapOrderField(orderField);\n\n\t\t// Validate order direction to prevent injection\n\t\tconst safeOrderDirection = orderDirection.toLowerCase() === \"asc\" ? \"ASC\" : \"DESC\";\n\n\t\t// Build query with parameterized values (no string interpolation)\n\t\t// Note: Dynamic content tables have deleted_at column, cast needed for Kysely\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.selectAll()\n\t\t\t.where(\"deleted_at\" as never, \"is\", null);\n\n\t\t// Apply filters with parameterized queries\n\t\tif (options.where?.status) {\n\t\t\tquery = query.where(\"status\", \"=\", options.where.status);\n\t\t}\n\n\t\tif (options.where?.authorId) {\n\t\t\tquery = query.where(\"author_id\", \"=\", options.where.authorId);\n\t\t}\n\n\t\tif (options.where?.locale) {\n\t\t\tquery = query.where(\"locale\" as any, \"=\", options.where.locale);\n\t\t}\n\n\t\t// Handle cursor pagination — decodeCursor throws InvalidCursorError\n\t\t// on malformed input; let it propagate so handlers surface a\n\t\t// structured INVALID_CURSOR rather than silently returning page 1.\n\t\tif (options.cursor) {\n\t\t\tconst { orderValue, id: cursorId } = decodeCursor(options.cursor);\n\n\t\t\tif (safeOrderDirection === \"DESC\") {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \"<\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \"<\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \">\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \">\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Apply ordering and limit\n\t\tquery = query\n\t\t\t.orderBy(dbField as any, safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.orderBy(\"id\", safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit);\n\n\t\tconst mappedResult: FindManyResult<ContentItem> = {\n\t\t\titems: items.map((row) => this.mapRow(type, row as Record<string, unknown>)),\n\t\t};\n\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst lastRow = items.at(-1) as Record<string, unknown>;\n\t\t\tconst lastOrderValue = lastRow[dbField];\n\t\t\tconst orderStr =\n\t\t\t\ttypeof lastOrderValue === \"string\" || typeof lastOrderValue === \"number\"\n\t\t\t\t\t? String(lastOrderValue)\n\t\t\t\t\t: \"\";\n\t\t\tmappedResult.nextCursor = encodeCursor(orderStr, String(lastRow.id));\n\t\t}\n\n\t\treturn mappedResult;\n\t}\n\n\t/**\n\t * Update content\n\t */\n\tasync update(type: string, id: string, input: UpdateContentInput): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\t// Build update object with parameterized values\n\t\tconst updates: Record<string, unknown> = {\n\t\t\tupdated_at: now,\n\t\t\tversion: sql`version + 1`,\n\t\t};\n\n\t\tif (input.status !== undefined) {\n\t\t\tupdates.status = input.status;\n\t\t}\n\n\t\tif (input.slug !== undefined) {\n\t\t\tupdates.slug = input.slug;\n\t\t}\n\n\t\tif (input.publishedAt !== undefined) {\n\t\t\tupdates.published_at = input.publishedAt;\n\t\t}\n\n\t\tif (input.scheduledAt !== undefined) {\n\t\t\tupdates.scheduled_at = input.scheduledAt;\n\t\t}\n\n\t\tif (input.authorId !== undefined) {\n\t\t\tupdates.author_id = input.authorId;\n\t\t}\n\n\t\tif (input.primaryBylineId !== undefined) {\n\t\t\tupdates.primary_byline_id = input.primaryBylineId;\n\t\t}\n\n\t\t// Update data fields (skip system columns to prevent injection via data)\n\t\tif (input.data !== undefined && typeof input.data === \"object\") {\n\t\t\tfor (const [key, value] of Object.entries(input.data)) {\n\t\t\t\tif (!SYSTEM_COLUMNS.has(key)) {\n\t\t\t\t\tvalidateIdentifier(key, \"content field name\");\n\t\t\t\t\tupdates[key] = serializeValue(value);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tawait this.db\n\t\t\t.updateTable(tableName as keyof Database)\n\t\t\t.set(updates)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.where(\"deleted_at\" as never, \"is\", null)\n\t\t\t.execute();\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Delete content (soft delete - moves to trash)\n\t */\n\tasync delete(type: string, id: string): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET deleted_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\treturn (result.numAffectedRows ?? 0n) > 0n;\n\t}\n\n\t/**\n\t * Restore content from trash\n\t */\n\tasync restore(type: string, id: string): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET deleted_at = NULL\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NOT NULL\n\t\t`.execute(this.db);\n\n\t\treturn (result.numAffectedRows ?? 0n) > 0n;\n\t}\n\n\t/**\n\t * Permanently delete content (cannot be undone)\n\t */\n\t/**\n\t * Permanently delete a soft-deleted content row.\n\t *\n\t * Returns `true` only when a soft-deleted (trashed) row was removed.\n\t * Returns `false` when no row exists OR when the row exists but is live —\n\t * the caller is responsible for distinguishing these cases (typically via\n\t * a follow-up `findByIdOrSlugIncludingTrashed` to surface NOT_FOUND vs\n\t * NOT_TRASHED). The `AND deleted_at IS NOT NULL` clause is the safety net\n\t * that prevents permanent delete from bypassing the trash workflow.\n\t */\n\tasync permanentDelete(type: string, id: string): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql`\n\t\t\tDELETE FROM ${sql.ref(tableName)}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NOT NULL\n\t\t`.execute(this.db);\n\n\t\treturn (result.numAffectedRows ?? 0n) > 0n;\n\t}\n\n\t/**\n\t * Find trashed content items\n\t */\n\tasync findTrashed(\n\t\ttype: string,\n\t\toptions: Omit<FindManyOptions, \"where\"> = {},\n\t): Promise<FindManyResult<ContentItem & { deletedAt: string }>> {\n\t\tconst tableName = getTableName(type);\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\t// Determine ordering - default to most recently deleted\n\t\tconst orderField = options.orderBy?.field || \"deletedAt\";\n\t\tconst orderDirection = options.orderBy?.direction || \"desc\";\n\t\tconst dbField = this.mapOrderField(orderField);\n\n\t\tconst safeOrderDirection = orderDirection.toLowerCase() === \"asc\" ? \"ASC\" : \"DESC\";\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.selectAll()\n\t\t\t.where(\"deleted_at\" as never, \"is not\", null);\n\n\t\t// Handle cursor pagination — decodeCursor throws on invalid input.\n\t\tif (options.cursor) {\n\t\t\tconst { orderValue, id: cursorId } = decodeCursor(options.cursor);\n\n\t\t\tif (safeOrderDirection === \"DESC\") {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \"<\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \"<\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \">\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \">\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(dbField as any, safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.orderBy(\"id\", safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit);\n\n\t\tconst mappedResult: FindManyResult<ContentItem & { deletedAt: string }> = {\n\t\t\titems: items.map((row) => {\n\t\t\t\tconst record = row as Record<string, unknown>;\n\t\t\t\treturn {\n\t\t\t\t\t...this.mapRow(type, record),\n\t\t\t\t\tdeletedAt: typeof record.deleted_at === \"string\" ? record.deleted_at : \"\",\n\t\t\t\t};\n\t\t\t}),\n\t\t};\n\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst lastRow = items.at(-1) as Record<string, unknown>;\n\t\t\tconst lastOrderValue = lastRow[dbField];\n\t\t\tconst orderStr =\n\t\t\t\ttypeof lastOrderValue === \"string\" || typeof lastOrderValue === \"number\"\n\t\t\t\t\t? String(lastOrderValue)\n\t\t\t\t\t: \"\";\n\t\t\tmappedResult.nextCursor = encodeCursor(orderStr, String(lastRow.id));\n\t\t}\n\n\t\treturn mappedResult;\n\t}\n\n\t/**\n\t * Count trashed content items\n\t */\n\tasync countTrashed(type: string): Promise<number> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"deleted_at\" as never, \"is not\", null)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Count content items\n\t */\n\tasync count(\n\t\ttype: string,\n\t\twhere?: { status?: string; authorId?: string; locale?: string },\n\t): Promise<number> {\n\t\tconst tableName = getTableName(type);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"deleted_at\" as never, \"is\", null);\n\n\t\tif (where?.status) {\n\t\t\tquery = query.where(\"status\", \"=\", where.status);\n\t\t}\n\n\t\tif (where?.authorId) {\n\t\t\tquery = query.where(\"author_id\", \"=\", where.authorId);\n\t\t}\n\n\t\tif (where?.locale) {\n\t\t\tquery = query.where(\"locale\" as any, \"=\", where.locale);\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t// get overall statistics (total, published, draft) for a content type in a single query\n\tasync getStats(type: string): Promise<{ total: number; published: number; draft: number }> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select((eb) => [\n\t\t\t\teb.fn.count(\"id\").as(\"total\"),\n\t\t\t\teb.fn.sum(eb.case().when(\"status\", \"=\", \"published\").then(1).else(0).end()).as(\"published\"),\n\t\t\t\teb.fn.sum(eb.case().when(\"status\", \"=\", \"draft\").then(1).else(0).end()).as(\"draft\"),\n\t\t\t])\n\t\t\t.where(\"deleted_at\" as never, \"is\", null)\n\t\t\t.executeTakeFirst();\n\n\t\treturn {\n\t\t\ttotal: Number(result?.total || 0),\n\t\t\tpublished: Number(result?.published || 0),\n\t\t\tdraft: Number(result?.draft || 0),\n\t\t};\n\t}\n\n\t/**\n\t * Schedule content for future publishing\n\t *\n\t * Sets status to 'scheduled' and stores the scheduled publish time.\n\t * The content will be auto-published when the scheduled time is reached.\n\t */\n\tasync schedule(type: string, id: string, scheduledAt: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\t// Validate scheduledAt is in the future\n\t\tconst scheduledDate = new Date(scheduledAt);\n\t\tif (isNaN(scheduledDate.getTime())) {\n\t\t\tthrow new EmDashValidationError(\"Invalid scheduled date\");\n\t\t}\n\t\tif (scheduledDate <= new Date()) {\n\t\t\tthrow new EmDashValidationError(\"Scheduled date must be in the future\");\n\t\t}\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// Published posts keep their status — the schedule applies to the\n\t\t// pending draft, not the currently-live revision. Unpublished posts\n\t\t// transition to 'scheduled' so they aren't visible before the time.\n\t\tconst newStatus = existing.status === \"published\" ? \"published\" : \"scheduled\";\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET status = ${newStatus},\n\t\t\t\tscheduled_at = ${scheduledAt},\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Unschedule content\n\t *\n\t * Clears the scheduled time. Published posts stay published;\n\t * draft/scheduled posts revert to 'draft'.\n\t */\n\tasync unschedule(type: string, id: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// Published posts keep their status — just clear the pending schedule.\n\t\t// Draft/scheduled posts revert to 'draft'.\n\t\tconst newStatus = existing.status === \"published\" ? \"published\" : \"draft\";\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET status = ${newStatus},\n\t\t\t\tscheduled_at = NULL,\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND scheduled_at IS NOT NULL\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Find content that is ready to be published\n\t *\n\t * Returns all content where scheduled_at <= now, regardless of status.\n\t * This covers both draft-scheduled posts (status='scheduled') and\n\t * published posts with scheduled draft changes (status='published').\n\t */\n\tasync findReadyToPublish(type: string): Promise<ContentItem[]> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE scheduled_at IS NOT NULL\n\t\t\tAND scheduled_at <= ${now}\n\t\t\tAND deleted_at IS NULL\n\t\t\tORDER BY scheduled_at ASC\n\t\t`.execute(this.db);\n\n\t\treturn result.rows.map((row) => this.mapRow(type, row));\n\t}\n\n\t/**\n\t * Find all translations in a translation group\n\t */\n\tasync findTranslations(type: string, translationGroup: string): Promise<ContentItem[]> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE translation_group = ${translationGroup}\n\t\t\tAND deleted_at IS NULL\n\t\t\tORDER BY locale ASC\n\t\t`.execute(this.db);\n\n\t\treturn result.rows.map((row) => this.mapRow(type, row));\n\t}\n\n\t/**\n\t * Publish the current draft\n\t *\n\t * Promotes draft_revision_id to live_revision_id and clears draft pointer.\n\t * Syncs the draft revision's data into the content table columns so the\n\t * content table always reflects the published version.\n\t * If no draft revision exists, creates one from current data and publishes it.\n\t *\n\t * `publishedAt` (optional) overrides the publication timestamp. If omitted,\n\t * the existing `published_at` is preserved (idempotent re-publish keeps the\n\t * original date) and falls back to the current time on first publish. Pass\n\t * an explicit value to backdate a publish (e.g. when migrating content from\n\t * another CMS).\n\t */\n\tasync publish(type: string, id: string, publishedAt?: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tlet revisionToPublish = existing.draftRevisionId || existing.liveRevisionId;\n\n\t\tif (!revisionToPublish) {\n\t\t\t// No revision exists - create one from current data\n\t\t\tconst revision = await revisionRepo.create({\n\t\t\t\tcollection: type,\n\t\t\t\tentryId: id,\n\t\t\t\tdata: existing.data,\n\t\t\t});\n\t\t\trevisionToPublish = revision.id;\n\t\t}\n\n\t\t// Sync the revision's data into the content table columns\n\t\t// so the content table always holds the published version\n\t\tconst revision = await revisionRepo.findById(revisionToPublish);\n\t\tif (revision) {\n\t\t\tawait this.syncDataColumns(type, id, revision.data);\n\n\t\t\t// Sync slug from revision if stored there\n\t\t\tif (typeof revision.data._slug === \"string\") {\n\t\t\t\tawait sql`\n\t\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\t\tSET slug = ${revision.data._slug}\n\t\t\t\t\tWHERE id = ${id}\n\t\t\t\t`.execute(this.db);\n\t\t\t}\n\t\t}\n\n\t\tif (publishedAt !== undefined) {\n\t\t\t// Caller supplied an explicit timestamp, so we overwrite published_at\n\t\t\t// directly (used to backdate a publish, e.g. for content migrations).\n\t\t\tawait sql`\n\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\tSET live_revision_id = ${revisionToPublish},\n\t\t\t\t\tdraft_revision_id = NULL,\n\t\t\t\t\tstatus = 'published',\n\t\t\t\t\tscheduled_at = NULL,\n\t\t\t\t\tpublished_at = ${publishedAt},\n\t\t\t\t\tupdated_at = ${now}\n\t\t\t\tWHERE id = ${id}\n\t\t\t\tAND deleted_at IS NULL\n\t\t\t`.execute(this.db);\n\t\t} else {\n\t\t\t// No timestamp supplied — preserve existing published_at on\n\t\t\t// idempotent re-publish, fall back to `now` on first publish.\n\t\t\tawait sql`\n\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\tSET live_revision_id = ${revisionToPublish},\n\t\t\t\t\tdraft_revision_id = NULL,\n\t\t\t\t\tstatus = 'published',\n\t\t\t\t\tscheduled_at = NULL,\n\t\t\t\t\tpublished_at = COALESCE(published_at, ${now}),\n\t\t\t\t\tupdated_at = ${now}\n\t\t\t\tWHERE id = ${id}\n\t\t\t\tAND deleted_at IS NULL\n\t\t\t`.execute(this.db);\n\t\t}\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Unpublish content\n\t *\n\t * Removes live pointer but preserves draft. If no draft exists,\n\t * creates one from the live version so the content isn't lost.\n\t */\n\tasync unpublish(type: string, id: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// If no draft exists, create one from the live version\n\t\tif (!existing.draftRevisionId && existing.liveRevisionId) {\n\t\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\t\tconst liveRevision = await revisionRepo.findById(existing.liveRevisionId);\n\t\t\tif (liveRevision) {\n\t\t\t\tconst draft = await revisionRepo.create({\n\t\t\t\t\tcollection: type,\n\t\t\t\t\tentryId: id,\n\t\t\t\t\tdata: liveRevision.data,\n\t\t\t\t});\n\n\t\t\t\tawait sql`\n\t\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\t\tSET draft_revision_id = ${draft.id}\n\t\t\t\t\tWHERE id = ${id}\n\t\t\t\t`.execute(this.db);\n\t\t\t}\n\t\t}\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET live_revision_id = NULL,\n\t\t\t\tstatus = 'draft',\n\t\t\t\tpublished_at = NULL,\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Set the draft revision pointer for a content item.\n\t *\n\t * Used by seed/import paths that stage a new revision's data before\n\t * promoting it to live via `publish()`.\n\t *\n\t * Validates that the content item exists and is not soft-deleted, that\n\t * the revision exists, and that the revision belongs to the same\n\t * collection and entry. Without these checks, a caller could leave the\n\t * content row pointing at a missing or unrelated revision.\n\t */\n\tasync setDraftRevision(type: string, id: string, revisionId: string): Promise<void> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tconst revision = await revisionRepo.findById(revisionId);\n\t\tif (!revision) {\n\t\t\tthrow new EmDashValidationError(\"Revision not found\");\n\t\t}\n\n\t\tif (revision.collection !== type || revision.entryId !== id) {\n\t\t\tthrow new EmDashValidationError(\"Revision does not belong to the specified content item\");\n\t\t}\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET draft_revision_id = ${revisionId},\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\t}\n\n\t/**\n\t * Discard pending draft changes\n\t *\n\t * Clears draft_revision_id. The content table columns already hold the\n\t * published version, so no data sync is needed.\n\t */\n\tasync discardDraft(type: string, id: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\tif (!existing.draftRevisionId) {\n\t\t\t// No draft to discard\n\t\t\treturn existing;\n\t\t}\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET draft_revision_id = NULL,\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Sync data columns in the content table from a data object.\n\t * Used to promote revision data into the content table on publish.\n\t * Keys starting with _ are revision metadata (e.g. _slug) and are skipped.\n\t */\n\tprivate async syncDataColumns(\n\t\ttype: string,\n\t\tid: string,\n\t\tdata: Record<string, unknown>,\n\t): Promise<void> {\n\t\tconst tableName = getTableName(type);\n\t\tconst updates: Record<string, unknown> = {};\n\n\t\tfor (const [key, value] of Object.entries(data)) {\n\t\t\tif (SYSTEM_COLUMNS.has(key)) continue;\n\t\t\tif (key.startsWith(\"_\")) continue; // revision metadata\n\t\t\tvalidateIdentifier(key, \"content field name\");\n\t\t\tupdates[key] = serializeValue(value);\n\t\t}\n\n\t\tif (Object.keys(updates).length === 0) return;\n\n\t\tawait this.db\n\t\t\t.updateTable(tableName as keyof Database)\n\t\t\t.set(updates)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\t}\n\n\t/**\n\t * Count content items with a pending schedule.\n\t * Includes both draft-scheduled (status='scheduled') and published\n\t * posts with scheduled draft changes (status='published', scheduled_at set).\n\t */\n\tasync countScheduled(type: string): Promise<number> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<{ count: number }>`\n\t\t\tSELECT COUNT(id) as count FROM ${sql.ref(tableName)}\n\t\t\tWHERE scheduled_at IS NOT NULL\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\treturn Number(result.rows[0]?.count || 0);\n\t}\n\n\t/**\n\t * Map database row to ContentItem\n\t * Extracts system columns and puts content fields in data\n\t * Excludes null values from data to match input semantics\n\t */\n\tprivate mapRow(type: string, row: Record<string, unknown>): ContentItem {\n\t\tconst data: Record<string, unknown> = {};\n\n\t\tfor (const [key, value] of Object.entries(row)) {\n\t\t\tif (!SYSTEM_COLUMNS.has(key) && value !== null) {\n\t\t\t\tdata[key] = deserializeValue(value);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tid: row.id as string,\n\t\t\ttype,\n\t\t\tslug: row.slug as string | null,\n\t\t\tstatus: row.status as string,\n\t\t\tdata,\n\t\t\tauthorId: row.author_id as string | null,\n\t\t\tprimaryBylineId: (row.primary_byline_id as string | null) ?? null,\n\t\t\tcreatedAt: row.created_at as string,\n\t\t\tupdatedAt: row.updated_at as string,\n\t\t\tpublishedAt: row.published_at as string | null,\n\t\t\tscheduledAt: row.scheduled_at as string | null,\n\t\t\tliveRevisionId: (row.live_revision_id as string | null) ?? null,\n\t\t\tdraftRevisionId: (row.draft_revision_id as string | null) ?? null,\n\t\t\tversion: typeof row.version === \"number\" ? row.version : 1,\n\t\t\tlocale: (row.locale as string) ?? null,\n\t\t\ttranslationGroup: (row.translation_group as string) ?? null,\n\t\t};\n\t}\n\n\t/**\n\t * Map order field names to database columns.\n\t * Only allows known fields to prevent column enumeration via crafted orderBy values.\n\t */\n\tprivate mapOrderField(field: string): string {\n\t\tconst mapping: Record<string, string> = {\n\t\t\tcreatedAt: \"created_at\",\n\t\t\tupdatedAt: \"updated_at\",\n\t\t\tpublishedAt: \"published_at\",\n\t\t\tscheduledAt: \"scheduled_at\",\n\t\t\tdeletedAt: \"deleted_at\",\n\t\t\ttitle: \"title\",\n\t\t\tname: \"name\",\n\t\t\tslug: \"slug\",\n\t\t\tstatus: \"status\",\n\t\t\tlocale: \"locale\",\n\t\t};\n\n\t\tconst mapped = mapping[field];\n\t\tif (!mapped) {\n\t\t\tthrow new EmDashValidationError(`Invalid order field: ${field}`);\n\t\t}\n\t\treturn mapped;\n\t}\n}\n"],"mappings":";;;;;;;AACA,MAAM,qBAAqB;AAC3B,MAAM,gCAAgC;AACtC,MAAM,kCAAkC;AACxC,MAAM,2BAA2B;AACjC,MAAM,kCAAkC;AACxC,MAAM,0BAA0B;;;;;;;;;;;;;;AAehC,SAAgB,WAAW,KAA6C;AACvE,QAAO,MAAM,mBAAmB,IAAI,GAAG;;AAGxC,SAAgB,QAAQ,MAAc,YAAoB,IAAY;AACrE,QACC,KACE,aAAa,CACb,UAAU,MAAM,CAChB,QAAQ,oBAAoB,GAAG,CAC/B,QAAQ,+BAA+B,IAAI,CAC3C,QAAQ,iCAAiC,GAAG,CAC5C,QAAQ,0BAA0B,IAAI,CACtC,QAAQ,iCAAiC,GAAG,CAC5C,MAAM,GAAG,UAAU,CAEnB,QAAQ,yBAAyB,GAAG;;;;;AChCxC,MAAM,YAAY,kBAAkB;;;;;;;AAwBpC,IAAa,qBAAb,MAAgC;CAC/B,YAAY,AAAQ,IAAsB;EAAtB;;;;;CAKpB,MAAM,OAAO,OAA+C;EAC3D,MAAM,KAAK,WAAW;EAEtB,MAAM,MAAyC;GAC9C;GACA,YAAY,MAAM;GAClB,UAAU,MAAM;GAChB,MAAM,KAAK,UAAU,MAAM,KAAK;GAChC,WAAW,MAAM,YAAY;GAC7B;AAED,QAAM,KAAK,GAAG,WAAW,YAAY,CAAC,OAAO,IAAI,CAAC,SAAS;EAE3D,MAAM,WAAW,MAAM,KAAK,SAAS,GAAG;AACxC,MAAI,CAAC,SACJ,OAAM,IAAI,MAAM,4BAA4B;AAE7C,SAAO;;;;;CAMR,MAAM,SAAS,IAAsC;EACpD,MAAM,MAAM,MAAM,KAAK,GACrB,WAAW,YAAY,CACvB,WAAW,CACX,MAAM,MAAM,KAAK,GAAG,CACpB,kBAAkB;AAEpB,SAAO,MAAM,KAAK,cAAc,IAAI,GAAG;;;;;;;;CASxC,MAAM,YACL,YACA,SACA,UAA8B,EAAE,EACV;EACtB,IAAI,QAAQ,KAAK,GACf,WAAW,YAAY,CACvB,WAAW,CACX,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,YAAY,KAAK,QAAQ,CAC/B,QAAQ,MAAM,OAAO;AAEvB,MAAI,QAAQ,MACX,SAAQ,MAAM,MAAM,QAAQ,MAAM;AAInC,UADa,MAAM,MAAM,SAAS,EACtB,KAAK,QAAQ,KAAK,cAAc,IAAI,CAAC;;;;;CAMlD,MAAM,WAAW,YAAoB,SAA2C;EAC/E,MAAM,MAAM,MAAM,KAAK,GACrB,WAAW,YAAY,CACvB,WAAW,CACX,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,YAAY,KAAK,QAAQ,CAC/B,QAAQ,MAAM,OAAO,CACrB,MAAM,EAAE,CACR,kBAAkB;AAEpB,SAAO,MAAM,KAAK,cAAc,IAAI,GAAG;;;;;CAMxC,MAAM,aAAa,YAAoB,SAAkC;EACxE,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,YAAY,CACvB,QAAQ,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC,CAC7C,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,YAAY,KAAK,QAAQ,CAC/B,kBAAkB;AAEpB,SAAO,OAAO,QAAQ,SAAS,EAAE;;;;;CAMlC,MAAM,cAAc,YAAoB,SAAkC;EACzE,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,YAAY,CACvB,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,YAAY,KAAK,QAAQ,CAC/B,kBAAkB;AAEpB,SAAO,OAAO,OAAO,kBAAkB,EAAE;;;;;CAM1C,MAAM,kBAAkB,YAAoB,SAAiB,WAAoC;EAYhG,MAAM,WAVO,MAAM,KAAK,GACtB,WAAW,YAAY,CACvB,OAAO,KAAK,CACZ,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,YAAY,KAAK,QAAQ,CAC/B,QAAQ,cAAc,OAAO,CAC7B,QAAQ,MAAM,OAAO,CACrB,MAAM,UAAU,CAChB,SAAS,EAEU,KAAK,MAAM,EAAE,GAAG;AAErC,MAAI,QAAQ,WAAW,EAAG,QAAO;EAGjC,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,YAAY,CACvB,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,YAAY,KAAK,QAAQ,CAC/B,MAAM,MAAM,UAAU,QAAQ,CAC9B,kBAAkB;AAEpB,SAAO,OAAO,OAAO,kBAAkB,EAAE;;;;;;CAO1C,MAAM,WAAW,IAAY,MAA8C;AAC1E,QAAM,KAAK,GACT,YAAY,YAAY,CACxB,IAAI,EAAE,MAAM,KAAK,UAAU,KAAK,EAAE,CAAC,CACnC,MAAM,MAAM,KAAK,GAAG,CACpB,SAAS;;;;;CAMZ,AAAQ,cAAc,KAOT;AACZ,SAAO;GACN,IAAI,IAAI;GACR,YAAY,IAAI;GAChB,SAAS,IAAI;GACb,MAAM,KAAK,MAAM,IAAI,KAAK;GAC1B,UAAU,IAAI;GACd,WAAW,IAAI;GACf;;;;;;;ACpLH,MAAM,eAAe;;;;AAKrB,MAAM,iBAAiB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,CAAC;;;;AAKF,SAAS,aAAa,MAAsB;AAC3C,oBAAmB,MAAM,kBAAkB;AAC3C,QAAO,MAAM;;;;;;;AAQd,SAAS,eAAe,OAAyB;AAChD,KAAI,UAAU,QAAQ,UAAU,OAC/B,QAAO;AAER,KAAI,OAAO,UAAU,UACpB,QAAO,QAAQ,IAAI;AAEpB,KAAI,OAAO,UAAU,SACpB,QAAO,KAAK,UAAU,MAAM;AAE7B,QAAO;;;;;;AAOR,SAAS,iBAAiB,OAAyB;AAClD,KAAI,OAAO,UAAU,UAEpB;MAAI,MAAM,WAAW,IAAI,IAAI,MAAM,WAAW,IAAI,CACjD,KAAI;AACH,UAAO,KAAK,MAAM,MAAM;UACjB;AACP,UAAO;;;AAIV,QAAO;;;AAIR,MAAM,uBAAuB;;;;AAK7B,SAAS,aAAa,GAAmB;AACxC,QAAO,EAAE,QAAQ,sBAAsB,OAAO;;;;;;;;AAS/C,IAAa,oBAAb,MAA+B;CAC9B,YAAY,AAAQ,IAAsB;EAAtB;;;;;CAKpB,MAAM,OAAO,OAAiD;EAC7D,MAAM,KAAK,MAAM;EACjB,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAEpC,MAAM,EACL,MACA,MACA,MACA,SAAS,SACT,UACA,iBACA,QACA,eACA,aACA,cACG;AAGJ,MAAI,CAAC,KACJ,OAAM,IAAI,sBAAsB,2BAA2B;EAG5D,MAAM,YAAY,aAAa,KAAK;EAGpC,IAAI,mBAA2B;AAC/B,MAAI,eAAe;GAClB,MAAM,SAAS,MAAM,KAAK,SAAS,MAAM,cAAc;AACvD,OAAI,CAAC,OACJ,OAAM,IAAI,sBAAsB,uCAAuC;AAExE,sBAAmB,OAAO,oBAAoB,OAAO;;EAItD,MAAM,UAAoB;GACzB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACD,MAAM,SAAoB;GACzB;GACA,QAAQ;GACR;GACA,YAAY;GACZ,mBAAmB;GACnB,aAAa;GACb;GACA,eAAe;GACf;GACA,UAAU;GACV;GACA;AAGD,MAAI,QAAQ,OAAO,SAAS,UAC3B;QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,CAC9C,KAAI,CAAC,eAAe,IAAI,IAAI,EAAE;AAC7B,uBAAmB,KAAK,qBAAqB;AAC7C,YAAQ,KAAK,IAAI;AACjB,WAAO,KAAK,eAAe,MAAM,CAAC;;;EAMrC,MAAM,aAAa,QAAQ,KAAK,MAAM,IAAI,IAAI,EAAE,CAAC;EACjD,MAAM,oBAAoB,OAAO,KAAK,MAAO,MAAM,OAAO,GAAG,SAAS,GAAG,GAAG,IAAK;AAEjF,QAAM,GAAG;iBACM,IAAI,IAAI,UAAU,CAAC,IAAI,IAAI,KAAK,YAAY,GAAG,KAAK,CAAC;aACzD,IAAI,KAAK,mBAAmB,GAAG,KAAK,CAAC;IAC9C,QAAQ,KAAK,GAAG;EAGlB,MAAM,OAAO,MAAM,KAAK,SAAS,MAAM,GAAG;AAC1C,MAAI,CAAC,KACJ,OAAM,IAAI,MAAM,2BAA2B;AAE5C,SAAO;;;;;;;;;;;CAYR,MAAM,mBAAmB,MAAc,MAAc,QAAyC;EAC7F,MAAM,WAAW,QAAQ,KAAK;AAC9B,MAAI,CAAC,SAAU,QAAO;EAEtB,MAAM,YAAY,aAAa,KAAK;AAgBpC,OAbiB,SACd,MAAM,GAAqB;wBACR,IAAI,IAAI,UAAU,CAAC;oBACvB,SAAS;oBACT,OAAO;;MAErB,QAAQ,KAAK,GAAG,GACjB,MAAM,GAAqB;wBACR,IAAI,IAAI,UAAU,CAAC;oBACvB,SAAS;;MAEvB,QAAQ,KAAK,GAAG,EAEP,KAAK,WAAW,EAC5B,QAAO;EAIR,MAAM,UAAU,GAAG,SAAS;EAC5B,MAAM,aAAa,SAChB,MAAM,GAAqB;wBACR,IAAI,IAAI,UAAU,CAAC;qBACtB,SAAS,gBAAgB,QAAQ;oBAClC,OAAO;MACrB,QAAQ,KAAK,GAAG,GACjB,MAAM,GAAqB;wBACR,IAAI,IAAI,UAAU,CAAC;oBACvB,SAAS,gBAAgB,QAAQ;MAC/C,QAAQ,KAAK,GAAG;EAGpB,IAAI,YAAY;EAChB,MAAM,gBAAgB,IAAI,OAAO,IAAI,aAAa,SAAS,CAAC,UAAU;AACtE,OAAK,MAAM,OAAO,WAAW,MAAM;GAClC,MAAM,QAAQ,cAAc,KAAK,IAAI,KAAK;AAC1C,OAAI,OAAO;IACV,MAAM,IAAI,SAAS,MAAM,IAAI,GAAG;AAChC,QAAI,IAAI,UAAW,aAAY;;;AAIjC,SAAO,GAAG,SAAS,GAAG,YAAY;;;;;;;CAQnC,MAAM,UAAU,MAAc,IAAY,UAAyC;EAElF,MAAM,WAAW,MAAM,KAAK,SAAS,MAAM,GAAG;AAC9C,MAAI,CAAC,SACJ,OAAM,IAAI,sBAAsB,yBAAyB;EAI1D,MAAM,UAAU,EAAE,GAAG,SAAS,MAAM;AAGpC,MAAI,OAAO,QAAQ,UAAU,SAC5B,SAAQ,QAAQ,GAAG,QAAQ,MAAM;WACvB,OAAO,QAAQ,SAAS,SAClC,SAAQ,OAAO,GAAG,QAAQ,KAAK;EAIhC,MAAM,aACL,OAAO,QAAQ,UAAU,WACtB,QAAQ,QACR,OAAO,QAAQ,SAAS,WACvB,QAAQ,OACR;EAEL,MAAM,OAAO,aACV,MAAM,KAAK,mBAAmB,MAAM,YAAY,SAAS,UAAU,OAAU,GAC7E;AAGH,SAAO,KAAK,OAAO;GAClB;GACA;GACA,MAAM;GACN,QAAQ;GACR,UAAU,YAAY,SAAS,YAAY;GAC3C,CAAC;;;;;CAMH,MAAM,SAAS,MAAc,IAAyC;EACrE,MAAM,YAAY,aAAa,KAAK;EAQpC,MAAM,OANS,MAAM,GAA4B;mBAChC,IAAI,IAAI,UAAU,CAAC;gBACtB,GAAG;;IAEf,QAAQ,KAAK,GAAG,EAEC,KAAK;AACxB,MAAI,CAAC,IACJ,QAAO;AAGR,SAAO,KAAK,OAAO,MAAM,IAAI;;;;;;CAO9B,MAAM,yBAAyB,MAAc,IAAyC;EACrF,MAAM,YAAY,aAAa,KAAK;EAOpC,MAAM,OALS,MAAM,GAA4B;mBAChC,IAAI,IAAI,UAAU,CAAC;gBACtB,GAAG;IACf,QAAQ,KAAK,GAAG,EAEC,KAAK;AACxB,MAAI,CAAC,IACJ,QAAO;AAGR,SAAO,KAAK,OAAO,MAAM,IAAI;;;;;;CAO9B,MAAM,eACL,MACA,YACA,QAC8B;AAC9B,SAAO,KAAK,gBAAgB,MAAM,YAAY,OAAO,OAAO;;;;;;CAO7D,MAAM,+BACL,MACA,YACA,QAC8B;AAC9B,SAAO,KAAK,gBAAgB,MAAM,YAAY,MAAM,OAAO;;CAG5D,MAAc,gBACb,MACA,YACA,gBACA,QAC8B;EAE9B,MAAM,gBAAgB,aAAa,KAAK,WAAW;EAEnD,MAAM,WAAW,kBACb,GAAW,OAAe,KAAK,yBAAyB,GAAG,GAAG,IAC9D,GAAW,OAAe,KAAK,SAAS,GAAG,GAAG;EAClD,MAAM,aAAa,kBACf,GAAW,MAAc,KAAK,2BAA2B,GAAG,GAAG,OAAO,IACtE,GAAW,MAAc,KAAK,WAAW,GAAG,GAAG,OAAO;AAE1D,MAAI,eAAe;GAElB,MAAM,OAAO,MAAM,SAAS,MAAM,WAAW;AAC7C,OAAI,KAAM,QAAO;AACjB,UAAO,WAAW,MAAM,WAAW;;EAGpC,MAAM,SAAS,MAAM,WAAW,MAAM,WAAW;AACjD,MAAI,OAAQ,QAAO;AACnB,SAAO,SAAS,MAAM,WAAW;;;;;CAMlC,MAAM,WAAW,MAAc,MAAc,QAA8C;EAC1F,MAAM,YAAY,aAAa,KAAK;EAiBpC,MAAM,OAfS,SACZ,MAAM,GAA4B;qBAClB,IAAI,IAAI,UAAU,CAAC;oBACpB,KAAK;oBACL,OAAO;;MAErB,QAAQ,KAAK,GAAG,GACjB,MAAM,GAA4B;qBAClB,IAAI,IAAI,UAAU,CAAC;oBACpB,KAAK;;;;MAInB,QAAQ,KAAK,GAAG,EAED,KAAK;AACxB,MAAI,CAAC,IACJ,QAAO;AAGR,SAAO,KAAK,OAAO,MAAM,IAAI;;;;;;CAO9B,MAAM,2BACL,MACA,MACA,QAC8B;EAC9B,MAAM,YAAY,aAAa,KAAK;EAepC,MAAM,OAbS,SACZ,MAAM,GAA4B;qBAClB,IAAI,IAAI,UAAU,CAAC;oBACpB,KAAK;oBACL,OAAO;MACrB,QAAQ,KAAK,GAAG,GACjB,MAAM,GAA4B;qBAClB,IAAI,IAAI,UAAU,CAAC;oBACpB,KAAK;;;MAGnB,QAAQ,KAAK,GAAG,EAED,KAAK;AACxB,MAAI,CAAC,IACJ,QAAO;AAGR,SAAO,KAAK,OAAO,MAAM,IAAI;;;;;CAM9B,MAAM,SACL,MACA,UAA2B,EAAE,EACU;EACvC,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,QAAQ,KAAK,IAAI,QAAQ,SAAS,IAAI,IAAI;EAGhD,MAAM,aAAa,QAAQ,SAAS,SAAS;EAC7C,MAAM,iBAAiB,QAAQ,SAAS,aAAa;EACrD,MAAM,UAAU,KAAK,cAAc,WAAW;EAG9C,MAAM,qBAAqB,eAAe,aAAa,KAAK,QAAQ,QAAQ;EAI5E,IAAI,QAAQ,KAAK,GACf,WAAW,UAA4B,CACvC,WAAW,CACX,MAAM,cAAuB,MAAM,KAAK;AAG1C,MAAI,QAAQ,OAAO,OAClB,SAAQ,MAAM,MAAM,UAAU,KAAK,QAAQ,MAAM,OAAO;AAGzD,MAAI,QAAQ,OAAO,SAClB,SAAQ,MAAM,MAAM,aAAa,KAAK,QAAQ,MAAM,SAAS;AAG9D,MAAI,QAAQ,OAAO,OAClB,SAAQ,MAAM,MAAM,UAAiB,KAAK,QAAQ,MAAM,OAAO;AAMhE,MAAI,QAAQ,QAAQ;GACnB,MAAM,EAAE,YAAY,IAAI,aAAa,aAAa,QAAQ,OAAO;AAEjE,OAAI,uBAAuB,OAC1B,SAAQ,MAAM,OAAO,OACpB,GAAG,GAAG,CACL,GAAG,SAAgB,KAAK,WAAW,EACnC,GAAG,IAAI,CAAC,GAAG,SAAgB,KAAK,WAAW,EAAE,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CACtE,CAAC,CACF;OAED,SAAQ,MAAM,OAAO,OACpB,GAAG,GAAG,CACL,GAAG,SAAgB,KAAK,WAAW,EACnC,GAAG,IAAI,CAAC,GAAG,SAAgB,KAAK,WAAW,EAAE,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CACtE,CAAC,CACF;;AAKH,UAAQ,MACN,QAAQ,SAAgB,uBAAuB,QAAQ,QAAQ,OAAO,CACtE,QAAQ,MAAM,uBAAuB,QAAQ,QAAQ,OAAO,CAC5D,MAAM,QAAQ,EAAE;EAElB,MAAM,OAAO,MAAM,MAAM,SAAS;EAClC,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,QAAQ,KAAK,MAAM,GAAG,MAAM;EAElC,MAAM,eAA4C,EACjD,OAAO,MAAM,KAAK,QAAQ,KAAK,OAAO,MAAM,IAA+B,CAAC,EAC5E;AAED,MAAI,WAAW,MAAM,SAAS,GAAG;GAChC,MAAM,UAAU,MAAM,GAAG,GAAG;GAC5B,MAAM,iBAAiB,QAAQ;AAK/B,gBAAa,aAAa,aAHzB,OAAO,mBAAmB,YAAY,OAAO,mBAAmB,WAC7D,OAAO,eAAe,GACtB,IAC6C,OAAO,QAAQ,GAAG,CAAC;;AAGrE,SAAO;;;;;CAMR,MAAM,OAAO,MAAc,IAAY,OAAiD;EACvF,MAAM,YAAY,aAAa,KAAK;EAIpC,MAAM,UAAmC;GACxC,6BAJW,IAAI,MAAM,EAAC,aAAa;GAKnC,SAAS,GAAG;GACZ;AAED,MAAI,MAAM,WAAW,OACpB,SAAQ,SAAS,MAAM;AAGxB,MAAI,MAAM,SAAS,OAClB,SAAQ,OAAO,MAAM;AAGtB,MAAI,MAAM,gBAAgB,OACzB,SAAQ,eAAe,MAAM;AAG9B,MAAI,MAAM,gBAAgB,OACzB,SAAQ,eAAe,MAAM;AAG9B,MAAI,MAAM,aAAa,OACtB,SAAQ,YAAY,MAAM;AAG3B,MAAI,MAAM,oBAAoB,OAC7B,SAAQ,oBAAoB,MAAM;AAInC,MAAI,MAAM,SAAS,UAAa,OAAO,MAAM,SAAS,UACrD;QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,KAAK,CACpD,KAAI,CAAC,eAAe,IAAI,IAAI,EAAE;AAC7B,uBAAmB,KAAK,qBAAqB;AAC7C,YAAQ,OAAO,eAAe,MAAM;;;AAKvC,QAAM,KAAK,GACT,YAAY,UAA4B,CACxC,IAAI,QAAQ,CACZ,MAAM,MAAM,KAAK,GAAG,CACpB,MAAM,cAAuB,MAAM,KAAK,CACxC,SAAS;EAEX,MAAM,UAAU,MAAM,KAAK,SAAS,MAAM,GAAG;AAC7C,MAAI,CAAC,QACJ,OAAM,IAAI,MAAM,oBAAoB;AAGrC,SAAO;;;;;CAMR,MAAM,OAAO,MAAc,IAA8B;EACxD,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AASpC,WAPe,MAAM,GAAG;YACd,IAAI,IAAI,UAAU,CAAC;sBACT,IAAI;gBACV,GAAG;;IAEf,QAAQ,KAAK,GAAG,EAEH,mBAAmB,MAAM;;;;;CAMzC,MAAM,QAAQ,MAAc,IAA8B;EACzD,MAAM,YAAY,aAAa,KAAK;AASpC,WAPe,MAAM,GAAG;YACd,IAAI,IAAI,UAAU,CAAC;;gBAEf,GAAG;;IAEf,QAAQ,KAAK,GAAG,EAEH,mBAAmB,MAAM;;;;;;;;;;;;;;;CAgBzC,MAAM,gBAAgB,MAAc,IAA8B;EACjE,MAAM,YAAY,aAAa,KAAK;AAQpC,WANe,MAAM,GAAG;iBACT,IAAI,IAAI,UAAU,CAAC;gBACpB,GAAG;;IAEf,QAAQ,KAAK,GAAG,EAEH,mBAAmB,MAAM;;;;;CAMzC,MAAM,YACL,MACA,UAA0C,EAAE,EACmB;EAC/D,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,QAAQ,KAAK,IAAI,QAAQ,SAAS,IAAI,IAAI;EAGhD,MAAM,aAAa,QAAQ,SAAS,SAAS;EAC7C,MAAM,iBAAiB,QAAQ,SAAS,aAAa;EACrD,MAAM,UAAU,KAAK,cAAc,WAAW;EAE9C,MAAM,qBAAqB,eAAe,aAAa,KAAK,QAAQ,QAAQ;EAE5E,IAAI,QAAQ,KAAK,GACf,WAAW,UAA4B,CACvC,WAAW,CACX,MAAM,cAAuB,UAAU,KAAK;AAG9C,MAAI,QAAQ,QAAQ;GACnB,MAAM,EAAE,YAAY,IAAI,aAAa,aAAa,QAAQ,OAAO;AAEjE,OAAI,uBAAuB,OAC1B,SAAQ,MAAM,OAAO,OACpB,GAAG,GAAG,CACL,GAAG,SAAgB,KAAK,WAAW,EACnC,GAAG,IAAI,CAAC,GAAG,SAAgB,KAAK,WAAW,EAAE,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CACtE,CAAC,CACF;OAED,SAAQ,MAAM,OAAO,OACpB,GAAG,GAAG,CACL,GAAG,SAAgB,KAAK,WAAW,EACnC,GAAG,IAAI,CAAC,GAAG,SAAgB,KAAK,WAAW,EAAE,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CACtE,CAAC,CACF;;AAIH,UAAQ,MACN,QAAQ,SAAgB,uBAAuB,QAAQ,QAAQ,OAAO,CACtE,QAAQ,MAAM,uBAAuB,QAAQ,QAAQ,OAAO,CAC5D,MAAM,QAAQ,EAAE;EAElB,MAAM,OAAO,MAAM,MAAM,SAAS;EAClC,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,QAAQ,KAAK,MAAM,GAAG,MAAM;EAElC,MAAM,eAAoE,EACzE,OAAO,MAAM,KAAK,QAAQ;GACzB,MAAM,SAAS;AACf,UAAO;IACN,GAAG,KAAK,OAAO,MAAM,OAAO;IAC5B,WAAW,OAAO,OAAO,eAAe,WAAW,OAAO,aAAa;IACvE;IACA,EACF;AAED,MAAI,WAAW,MAAM,SAAS,GAAG;GAChC,MAAM,UAAU,MAAM,GAAG,GAAG;GAC5B,MAAM,iBAAiB,QAAQ;AAK/B,gBAAa,aAAa,aAHzB,OAAO,mBAAmB,YAAY,OAAO,mBAAmB,WAC7D,OAAO,eAAe,GACtB,IAC6C,OAAO,QAAQ,GAAG,CAAC;;AAGrE,SAAO;;;;;CAMR,MAAM,aAAa,MAA+B;EACjD,MAAM,YAAY,aAAa,KAAK;EAEpC,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,UAA4B,CACvC,QAAQ,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC,CAC7C,MAAM,cAAuB,UAAU,KAAK,CAC5C,kBAAkB;AAEpB,SAAO,OAAO,QAAQ,SAAS,EAAE;;;;;CAMlC,MAAM,MACL,MACA,OACkB;EAClB,MAAM,YAAY,aAAa,KAAK;EAEpC,IAAI,QAAQ,KAAK,GACf,WAAW,UAA4B,CACvC,QAAQ,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC,CAC7C,MAAM,cAAuB,MAAM,KAAK;AAE1C,MAAI,OAAO,OACV,SAAQ,MAAM,MAAM,UAAU,KAAK,MAAM,OAAO;AAGjD,MAAI,OAAO,SACV,SAAQ,MAAM,MAAM,aAAa,KAAK,MAAM,SAAS;AAGtD,MAAI,OAAO,OACV,SAAQ,MAAM,MAAM,UAAiB,KAAK,MAAM,OAAO;EAGxD,MAAM,SAAS,MAAM,MAAM,kBAAkB;AAC7C,SAAO,OAAO,QAAQ,SAAS,EAAE;;CAIlC,MAAM,SAAS,MAA4E;EAC1F,MAAM,YAAY,aAAa,KAAK;EAEpC,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,UAA4B,CACvC,QAAQ,OAAO;GACf,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ;GAC7B,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC,KAAK,UAAU,KAAK,YAAY,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,YAAY;GAC3F,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC,KAAK,UAAU,KAAK,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,QAAQ;GACnF,CAAC,CACD,MAAM,cAAuB,MAAM,KAAK,CACxC,kBAAkB;AAEpB,SAAO;GACN,OAAO,OAAO,QAAQ,SAAS,EAAE;GACjC,WAAW,OAAO,QAAQ,aAAa,EAAE;GACzC,OAAO,OAAO,QAAQ,SAAS,EAAE;GACjC;;;;;;;;CASF,MAAM,SAAS,MAAc,IAAY,aAA2C;EACnF,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAGpC,MAAM,gBAAgB,IAAI,KAAK,YAAY;AAC3C,MAAI,MAAM,cAAc,SAAS,CAAC,CACjC,OAAM,IAAI,sBAAsB,yBAAyB;AAE1D,MAAI,iCAAiB,IAAI,MAAM,CAC9B,OAAM,IAAI,sBAAsB,uCAAuC;EAGxE,MAAM,WAAW,MAAM,KAAK,SAAS,MAAM,GAAG;AAC9C,MAAI,CAAC,SACJ,OAAM,IAAI,sBAAsB,yBAAyB;EAM1D,MAAM,YAAY,SAAS,WAAW,cAAc,cAAc;AAElE,QAAM,GAAG;YACC,IAAI,IAAI,UAAU,CAAC;kBACb,UAAU;qBACP,YAAY;mBACd,IAAI;gBACP,GAAG;;IAEf,QAAQ,KAAK,GAAG;EAElB,MAAM,UAAU,MAAM,KAAK,SAAS,MAAM,GAAG;AAC7C,MAAI,CAAC,QACJ,OAAM,IAAI,MAAM,oBAAoB;AAGrC,SAAO;;;;;;;;CASR,MAAM,WAAW,MAAc,IAAkC;EAChE,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAEpC,MAAM,WAAW,MAAM,KAAK,SAAS,MAAM,GAAG;AAC9C,MAAI,CAAC,SACJ,OAAM,IAAI,sBAAsB,yBAAyB;EAK1D,MAAM,YAAY,SAAS,WAAW,cAAc,cAAc;AAElE,QAAM,GAAG;YACC,IAAI,IAAI,UAAU,CAAC;kBACb,UAAU;;mBAET,IAAI;gBACP,GAAG;;;IAGf,QAAQ,KAAK,GAAG;EAElB,MAAM,UAAU,MAAM,KAAK,SAAS,MAAM,GAAG;AAC7C,MAAI,CAAC,QACJ,OAAM,IAAI,MAAM,oBAAoB;AAGrC,SAAO;;;;;;;;;CAUR,MAAM,mBAAmB,MAAsC;EAC9D,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAUpC,UARe,MAAM,GAA4B;mBAChC,IAAI,IAAI,UAAU,CAAC;;yBAEb,IAAI;;;IAGzB,QAAQ,KAAK,GAAG,EAEJ,KAAK,KAAK,QAAQ,KAAK,OAAO,MAAM,IAAI,CAAC;;;;;CAMxD,MAAM,iBAAiB,MAAc,kBAAkD;EACtF,MAAM,YAAY,aAAa,KAAK;AASpC,UAPe,MAAM,GAA4B;mBAChC,IAAI,IAAI,UAAU,CAAC;+BACP,iBAAiB;;;IAG5C,QAAQ,KAAK,GAAG,EAEJ,KAAK,KAAK,QAAQ,KAAK,OAAO,MAAM,IAAI,CAAC;;;;;;;;;;;;;;;;CAiBxD,MAAM,QAAQ,MAAc,IAAY,aAA4C;EACnF,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAEpC,MAAM,WAAW,MAAM,KAAK,SAAS,MAAM,GAAG;AAC9C,MAAI,CAAC,SACJ,OAAM,IAAI,sBAAsB,yBAAyB;EAG1D,MAAM,eAAe,IAAI,mBAAmB,KAAK,GAAG;EACpD,IAAI,oBAAoB,SAAS,mBAAmB,SAAS;AAE7D,MAAI,CAAC,kBAOJ,sBALiB,MAAM,aAAa,OAAO;GAC1C,YAAY;GACZ,SAAS;GACT,MAAM,SAAS;GACf,CAAC,EAC2B;EAK9B,MAAM,WAAW,MAAM,aAAa,SAAS,kBAAkB;AAC/D,MAAI,UAAU;AACb,SAAM,KAAK,gBAAgB,MAAM,IAAI,SAAS,KAAK;AAGnD,OAAI,OAAO,SAAS,KAAK,UAAU,SAClC,OAAM,GAAG;cACC,IAAI,IAAI,UAAU,CAAC;kBACf,SAAS,KAAK,MAAM;kBACpB,GAAG;MACf,QAAQ,KAAK,GAAG;;AAIpB,MAAI,gBAAgB,OAGnB,OAAM,GAAG;aACC,IAAI,IAAI,UAAU,CAAC;6BACH,kBAAkB;;;;sBAIzB,YAAY;oBACd,IAAI;iBACP,GAAG;;KAEf,QAAQ,KAAK,GAAG;MAIlB,OAAM,GAAG;aACC,IAAI,IAAI,UAAU,CAAC;6BACH,kBAAkB;;;;6CAIF,IAAI;oBAC7B,IAAI;iBACP,GAAG;;KAEf,QAAQ,KAAK,GAAG;EAGnB,MAAM,UAAU,MAAM,KAAK,SAAS,MAAM,GAAG;AAC7C,MAAI,CAAC,QACJ,OAAM,IAAI,MAAM,oBAAoB;AAGrC,SAAO;;;;;;;;CASR,MAAM,UAAU,MAAc,IAAkC;EAC/D,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAEpC,MAAM,WAAW,MAAM,KAAK,SAAS,MAAM,GAAG;AAC9C,MAAI,CAAC,SACJ,OAAM,IAAI,sBAAsB,yBAAyB;AAI1D,MAAI,CAAC,SAAS,mBAAmB,SAAS,gBAAgB;GACzD,MAAM,eAAe,IAAI,mBAAmB,KAAK,GAAG;GACpD,MAAM,eAAe,MAAM,aAAa,SAAS,SAAS,eAAe;AACzE,OAAI,cAAc;IACjB,MAAM,QAAQ,MAAM,aAAa,OAAO;KACvC,YAAY;KACZ,SAAS;KACT,MAAM,aAAa;KACnB,CAAC;AAEF,UAAM,GAAG;cACC,IAAI,IAAI,UAAU,CAAC;+BACF,MAAM,GAAG;kBACtB,GAAG;MACf,QAAQ,KAAK,GAAG;;;AAIpB,QAAM,GAAG;YACC,IAAI,IAAI,UAAU,CAAC;;;;mBAIZ,IAAI;gBACP,GAAG;;IAEf,QAAQ,KAAK,GAAG;EAElB,MAAM,UAAU,MAAM,KAAK,SAAS,MAAM,GAAG;AAC7C,MAAI,CAAC,QACJ,OAAM,IAAI,MAAM,oBAAoB;AAGrC,SAAO;;;;;;;;;;;;;CAcR,MAAM,iBAAiB,MAAc,IAAY,YAAmC;EACnF,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAGpC,MAAI,CADa,MAAM,KAAK,SAAS,MAAM,GAAG,CAE7C,OAAM,IAAI,sBAAsB,yBAAyB;EAI1D,MAAM,WAAW,MADI,IAAI,mBAAmB,KAAK,GAAG,CAChB,SAAS,WAAW;AACxD,MAAI,CAAC,SACJ,OAAM,IAAI,sBAAsB,qBAAqB;AAGtD,MAAI,SAAS,eAAe,QAAQ,SAAS,YAAY,GACxD,OAAM,IAAI,sBAAsB,yDAAyD;AAG1F,QAAM,GAAG;YACC,IAAI,IAAI,UAAU,CAAC;6BACF,WAAW;mBACrB,IAAI;gBACP,GAAG;;IAEf,QAAQ,KAAK,GAAG;;;;;;;;CASnB,MAAM,aAAa,MAAc,IAAkC;EAClE,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAEpC,MAAM,WAAW,MAAM,KAAK,SAAS,MAAM,GAAG;AAC9C,MAAI,CAAC,SACJ,OAAM,IAAI,sBAAsB,yBAAyB;AAG1D,MAAI,CAAC,SAAS,gBAEb,QAAO;AAGR,QAAM,GAAG;YACC,IAAI,IAAI,UAAU,CAAC;;mBAEZ,IAAI;gBACP,GAAG;;IAEf,QAAQ,KAAK,GAAG;EAElB,MAAM,UAAU,MAAM,KAAK,SAAS,MAAM,GAAG;AAC7C,MAAI,CAAC,QACJ,OAAM,IAAI,MAAM,oBAAoB;AAGrC,SAAO;;;;;;;CAQR,MAAc,gBACb,MACA,IACA,MACgB;EAChB,MAAM,YAAY,aAAa,KAAK;EACpC,MAAM,UAAmC,EAAE;AAE3C,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,EAAE;AAChD,OAAI,eAAe,IAAI,IAAI,CAAE;AAC7B,OAAI,IAAI,WAAW,IAAI,CAAE;AACzB,sBAAmB,KAAK,qBAAqB;AAC7C,WAAQ,OAAO,eAAe,MAAM;;AAGrC,MAAI,OAAO,KAAK,QAAQ,CAAC,WAAW,EAAG;AAEvC,QAAM,KAAK,GACT,YAAY,UAA4B,CACxC,IAAI,QAAQ,CACZ,MAAM,MAAM,KAAK,GAAG,CACpB,SAAS;;;;;;;CAQZ,MAAM,eAAe,MAA+B;EACnD,MAAM,YAAY,aAAa,KAAK;EAEpC,MAAM,SAAS,MAAM,GAAsB;oCACT,IAAI,IAAI,UAAU,CAAC;;;IAGnD,QAAQ,KAAK,GAAG;AAElB,SAAO,OAAO,OAAO,KAAK,IAAI,SAAS,EAAE;;;;;;;CAQ1C,AAAQ,OAAO,MAAc,KAA2C;EACvE,MAAM,OAAgC,EAAE;AAExC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,CAC7C,KAAI,CAAC,eAAe,IAAI,IAAI,IAAI,UAAU,KACzC,MAAK,OAAO,iBAAiB,MAAM;AAIrC,SAAO;GACN,IAAI,IAAI;GACR;GACA,MAAM,IAAI;GACV,QAAQ,IAAI;GACZ;GACA,UAAU,IAAI;GACd,iBAAkB,IAAI,qBAAuC;GAC7D,WAAW,IAAI;GACf,WAAW,IAAI;GACf,aAAa,IAAI;GACjB,aAAa,IAAI;GACjB,gBAAiB,IAAI,oBAAsC;GAC3D,iBAAkB,IAAI,qBAAuC;GAC7D,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;GACzD,QAAS,IAAI,UAAqB;GAClC,kBAAmB,IAAI,qBAAgC;GACvD;;;;;;CAOF,AAAQ,cAAc,OAAuB;EAc5C,MAAM,SAbkC;GACvC,WAAW;GACX,WAAW;GACX,aAAa;GACb,aAAa;GACb,WAAW;GACX,OAAO;GACP,MAAM;GACN,MAAM;GACN,QAAQ;GACR,QAAQ;GACR,CAEsB;AACvB,MAAI,CAAC,OACJ,OAAM,IAAI,sBAAsB,wBAAwB,QAAQ;AAEjE,SAAO"}
|
|
1
|
+
{"version":3,"file":"content-CERxPUN0.mjs","names":[],"sources":["../src/utils/slugify.ts","../src/database/repositories/revision.ts","../src/database/repositories/content.ts"],"sourcesContent":["// Regex patterns for slug normalization\nconst DIACRITICS_PATTERN = /[\\u0300-\\u036f]/g;\nconst WHITESPACE_UNDERSCORE_PATTERN = /[\\s_]+/g;\nconst NON_ALPHANUMERIC_HYPHEN_PATTERN = /[^a-z0-9-]/g;\nconst MULTIPLE_HYPHENS_PATTERN = /-+/g;\nconst LEADING_TRAILING_HYPHEN_PATTERN = /^-|-$/g;\nconst TRAILING_HYPHEN_PATTERN = /-$/;\n\n/**\n * Convert a string to a URL-friendly slug.\n *\n * Handles unicode by normalizing to NFD and stripping diacritics,\n * so \"café\" becomes \"cafe\", \"naïve\" becomes \"naive\", etc.\n */\n/**\n * Decode a URI-encoded slug parameter.\n *\n * Browsers percent-encode non-ASCII characters in URLs, so a slug like\n * \"మేష-రాసి\" arrives as \"%e0%b0%ae%e0%b1%87%e0%b0%b7-%e0%b0%b0%e0%b0%be%e0%b0%b8%e0%b0%bf\".\n * Call this on `Astro.params.slug` before using it in database lookups.\n */\nexport function decodeSlug(raw: string | undefined): string | undefined {\n\treturn raw ? decodeURIComponent(raw) : undefined;\n}\n\nexport function slugify(text: string, maxLength: number = 80): string {\n\treturn (\n\t\ttext\n\t\t\t.toLowerCase()\n\t\t\t.normalize(\"NFD\")\n\t\t\t.replace(DIACRITICS_PATTERN, \"\")\n\t\t\t.replace(WHITESPACE_UNDERSCORE_PATTERN, \"-\")\n\t\t\t.replace(NON_ALPHANUMERIC_HYPHEN_PATTERN, \"\")\n\t\t\t.replace(MULTIPLE_HYPHENS_PATTERN, \"-\")\n\t\t\t.replace(LEADING_TRAILING_HYPHEN_PATTERN, \"\")\n\t\t\t.slice(0, maxLength)\n\t\t\t// Clean trailing hyphen from truncation\n\t\t\t.replace(TRAILING_HYPHEN_PATTERN, \"\")\n\t);\n}\n","import type { Kysely } from \"kysely\";\nimport { monotonicFactory } from \"ulidx\";\n\nimport type { Database, RevisionTable } from \"../types.js\";\n\nconst monotonic = monotonicFactory();\n\nexport interface Revision {\n\tid: string;\n\tcollection: string;\n\tentryId: string;\n\tdata: Record<string, unknown>;\n\tauthorId: string | null;\n\tcreatedAt: string;\n}\n\nexport interface CreateRevisionInput {\n\tcollection: string;\n\tentryId: string;\n\tdata: Record<string, unknown>;\n\tauthorId?: string;\n}\n\n/**\n * Revision repository for version history\n *\n * Each revision stores a JSON snapshot of the content at a point in time.\n * Used when collection has `supports: [\"revisions\"]` enabled.\n */\nexport class RevisionRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new revision\n\t */\n\tasync create(input: CreateRevisionInput): Promise<Revision> {\n\t\tconst id = monotonic();\n\n\t\tconst row: Omit<RevisionTable, \"created_at\"> = {\n\t\t\tid,\n\t\t\tcollection: input.collection,\n\t\t\tentry_id: input.entryId,\n\t\t\tdata: JSON.stringify(input.data),\n\t\t\tauthor_id: input.authorId ?? null,\n\t\t};\n\n\t\tawait this.db.insertInto(\"revisions\").values(row).execute();\n\n\t\tconst revision = await this.findById(id);\n\t\tif (!revision) {\n\t\t\tthrow new Error(\"Failed to create revision\");\n\t\t}\n\t\treturn revision;\n\t}\n\n\t/**\n\t * Find revision by ID\n\t */\n\tasync findById(id: string): Promise<Revision | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToRevision(row) : null;\n\t}\n\n\t/**\n\t * Get all revisions for an entry (newest first)\n\t *\n\t * Orders by monotonic ULID (descending). The monotonic factory\n\t * guarantees strictly increasing IDs even within the same millisecond.\n\t */\n\tasync findByEntry(\n\t\tcollection: string,\n\t\tentryId: string,\n\t\toptions: { limit?: number } = {},\n\t): Promise<Revision[]> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.orderBy(\"id\", \"desc\");\n\n\t\tif (options.limit) {\n\t\t\tquery = query.limit(options.limit);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row) => this.rowToRevision(row));\n\t}\n\n\t/**\n\t * Get the most recent revision for an entry\n\t */\n\tasync findLatest(collection: string, entryId: string): Promise<Revision | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(1)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToRevision(row) : null;\n\t}\n\n\t/**\n\t * Count revisions for an entry\n\t */\n\tasync countByEntry(collection: string, entryId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Delete all revisions for an entry (use when entry is deleted)\n\t */\n\tasync deleteByEntry(collection: string, entryId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"revisions\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Delete old revisions, keeping the most recent N\n\t */\n\tasync pruneOldRevisions(collection: string, entryId: string, keepCount: number): Promise<number> {\n\t\t// Get IDs of revisions to keep\n\t\tconst keep = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\") // ULID tiebreaker\n\t\t\t.limit(keepCount)\n\t\t\t.execute();\n\n\t\tconst keepIds = keep.map((r) => r.id);\n\n\t\tif (keepIds.length === 0) return 0;\n\n\t\t// Delete everything else for this entry\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"revisions\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.where(\"id\", \"not in\", keepIds)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Update revision data in place\n\t * Used for autosave to avoid creating many small revisions.\n\t */\n\tasync updateData(id: string, data: Record<string, unknown>): Promise<void> {\n\t\tawait this.db\n\t\t\t.updateTable(\"revisions\")\n\t\t\t.set({ data: JSON.stringify(data) })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\t}\n\n\t/**\n\t * Convert database row to Revision object\n\t */\n\tprivate rowToRevision(row: {\n\t\tid: string;\n\t\tcollection: string;\n\t\tentry_id: string;\n\t\tdata: string;\n\t\tauthor_id: string | null;\n\t\tcreated_at: string;\n\t}): Revision {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tcollection: row.collection,\n\t\t\tentryId: row.entry_id,\n\t\t\tdata: JSON.parse(row.data),\n\t\t\tauthorId: row.author_id,\n\t\t\tcreatedAt: row.created_at,\n\t\t};\n\t}\n}\n","import { sql, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { slugify } from \"../../utils/slugify.js\";\nimport type { Database } from \"../types.js\";\nimport { validateIdentifier } from \"../validate.js\";\nimport { RevisionRepository } from \"./revision.js\";\nimport type {\n\tCreateContentInput,\n\tUpdateContentInput,\n\tFindManyOptions,\n\tFindManyResult,\n\tContentItem,\n} from \"./types.js\";\nimport { EmDashValidationError, encodeCursor, decodeCursor } from \"./types.js\";\n\n// Regex pattern for ULID validation\nconst ULID_PATTERN = /^[0-9A-Z]{26}$/;\n\n/**\n * System columns that exist in every ec_* table\n */\nconst SYSTEM_COLUMNS = new Set([\n\t\"id\",\n\t\"slug\",\n\t\"status\",\n\t\"author_id\",\n\t\"primary_byline_id\",\n\t\"created_at\",\n\t\"updated_at\",\n\t\"published_at\",\n\t\"scheduled_at\",\n\t\"deleted_at\",\n\t\"version\",\n\t\"live_revision_id\",\n\t\"draft_revision_id\",\n\t\"locale\",\n\t\"translation_group\",\n]);\n\n/**\n * Get the table name for a collection type\n */\nfunction getTableName(type: string): string {\n\tvalidateIdentifier(type, \"collection type\");\n\treturn `ec_${type}`;\n}\n\n/**\n * Serialize a value for database storage\n * Objects/arrays are JSON-stringified\n * Booleans are converted to 0/1 for SQLite\n */\nfunction serializeValue(value: unknown): unknown {\n\tif (value === null || value === undefined) {\n\t\treturn null;\n\t}\n\tif (typeof value === \"boolean\") {\n\t\treturn value ? 1 : 0;\n\t}\n\tif (typeof value === \"object\") {\n\t\treturn JSON.stringify(value);\n\t}\n\treturn value;\n}\n\n/**\n * Deserialize a value from database storage\n * Attempts to parse JSON strings that look like objects/arrays\n */\nfunction deserializeValue(value: unknown): unknown {\n\tif (typeof value === \"string\") {\n\t\t// Try to parse if it looks like JSON\n\t\tif (value.startsWith(\"{\") || value.startsWith(\"[\")) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(value);\n\t\t\t} catch {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t}\n\treturn value;\n}\n\n/** Pattern for escaping special regex characters */\nconst REGEX_ESCAPE_PATTERN = /[.*+?^${}()|[\\]\\\\]/g;\n\n/**\n * Escape special regex characters in a string for use in `new RegExp()`\n */\nfunction escapeRegExp(s: string): string {\n\treturn s.replace(REGEX_ESCAPE_PATTERN, \"\\\\$&\");\n}\n\n/**\n * Repository for content CRUD operations\n *\n * Content is stored in per-collection tables (ec_posts, ec_pages, etc.)\n * Each field becomes a real column in the table.\n */\nexport class ContentRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new content item\n\t */\n\tasync create(input: CreateContentInput): Promise<ContentItem> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tconst {\n\t\t\ttype,\n\t\t\tslug,\n\t\t\tdata,\n\t\t\tstatus = \"draft\",\n\t\t\tauthorId,\n\t\t\tprimaryBylineId,\n\t\t\tlocale,\n\t\t\ttranslationOf,\n\t\t\tpublishedAt,\n\t\t\tcreatedAt,\n\t\t} = input;\n\n\t\t// Validate required fields\n\t\tif (!type) {\n\t\t\tthrow new EmDashValidationError(\"Content type is required\");\n\t\t}\n\n\t\tconst tableName = getTableName(type);\n\n\t\t// Resolve translation_group: if translationOf is set, look up the source item's group\n\t\tlet translationGroup: string = id; // default: self-reference\n\t\tif (translationOf) {\n\t\t\tconst source = await this.findById(type, translationOf);\n\t\t\tif (!source) {\n\t\t\t\tthrow new EmDashValidationError(\"Translation source content not found\");\n\t\t\t}\n\t\t\ttranslationGroup = source.translationGroup || source.id;\n\t\t}\n\n\t\t// Build column names and values\n\t\tconst columns: string[] = [\n\t\t\t\"id\",\n\t\t\t\"slug\",\n\t\t\t\"status\",\n\t\t\t\"author_id\",\n\t\t\t\"primary_byline_id\",\n\t\t\t\"created_at\",\n\t\t\t\"updated_at\",\n\t\t\t\"published_at\",\n\t\t\t\"version\",\n\t\t\t\"locale\",\n\t\t\t\"translation_group\",\n\t\t];\n\t\tconst values: unknown[] = [\n\t\t\tid,\n\t\t\tslug || null,\n\t\t\tstatus,\n\t\t\tauthorId || null,\n\t\t\tprimaryBylineId ?? null,\n\t\t\tcreatedAt || now,\n\t\t\tnow,\n\t\t\tpublishedAt || null,\n\t\t\t1,\n\t\t\tlocale || \"en\",\n\t\t\ttranslationGroup,\n\t\t];\n\n\t\t// Add data fields as columns (skip system columns to prevent injection via data)\n\t\tif (data && typeof data === \"object\") {\n\t\t\tfor (const [key, value] of Object.entries(data)) {\n\t\t\t\tif (!SYSTEM_COLUMNS.has(key)) {\n\t\t\t\t\tvalidateIdentifier(key, \"content field name\");\n\t\t\t\t\tcolumns.push(key);\n\t\t\t\t\tvalues.push(serializeValue(value));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Build dynamic INSERT using raw SQL\n\t\tconst columnRefs = columns.map((c) => sql.ref(c));\n\t\tconst valuePlaceholders = values.map((v) => (v === null ? sql`NULL` : sql`${v}`));\n\n\t\tawait sql`\n\t\t\tINSERT INTO ${sql.ref(tableName)} (${sql.join(columnRefs, sql`, `)})\n\t\t\tVALUES (${sql.join(valuePlaceholders, sql`, `)})\n\t\t`.execute(this.db);\n\n\t\t// Fetch and return the created item\n\t\tconst item = await this.findById(type, id);\n\t\tif (!item) {\n\t\t\tthrow new Error(\"Failed to create content\");\n\t\t}\n\t\treturn item;\n\t}\n\n\t/**\n\t * Generate a unique slug for a content item within a collection.\n\t *\n\t * Checks the collection table for existing slugs that match `baseSlug`\n\t * (optionally scoped to a locale) and appends a numeric suffix (`-1`,\n\t * `-2`, etc.) on collision to guarantee uniqueness.\n\t *\n\t * Returns `null` if `baseSlug` is empty after slugification.\n\t */\n\tasync generateUniqueSlug(type: string, text: string, locale?: string): Promise<string | null> {\n\t\tconst baseSlug = slugify(text);\n\t\tif (!baseSlug) return null;\n\n\t\tconst tableName = getTableName(type);\n\n\t\t// Check if the base slug is available\n\t\tconst existing = locale\n\t\t\t? await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${baseSlug}\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${baseSlug}\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db);\n\n\t\tif (existing.rows.length === 0) {\n\t\t\treturn baseSlug;\n\t\t}\n\n\t\t// Find all slugs matching the pattern `baseSlug` or `baseSlug-N`\n\t\tconst pattern = `${baseSlug}-%`;\n\t\tconst candidates = locale\n\t\t\t? await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE (slug = ${baseSlug} OR slug LIKE ${pattern})\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${baseSlug} OR slug LIKE ${pattern}\n\t\t\t\t`.execute(this.db);\n\n\t\t// Find the highest numeric suffix in use\n\t\tlet maxSuffix = 0;\n\t\tconst suffixPattern = new RegExp(`^${escapeRegExp(baseSlug)}-(\\\\d+)$`);\n\t\tfor (const row of candidates.rows) {\n\t\t\tconst match = suffixPattern.exec(row.slug);\n\t\t\tif (match) {\n\t\t\t\tconst n = parseInt(match[1], 10);\n\t\t\t\tif (n > maxSuffix) maxSuffix = n;\n\t\t\t}\n\t\t}\n\n\t\treturn `${baseSlug}-${maxSuffix + 1}`;\n\t}\n\n\t/**\n\t * Duplicate a content item\n\t * Creates a new draft copy with \"(Copy)\" appended to the title.\n\t * A slug is auto-generated from the new title by the handler layer.\n\t */\n\tasync duplicate(type: string, id: string, authorId?: string): Promise<ContentItem> {\n\t\t// Fetch the original item\n\t\tconst original = await this.findById(type, id);\n\t\tif (!original) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// Prepare the new data\n\t\tconst newData = { ...original.data };\n\n\t\t// Append \"(Copy)\" to title if present\n\t\tif (typeof newData.title === \"string\") {\n\t\t\tnewData.title = `${newData.title} (Copy)`;\n\t\t} else if (typeof newData.name === \"string\") {\n\t\t\tnewData.name = `${newData.name} (Copy)`;\n\t\t}\n\n\t\t// Auto-generate a unique slug from the new title/name\n\t\tconst slugSource =\n\t\t\ttypeof newData.title === \"string\"\n\t\t\t\t? newData.title\n\t\t\t\t: typeof newData.name === \"string\"\n\t\t\t\t\t? newData.name\n\t\t\t\t\t: null;\n\n\t\tconst slug = slugSource\n\t\t\t? await this.generateUniqueSlug(type, slugSource, original.locale ?? undefined)\n\t\t\t: null;\n\n\t\t// Create the duplicate as a draft — use override authorId if provided (caller owns the copy)\n\t\treturn this.create({\n\t\t\ttype,\n\t\t\tslug,\n\t\t\tdata: newData,\n\t\t\tstatus: \"draft\",\n\t\t\tauthorId: authorId || original.authorId || undefined,\n\t\t});\n\t}\n\n\t/**\n\t * Find content by ID\n\t */\n\tasync findById(type: string, id: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find content by id, including trashed (soft-deleted) items.\n\t * Used by restore endpoint for ownership checks.\n\t */\n\tasync findByIdIncludingTrashed(type: string, id: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE id = ${id}\n\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find content by ID or slug. Tries ID first if it looks like a ULID,\n\t * otherwise tries slug. Falls back to the other if the first lookup misses.\n\t */\n\tasync findByIdOrSlug(\n\t\ttype: string,\n\t\tidentifier: string,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\treturn this._findByIdOrSlug(type, identifier, false, locale);\n\t}\n\n\t/**\n\t * Find content by ID or slug, including trashed (soft-deleted) items.\n\t * Used by restore/permanent-delete endpoints.\n\t */\n\tasync findByIdOrSlugIncludingTrashed(\n\t\ttype: string,\n\t\tidentifier: string,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\treturn this._findByIdOrSlug(type, identifier, true, locale);\n\t}\n\n\tprivate async _findByIdOrSlug(\n\t\ttype: string,\n\t\tidentifier: string,\n\t\tincludeTrashed: boolean,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\t// ULIDs are 26 uppercase alphanumeric chars\n\t\tconst looksLikeUlid = ULID_PATTERN.test(identifier);\n\n\t\tconst findById = includeTrashed\n\t\t\t? (t: string, id: string) => this.findByIdIncludingTrashed(t, id)\n\t\t\t: (t: string, id: string) => this.findById(t, id);\n\t\tconst findBySlug = includeTrashed\n\t\t\t? (t: string, s: string) => this.findBySlugIncludingTrashed(t, s, locale)\n\t\t\t: (t: string, s: string) => this.findBySlug(t, s, locale);\n\n\t\tif (looksLikeUlid) {\n\t\t\t// Try ID first, fall back to slug\n\t\t\tconst byId = await findById(type, identifier);\n\t\t\tif (byId) return byId;\n\t\t\treturn findBySlug(type, identifier);\n\t\t}\n\t\t// Try slug first, fall back to ID\n\t\tconst bySlug = await findBySlug(type, identifier);\n\t\tif (bySlug) return bySlug;\n\t\treturn findById(type, identifier);\n\t}\n\n\t/**\n\t * Find content by slug\n\t */\n\tasync findBySlug(type: string, slug: string, locale?: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = locale\n\t\t\t? await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t\tORDER BY locale ASC\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find content by slug, including trashed (soft-deleted) items.\n\t * Used by restore/permanent-delete endpoints.\n\t */\n\tasync findBySlugIncludingTrashed(\n\t\ttype: string,\n\t\tslug: string,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = locale\n\t\t\t? await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tORDER BY locale ASC\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find many content items with filtering and pagination\n\t */\n\tasync findMany(\n\t\ttype: string,\n\t\toptions: FindManyOptions = {},\n\t): Promise<FindManyResult<ContentItem>> {\n\t\tconst tableName = getTableName(type);\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\t// Determine ordering\n\t\tconst orderField = options.orderBy?.field || \"createdAt\";\n\t\tconst orderDirection = options.orderBy?.direction || \"desc\";\n\t\tconst dbField = this.mapOrderField(orderField);\n\n\t\t// Validate order direction to prevent injection\n\t\tconst safeOrderDirection = orderDirection.toLowerCase() === \"asc\" ? \"ASC\" : \"DESC\";\n\n\t\t// Build query with parameterized values (no string interpolation)\n\t\t// Note: Dynamic content tables have deleted_at column, cast needed for Kysely\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.selectAll()\n\t\t\t.where(\"deleted_at\" as never, \"is\", null);\n\n\t\t// Apply filters with parameterized queries\n\t\tif (options.where?.status) {\n\t\t\tquery = query.where(\"status\", \"=\", options.where.status);\n\t\t}\n\n\t\tif (options.where?.authorId) {\n\t\t\tquery = query.where(\"author_id\", \"=\", options.where.authorId);\n\t\t}\n\n\t\tif (options.where?.locale) {\n\t\t\tquery = query.where(\"locale\" as any, \"=\", options.where.locale);\n\t\t}\n\n\t\t// Handle cursor pagination — decodeCursor throws InvalidCursorError\n\t\t// on malformed input; let it propagate so handlers surface a\n\t\t// structured INVALID_CURSOR rather than silently returning page 1.\n\t\tif (options.cursor) {\n\t\t\tconst { orderValue, id: cursorId } = decodeCursor(options.cursor);\n\n\t\t\tif (safeOrderDirection === \"DESC\") {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \"<\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \"<\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \">\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \">\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Apply ordering and limit\n\t\tquery = query\n\t\t\t.orderBy(dbField as any, safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.orderBy(\"id\", safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit);\n\n\t\tconst mappedResult: FindManyResult<ContentItem> = {\n\t\t\titems: items.map((row) => this.mapRow(type, row as Record<string, unknown>)),\n\t\t};\n\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst lastRow = items.at(-1) as Record<string, unknown>;\n\t\t\tconst lastOrderValue = lastRow[dbField];\n\t\t\tconst orderStr =\n\t\t\t\ttypeof lastOrderValue === \"string\" || typeof lastOrderValue === \"number\"\n\t\t\t\t\t? String(lastOrderValue)\n\t\t\t\t\t: \"\";\n\t\t\tmappedResult.nextCursor = encodeCursor(orderStr, String(lastRow.id));\n\t\t}\n\n\t\treturn mappedResult;\n\t}\n\n\t/**\n\t * Update content\n\t */\n\tasync update(type: string, id: string, input: UpdateContentInput): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\t// Build update object with parameterized values\n\t\tconst updates: Record<string, unknown> = {\n\t\t\tupdated_at: now,\n\t\t\tversion: sql`version + 1`,\n\t\t};\n\n\t\tif (input.status !== undefined) {\n\t\t\tupdates.status = input.status;\n\t\t}\n\n\t\tif (input.slug !== undefined) {\n\t\t\tupdates.slug = input.slug;\n\t\t}\n\n\t\tif (input.publishedAt !== undefined) {\n\t\t\tupdates.published_at = input.publishedAt;\n\t\t}\n\n\t\tif (input.scheduledAt !== undefined) {\n\t\t\tupdates.scheduled_at = input.scheduledAt;\n\t\t}\n\n\t\tif (input.authorId !== undefined) {\n\t\t\tupdates.author_id = input.authorId;\n\t\t}\n\n\t\tif (input.primaryBylineId !== undefined) {\n\t\t\tupdates.primary_byline_id = input.primaryBylineId;\n\t\t}\n\n\t\t// Update data fields (skip system columns to prevent injection via data)\n\t\tif (input.data !== undefined && typeof input.data === \"object\") {\n\t\t\tfor (const [key, value] of Object.entries(input.data)) {\n\t\t\t\tif (!SYSTEM_COLUMNS.has(key)) {\n\t\t\t\t\tvalidateIdentifier(key, \"content field name\");\n\t\t\t\t\tupdates[key] = serializeValue(value);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tawait this.db\n\t\t\t.updateTable(tableName as keyof Database)\n\t\t\t.set(updates)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.where(\"deleted_at\" as never, \"is\", null)\n\t\t\t.execute();\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Delete content (soft delete - moves to trash)\n\t */\n\tasync delete(type: string, id: string): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET deleted_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\treturn (result.numAffectedRows ?? 0n) > 0n;\n\t}\n\n\t/**\n\t * Restore content from trash\n\t */\n\tasync restore(type: string, id: string): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET deleted_at = NULL\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NOT NULL\n\t\t`.execute(this.db);\n\n\t\treturn (result.numAffectedRows ?? 0n) > 0n;\n\t}\n\n\t/**\n\t * Permanently delete content (cannot be undone)\n\t */\n\t/**\n\t * Permanently delete a soft-deleted content row.\n\t *\n\t * Returns `true` only when a soft-deleted (trashed) row was removed.\n\t * Returns `false` when no row exists OR when the row exists but is live —\n\t * the caller is responsible for distinguishing these cases (typically via\n\t * a follow-up `findByIdOrSlugIncludingTrashed` to surface NOT_FOUND vs\n\t * NOT_TRASHED). The `AND deleted_at IS NOT NULL` clause is the safety net\n\t * that prevents permanent delete from bypassing the trash workflow.\n\t */\n\tasync permanentDelete(type: string, id: string): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql`\n\t\t\tDELETE FROM ${sql.ref(tableName)}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NOT NULL\n\t\t`.execute(this.db);\n\n\t\treturn (result.numAffectedRows ?? 0n) > 0n;\n\t}\n\n\t/**\n\t * Find trashed content items\n\t */\n\tasync findTrashed(\n\t\ttype: string,\n\t\toptions: Omit<FindManyOptions, \"where\"> = {},\n\t): Promise<FindManyResult<ContentItem & { deletedAt: string }>> {\n\t\tconst tableName = getTableName(type);\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\t// Determine ordering - default to most recently deleted\n\t\tconst orderField = options.orderBy?.field || \"deletedAt\";\n\t\tconst orderDirection = options.orderBy?.direction || \"desc\";\n\t\tconst dbField = this.mapOrderField(orderField);\n\n\t\tconst safeOrderDirection = orderDirection.toLowerCase() === \"asc\" ? \"ASC\" : \"DESC\";\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.selectAll()\n\t\t\t.where(\"deleted_at\" as never, \"is not\", null);\n\n\t\t// Handle cursor pagination — decodeCursor throws on invalid input.\n\t\tif (options.cursor) {\n\t\t\tconst { orderValue, id: cursorId } = decodeCursor(options.cursor);\n\n\t\t\tif (safeOrderDirection === \"DESC\") {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \"<\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \"<\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \">\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \">\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(dbField as any, safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.orderBy(\"id\", safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit);\n\n\t\tconst mappedResult: FindManyResult<ContentItem & { deletedAt: string }> = {\n\t\t\titems: items.map((row) => {\n\t\t\t\tconst record = row as Record<string, unknown>;\n\t\t\t\treturn {\n\t\t\t\t\t...this.mapRow(type, record),\n\t\t\t\t\tdeletedAt: typeof record.deleted_at === \"string\" ? record.deleted_at : \"\",\n\t\t\t\t};\n\t\t\t}),\n\t\t};\n\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst lastRow = items.at(-1) as Record<string, unknown>;\n\t\t\tconst lastOrderValue = lastRow[dbField];\n\t\t\tconst orderStr =\n\t\t\t\ttypeof lastOrderValue === \"string\" || typeof lastOrderValue === \"number\"\n\t\t\t\t\t? String(lastOrderValue)\n\t\t\t\t\t: \"\";\n\t\t\tmappedResult.nextCursor = encodeCursor(orderStr, String(lastRow.id));\n\t\t}\n\n\t\treturn mappedResult;\n\t}\n\n\t/**\n\t * Count trashed content items\n\t */\n\tasync countTrashed(type: string): Promise<number> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"deleted_at\" as never, \"is not\", null)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Count content items\n\t */\n\tasync count(\n\t\ttype: string,\n\t\twhere?: { status?: string; authorId?: string; locale?: string },\n\t): Promise<number> {\n\t\tconst tableName = getTableName(type);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"deleted_at\" as never, \"is\", null);\n\n\t\tif (where?.status) {\n\t\t\tquery = query.where(\"status\", \"=\", where.status);\n\t\t}\n\n\t\tif (where?.authorId) {\n\t\t\tquery = query.where(\"author_id\", \"=\", where.authorId);\n\t\t}\n\n\t\tif (where?.locale) {\n\t\t\tquery = query.where(\"locale\" as any, \"=\", where.locale);\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t// get overall statistics (total, published, draft) for a content type in a single query\n\tasync getStats(type: string): Promise<{ total: number; published: number; draft: number }> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select((eb) => [\n\t\t\t\teb.fn.count(\"id\").as(\"total\"),\n\t\t\t\teb.fn.sum(eb.case().when(\"status\", \"=\", \"published\").then(1).else(0).end()).as(\"published\"),\n\t\t\t\teb.fn.sum(eb.case().when(\"status\", \"=\", \"draft\").then(1).else(0).end()).as(\"draft\"),\n\t\t\t])\n\t\t\t.where(\"deleted_at\" as never, \"is\", null)\n\t\t\t.executeTakeFirst();\n\n\t\treturn {\n\t\t\ttotal: Number(result?.total || 0),\n\t\t\tpublished: Number(result?.published || 0),\n\t\t\tdraft: Number(result?.draft || 0),\n\t\t};\n\t}\n\n\t/**\n\t * Schedule content for future publishing\n\t *\n\t * Sets status to 'scheduled' and stores the scheduled publish time.\n\t * The content will be auto-published when the scheduled time is reached.\n\t */\n\tasync schedule(type: string, id: string, scheduledAt: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\t// Validate scheduledAt is in the future\n\t\tconst scheduledDate = new Date(scheduledAt);\n\t\tif (isNaN(scheduledDate.getTime())) {\n\t\t\tthrow new EmDashValidationError(\"Invalid scheduled date\");\n\t\t}\n\t\tif (scheduledDate <= new Date()) {\n\t\t\tthrow new EmDashValidationError(\"Scheduled date must be in the future\");\n\t\t}\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// Published posts keep their status — the schedule applies to the\n\t\t// pending draft, not the currently-live revision. Unpublished posts\n\t\t// transition to 'scheduled' so they aren't visible before the time.\n\t\tconst newStatus = existing.status === \"published\" ? \"published\" : \"scheduled\";\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET status = ${newStatus},\n\t\t\t\tscheduled_at = ${scheduledAt},\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Unschedule content\n\t *\n\t * Clears the scheduled time. Published posts stay published;\n\t * draft/scheduled posts revert to 'draft'.\n\t */\n\tasync unschedule(type: string, id: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// Published posts keep their status — just clear the pending schedule.\n\t\t// Draft/scheduled posts revert to 'draft'.\n\t\tconst newStatus = existing.status === \"published\" ? \"published\" : \"draft\";\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET status = ${newStatus},\n\t\t\t\tscheduled_at = NULL,\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND scheduled_at IS NOT NULL\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Find content that is ready to be published\n\t *\n\t * Returns all content where scheduled_at <= now, regardless of status.\n\t * This covers both draft-scheduled posts (status='scheduled') and\n\t * published posts with scheduled draft changes (status='published').\n\t */\n\tasync findReadyToPublish(type: string): Promise<ContentItem[]> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE scheduled_at IS NOT NULL\n\t\t\tAND scheduled_at <= ${now}\n\t\t\tAND deleted_at IS NULL\n\t\t\tORDER BY scheduled_at ASC\n\t\t`.execute(this.db);\n\n\t\treturn result.rows.map((row) => this.mapRow(type, row));\n\t}\n\n\t/**\n\t * Find all translations in a translation group\n\t */\n\tasync findTranslations(type: string, translationGroup: string): Promise<ContentItem[]> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE translation_group = ${translationGroup}\n\t\t\tAND deleted_at IS NULL\n\t\t\tORDER BY locale ASC\n\t\t`.execute(this.db);\n\n\t\treturn result.rows.map((row) => this.mapRow(type, row));\n\t}\n\n\t/**\n\t * Publish the current draft\n\t *\n\t * Promotes draft_revision_id to live_revision_id and clears draft pointer.\n\t * Syncs the draft revision's data into the content table columns so the\n\t * content table always reflects the published version.\n\t * If no draft revision exists, creates one from current data and publishes it.\n\t *\n\t * `publishedAt` (optional) overrides the publication timestamp. If omitted,\n\t * the existing `published_at` is preserved (idempotent re-publish keeps the\n\t * original date) and falls back to the current time on first publish. Pass\n\t * an explicit value to backdate a publish (e.g. when migrating content from\n\t * another CMS).\n\t */\n\tasync publish(type: string, id: string, publishedAt?: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tlet revisionToPublish = existing.draftRevisionId || existing.liveRevisionId;\n\n\t\tif (!revisionToPublish) {\n\t\t\t// No revision exists - create one from current data\n\t\t\tconst revision = await revisionRepo.create({\n\t\t\t\tcollection: type,\n\t\t\t\tentryId: id,\n\t\t\t\tdata: existing.data,\n\t\t\t});\n\t\t\trevisionToPublish = revision.id;\n\t\t}\n\n\t\t// Sync the revision's data into the content table columns\n\t\t// so the content table always holds the published version\n\t\tconst revision = await revisionRepo.findById(revisionToPublish);\n\t\tif (revision) {\n\t\t\tawait this.syncDataColumns(type, id, revision.data);\n\n\t\t\t// Sync slug from revision if stored there\n\t\t\tif (typeof revision.data._slug === \"string\") {\n\t\t\t\tawait sql`\n\t\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\t\tSET slug = ${revision.data._slug}\n\t\t\t\t\tWHERE id = ${id}\n\t\t\t\t`.execute(this.db);\n\t\t\t}\n\t\t}\n\n\t\tif (publishedAt !== undefined) {\n\t\t\t// Caller supplied an explicit timestamp, so we overwrite published_at\n\t\t\t// directly (used to backdate a publish, e.g. for content migrations).\n\t\t\tawait sql`\n\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\tSET live_revision_id = ${revisionToPublish},\n\t\t\t\t\tdraft_revision_id = NULL,\n\t\t\t\t\tstatus = 'published',\n\t\t\t\t\tscheduled_at = NULL,\n\t\t\t\t\tpublished_at = ${publishedAt},\n\t\t\t\t\tupdated_at = ${now}\n\t\t\t\tWHERE id = ${id}\n\t\t\t\tAND deleted_at IS NULL\n\t\t\t`.execute(this.db);\n\t\t} else {\n\t\t\t// No timestamp supplied — preserve existing published_at on\n\t\t\t// idempotent re-publish, fall back to `now` on first publish.\n\t\t\tawait sql`\n\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\tSET live_revision_id = ${revisionToPublish},\n\t\t\t\t\tdraft_revision_id = NULL,\n\t\t\t\t\tstatus = 'published',\n\t\t\t\t\tscheduled_at = NULL,\n\t\t\t\t\tpublished_at = COALESCE(published_at, ${now}),\n\t\t\t\t\tupdated_at = ${now}\n\t\t\t\tWHERE id = ${id}\n\t\t\t\tAND deleted_at IS NULL\n\t\t\t`.execute(this.db);\n\t\t}\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Unpublish content\n\t *\n\t * Removes live pointer but preserves draft. If no draft exists,\n\t * creates one from the live version so the content isn't lost.\n\t */\n\tasync unpublish(type: string, id: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// If no draft exists, create one from the live version\n\t\tif (!existing.draftRevisionId && existing.liveRevisionId) {\n\t\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\t\tconst liveRevision = await revisionRepo.findById(existing.liveRevisionId);\n\t\t\tif (liveRevision) {\n\t\t\t\tconst draft = await revisionRepo.create({\n\t\t\t\t\tcollection: type,\n\t\t\t\t\tentryId: id,\n\t\t\t\t\tdata: liveRevision.data,\n\t\t\t\t});\n\n\t\t\t\tawait sql`\n\t\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\t\tSET draft_revision_id = ${draft.id}\n\t\t\t\t\tWHERE id = ${id}\n\t\t\t\t`.execute(this.db);\n\t\t\t}\n\t\t}\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET live_revision_id = NULL,\n\t\t\t\tstatus = 'draft',\n\t\t\t\tpublished_at = NULL,\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Set the draft revision pointer for a content item.\n\t *\n\t * Used by seed/import paths that stage a new revision's data before\n\t * promoting it to live via `publish()`.\n\t *\n\t * Validates that the content item exists and is not soft-deleted, that\n\t * the revision exists, and that the revision belongs to the same\n\t * collection and entry. Without these checks, a caller could leave the\n\t * content row pointing at a missing or unrelated revision.\n\t */\n\tasync setDraftRevision(type: string, id: string, revisionId: string): Promise<void> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tconst revision = await revisionRepo.findById(revisionId);\n\t\tif (!revision) {\n\t\t\tthrow new EmDashValidationError(\"Revision not found\");\n\t\t}\n\n\t\tif (revision.collection !== type || revision.entryId !== id) {\n\t\t\tthrow new EmDashValidationError(\"Revision does not belong to the specified content item\");\n\t\t}\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET draft_revision_id = ${revisionId},\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\t}\n\n\t/**\n\t * Discard pending draft changes\n\t *\n\t * Clears draft_revision_id. The content table columns already hold the\n\t * published version, so no data sync is needed.\n\t */\n\tasync discardDraft(type: string, id: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\tif (!existing.draftRevisionId) {\n\t\t\t// No draft to discard\n\t\t\treturn existing;\n\t\t}\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET draft_revision_id = NULL,\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Sync data columns in the content table from a data object.\n\t * Used to promote revision data into the content table on publish.\n\t * Keys starting with _ are revision metadata (e.g. _slug) and are skipped.\n\t */\n\tprivate async syncDataColumns(\n\t\ttype: string,\n\t\tid: string,\n\t\tdata: Record<string, unknown>,\n\t): Promise<void> {\n\t\tconst tableName = getTableName(type);\n\t\tconst updates: Record<string, unknown> = {};\n\n\t\tfor (const [key, value] of Object.entries(data)) {\n\t\t\tif (SYSTEM_COLUMNS.has(key)) continue;\n\t\t\tif (key.startsWith(\"_\")) continue; // revision metadata\n\t\t\tvalidateIdentifier(key, \"content field name\");\n\t\t\tupdates[key] = serializeValue(value);\n\t\t}\n\n\t\tif (Object.keys(updates).length === 0) return;\n\n\t\tawait this.db\n\t\t\t.updateTable(tableName as keyof Database)\n\t\t\t.set(updates)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\t}\n\n\t/**\n\t * Count content items with a pending schedule.\n\t * Includes both draft-scheduled (status='scheduled') and published\n\t * posts with scheduled draft changes (status='published', scheduled_at set).\n\t */\n\tasync countScheduled(type: string): Promise<number> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<{ count: number }>`\n\t\t\tSELECT COUNT(id) as count FROM ${sql.ref(tableName)}\n\t\t\tWHERE scheduled_at IS NOT NULL\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\treturn Number(result.rows[0]?.count || 0);\n\t}\n\n\t/**\n\t * Map database row to ContentItem\n\t * Extracts system columns and puts content fields in data\n\t * Excludes null values from data to match input semantics\n\t */\n\tprivate mapRow(type: string, row: Record<string, unknown>): ContentItem {\n\t\tconst data: Record<string, unknown> = {};\n\n\t\tfor (const [key, value] of Object.entries(row)) {\n\t\t\tif (!SYSTEM_COLUMNS.has(key) && value !== null) {\n\t\t\t\tdata[key] = deserializeValue(value);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tid: row.id as string,\n\t\t\ttype,\n\t\t\tslug: row.slug as string | null,\n\t\t\tstatus: row.status as string,\n\t\t\tdata,\n\t\t\tauthorId: row.author_id as string | null,\n\t\t\tprimaryBylineId: (row.primary_byline_id as string | null) ?? null,\n\t\t\tcreatedAt: row.created_at as string,\n\t\t\tupdatedAt: row.updated_at as string,\n\t\t\tpublishedAt: row.published_at as string | null,\n\t\t\tscheduledAt: row.scheduled_at as string | null,\n\t\t\tliveRevisionId: (row.live_revision_id as string | null) ?? null,\n\t\t\tdraftRevisionId: (row.draft_revision_id as string | null) ?? null,\n\t\t\tversion: typeof row.version === \"number\" ? row.version : 1,\n\t\t\tlocale: (row.locale as string) ?? null,\n\t\t\ttranslationGroup: (row.translation_group as string) ?? null,\n\t\t};\n\t}\n\n\t/**\n\t * Map order field names to database columns.\n\t * Only allows known fields to prevent column enumeration via crafted orderBy values.\n\t */\n\tprivate mapOrderField(field: string): string {\n\t\tconst mapping: Record<string, string> = {\n\t\t\tcreatedAt: \"created_at\",\n\t\t\tupdatedAt: \"updated_at\",\n\t\t\tpublishedAt: \"published_at\",\n\t\t\tscheduledAt: \"scheduled_at\",\n\t\t\tdeletedAt: \"deleted_at\",\n\t\t\ttitle: \"title\",\n\t\t\tname: \"name\",\n\t\t\tslug: \"slug\",\n\t\t\tstatus: \"status\",\n\t\t\tlocale: \"locale\",\n\t\t};\n\n\t\tconst mapped = mapping[field];\n\t\tif (!mapped) {\n\t\t\tthrow new EmDashValidationError(`Invalid order field: ${field}`);\n\t\t}\n\t\treturn mapped;\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -35,11 +35,13 @@ declare function flushRecorder(rec: QueryRecorder): void;
|
|
|
35
35
|
*/
|
|
36
36
|
declare function isInstrumentationEnabled(): boolean;
|
|
37
37
|
/**
|
|
38
|
-
* Returns a Kysely `log`
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* Returns a Kysely `log` callback. Always returns a function so per-request
|
|
39
|
+
* counters (db.count, db.total, db.first, db.last) and the optional NDJSON
|
|
40
|
+
* recorder both get fed. The cost over the previous "undefined when off"
|
|
41
|
+
* behaviour is one `performance.now()` pair per query inside Kysely, which
|
|
42
|
+
* is in the noise compared to any real query.
|
|
41
43
|
*/
|
|
42
|
-
declare function kyselyLogOption(): Logger
|
|
44
|
+
declare function kyselyLogOption(): Logger;
|
|
43
45
|
//#endregion
|
|
44
46
|
export { QUERY_LOG_ENV, QUERY_LOG_PREFIX, QueryEvent, QueryRecorder, createRecorder, flushRecorder, isInstrumentationEnabled, kyselyLogOption, recordEvent };
|
|
45
47
|
//# sourceMappingURL=instrumentation.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instrumentation.d.mts","names":[],"sources":["../../src/database/instrumentation.ts"],"mappings":";;;cAkBa,aAAA;AAAA,cACA,gBAAA;AAAA,UAEI,UAAA;EAChB,GAAA;EACA,MAAA;EACA,UAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAA,UAGgB,aAAA;EAChB,MAAA,EAAQ,UAAA;EACR,KAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAA,iBAGe,cAAA,CAAe,KAAA,UAAe,MAAA,UAAgB,KAAA,WAAgB,aAAA;AAAA,iBAI9D,WAAA,CACf,GAAA,EAAK,aAAA,EACL,GAAA,UACA,MAAA,sBACA,UAAA;;;;;AARD;;iBA0BgB,aAAA,CAAc,GAAA,EAAK,aAAA;;;;;;;;iBAcnB,wBAAA,CAAA
|
|
1
|
+
{"version":3,"file":"instrumentation.d.mts","names":[],"sources":["../../src/database/instrumentation.ts"],"mappings":";;;cAkBa,aAAA;AAAA,cACA,gBAAA;AAAA,UAEI,UAAA;EAChB,GAAA;EACA,MAAA;EACA,UAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAA,UAGgB,aAAA;EAChB,MAAA,EAAQ,UAAA;EACR,KAAA;EACA,MAAA;EACA,KAAA;AAAA;AAAA,iBAGe,cAAA,CAAe,KAAA,UAAe,MAAA,UAAgB,KAAA,WAAgB,aAAA;AAAA,iBAI9D,WAAA,CACf,GAAA,EAAK,aAAA,EACL,GAAA,UACA,MAAA,sBACA,UAAA;;;;;AARD;;iBA0BgB,aAAA,CAAc,GAAA,EAAK,aAAA;;;;;;;;iBAcnB,wBAAA,CAAA;;;;;;;;iBAgCA,eAAA,CAAA,GAAmB,MAAA"}
|
|
@@ -43,17 +43,29 @@ function isInstrumentationEnabled() {
|
|
|
43
43
|
}
|
|
44
44
|
function kyselyLog(event) {
|
|
45
45
|
if (event.level !== "query") return;
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
48
|
-
|
|
46
|
+
const ctx = getRequestContext();
|
|
47
|
+
if (!ctx) return;
|
|
48
|
+
const dur = event.queryDurationMillis;
|
|
49
|
+
if (ctx.metrics) {
|
|
50
|
+
const m = ctx.metrics;
|
|
51
|
+
m.dbCount += 1;
|
|
52
|
+
m.dbTotalMs += dur;
|
|
53
|
+
const finishedAt = performance.now() - m.start;
|
|
54
|
+
const startedAt = finishedAt - dur;
|
|
55
|
+
if (m.dbFirstOffset === null) m.dbFirstOffset = startedAt;
|
|
56
|
+
m.dbLastOffset = finishedAt;
|
|
57
|
+
}
|
|
58
|
+
if (ctx.queryRecorder) recordEvent(ctx.queryRecorder, event.query.sql, event.query.parameters, dur);
|
|
49
59
|
}
|
|
50
60
|
/**
|
|
51
|
-
* Returns a Kysely `log`
|
|
52
|
-
*
|
|
53
|
-
*
|
|
61
|
+
* Returns a Kysely `log` callback. Always returns a function so per-request
|
|
62
|
+
* counters (db.count, db.total, db.first, db.last) and the optional NDJSON
|
|
63
|
+
* recorder both get fed. The cost over the previous "undefined when off"
|
|
64
|
+
* behaviour is one `performance.now()` pair per query inside Kysely, which
|
|
65
|
+
* is in the noise compared to any real query.
|
|
54
66
|
*/
|
|
55
67
|
function kyselyLogOption() {
|
|
56
|
-
return
|
|
68
|
+
return kyselyLog;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instrumentation.mjs","names":[],"sources":["../../src/database/instrumentation.ts"],"sourcesContent":["/**\n * Query instrumentation\n *\n * Dev/test-only: captures every Kysely query executed inside a request,\n * tagged with the route, method, and a caller-supplied phase (e.g. \"cold\"\n * or \"warm\"). Events are emitted as prefixed NDJSON on stdout so the\n * harness can capture them from both Node and workerd — workerd has no\n * filesystem access, but `console.log` is portable.\n *\n * The recorder lives on the request context (AsyncLocalStorage). The\n * Kysely `log` hook reads the recorder at query time and appends an\n * event. When no recorder is attached, the hook is a null check.\n */\n\nimport type { LogEvent, Logger } from \"kysely\";\n\nimport { getRequestContext } from \"../request-context.js\";\n\nexport const QUERY_LOG_ENV = \"EMDASH_QUERY_LOG\";\nexport const QUERY_LOG_PREFIX = \"[emdash-query-log]\";\n\nexport interface QueryEvent {\n\tsql: string;\n\tparams: readonly unknown[];\n\tdurationMs: number;\n\troute: string;\n\tmethod: string;\n\tphase: string;\n}\n\nexport interface QueryRecorder {\n\tevents: QueryEvent[];\n\troute: string;\n\tmethod: string;\n\tphase: string;\n}\n\nexport function createRecorder(route: string, method: string, phase: string): QueryRecorder {\n\treturn { events: [], route, method, phase };\n}\n\nexport function recordEvent(\n\trec: QueryRecorder,\n\tsql: string,\n\tparams: readonly unknown[],\n\tdurationMs: number,\n): void {\n\trec.events.push({\n\t\tsql,\n\t\tparams,\n\t\tdurationMs,\n\t\troute: rec.route,\n\t\tmethod: rec.method,\n\t\tphase: rec.phase,\n\t});\n}\n\n/**\n * Emit all events from a recorder as prefixed NDJSON on stdout. The\n * harness pipes the child's stdout, filters lines beginning with\n * QUERY_LOG_PREFIX, and writes them to its own file. Using stdout means\n * the sink works uniformly in Node and in workerd (which has no fs).\n */\nexport function flushRecorder(rec: QueryRecorder): void {\n\tif (rec.events.length === 0) return;\n\tfor (const e of rec.events) {\n\t\tconsole.log(`${QUERY_LOG_PREFIX} ${JSON.stringify(e)}`);\n\t}\n}\n\n/**\n * Whether query instrumentation is enabled. Read at Kysely construction\n * time and middleware entry — the env var is a process-lifetime flag, not\n * per-request. Gated via `process.env` so adapters that ship env through\n * to the worker (e.g. Miniflare via wrangler.jsonc `vars` or host env\n * pass-through) can enable it at runtime.\n */\nexport function isInstrumentationEnabled(): boolean {\n\treturn Boolean(\n\t\ttypeof process !== \"undefined\" && process.env && process.env[QUERY_LOG_ENV] === \"1\",\n\t);\n}\n\nfunction kyselyLog(event: LogEvent): void {\n\tif (event.level !== \"query\") return;\n\tconst
|
|
1
|
+
{"version":3,"file":"instrumentation.mjs","names":[],"sources":["../../src/database/instrumentation.ts"],"sourcesContent":["/**\n * Query instrumentation\n *\n * Dev/test-only: captures every Kysely query executed inside a request,\n * tagged with the route, method, and a caller-supplied phase (e.g. \"cold\"\n * or \"warm\"). Events are emitted as prefixed NDJSON on stdout so the\n * harness can capture them from both Node and workerd — workerd has no\n * filesystem access, but `console.log` is portable.\n *\n * The recorder lives on the request context (AsyncLocalStorage). The\n * Kysely `log` hook reads the recorder at query time and appends an\n * event. When no recorder is attached, the hook is a null check.\n */\n\nimport type { LogEvent, Logger } from \"kysely\";\n\nimport { getRequestContext } from \"../request-context.js\";\n\nexport const QUERY_LOG_ENV = \"EMDASH_QUERY_LOG\";\nexport const QUERY_LOG_PREFIX = \"[emdash-query-log]\";\n\nexport interface QueryEvent {\n\tsql: string;\n\tparams: readonly unknown[];\n\tdurationMs: number;\n\troute: string;\n\tmethod: string;\n\tphase: string;\n}\n\nexport interface QueryRecorder {\n\tevents: QueryEvent[];\n\troute: string;\n\tmethod: string;\n\tphase: string;\n}\n\nexport function createRecorder(route: string, method: string, phase: string): QueryRecorder {\n\treturn { events: [], route, method, phase };\n}\n\nexport function recordEvent(\n\trec: QueryRecorder,\n\tsql: string,\n\tparams: readonly unknown[],\n\tdurationMs: number,\n): void {\n\trec.events.push({\n\t\tsql,\n\t\tparams,\n\t\tdurationMs,\n\t\troute: rec.route,\n\t\tmethod: rec.method,\n\t\tphase: rec.phase,\n\t});\n}\n\n/**\n * Emit all events from a recorder as prefixed NDJSON on stdout. The\n * harness pipes the child's stdout, filters lines beginning with\n * QUERY_LOG_PREFIX, and writes them to its own file. Using stdout means\n * the sink works uniformly in Node and in workerd (which has no fs).\n */\nexport function flushRecorder(rec: QueryRecorder): void {\n\tif (rec.events.length === 0) return;\n\tfor (const e of rec.events) {\n\t\tconsole.log(`${QUERY_LOG_PREFIX} ${JSON.stringify(e)}`);\n\t}\n}\n\n/**\n * Whether query instrumentation is enabled. Read at Kysely construction\n * time and middleware entry — the env var is a process-lifetime flag, not\n * per-request. Gated via `process.env` so adapters that ship env through\n * to the worker (e.g. Miniflare via wrangler.jsonc `vars` or host env\n * pass-through) can enable it at runtime.\n */\nexport function isInstrumentationEnabled(): boolean {\n\treturn Boolean(\n\t\ttypeof process !== \"undefined\" && process.env && process.env[QUERY_LOG_ENV] === \"1\",\n\t);\n}\n\nfunction kyselyLog(event: LogEvent): void {\n\tif (event.level !== \"query\") return;\n\tconst ctx = getRequestContext();\n\tif (!ctx) return;\n\tconst dur = event.queryDurationMillis;\n\tif (ctx.metrics) {\n\t\tconst m = ctx.metrics;\n\t\tm.dbCount += 1;\n\t\tm.dbTotalMs += dur;\n\t\tconst finishedAt = performance.now() - m.start;\n\t\tconst startedAt = finishedAt - dur;\n\t\tif (m.dbFirstOffset === null) m.dbFirstOffset = startedAt;\n\t\tm.dbLastOffset = finishedAt;\n\t}\n\tif (ctx.queryRecorder) {\n\t\trecordEvent(ctx.queryRecorder, event.query.sql, event.query.parameters, dur);\n\t}\n}\n\n/**\n * Returns a Kysely `log` callback. Always returns a function so per-request\n * counters (db.count, db.total, db.first, db.last) and the optional NDJSON\n * recorder both get fed. The cost over the previous \"undefined when off\"\n * behaviour is one `performance.now()` pair per query inside Kysely, which\n * is in the noise compared to any real query.\n */\nexport function kyselyLogOption(): Logger {\n\treturn kyselyLog;\n}\n"],"mappings":";;;AAkBA,MAAa,gBAAgB;AAC7B,MAAa,mBAAmB;AAkBhC,SAAgB,eAAe,OAAe,QAAgB,OAA8B;AAC3F,QAAO;EAAE,QAAQ,EAAE;EAAE;EAAO;EAAQ;EAAO;;AAG5C,SAAgB,YACf,KACA,KACA,QACA,YACO;AACP,KAAI,OAAO,KAAK;EACf;EACA;EACA;EACA,OAAO,IAAI;EACX,QAAQ,IAAI;EACZ,OAAO,IAAI;EACX,CAAC;;;;;;;;AASH,SAAgB,cAAc,KAA0B;AACvD,KAAI,IAAI,OAAO,WAAW,EAAG;AAC7B,MAAK,MAAM,KAAK,IAAI,OACnB,SAAQ,IAAI,GAAG,iBAAiB,GAAG,KAAK,UAAU,EAAE,GAAG;;;;;;;;;AAWzD,SAAgB,2BAAoC;AACnD,QAAO,QACN,OAAO,YAAY,eAAe,QAAQ,OAAO,QAAQ,IAAI,mBAAmB,IAChF;;AAGF,SAAS,UAAU,OAAuB;AACzC,KAAI,MAAM,UAAU,QAAS;CAC7B,MAAM,MAAM,mBAAmB;AAC/B,KAAI,CAAC,IAAK;CACV,MAAM,MAAM,MAAM;AAClB,KAAI,IAAI,SAAS;EAChB,MAAM,IAAI,IAAI;AACd,IAAE,WAAW;AACb,IAAE,aAAa;EACf,MAAM,aAAa,YAAY,KAAK,GAAG,EAAE;EACzC,MAAM,YAAY,aAAa;AAC/B,MAAI,EAAE,kBAAkB,KAAM,GAAE,gBAAgB;AAChD,IAAE,eAAe;;AAElB,KAAI,IAAI,cACP,aAAY,IAAI,eAAe,MAAM,MAAM,KAAK,MAAM,MAAM,YAAY,IAAI;;;;;;;;;AAW9E,SAAgB,kBAA0B;AACzC,QAAO"}
|
package/dist/db/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "../types-
|
|
2
|
-
import { i as runMigrations, n as getMigrationStatus, r as rollbackMigration, t as MigrationStatus } from "../runner-
|
|
1
|
+
import "../types-BQx6ZXpR.mjs";
|
|
2
|
+
import { i as runMigrations, n as getMigrationStatus, r as rollbackMigration, t as MigrationStatus } from "../runner-Iu3IZSDM.mjs";
|
|
3
3
|
import { a as SqliteConfig, c as sqlite, i as PostgresConfig, n as DatabaseDialectType, o as libsql, r as LibsqlConfig, s as postgres, t as DatabaseDescriptor } from "../adapters-BktHA7EO.mjs";
|
|
4
4
|
export { type DatabaseDescriptor, type DatabaseDialectType, type LibsqlConfig, type MigrationStatus, type PostgresConfig, type SqliteConfig, getMigrationStatus, libsql, postgres, rollbackMigration, runMigrations, sqlite };
|
package/dist/db/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as rollbackMigration, r as runMigrations, t as getMigrationStatus } from "../runner-
|
|
1
|
+
import { n as rollbackMigration, r as runMigrations, t as getMigrationStatus } from "../runner-DIcU2UCC.mjs";
|
|
2
2
|
import "../dialect-helpers-BKCvISIQ.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/db/adapters.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { _ as MediaValue, m as MediaProviderDescriptor } from "./placeholder-CDPtkelt.mjs";
|
|
2
|
-
import { t as Database } from "./types-
|
|
2
|
+
import { t as Database } from "./types-BQx6ZXpR.mjs";
|
|
3
3
|
import { a as ContentSeoInput, c as FindManyOptions, d as UpdateContentInput, l as FindManyResult, o as CreateContentInput, r as ContentItem, t as BylineSummary } from "./types-B_CXXnzh.mjs";
|
|
4
|
-
import { $ as ResolvedPlugin, A as PageFragmentContribution, D as MediaItem$1, G as PluginManifest, N as PageMetadataContribution, P as PageMetadataEvent, U as PluginDefinition, Z as RequestMeta, c as CommentBeforeCreateEvent, g as CronEvent, i as CommentAfterCreateEvent, it as StandardPluginDefinition, j as PageFragmentEvent, o as CommentAfterModerateEvent, q as PluginStorageConfig, y as EmailMessage } from "./types-
|
|
5
|
-
import { a as Collection, b as UpdateFieldInput, c as CollectionWithFields, d as CreateFieldInput, i as SiteSettings, p as Field, r as SiteSettingKey, u as CreateCollectionInput, y as UpdateCollectionInput } from "./types-
|
|
4
|
+
import { $ as ResolvedPlugin, A as PageFragmentContribution, D as MediaItem$1, G as PluginManifest, N as PageMetadataContribution, P as PageMetadataEvent, U as PluginDefinition, Z as RequestMeta, c as CommentBeforeCreateEvent, g as CronEvent, i as CommentAfterCreateEvent, it as StandardPluginDefinition, j as PageFragmentEvent, o as CommentAfterModerateEvent, q as PluginStorageConfig, y as EmailMessage } from "./types-IN5z_S3P.mjs";
|
|
5
|
+
import { a as Collection, b as UpdateFieldInput, c as CollectionWithFields, d as CreateFieldInput, h as FieldValidation, i as SiteSettings, p as Field, r as SiteSettingKey, u as CreateCollectionInput, y as UpdateCollectionInput } from "./types-IZSZfEwv.mjs";
|
|
6
6
|
import { d as Storage } from "./types-C-aFbqmA.mjs";
|
|
7
7
|
import { t as DatabaseDescriptor } from "./adapters-BktHA7EO.mjs";
|
|
8
8
|
import { Kysely } from "kysely";
|
|
@@ -264,7 +264,8 @@ interface CreateMediaInput {
|
|
|
264
264
|
interface FindManyMediaOptions {
|
|
265
265
|
limit?: number;
|
|
266
266
|
cursor?: string;
|
|
267
|
-
|
|
267
|
+
/** 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. */
|
|
268
|
+
mimeType?: string | readonly string[];
|
|
268
269
|
status?: MediaStatus | "all";
|
|
269
270
|
}
|
|
270
271
|
/**
|
|
@@ -332,7 +333,7 @@ declare class MediaRepository {
|
|
|
332
333
|
/**
|
|
333
334
|
* Count media items
|
|
334
335
|
*/
|
|
335
|
-
count(mimeType?: string): Promise<number>;
|
|
336
|
+
count(mimeType?: string | readonly string[]): Promise<number>;
|
|
336
337
|
/**
|
|
337
338
|
* Delete pending uploads older than the given age.
|
|
338
339
|
* Pending uploads that were never confirmed indicate abandoned upload flows.
|
|
@@ -394,6 +395,7 @@ interface FieldDefinition<_T = unknown> {
|
|
|
394
395
|
schema: z.ZodTypeAny;
|
|
395
396
|
options?: unknown;
|
|
396
397
|
ui?: FieldUIHints;
|
|
398
|
+
validation?: FieldValidation;
|
|
397
399
|
}
|
|
398
400
|
/**
|
|
399
401
|
* UI hints for admin rendering
|
|
@@ -431,15 +433,21 @@ interface FileValue {
|
|
|
431
433
|
}
|
|
432
434
|
//#endregion
|
|
433
435
|
//#region src/fields/image.d.ts
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
436
|
+
interface ImageOptions {
|
|
437
|
+
required?: boolean;
|
|
438
|
+
maxSize?: number;
|
|
439
|
+
allowedTypes?: string[];
|
|
440
|
+
}
|
|
441
|
+
declare function image(options?: ImageOptions): FieldDefinition<ImageValue | undefined>;
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/fields/file.d.ts
|
|
444
|
+
interface FileOptions {
|
|
439
445
|
required?: boolean;
|
|
440
446
|
maxSize?: number;
|
|
441
447
|
allowedTypes?: string[];
|
|
442
|
-
|
|
448
|
+
helpText?: string;
|
|
449
|
+
}
|
|
450
|
+
declare function file(options?: FileOptions): FieldDefinition<FileValue>;
|
|
443
451
|
//#endregion
|
|
444
452
|
//#region src/fields/reference.d.ts
|
|
445
453
|
/**
|
|
@@ -777,7 +785,7 @@ interface MediaResponse {
|
|
|
777
785
|
declare function handleMediaList(db: Kysely<Database>, params: {
|
|
778
786
|
cursor?: string;
|
|
779
787
|
limit?: number;
|
|
780
|
-
mimeType?: string;
|
|
788
|
+
mimeType?: string | readonly string[];
|
|
781
789
|
}): Promise<ApiResult<MediaListResponse>>;
|
|
782
790
|
/**
|
|
783
791
|
* Get single media item
|
|
@@ -2758,16 +2766,6 @@ declare const pluginManifestSchema: z$1.ZodObject<{
|
|
|
2758
2766
|
id: z$1.ZodString;
|
|
2759
2767
|
version: z$1.ZodString;
|
|
2760
2768
|
capabilities: z$1.ZodArray<z$1.ZodEnum<{
|
|
2761
|
-
"network:fetch": "network:fetch";
|
|
2762
|
-
"network:fetch:any": "network:fetch:any";
|
|
2763
|
-
"read:content": "read:content";
|
|
2764
|
-
"write:content": "write:content";
|
|
2765
|
-
"read:media": "read:media";
|
|
2766
|
-
"write:media": "write:media";
|
|
2767
|
-
"read:users": "read:users";
|
|
2768
|
-
"email:provide": "email:provide";
|
|
2769
|
-
"email:intercept": "email:intercept";
|
|
2770
|
-
"page:inject": "page:inject";
|
|
2771
2769
|
"network:request": "network:request";
|
|
2772
2770
|
"network:request:unrestricted": "network:request:unrestricted";
|
|
2773
2771
|
"content:read": "content:read";
|
|
@@ -2779,6 +2777,16 @@ declare const pluginManifestSchema: z$1.ZodObject<{
|
|
|
2779
2777
|
"hooks.email-transport:register": "hooks.email-transport:register";
|
|
2780
2778
|
"hooks.email-events:register": "hooks.email-events:register";
|
|
2781
2779
|
"hooks.page-fragments:register": "hooks.page-fragments:register";
|
|
2780
|
+
"network:fetch": "network:fetch";
|
|
2781
|
+
"network:fetch:any": "network:fetch:any";
|
|
2782
|
+
"read:content": "read:content";
|
|
2783
|
+
"write:content": "write:content";
|
|
2784
|
+
"read:media": "read:media";
|
|
2785
|
+
"write:media": "write:media";
|
|
2786
|
+
"read:users": "read:users";
|
|
2787
|
+
"email:provide": "email:provide";
|
|
2788
|
+
"email:intercept": "email:intercept";
|
|
2789
|
+
"page:inject": "page:inject";
|
|
2782
2790
|
}>>;
|
|
2783
2791
|
allowedHosts: z$1.ZodArray<z$1.ZodString>;
|
|
2784
2792
|
storage: z$1.ZodRecord<z$1.ZodString, z$1.ZodObject<{
|
|
@@ -5026,5 +5034,5 @@ declare function extractPlainText(blocks: PortableTextBlock$1[] | string | null
|
|
|
5026
5034
|
*/
|
|
5027
5035
|
declare function extractSearchableFields(entry: Record<string, unknown>, fields: string[]): Record<string, string>;
|
|
5028
5036
|
//#endregion
|
|
5029
|
-
export { UpdateMenuInput as $, EntryResult as $n, handleMediaList as $r, SuggestedAction as $t, getEntriesByTerm as A, ListResponse as Ai, WxrData as An, ProseMirrorNode as Ar, probeUrl as At, TaxonomyTerm as B,
|
|
5030
|
-
//# sourceMappingURL=index-
|
|
5037
|
+
export { UpdateMenuInput as $, EntryResult as $n, handleMediaList as $r, SuggestedAction as $t, getEntriesByTerm as A, ListResponse as Ai, WxrData as An, ProseMirrorNode as Ar, probeUrl as At, TaxonomyTerm as B, PortableTextBlock$2 as Bi, getDb as Bn, CreateSectionInput as Br, ImportContext as Bt, ReorderWidgetsInput as C, handleContentUnpublish as Ci, AuthResult as Cn, PortableTextLinkMark as Cr, parseWxrDate as Ct, WidgetComponentDef as D, ContentListResponse as Di, WxrAttachment as Dn, PortableTextUnknownBlock as Dr, getFileSources as Dt, WidgetArea as E, ApiContext as Ei, definePlugin as En, PortableTextTextBlock as Er, getAllSources as Et, getTerm as F, image as Fi, parseWxrString as Fn, SandboxRunnerFactory as Fr, FetchOptions as Ft, getMenu as G, DatabaseConfig as Gi, WaitUntilFn as Gn, getCollectionInfo as Gr, OAuthInput as Gt, UpdateTermInput as H, MediaItem as Hi, getFallbackChain as Hn, Section as Hr, ImportResult as Ht, getTermsForEntries as I, FieldDefinition as Ii, CollectionFilter as In, SandboxedPlugin as Ir, FieldCompatibility as It, CreateMenuItemInput as J, CollectionFilter$1 as Jn, MediaListResponse as Jr, ProbeResult as Jt, getMenus as K, EmDashDatabaseError as Ki, after as Kn, SchemaError as Kr, PostTypeAnalysis as Kt, invalidateTermCache as L, FieldUIHints as Li, EntryData as Ln, SerializedRequest as Lr, FileInput as Lt, getTaxonomyDef as M, portableText as Mi, WxrSite as Mn, SandboxEmailSendCallback as Mr, importReusableBlocksAsSections as Mt, getTaxonomyDefs as N, reference as Ni, WxrTag as Nn, SandboxOptions as Nr, AttachmentInfo as Nt, WidgetType as O, ContentResponse as Oi, WxrAuthor as On, ProseMirrorDocument as Or, getSource as Ot, getTaxonomyTerms as P, file as Pi, parseWxr as Pn, SandboxRunner as Pr, CollectionSchemaStatus as Pt, ReorderMenuItemsInput as Q, EmDashCollections as Qn, handleMediaGet as Qr, SourceProbeResult as Qt, CreateTermInput as R, FileValue as Ri, EntryFilter as Rn, getSection as Rr, ImportAnalysis as Rt, PropDef as S, handleContentTranslations as Si, AuthProviderModule as Sn, PortableTextImageBlock as Sr, wordpressRestSource as St, Widget as T, handleContentUpdate as Ti, ExternalAuthConfig as Tn, PortableTextSpan as Tr, clearSources as Tt, getByline as U, MediaRepository as Ui, getI18nConfig as Un, SectionSource as Ur, ImportSource as Ut, TaxonomyTermRow as V, CreateMediaInput as Vi, I18nConfig as Vn, GetSectionsOptions as Vr, ImportFieldDef as Vt, getBylineBySlug as W, ContentRepository as Wi, isI18nEnabled as Wn, UpdateSectionInput as Wr, NormalizedItem as Wt, MenuItem as X, ContentEntry as Xn, handleMediaCreate as Xr, SourceCapabilities as Xt, Menu as Y, CollectionResult as Yn, MediaResponse as Yr, SourceAuth as Yt, MenuItemType as Z, EditFieldMeta as Zn, handleMediaDelete as Zr, SourceInput as Zt, getWidgetArea as _, handleContentListTrashed as _i, S3StorageConfig as _n, computeContentHash as _r, VerifyPreviewTokenOptions as _t, search as a, handleRevisionRestore as ai, createPluginManager as an, getEmDashCollection as ar, getPluginSetting as at, CreateWidgetAreaInput as b, handleContentRestore as bi, AuthProviderAdminExports as bn, prosemirrorToPortableText as br, parseContentId as bt, FTSManager as c, handleContentCountScheduled as ci, HookPipeline as cn, resolveEmDashPath as cr, getSiteSettings as ct, SearchOptions as d, handleContentDelete as di, ValidatedPluginManifest as dn, FieldAnnotation as dr, isPreviewRequest as dt, handleMediaUpdate as ei, UrlInput as en, InferCollectionData as er, UpdateMenuItemInput as et, SearchResponse as f, handleContentDiscardDraft as fi, pluginManifestSchema as fn, createEditable as fr, GetPreviewUrlOptions as ft, Suggestion as g, handleContentList as gi, LocalStorageConfig as gn, sanitizeHref as gr, PreviewTokenPayload as gt, SuggestOptions as h, handleContentGetIncludingTrashed as hi, getStoredConfig as hn, isSafeHref as hr, GeneratePreviewTokenOptions as ht, getSuggestions as i, handleRevisionList as ii, PluginManager as in, getEditMeta as ir, getComments as it, getEntryTerms as j, ManifestResponse as ji, WxrPost as jn, SandboxEmailMessage as jr, registerSource as jt, getAllTermsForEntries as k, FieldDescriptor as ki, WxrCategory as kn, ProseMirrorMark as kr, getUrlSources as kt, CollectionSearchOptions as l, handleContentCountTrashed as li, HookResult as ln, CMSAnnotation as lr, setSiteSettings as lt, SearchStats as m, handleContentGet as mi, PluginDescriptor as mn, decodeSlug as mr, getPreviewUrl as mt, extractSearchableFields as n, RevisionResponse as ni, SandboxNotAvailableError as nn, TranslationSummary as nr, GetCommentsResult as nt, searchCollection as o, generateManifest as oi, PluginRouteError as on, getEmDashEntry as or, getPluginSettings as ot, SearchResult as p, handleContentDuplicate as pi, EmDashConfig as pn, createNoop as pr, buildPreviewUrl as pt, CreateMenuInput as q, CacheHint as qn, SchemaRegistry as qr, PostTypeMapping as qt, getSearchStats as r, handleRevisionGet as ri, createNoopSandboxRunner as rn, TranslationsResult as rr, getCommentCount as rt, searchWithDb as s, handleContentCompare as si, EmailPipeline as sn, getTranslations as sr, getSiteSetting as st, extractPlainText as t, RevisionListResponse as ti, NoopSandboxRunner as tn, ResolvePathResult as tr, GetCommentsOptions as tt, SearchConfig as u, handleContentCreate as ui, createHookPipeline as un, EditProxy as ur, getPreviewToken as ut, getWidgetAreas as v, handleContentPermanentDelete as vi, StorageDescriptor as vn, hashString as vr, VerifyPreviewTokenResult as vt, UpdateWidgetInput as w, handleContentUnschedule as wi, AuthRouteDescriptor as wn, PortableTextMarkDef as wr, wxrSource as wt, CreateWidgetInput as x, handleContentSchedule as xi, AuthProviderDescriptor as xn, PortableTextCodeBlock as xr, verifyPreviewToken as xt, getWidgetComponents as y, handleContentPublish as yi, AuthDescriptor as yn, portableTextToProsemirror as yr, generatePreviewToken as yt, TaxonomyDef as z, ImageValue as zi, emdashLoader as zn, getSections as zr, ImportConfig as zt };
|
|
5038
|
+
//# sourceMappingURL=index-Cg-rC4Gj.d.mts.map
|