@bobbykim/manguito-cms-api 0.4.1 → 0.4.2
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/{chunk-4F2CRXPG.mjs → chunk-4Y4G564P.mjs} +14 -2
- package/dist/{chunk-4F2CRXPG.mjs.map → chunk-4Y4G564P.mjs.map} +1 -1
- package/dist/{chunk-S4NT6FUM.mjs → chunk-HEYT5CH5.mjs} +2 -2
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-45O5OX2C.mjs +8 -0
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/handler-7LVYEU5C.mjs +0 -8
- /package/dist/{chunk-S4NT6FUM.mjs.map → chunk-HEYT5CH5.mjs.map} +0 -0
- /package/dist/{handler-7LVYEU5C.mjs.map → handler-45O5OX2C.mjs.map} +0 -0
|
@@ -306,10 +306,16 @@ function isColumnBacked(field) {
|
|
|
306
306
|
function createFieldKeyMap(fields) {
|
|
307
307
|
const labelToColumn = /* @__PURE__ */ new Map();
|
|
308
308
|
const columnToLabel = /* @__PURE__ */ new Map();
|
|
309
|
+
const droppedKeys = /* @__PURE__ */ new Set();
|
|
309
310
|
for (const f of fields) {
|
|
310
311
|
if (!isColumnBacked(f)) continue;
|
|
311
|
-
labelToColumn.set(f.name, f.db_column.column_name);
|
|
312
312
|
columnToLabel.set(f.db_column.column_name, f.name);
|
|
313
|
+
if (f.removed === true) {
|
|
314
|
+
droppedKeys.add(f.name);
|
|
315
|
+
droppedKeys.add(f.db_column.column_name);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
labelToColumn.set(f.name, f.db_column.column_name);
|
|
313
319
|
}
|
|
314
320
|
for (const f of fields) {
|
|
315
321
|
const columnOwner = columnToLabel.get(f.name);
|
|
@@ -319,10 +325,16 @@ function createFieldKeyMap(fields) {
|
|
|
319
325
|
);
|
|
320
326
|
}
|
|
321
327
|
}
|
|
328
|
+
for (const f of fields) {
|
|
329
|
+
if (isColumnBacked(f) && f.removed === true) {
|
|
330
|
+
columnToLabel.delete(f.db_column.column_name);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
322
333
|
const diverges = [...labelToColumn].some(([label, column]) => label !== column);
|
|
323
334
|
function remap(input, lookup) {
|
|
324
335
|
const out = {};
|
|
325
336
|
for (const key of Object.keys(input)) {
|
|
337
|
+
if (droppedKeys.has(key)) continue;
|
|
326
338
|
out[lookup.get(key) ?? key] = input[key];
|
|
327
339
|
}
|
|
328
340
|
return out;
|
|
@@ -347,4 +359,4 @@ export {
|
|
|
347
359
|
isColumnBacked,
|
|
348
360
|
createFieldKeyMap
|
|
349
361
|
};
|
|
350
|
-
//# sourceMappingURL=chunk-
|
|
362
|
+
//# sourceMappingURL=chunk-4Y4G564P.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/relations.ts","../src/field-keys.ts"],"sourcesContent":["import crypto from 'node:crypto'\nimport { sql } from 'drizzle-orm'\nimport type { ParsedField, ParsedParagraphType, SchemaRegistry } from '@bobbykim/manguito-cms-core'\nimport type { DrizzlePostgresInstance } from '@bobbykim/manguito-cms-db'\nimport type { MediaDelta } from './media-references.js'\n\n// ─── Relation persistence ─────────────────────────────────────────────────────\n//\n// Owns paragraph/junction/reference/media relation table SQL — both the write\n// side (persist/delete below) and the read side (descriptors + resolvers further\n// down). Each persist/delete returns the MediaDelta of media ids it gained and\n// lost (see media-references.ts), which the caller merges and reconciles once.\n// The repository's resolveRows() drives the read resolvers.\n\nfunction quoteIdent(name: string): string {\n if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`)\n return `\"${name}\"`\n}\n\n// Paragraph rows live on a child table with their own image/video/file columns\n// (e.g. a \"photo card\" block embeds an image) — these need the same reference-count\n// bookkeeping as top-level content fields, one level down.\nfunction paragraphMediaFieldNames(pType: ParsedParagraphType): Set<string> {\n return new Set(\n pType.fields\n .filter((f) => f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n .map((f) => f.name)\n )\n}\n\nasync function fetchParagraphMediaIds(\n db: DrizzlePostgresInstance,\n pType: ParsedParagraphType,\n itemId: string,\n fieldName: string\n): Promise<string[]> {\n const mediaColumns = pType.fields\n .filter((f) => f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n .map((f) => f.db_column!.column_name)\n if (mediaColumns.length === 0) return []\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n const cols = sql.join(mediaColumns.map((c) => sql.raw(quoteIdent(c))), sql`, `)\n const result = await db.execute(\n sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`\n )\n\n const ids: string[] = []\n for (const row of result.rows as Record<string, unknown>[]) {\n for (const col of mediaColumns) {\n const v = row[col]\n if (typeof v === 'string' && v !== '') ids.push(v)\n }\n }\n return ids\n}\n\n// A paragraph type's own paragraph fields — the one level of nesting ADR\n// core/0005 permits. Empty without a registry, which is how callers that have no\n// nesting to persist opt out.\nfunction nestedParagraphFields(\n pType: ParsedParagraphType,\n registry?: SchemaRegistry\n): Array<{ fieldName: string; nType: ParsedParagraphType }> {\n if (!registry) return []\n const out: Array<{ fieldName: string; nType: ParsedParagraphType }> = []\n for (const f of pType.fields) {\n if (f.db_column !== null) continue\n const comp = f.ui_component as { component: string; ref?: string }\n if (comp.component !== 'paragraph-embed' || !comp.ref) continue\n const nType = registry.paragraph_types[comp.ref]\n if (nType) out.push({ fieldName: f.name, nType })\n }\n return out\n}\n\n// Ids of the paragraph rows currently stored for one field of one parent.\nasync function fetchParagraphRowIds(\n db: DrizzlePostgresInstance,\n pType: ParsedParagraphType,\n itemId: string,\n fieldName: string\n): Promise<string[]> {\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n const result = await db.execute(\n sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`\n )\n return (result.rows as Record<string, unknown>[])\n .map((r) => r['id'])\n .filter((id): id is string => typeof id === 'string')\n}\n\n// Replaces every paragraph row for one field of one parent (delete + reinsert),\n// returning the media ids removed (old rows) and added (new rows).\n//\n// `registry` enables one level of paragraph nesting (ADR core/0005): a nested\n// paragraph field has no column on its parent's table, so without this its items\n// were silently dropped on write. Nested rows hang off the *paragraph* row's id,\n// and are torn down with it so the delete+reinsert cannot orphan them.\nexport async function persistParagraphField(\n db: DrizzlePostgresInstance,\n itemId: string,\n parentType: string,\n fieldName: string,\n pType: ParsedParagraphType,\n items: unknown[],\n registry?: SchemaRegistry\n): Promise<MediaDelta> {\n const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName)\n const nested = nestedParagraphFields(pType, registry)\n\n // Tear down nested rows before their parents disappear, collecting the media\n // they referenced so reference counts stay correct.\n if (nested.length > 0) {\n const staleIds = await fetchParagraphRowIds(db, pType, itemId, fieldName)\n for (const rowId of staleIds) {\n for (const n of nested) {\n const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry)\n removed.push(...d.removed)\n }\n }\n }\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`)\n\n const mediaFieldNames = paragraphMediaFieldNames(pType)\n const added: string[] = []\n\n for (let i = 0; i < items.length; i++) {\n const pItem = items[i] as Record<string, unknown>\n const rowId = crypto.randomUUID()\n const data: Record<string, unknown> = {\n id: rowId,\n parent_id: itemId,\n parent_type: parentType,\n parent_field: fieldName,\n order: i,\n created_at: new Date(),\n updated_at: new Date(),\n }\n for (const pf of pType.fields) {\n if (!pf.db_column || pf.db_column.junction) continue\n data[pf.db_column.column_name] = pItem[pf.name] ?? null\n }\n const cols = sql.join(Object.keys(data).map((k) => sql.raw(quoteIdent(k))), sql`, `)\n const vals = sql.join(Object.values(data).map((v) => sql`${v}`), sql`, `)\n await db.execute(sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`)\n\n for (const name of mediaFieldNames) {\n const val = pItem[name]\n if (typeof val === 'string' && val !== '') added.push(val)\n }\n\n // Persist this row's nested paragraph items against the row just inserted.\n for (const n of nested) {\n const nestedItems = Array.isArray(pItem[n.fieldName]) ? (pItem[n.fieldName] as unknown[]) : []\n const d = await persistParagraphField(\n db,\n rowId,\n pType.db.table_name,\n n.fieldName,\n n.nType,\n nestedItems,\n registry\n )\n added.push(...d.added)\n removed.push(...d.removed)\n }\n }\n\n return { added, removed }\n}\n\n// Deletes all paragraph rows for one field of one parent (used when the parent is\n// being deleted — paragraph rows have no FK/cascade back to their parent, so they'd\n// leak otherwise). Returns the media those rows referenced, all removed.\nexport async function deleteParagraphField(\n db: DrizzlePostgresInstance,\n itemId: string,\n fieldName: string,\n pType: ParsedParagraphType,\n registry?: SchemaRegistry\n): Promise<MediaDelta> {\n const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName)\n\n // Cascade to nested paragraph rows, which hang off these rows' ids.\n const nested = nestedParagraphFields(pType, registry)\n if (nested.length > 0) {\n const rowIds = await fetchParagraphRowIds(db, pType, itemId, fieldName)\n for (const rowId of rowIds) {\n for (const n of nested) {\n const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry)\n removed.push(...d.removed)\n }\n }\n }\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`)\n return { added: [], removed }\n}\n\n// Replaces every junction row for one many-to-many field of one parent.\nexport async function persistJunctionField(\n db: DrizzlePostgresInstance,\n itemId: string,\n junction: { table_name: string; left_column: string; right_column: string },\n relatedIds: string[]\n): Promise<void> {\n const tbl = sql.raw(quoteIdent(junction.table_name))\n const left = sql.raw(quoteIdent(junction.left_column))\n const right = sql.raw(quoteIdent(junction.right_column))\n await db.execute(sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`)\n for (const rid of relatedIds) {\n await db.execute(sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`)\n }\n}\n\n// ─── Relation descriptors and resolution (read side) ──────────────────────────\n//\n// Relation read resolution. These were lifted out of the repository so that all\n// paragraph/junction/reference/media table SQL lives in one module alongside the\n// write persistence above. The repository's resolveRows() calls into here.\n\nexport type ParagraphRelationDef = {\n type: 'paragraph'\n table: string\n}\n\nexport type ReferenceRelationDef = {\n type: 'reference'\n table: string\n fk_column: string\n}\n\nexport type JunctionRelationDef = {\n type: 'junction'\n table: string\n junction_table: string\n left_column: string\n right_column: string\n // Only ordered junctions have an \"order\" column (db codegen emits it only when\n // order_column is true) — the resolver must not ORDER BY it otherwise.\n order_column: boolean\n}\n\nexport type MediaRelationDef = {\n type: 'media'\n fk_column: string\n}\n\nexport type RelationDef =\n | ParagraphRelationDef\n | ReferenceRelationDef\n | JunctionRelationDef\n | MediaRelationDef\n\n// Derives a content type's relations map from its parsed fields, so paragraph/\n// reference/media fields are actually resolved instead of silently dropped.\n// Junction and foreign_key info is already on each field's db_column; paragraph\n// fields store their target table on the paragraph type, hence the registry.\nexport function buildRelationsMap(\n fields: ParsedField[],\n registry: SchemaRegistry\n): Record<string, RelationDef> {\n const relations: Record<string, RelationDef> = {}\n\n for (const field of fields) {\n if (field.field_type === 'image' || field.field_type === 'video' || field.field_type === 'file') {\n if (!field.db_column) continue\n relations[field.name] = { type: 'media', fk_column: field.db_column.column_name }\n } else if (field.field_type === 'paragraph') {\n const ref = (field.ui_component as { ref?: string }).ref\n const pType = ref ? registry.paragraph_types[ref] : undefined\n if (!pType) continue\n relations[field.name] = { type: 'paragraph', table: pType.db.table_name }\n } else if (field.field_type === 'reference') {\n if (!field.db_column) continue\n if (field.db_column.junction) {\n const j = field.db_column.junction\n relations[field.name] = {\n type: 'junction',\n table: j.right_table,\n junction_table: j.table_name,\n left_column: j.left_column,\n right_column: j.right_column,\n order_column: j.order_column,\n }\n } else if (field.db_column.foreign_key) {\n relations[field.name] = {\n type: 'reference',\n table: field.db_column.foreign_key.table,\n fk_column: field.db_column.column_name,\n }\n }\n }\n }\n\n return relations\n}\n\nfunction groupBy<T extends Record<string, unknown>>(\n items: T[],\n key: string\n): Record<string, T[]> {\n const result: Record<string, T[]> = {}\n for (const item of items) {\n const k = String(item[key])\n if (!result[k]) result[k] = []\n result[k]!.push(item)\n }\n return result\n}\n\n// A reference/media pass is DESTRUCTIVE: the resolved object replaces the raw FK\n// value, and when the field's label differs from its FK column (a renamed field)\n// the raw FK key is deleted outright — otherwise the response would carry both\n// the column name and the label.\n//\n// That makes a second pass over the same row object dangerous: the FK the first\n// pass consumed is gone, so the row would be nulled out. And the same object\n// really does recur — reference/junction targets are cached by `table:id`, so two\n// parents receive ONE shared object, and the GraphQL dataloaders deliberately do\n// not memoize by parent identity (\"rows are mutated and may recur across nesting\n// levels\", dataloaders.ts). So a row that already holds a resolved value is\n// skipped, which makes the pass idempotent.\nfunction needsFkResolution(\n row: Record<string, unknown>,\n fkColumn: string,\n fieldName: string\n): boolean {\n // Label diverges from the column: a missing FK key means it was already dropped.\n if (fkColumn !== fieldName && !(fkColumn in row)) return false\n // Label equals the column: the resolved object sits where the raw id was, so an\n // object (never a bare id) means this row is already done.\n const current = row[fieldName]\n return !(typeof current === 'object' && current !== null)\n}\n\n// Fully resolves a relation field on a batch of rows: paragraph → ordered rows,\n// reference → the target row, junction → target rows, media → the media row.\n// The cache (keyed table:id) dedupes target lookups within a request.\n//\n// `publishedOnly` (default false, preserving prior behavior for admin callers)\n// adds `AND published = true` to the reference/junction target-row SELECTs.\n// Reference and junction targets are always content or taxonomy tables, which\n// always carry a `published` boolean system field — see CONTENT_SYSTEM_FIELDS /\n// TAXONOMY_SYSTEM_FIELDS in packages/core/src/parser/parseSchema.ts. Paragraph\n// and media targets are deliberately excluded: paragraph rows are owned by\n// their parent (no independent publish state) and media rows have no\n// `published` column at all.\nexport async function resolveRelationField(\n db: DrizzlePostgresInstance,\n rows: Record<string, unknown>[],\n fieldName: string,\n rel: RelationDef,\n cache: Map<string, unknown>,\n publishedOnly = false\n): Promise<void> {\n if (rows.length === 0) return\n\n // Deduped by object identity: one batch can hold the same row twice (see\n // needsFkResolution above), and the destructive branches would then read an FK\n // the first visit already consumed. The caller's array is left untouched — the\n // GraphQL dataloader maps over it to build its results.\n const batch = [...new Set(rows)]\n\n if (rel.type === 'paragraph') {\n const parentIds = batch.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n // Scoped by parent_field as well as parent_id: two paragraph fields of the\n // same paragraph type share one table, so filtering on the parent alone\n // hands each field the other's rows.\n const result = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY \"order\" ASC`\n )\n const byParent = groupBy(result.rows as Record<string, unknown>[], 'parent_id')\n for (const row of batch) {\n row[fieldName] = byParent[row['id'] as string] ?? []\n }\n } else if (rel.type === 'reference') {\n const dropFk = rel.fk_column !== fieldName\n const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName))\n if (pending.length === 0) return\n\n const fkValues = pending.map((r) => r[rel.fk_column] as string).filter(Boolean)\n if (fkValues.length === 0) {\n for (const row of pending) {\n row[fieldName] = null\n if (dropFk) delete row[rel.fk_column]\n }\n return\n }\n const unique = [...new Set(fkValues)]\n const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`))\n if (uncached.length > 0) {\n const inList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const publishedCond = publishedOnly ? sql` AND published = true` : sql``\n const result = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})${publishedCond}`\n )\n for (const item of result.rows as Record<string, unknown>[]) {\n cache.set(`${rel.table}:${item['id']}`, item)\n }\n }\n for (const row of pending) {\n const fkVal = row[rel.fk_column] as string\n row[fieldName] = fkVal ? (cache.get(`${rel.table}:${fkVal}`) ?? null) : null\n if (dropFk) delete row[rel.fk_column]\n }\n } else if (rel.type === 'junction') {\n const parentIds = batch.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const orderBy = rel.order_column ? sql` ORDER BY \"order\" ASC` : sql``\n const junctionResult = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`\n )\n const jRows = junctionResult.rows as Record<string, unknown>[]\n const rightIds = [...new Set(jRows.map((r) => r[rel.right_column] as string))]\n const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`))\n if (uncached.length > 0) {\n const rightInList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const publishedCond = publishedOnly ? sql` AND published = true` : sql``\n const entitiesResult = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})${publishedCond}`\n )\n for (const item of entitiesResult.rows as Record<string, unknown>[]) {\n cache.set(`${rel.table}:${item['id']}`, item)\n }\n }\n const byLeft = groupBy(jRows, rel.left_column)\n for (const row of batch) {\n const jEntries = byLeft[row['id'] as string] ?? []\n row[fieldName] = jEntries\n .map((jr) => cache.get(`${rel.table}:${jr[rel.right_column] as string}`))\n .filter(Boolean)\n }\n } else if (rel.type === 'media') {\n const dropFk = rel.fk_column !== fieldName\n const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName))\n if (pending.length === 0) return\n\n const fkValues = pending.map((r) => r[rel.fk_column] as string).filter(Boolean)\n if (fkValues.length === 0) {\n for (const row of pending) {\n row[fieldName] = null\n if (dropFk) delete row[rel.fk_column]\n }\n return\n }\n const unique = [...new Set(fkValues)]\n const uncached = unique.filter((id) => !cache.has(`media:${id}`))\n if (uncached.length > 0) {\n const inList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(sql`SELECT * FROM \"media\" WHERE id IN (${inList})`)\n for (const item of result.rows as Record<string, unknown>[]) {\n cache.set(`media:${item['id']}`, item)\n }\n }\n for (const row of pending) {\n const fkVal = row[rel.fk_column] as string\n row[fieldName] = fkVal ? (cache.get(`media:${fkVal}`) ?? null) : null\n if (dropFk) delete row[rel.fk_column]\n }\n }\n}\n\n// Populates paragraph/junction fields with bare ID arrays when they are not\n// explicitly included — they have no column on the owning table, so without this\n// they'd be silently absent. Reference fields already carry their bare ID as a\n// real column from `SELECT *`, so they need no extra query.\nexport async function resolveRelationBareIds(\n db: DrizzlePostgresInstance,\n rows: Record<string, unknown>[],\n fieldName: string,\n rel: RelationDef\n): Promise<void> {\n if (rows.length === 0) return\n\n if (rel.type === 'paragraph') {\n const parentIds = rows.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(\n sql`SELECT id, parent_id FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY \"order\" ASC`\n )\n const byParent = groupBy(result.rows as Record<string, unknown>[], 'parent_id')\n for (const row of rows) {\n row[fieldName] = (byParent[row['id'] as string] ?? []).map((r) => r['id'])\n }\n } else if (rel.type === 'junction') {\n const parentIds = rows.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(\n sql`SELECT ${sql.raw(quoteIdent(rel.left_column))} AS left_id, ${sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})`\n )\n const byLeft = groupBy(result.rows as Record<string, unknown>[], 'left_id')\n for (const row of rows) {\n row[fieldName] = (byLeft[row['id'] as string] ?? []).map((r) => r['right_id'])\n }\n }\n}\n","import type { DbColumn, ParsedField } from '@bobbykim/manguito-cms-core'\n\n// ─── Label ↔ storage key mapping ──────────────────────────────────────────────\n//\n// A field has two names: its public LABEL (`field.name`, what API consumers and\n// the admin panel see) and its STORAGE key (`db_column.column_name`, the actual\n// Postgres column). They are identical today, but schema versioning makes a\n// rename change only the label — the column keeps its original name for the life\n// of the data. Every place that reads or writes a DB row must therefore use the\n// storage key, and every place that reads a request body or writes a response\n// must use the label.\n//\n// Only column-backed fields participate. Paragraph fields have no column\n// (their association lives on the paragraph table via parent_field) and\n// many-to-many references have no column either (the junction table owns the\n// association), so both keep the field name as their identity and are excluded.\n\n// A type predicate, not a plain boolean: narrowing `db_column` to non-null lets\n// the compiler enforce the invariant at every call site instead of each one\n// re-asserting it with `db_column!`.\nexport function isColumnBacked(field: ParsedField): field is ParsedField & { db_column: DbColumn } {\n const col = field.db_column\n if (col === null) return false\n if (col.column_name === '') return false\n if (col.junction) return false\n return true\n}\n\nexport type FieldKeyMap = {\n /** Request body (label-keyed) → storage-keyed. Unknown keys pass through. */\n toStorage(input: Record<string, unknown>): Record<string, unknown>\n /** DB row (storage-keyed) → label-keyed. Unknown keys pass through. */\n toLabels(row: Record<string, unknown>): Record<string, unknown>\n columnFor(label: string): string | undefined\n labelFor(column: string): string | undefined\n /** Labels of column-backed fields — the valid filter/sort surface. */\n labels: string[]\n /** False when every label equals its column, letting callers skip the copy. */\n diverges: boolean\n}\n\n/**\n * Built ONCE per content type at startup, not per request. Throws on a key\n * collision, matching how createCmsApp refuses to boot on a broken roles\n * registry: an ambiguous mapping would silently corrupt responses.\n */\nexport function createFieldKeyMap(fields: ParsedField[]): FieldKeyMap {\n const labelToColumn = new Map<string, string>()\n const columnToLabel = new Map<string, string>()\n\n for (const f of fields) {\n if (!isColumnBacked(f)) continue\n labelToColumn.set(f.name, f.db_column.column_name)\n columnToLabel.set(f.db_column.column_name, f.name)\n }\n\n // A label that is also some OTHER field's column name would make toLabels\n // ambiguous: two source keys would map onto one destination key.\n //\n // Checked against EVERY field's label, not just the column-backed subset.\n // Paragraph, many-to-many and programmatic fields have no column of their\n // own, but their labels are written into the same key space as storage\n // columns before toLabels runs (routes/admin/content.ts, relations.ts,\n // programmatic/resolve.ts). A paragraph field named after another field's\n // column would overwrite that column's value on the row and then be renamed\n // onto the other field's label — serving the wrong value under the wrong key.\n //\n // Not detected: two fields declaring the SAME column name. That is\n // unreachable from the parser, which derives every column from its own field.\n for (const f of fields) {\n const columnOwner = columnToLabel.get(f.name)\n if (columnOwner !== undefined && columnOwner !== f.name) {\n throw new Error(\n `Fatal: field key map failed to build — field label \"${f.name}\" collides with the ` +\n `storage column of field \"${columnOwner}\". A field label may not reuse another ` +\n `field's column name. Rename the field, then run \\`manguito validate\\` to check your schema.`\n )\n }\n }\n\n const diverges = [...labelToColumn].some(([label, column]) => label !== column)\n\n function remap(\n input: Record<string, unknown>,\n lookup: Map<string, string>\n ): Record<string, unknown> {\n // Always returns a NEW object, even when nothing diverges. Returning `input`\n // unchanged would make aliasing depend on the schema, so a bug where a\n // caller mutates a row after mapping would reproduce only on renamed\n // fields. A shallow copy per row is not worth that class of bug.\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(input)) {\n out[lookup.get(key) ?? key] = input[key]\n }\n return out\n }\n\n return {\n toStorage: (input) => remap(input, labelToColumn),\n toLabels: (row) => remap(row, columnToLabel),\n columnFor: (label) => labelToColumn.get(label),\n labelFor: (column) => columnToLabel.get(column),\n labels: [...labelToColumn.keys()],\n diverges,\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/relations.ts","../src/field-keys.ts"],"sourcesContent":["import crypto from 'node:crypto'\nimport { sql } from 'drizzle-orm'\nimport type { ParsedField, ParsedParagraphType, SchemaRegistry } from '@bobbykim/manguito-cms-core'\nimport type { DrizzlePostgresInstance } from '@bobbykim/manguito-cms-db'\nimport type { MediaDelta } from './media-references.js'\n\n// ─── Relation persistence ─────────────────────────────────────────────────────\n//\n// Owns paragraph/junction/reference/media relation table SQL — both the write\n// side (persist/delete below) and the read side (descriptors + resolvers further\n// down). Each persist/delete returns the MediaDelta of media ids it gained and\n// lost (see media-references.ts), which the caller merges and reconciles once.\n// The repository's resolveRows() drives the read resolvers.\n\nfunction quoteIdent(name: string): string {\n if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`)\n return `\"${name}\"`\n}\n\n// Paragraph rows live on a child table with their own image/video/file columns\n// (e.g. a \"photo card\" block embeds an image) — these need the same reference-count\n// bookkeeping as top-level content fields, one level down.\nfunction paragraphMediaFieldNames(pType: ParsedParagraphType): Set<string> {\n return new Set(\n pType.fields\n .filter((f) => f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n .map((f) => f.name)\n )\n}\n\nasync function fetchParagraphMediaIds(\n db: DrizzlePostgresInstance,\n pType: ParsedParagraphType,\n itemId: string,\n fieldName: string\n): Promise<string[]> {\n const mediaColumns = pType.fields\n .filter((f) => f.field_type === 'image' || f.field_type === 'video' || f.field_type === 'file')\n .map((f) => f.db_column!.column_name)\n if (mediaColumns.length === 0) return []\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n const cols = sql.join(mediaColumns.map((c) => sql.raw(quoteIdent(c))), sql`, `)\n const result = await db.execute(\n sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`\n )\n\n const ids: string[] = []\n for (const row of result.rows as Record<string, unknown>[]) {\n for (const col of mediaColumns) {\n const v = row[col]\n if (typeof v === 'string' && v !== '') ids.push(v)\n }\n }\n return ids\n}\n\n// A paragraph type's own paragraph fields — the one level of nesting ADR\n// core/0005 permits. Empty without a registry, which is how callers that have no\n// nesting to persist opt out.\nfunction nestedParagraphFields(\n pType: ParsedParagraphType,\n registry?: SchemaRegistry\n): Array<{ fieldName: string; nType: ParsedParagraphType }> {\n if (!registry) return []\n const out: Array<{ fieldName: string; nType: ParsedParagraphType }> = []\n for (const f of pType.fields) {\n if (f.db_column !== null) continue\n const comp = f.ui_component as { component: string; ref?: string }\n if (comp.component !== 'paragraph-embed' || !comp.ref) continue\n const nType = registry.paragraph_types[comp.ref]\n if (nType) out.push({ fieldName: f.name, nType })\n }\n return out\n}\n\n// Ids of the paragraph rows currently stored for one field of one parent.\nasync function fetchParagraphRowIds(\n db: DrizzlePostgresInstance,\n pType: ParsedParagraphType,\n itemId: string,\n fieldName: string\n): Promise<string[]> {\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n const result = await db.execute(\n sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`\n )\n return (result.rows as Record<string, unknown>[])\n .map((r) => r['id'])\n .filter((id): id is string => typeof id === 'string')\n}\n\n// Replaces every paragraph row for one field of one parent (delete + reinsert),\n// returning the media ids removed (old rows) and added (new rows).\n//\n// `registry` enables one level of paragraph nesting (ADR core/0005): a nested\n// paragraph field has no column on its parent's table, so without this its items\n// were silently dropped on write. Nested rows hang off the *paragraph* row's id,\n// and are torn down with it so the delete+reinsert cannot orphan them.\nexport async function persistParagraphField(\n db: DrizzlePostgresInstance,\n itemId: string,\n parentType: string,\n fieldName: string,\n pType: ParsedParagraphType,\n items: unknown[],\n registry?: SchemaRegistry\n): Promise<MediaDelta> {\n const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName)\n const nested = nestedParagraphFields(pType, registry)\n\n // Tear down nested rows before their parents disappear, collecting the media\n // they referenced so reference counts stay correct.\n if (nested.length > 0) {\n const staleIds = await fetchParagraphRowIds(db, pType, itemId, fieldName)\n for (const rowId of staleIds) {\n for (const n of nested) {\n const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry)\n removed.push(...d.removed)\n }\n }\n }\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`)\n\n const mediaFieldNames = paragraphMediaFieldNames(pType)\n const added: string[] = []\n\n for (let i = 0; i < items.length; i++) {\n const pItem = items[i] as Record<string, unknown>\n const rowId = crypto.randomUUID()\n const data: Record<string, unknown> = {\n id: rowId,\n parent_id: itemId,\n parent_type: parentType,\n parent_field: fieldName,\n order: i,\n created_at: new Date(),\n updated_at: new Date(),\n }\n for (const pf of pType.fields) {\n if (!pf.db_column || pf.db_column.junction) continue\n data[pf.db_column.column_name] = pItem[pf.name] ?? null\n }\n const cols = sql.join(Object.keys(data).map((k) => sql.raw(quoteIdent(k))), sql`, `)\n const vals = sql.join(Object.values(data).map((v) => sql`${v}`), sql`, `)\n await db.execute(sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`)\n\n for (const name of mediaFieldNames) {\n const val = pItem[name]\n if (typeof val === 'string' && val !== '') added.push(val)\n }\n\n // Persist this row's nested paragraph items against the row just inserted.\n for (const n of nested) {\n const nestedItems = Array.isArray(pItem[n.fieldName]) ? (pItem[n.fieldName] as unknown[]) : []\n const d = await persistParagraphField(\n db,\n rowId,\n pType.db.table_name,\n n.fieldName,\n n.nType,\n nestedItems,\n registry\n )\n added.push(...d.added)\n removed.push(...d.removed)\n }\n }\n\n return { added, removed }\n}\n\n// Deletes all paragraph rows for one field of one parent (used when the parent is\n// being deleted — paragraph rows have no FK/cascade back to their parent, so they'd\n// leak otherwise). Returns the media those rows referenced, all removed.\nexport async function deleteParagraphField(\n db: DrizzlePostgresInstance,\n itemId: string,\n fieldName: string,\n pType: ParsedParagraphType,\n registry?: SchemaRegistry\n): Promise<MediaDelta> {\n const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName)\n\n // Cascade to nested paragraph rows, which hang off these rows' ids.\n const nested = nestedParagraphFields(pType, registry)\n if (nested.length > 0) {\n const rowIds = await fetchParagraphRowIds(db, pType, itemId, fieldName)\n for (const rowId of rowIds) {\n for (const n of nested) {\n const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry)\n removed.push(...d.removed)\n }\n }\n }\n\n const tbl = sql.raw(quoteIdent(pType.db.table_name))\n await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`)\n return { added: [], removed }\n}\n\n// Replaces every junction row for one many-to-many field of one parent.\nexport async function persistJunctionField(\n db: DrizzlePostgresInstance,\n itemId: string,\n junction: { table_name: string; left_column: string; right_column: string },\n relatedIds: string[]\n): Promise<void> {\n const tbl = sql.raw(quoteIdent(junction.table_name))\n const left = sql.raw(quoteIdent(junction.left_column))\n const right = sql.raw(quoteIdent(junction.right_column))\n await db.execute(sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`)\n for (const rid of relatedIds) {\n await db.execute(sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`)\n }\n}\n\n// ─── Relation descriptors and resolution (read side) ──────────────────────────\n//\n// Relation read resolution. These were lifted out of the repository so that all\n// paragraph/junction/reference/media table SQL lives in one module alongside the\n// write persistence above. The repository's resolveRows() calls into here.\n\nexport type ParagraphRelationDef = {\n type: 'paragraph'\n table: string\n}\n\nexport type ReferenceRelationDef = {\n type: 'reference'\n table: string\n fk_column: string\n}\n\nexport type JunctionRelationDef = {\n type: 'junction'\n table: string\n junction_table: string\n left_column: string\n right_column: string\n // Only ordered junctions have an \"order\" column (db codegen emits it only when\n // order_column is true) — the resolver must not ORDER BY it otherwise.\n order_column: boolean\n}\n\nexport type MediaRelationDef = {\n type: 'media'\n fk_column: string\n}\n\nexport type RelationDef =\n | ParagraphRelationDef\n | ReferenceRelationDef\n | JunctionRelationDef\n | MediaRelationDef\n\n// Derives a content type's relations map from its parsed fields, so paragraph/\n// reference/media fields are actually resolved instead of silently dropped.\n// Junction and foreign_key info is already on each field's db_column; paragraph\n// fields store their target table on the paragraph type, hence the registry.\nexport function buildRelationsMap(\n fields: ParsedField[],\n registry: SchemaRegistry\n): Record<string, RelationDef> {\n const relations: Record<string, RelationDef> = {}\n\n for (const field of fields) {\n if (field.field_type === 'image' || field.field_type === 'video' || field.field_type === 'file') {\n if (!field.db_column) continue\n relations[field.name] = { type: 'media', fk_column: field.db_column.column_name }\n } else if (field.field_type === 'paragraph') {\n const ref = (field.ui_component as { ref?: string }).ref\n const pType = ref ? registry.paragraph_types[ref] : undefined\n if (!pType) continue\n relations[field.name] = { type: 'paragraph', table: pType.db.table_name }\n } else if (field.field_type === 'reference') {\n if (!field.db_column) continue\n if (field.db_column.junction) {\n const j = field.db_column.junction\n relations[field.name] = {\n type: 'junction',\n table: j.right_table,\n junction_table: j.table_name,\n left_column: j.left_column,\n right_column: j.right_column,\n order_column: j.order_column,\n }\n } else if (field.db_column.foreign_key) {\n relations[field.name] = {\n type: 'reference',\n table: field.db_column.foreign_key.table,\n fk_column: field.db_column.column_name,\n }\n }\n }\n }\n\n return relations\n}\n\nfunction groupBy<T extends Record<string, unknown>>(\n items: T[],\n key: string\n): Record<string, T[]> {\n const result: Record<string, T[]> = {}\n for (const item of items) {\n const k = String(item[key])\n if (!result[k]) result[k] = []\n result[k]!.push(item)\n }\n return result\n}\n\n// A reference/media pass is DESTRUCTIVE: the resolved object replaces the raw FK\n// value, and when the field's label differs from its FK column (a renamed field)\n// the raw FK key is deleted outright — otherwise the response would carry both\n// the column name and the label.\n//\n// That makes a second pass over the same row object dangerous: the FK the first\n// pass consumed is gone, so the row would be nulled out. And the same object\n// really does recur — reference/junction targets are cached by `table:id`, so two\n// parents receive ONE shared object, and the GraphQL dataloaders deliberately do\n// not memoize by parent identity (\"rows are mutated and may recur across nesting\n// levels\", dataloaders.ts). So a row that already holds a resolved value is\n// skipped, which makes the pass idempotent.\nfunction needsFkResolution(\n row: Record<string, unknown>,\n fkColumn: string,\n fieldName: string\n): boolean {\n // Label diverges from the column: a missing FK key means it was already dropped.\n if (fkColumn !== fieldName && !(fkColumn in row)) return false\n // Label equals the column: the resolved object sits where the raw id was, so an\n // object (never a bare id) means this row is already done.\n const current = row[fieldName]\n return !(typeof current === 'object' && current !== null)\n}\n\n// Fully resolves a relation field on a batch of rows: paragraph → ordered rows,\n// reference → the target row, junction → target rows, media → the media row.\n// The cache (keyed table:id) dedupes target lookups within a request.\n//\n// `publishedOnly` (default false, preserving prior behavior for admin callers)\n// adds `AND published = true` to the reference/junction target-row SELECTs.\n// Reference and junction targets are always content or taxonomy tables, which\n// always carry a `published` boolean system field — see CONTENT_SYSTEM_FIELDS /\n// TAXONOMY_SYSTEM_FIELDS in packages/core/src/parser/parseSchema.ts. Paragraph\n// and media targets are deliberately excluded: paragraph rows are owned by\n// their parent (no independent publish state) and media rows have no\n// `published` column at all.\nexport async function resolveRelationField(\n db: DrizzlePostgresInstance,\n rows: Record<string, unknown>[],\n fieldName: string,\n rel: RelationDef,\n cache: Map<string, unknown>,\n publishedOnly = false\n): Promise<void> {\n if (rows.length === 0) return\n\n // Deduped by object identity: one batch can hold the same row twice (see\n // needsFkResolution above), and the destructive branches would then read an FK\n // the first visit already consumed. The caller's array is left untouched — the\n // GraphQL dataloader maps over it to build its results.\n const batch = [...new Set(rows)]\n\n if (rel.type === 'paragraph') {\n const parentIds = batch.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n // Scoped by parent_field as well as parent_id: two paragraph fields of the\n // same paragraph type share one table, so filtering on the parent alone\n // hands each field the other's rows.\n const result = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY \"order\" ASC`\n )\n const byParent = groupBy(result.rows as Record<string, unknown>[], 'parent_id')\n for (const row of batch) {\n row[fieldName] = byParent[row['id'] as string] ?? []\n }\n } else if (rel.type === 'reference') {\n const dropFk = rel.fk_column !== fieldName\n const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName))\n if (pending.length === 0) return\n\n const fkValues = pending.map((r) => r[rel.fk_column] as string).filter(Boolean)\n if (fkValues.length === 0) {\n for (const row of pending) {\n row[fieldName] = null\n if (dropFk) delete row[rel.fk_column]\n }\n return\n }\n const unique = [...new Set(fkValues)]\n const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`))\n if (uncached.length > 0) {\n const inList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const publishedCond = publishedOnly ? sql` AND published = true` : sql``\n const result = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})${publishedCond}`\n )\n for (const item of result.rows as Record<string, unknown>[]) {\n cache.set(`${rel.table}:${item['id']}`, item)\n }\n }\n for (const row of pending) {\n const fkVal = row[rel.fk_column] as string\n row[fieldName] = fkVal ? (cache.get(`${rel.table}:${fkVal}`) ?? null) : null\n if (dropFk) delete row[rel.fk_column]\n }\n } else if (rel.type === 'junction') {\n const parentIds = batch.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const orderBy = rel.order_column ? sql` ORDER BY \"order\" ASC` : sql``\n const junctionResult = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`\n )\n const jRows = junctionResult.rows as Record<string, unknown>[]\n const rightIds = [...new Set(jRows.map((r) => r[rel.right_column] as string))]\n const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`))\n if (uncached.length > 0) {\n const rightInList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const publishedCond = publishedOnly ? sql` AND published = true` : sql``\n const entitiesResult = await db.execute(\n sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})${publishedCond}`\n )\n for (const item of entitiesResult.rows as Record<string, unknown>[]) {\n cache.set(`${rel.table}:${item['id']}`, item)\n }\n }\n const byLeft = groupBy(jRows, rel.left_column)\n for (const row of batch) {\n const jEntries = byLeft[row['id'] as string] ?? []\n row[fieldName] = jEntries\n .map((jr) => cache.get(`${rel.table}:${jr[rel.right_column] as string}`))\n .filter(Boolean)\n }\n } else if (rel.type === 'media') {\n const dropFk = rel.fk_column !== fieldName\n const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName))\n if (pending.length === 0) return\n\n const fkValues = pending.map((r) => r[rel.fk_column] as string).filter(Boolean)\n if (fkValues.length === 0) {\n for (const row of pending) {\n row[fieldName] = null\n if (dropFk) delete row[rel.fk_column]\n }\n return\n }\n const unique = [...new Set(fkValues)]\n const uncached = unique.filter((id) => !cache.has(`media:${id}`))\n if (uncached.length > 0) {\n const inList = sql.join(uncached.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(sql`SELECT * FROM \"media\" WHERE id IN (${inList})`)\n for (const item of result.rows as Record<string, unknown>[]) {\n cache.set(`media:${item['id']}`, item)\n }\n }\n for (const row of pending) {\n const fkVal = row[rel.fk_column] as string\n row[fieldName] = fkVal ? (cache.get(`media:${fkVal}`) ?? null) : null\n if (dropFk) delete row[rel.fk_column]\n }\n }\n}\n\n// Populates paragraph/junction fields with bare ID arrays when they are not\n// explicitly included — they have no column on the owning table, so without this\n// they'd be silently absent. Reference fields already carry their bare ID as a\n// real column from `SELECT *`, so they need no extra query.\nexport async function resolveRelationBareIds(\n db: DrizzlePostgresInstance,\n rows: Record<string, unknown>[],\n fieldName: string,\n rel: RelationDef\n): Promise<void> {\n if (rows.length === 0) return\n\n if (rel.type === 'paragraph') {\n const parentIds = rows.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(\n sql`SELECT id, parent_id FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY \"order\" ASC`\n )\n const byParent = groupBy(result.rows as Record<string, unknown>[], 'parent_id')\n for (const row of rows) {\n row[fieldName] = (byParent[row['id'] as string] ?? []).map((r) => r['id'])\n }\n } else if (rel.type === 'junction') {\n const parentIds = rows.map((r) => r['id'] as string)\n const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `)\n const result = await db.execute(\n sql`SELECT ${sql.raw(quoteIdent(rel.left_column))} AS left_id, ${sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})`\n )\n const byLeft = groupBy(result.rows as Record<string, unknown>[], 'left_id')\n for (const row of rows) {\n row[fieldName] = (byLeft[row['id'] as string] ?? []).map((r) => r['right_id'])\n }\n }\n}\n","import type { DbColumn, ParsedField } from '@bobbykim/manguito-cms-core'\n\n// ─── Label ↔ storage key mapping ──────────────────────────────────────────────\n//\n// A field has two names: its public LABEL (`field.name`, what API consumers and\n// the admin panel see) and its STORAGE key (`db_column.column_name`, the actual\n// Postgres column). They are identical today, but schema versioning makes a\n// rename change only the label — the column keeps its original name for the life\n// of the data. Every place that reads or writes a DB row must therefore use the\n// storage key, and every place that reads a request body or writes a response\n// must use the label.\n//\n// Only column-backed fields participate. Paragraph fields have no column\n// (their association lives on the paragraph table via parent_field) and\n// many-to-many references have no column either (the junction table owns the\n// association), so both keep the field name as their identity and are excluded.\n\n// A type predicate, not a plain boolean: narrowing `db_column` to non-null lets\n// the compiler enforce the invariant at every call site instead of each one\n// re-asserting it with `db_column!`.\nexport function isColumnBacked(field: ParsedField): field is ParsedField & { db_column: DbColumn } {\n const col = field.db_column\n if (col === null) return false\n if (col.column_name === '') return false\n if (col.junction) return false\n return true\n}\n\nexport type FieldKeyMap = {\n /** Request body (label-keyed) → storage-keyed. Unknown keys pass through. */\n toStorage(input: Record<string, unknown>): Record<string, unknown>\n /** DB row (storage-keyed) → label-keyed. Unknown keys pass through. */\n toLabels(row: Record<string, unknown>): Record<string, unknown>\n columnFor(label: string): string | undefined\n labelFor(column: string): string | undefined\n /** Labels of column-backed fields — the valid filter/sort surface. */\n labels: string[]\n /** False when every label equals its column, letting callers skip the copy. */\n diverges: boolean\n}\n\n/**\n * Built ONCE per content type at startup, not per request. Throws on a key\n * collision, matching how createCmsApp refuses to boot on a broken roles\n * registry: an ambiguous mapping would silently corrupt responses.\n *\n * Tombstones (`field.removed === true`) are column-backed — the parser keeps\n * their column alive for older live versions — but the CURRENT version must\n * never serve or accept them. They are excluded from `labelToColumn` and the\n * public `columnToLabel` below, and additionally dropped by key in `remap`:\n * without that second step, an unmapped retained column would pass through\n * `remap` unchanged (see the comment on `remap`) and reach the response under\n * its raw column name.\n */\nexport function createFieldKeyMap(fields: ParsedField[]): FieldKeyMap {\n const labelToColumn = new Map<string, string>()\n // Includes tombstoned columns until the collision check below has run —\n // see the trap note there before touching this map.\n const columnToLabel = new Map<string, string>()\n // Both the name and the column of every tombstone, so remap can drop a\n // tombstone under either key. They differ when a field was renamed and\n // THEN removed: it carries both `column` (the original, still-live column)\n // and `removed`.\n const droppedKeys = new Set<string>()\n\n for (const f of fields) {\n if (!isColumnBacked(f)) continue\n columnToLabel.set(f.db_column.column_name, f.name)\n if (f.removed === true) {\n droppedKeys.add(f.name)\n droppedKeys.add(f.db_column.column_name)\n continue\n }\n labelToColumn.set(f.name, f.db_column.column_name)\n }\n\n // A label that is also some OTHER field's column name would make toLabels\n // ambiguous: two source keys would map onto one destination key.\n //\n // Checked against EVERY field's label, not just the column-backed subset.\n // Paragraph, many-to-many and programmatic fields have no column of their\n // own, but their labels are written into the same key space as storage\n // columns before toLabels runs (routes/admin/content.ts, relations.ts,\n // programmatic/resolve.ts). A paragraph field named after another field's\n // column would overwrite that column's value on the row and then be renamed\n // onto the other field's label — serving the wrong value under the wrong key.\n //\n // Deliberately run against `columnToLabel` BEFORE tombstoned columns are\n // stripped from it below: a live field's label may collide with a\n // TOMBSTONE's column (the column still physically exists on the row), and\n // that must keep throwing. Stripping tombstones first would silently pass\n // this configuration, and the exclusion step afterward would then delete\n // the live field's column from every response instead of the tombstone's.\n //\n // Not detected: two fields declaring the SAME column name. A field can now\n // declare its own `column`, so this is no longer structurally impossible —\n // but core rejects it with `DUPLICATE_COLUMN` at parse time, so it is still\n // unreachable here.\n for (const f of fields) {\n const columnOwner = columnToLabel.get(f.name)\n if (columnOwner !== undefined && columnOwner !== f.name) {\n throw new Error(\n `Fatal: field key map failed to build — field label \"${f.name}\" collides with the ` +\n `storage column of field \"${columnOwner}\". A field label may not reuse another ` +\n `field's column name. Rename the field, then run \\`manguito validate\\` to check your schema.`\n )\n }\n }\n\n // Only now strip tombstoned columns — the collision check above has already\n // run against the full map.\n for (const f of fields) {\n if (isColumnBacked(f) && f.removed === true) {\n columnToLabel.delete(f.db_column.column_name)\n }\n }\n\n const diverges = [...labelToColumn].some(([label, column]) => label !== column)\n\n function remap(\n input: Record<string, unknown>,\n lookup: Map<string, string>\n ): Record<string, unknown> {\n // Always returns a NEW object, even when nothing diverges. Returning `input`\n // unchanged would make aliasing depend on the schema, so a bug where a\n // caller mutates a row after mapping would reproduce only on renamed\n // fields. A shallow copy per row is not worth that class of bug.\n //\n // Unknown keys pass through unchanged — which is exactly why a tombstone\n // key must be checked and skipped explicitly here, rather than relying on\n // its absence from `lookup`: absence alone would let it pass through under\n // its raw key instead of being dropped.\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(input)) {\n if (droppedKeys.has(key)) continue\n out[lookup.get(key) ?? key] = input[key]\n }\n return out\n }\n\n return {\n toStorage: (input) => remap(input, labelToColumn),\n toLabels: (row) => remap(row, columnToLabel),\n columnFor: (label) => labelToColumn.get(label),\n labelFor: (column) => columnToLabel.get(column),\n labels: [...labelToColumn.keys()],\n diverges,\n }\n}\n"],"mappings":";AAAA,OAAO,YAAY;AACnB,SAAS,WAAW;AAapB,SAAS,WAAW,MAAsB;AACxC,MAAI,CAAC,qBAAqB,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,sBAAsB,IAAI,EAAE;AAClF,SAAO,IAAI,IAAI;AACjB;AAKA,SAAS,yBAAyB,OAAyC;AACzE,SAAO,IAAI;AAAA,IACT,MAAM,OACH,OAAO,CAAC,MAAM,EAAE,eAAe,WAAW,EAAE,eAAe,WAAW,EAAE,eAAe,MAAM,EAC7F,IAAI,CAAC,MAAM,EAAE,IAAI;AAAA,EACtB;AACF;AAEA,eAAe,uBACb,IACA,OACA,QACA,WACmB;AACnB,QAAM,eAAe,MAAM,OACxB,OAAO,CAAC,MAAM,EAAE,eAAe,WAAW,EAAE,eAAe,WAAW,EAAE,eAAe,MAAM,EAC7F,IAAI,CAAC,MAAM,EAAE,UAAW,WAAW;AACtC,MAAI,aAAa,WAAW,EAAG,QAAO,CAAC;AAEvC,QAAM,MAAM,IAAI,IAAI,WAAW,MAAM,GAAG,UAAU,CAAC;AACnD,QAAM,OAAO,IAAI,KAAK,aAAa,IAAI,CAAC,MAAM,IAAI,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO;AAC9E,QAAM,SAAS,MAAM,GAAG;AAAA,IACtB,aAAa,IAAI,SAAS,GAAG,sBAAsB,MAAM,uBAAuB,SAAS;AAAA,EAC3F;AAEA,QAAM,MAAgB,CAAC;AACvB,aAAW,OAAO,OAAO,MAAmC;AAC1D,eAAW,OAAO,cAAc;AAC9B,YAAM,IAAI,IAAI,GAAG;AACjB,UAAI,OAAO,MAAM,YAAY,MAAM,GAAI,KAAI,KAAK,CAAC;AAAA,IACnD;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,sBACP,OACA,UAC0D;AAC1D,MAAI,CAAC,SAAU,QAAO,CAAC;AACvB,QAAM,MAAgE,CAAC;AACvE,aAAW,KAAK,MAAM,QAAQ;AAC5B,QAAI,EAAE,cAAc,KAAM;AAC1B,UAAM,OAAO,EAAE;AACf,QAAI,KAAK,cAAc,qBAAqB,CAAC,KAAK,IAAK;AACvD,UAAM,QAAQ,SAAS,gBAAgB,KAAK,GAAG;AAC/C,QAAI,MAAO,KAAI,KAAK,EAAE,WAAW,EAAE,MAAM,MAAM,CAAC;AAAA,EAClD;AACA,SAAO;AACT;AAGA,eAAe,qBACb,IACA,OACA,QACA,WACmB;AACnB,QAAM,MAAM,IAAI,IAAI,WAAW,MAAM,GAAG,UAAU,CAAC;AACnD,QAAM,SAAS,MAAM,GAAG;AAAA,IACtB,qBAAqB,GAAG,sBAAsB,MAAM,uBAAuB,SAAS;AAAA,EACtF;AACA,SAAQ,OAAO,KACZ,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAClB,OAAO,CAAC,OAAqB,OAAO,OAAO,QAAQ;AACxD;AASA,eAAsB,sBACpB,IACA,QACA,YACA,WACA,OACA,OACA,UACqB;AACrB,QAAM,UAAU,MAAM,uBAAuB,IAAI,OAAO,QAAQ,SAAS;AACzE,QAAM,SAAS,sBAAsB,OAAO,QAAQ;AAIpD,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,WAAW,MAAM,qBAAqB,IAAI,OAAO,QAAQ,SAAS;AACxE,eAAW,SAAS,UAAU;AAC5B,iBAAW,KAAK,QAAQ;AACtB,cAAM,IAAI,MAAM,qBAAqB,IAAI,OAAO,EAAE,WAAW,EAAE,OAAO,QAAQ;AAC9E,gBAAQ,KAAK,GAAG,EAAE,OAAO;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,IAAI,IAAI,WAAW,MAAM,GAAG,UAAU,CAAC;AACnD,QAAM,GAAG,QAAQ,kBAAkB,GAAG,sBAAsB,MAAM,uBAAuB,SAAS,EAAE;AAEpG,QAAM,kBAAkB,yBAAyB,KAAK;AACtD,QAAM,QAAkB,CAAC;AAEzB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,QAAQ,MAAM,CAAC;AACrB,UAAM,QAAQ,OAAO,WAAW;AAChC,UAAM,OAAgC;AAAA,MACpC,IAAI;AAAA,MACJ,WAAW;AAAA,MACX,aAAa;AAAA,MACb,cAAc;AAAA,MACd,OAAO;AAAA,MACP,YAAY,oBAAI,KAAK;AAAA,MACrB,YAAY,oBAAI,KAAK;AAAA,IACvB;AACA,eAAW,MAAM,MAAM,QAAQ;AAC7B,UAAI,CAAC,GAAG,aAAa,GAAG,UAAU,SAAU;AAC5C,WAAK,GAAG,UAAU,WAAW,IAAI,MAAM,GAAG,IAAI,KAAK;AAAA,IACrD;AACA,UAAM,OAAO,IAAI,KAAK,OAAO,KAAK,IAAI,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO;AACnF,UAAM,OAAO,IAAI,KAAK,OAAO,OAAO,IAAI,EAAE,IAAI,CAAC,MAAM,MAAM,CAAC,EAAE,GAAG,OAAO;AACxE,UAAM,GAAG,QAAQ,kBAAkB,GAAG,KAAK,IAAI,aAAa,IAAI,GAAG;AAEnE,eAAW,QAAQ,iBAAiB;AAClC,YAAM,MAAM,MAAM,IAAI;AACtB,UAAI,OAAO,QAAQ,YAAY,QAAQ,GAAI,OAAM,KAAK,GAAG;AAAA,IAC3D;AAGA,eAAW,KAAK,QAAQ;AACtB,YAAM,cAAc,MAAM,QAAQ,MAAM,EAAE,SAAS,CAAC,IAAK,MAAM,EAAE,SAAS,IAAkB,CAAC;AAC7F,YAAM,IAAI,MAAM;AAAA,QACd;AAAA,QACA;AAAA,QACA,MAAM,GAAG;AAAA,QACT,EAAE;AAAA,QACF,EAAE;AAAA,QACF;AAAA,QACA;AAAA,MACF;AACA,YAAM,KAAK,GAAG,EAAE,KAAK;AACrB,cAAQ,KAAK,GAAG,EAAE,OAAO;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,QAAQ;AAC1B;AAKA,eAAsB,qBACpB,IACA,QACA,WACA,OACA,UACqB;AACrB,QAAM,UAAU,MAAM,uBAAuB,IAAI,OAAO,QAAQ,SAAS;AAGzE,QAAM,SAAS,sBAAsB,OAAO,QAAQ;AACpD,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,SAAS,MAAM,qBAAqB,IAAI,OAAO,QAAQ,SAAS;AACtE,eAAW,SAAS,QAAQ;AAC1B,iBAAW,KAAK,QAAQ;AACtB,cAAM,IAAI,MAAM,qBAAqB,IAAI,OAAO,EAAE,WAAW,EAAE,OAAO,QAAQ;AAC9E,gBAAQ,KAAK,GAAG,EAAE,OAAO;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,IAAI,IAAI,WAAW,MAAM,GAAG,UAAU,CAAC;AACnD,QAAM,GAAG,QAAQ,kBAAkB,GAAG,sBAAsB,MAAM,uBAAuB,SAAS,EAAE;AACpG,SAAO,EAAE,OAAO,CAAC,GAAG,QAAQ;AAC9B;AAGA,eAAsB,qBACpB,IACA,QACA,UACA,YACe;AACf,QAAM,MAAM,IAAI,IAAI,WAAW,SAAS,UAAU,CAAC;AACnD,QAAM,OAAO,IAAI,IAAI,WAAW,SAAS,WAAW,CAAC;AACrD,QAAM,QAAQ,IAAI,IAAI,WAAW,SAAS,YAAY,CAAC;AACvD,QAAM,GAAG,QAAQ,kBAAkB,GAAG,UAAU,IAAI,MAAM,MAAM,EAAE;AAClE,aAAW,OAAO,YAAY;AAC5B,UAAM,GAAG,QAAQ,kBAAkB,GAAG,KAAK,IAAI,KAAK,KAAK,aAAa,MAAM,KAAK,GAAG,GAAG;AAAA,EACzF;AACF;AA6CO,SAAS,kBACd,QACA,UAC6B;AAC7B,QAAM,YAAyC,CAAC;AAEhD,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,eAAe,WAAW,MAAM,eAAe,WAAW,MAAM,eAAe,QAAQ;AAC/F,UAAI,CAAC,MAAM,UAAW;AACtB,gBAAU,MAAM,IAAI,IAAI,EAAE,MAAM,SAAS,WAAW,MAAM,UAAU,YAAY;AAAA,IAClF,WAAW,MAAM,eAAe,aAAa;AAC3C,YAAM,MAAO,MAAM,aAAkC;AACrD,YAAM,QAAQ,MAAM,SAAS,gBAAgB,GAAG,IAAI;AACpD,UAAI,CAAC,MAAO;AACZ,gBAAU,MAAM,IAAI,IAAI,EAAE,MAAM,aAAa,OAAO,MAAM,GAAG,WAAW;AAAA,IAC1E,WAAW,MAAM,eAAe,aAAa;AAC3C,UAAI,CAAC,MAAM,UAAW;AACtB,UAAI,MAAM,UAAU,UAAU;AAC5B,cAAM,IAAI,MAAM,UAAU;AAC1B,kBAAU,MAAM,IAAI,IAAI;AAAA,UACtB,MAAM;AAAA,UACN,OAAO,EAAE;AAAA,UACT,gBAAgB,EAAE;AAAA,UAClB,aAAa,EAAE;AAAA,UACf,cAAc,EAAE;AAAA,UAChB,cAAc,EAAE;AAAA,QAClB;AAAA,MACF,WAAW,MAAM,UAAU,aAAa;AACtC,kBAAU,MAAM,IAAI,IAAI;AAAA,UACtB,MAAM;AAAA,UACN,OAAO,MAAM,UAAU,YAAY;AAAA,UACnC,WAAW,MAAM,UAAU;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,QACP,OACA,KACqB;AACrB,QAAM,SAA8B,CAAC;AACrC,aAAW,QAAQ,OAAO;AACxB,UAAM,IAAI,OAAO,KAAK,GAAG,CAAC;AAC1B,QAAI,CAAC,OAAO,CAAC,EAAG,QAAO,CAAC,IAAI,CAAC;AAC7B,WAAO,CAAC,EAAG,KAAK,IAAI;AAAA,EACtB;AACA,SAAO;AACT;AAcA,SAAS,kBACP,KACA,UACA,WACS;AAET,MAAI,aAAa,aAAa,EAAE,YAAY,KAAM,QAAO;AAGzD,QAAM,UAAU,IAAI,SAAS;AAC7B,SAAO,EAAE,OAAO,YAAY,YAAY,YAAY;AACtD;AAcA,eAAsB,qBACpB,IACA,MACA,WACA,KACA,OACA,gBAAgB,OACD;AACf,MAAI,KAAK,WAAW,EAAG;AAMvB,QAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;AAE/B,MAAI,IAAI,SAAS,aAAa;AAC5B,UAAM,YAAY,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,CAAW;AACpD,UAAM,SAAS,IAAI,KAAK,UAAU,IAAI,CAAC,OAAO,MAAM,EAAE,EAAE,GAAG,OAAO;AAIlE,UAAM,SAAS,MAAM,GAAG;AAAA,MACtB,oBAAoB,IAAI,IAAI,WAAW,IAAI,KAAK,CAAC,CAAC,wBAAwB,MAAM,wBAAwB,SAAS;AAAA,IACnH;AACA,UAAM,WAAW,QAAQ,OAAO,MAAmC,WAAW;AAC9E,eAAW,OAAO,OAAO;AACvB,UAAI,SAAS,IAAI,SAAS,IAAI,IAAI,CAAW,KAAK,CAAC;AAAA,IACrD;AAAA,EACF,WAAW,IAAI,SAAS,aAAa;AACnC,UAAM,SAAS,IAAI,cAAc;AACjC,UAAM,UAAU,MAAM,OAAO,CAAC,MAAM,kBAAkB,GAAG,IAAI,WAAW,SAAS,CAAC;AAClF,QAAI,QAAQ,WAAW,EAAG;AAE1B,UAAM,WAAW,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,SAAS,CAAW,EAAE,OAAO,OAAO;AAC9E,QAAI,SAAS,WAAW,GAAG;AACzB,iBAAW,OAAO,SAAS;AACzB,YAAI,SAAS,IAAI;AACjB,YAAI,OAAQ,QAAO,IAAI,IAAI,SAAS;AAAA,MACtC;AACA;AAAA,IACF;AACA,UAAM,SAAS,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC;AACpC,UAAM,WAAW,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,GAAG,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;AACvE,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,SAAS,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,MAAM,EAAE,EAAE,GAAG,OAAO;AACjE,YAAM,gBAAgB,gBAAgB,6BAA6B;AACnE,YAAM,SAAS,MAAM,GAAG;AAAA,QACtB,oBAAoB,IAAI,IAAI,WAAW,IAAI,KAAK,CAAC,CAAC,iBAAiB,MAAM,IAAI,aAAa;AAAA,MAC5F;AACA,iBAAW,QAAQ,OAAO,MAAmC;AAC3D,cAAM,IAAI,GAAG,IAAI,KAAK,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI;AAAA,MAC9C;AAAA,IACF;AACA,eAAW,OAAO,SAAS;AACzB,YAAM,QAAQ,IAAI,IAAI,SAAS;AAC/B,UAAI,SAAS,IAAI,QAAS,MAAM,IAAI,GAAG,IAAI,KAAK,IAAI,KAAK,EAAE,KAAK,OAAQ;AACxE,UAAI,OAAQ,QAAO,IAAI,IAAI,SAAS;AAAA,IACtC;AAAA,EACF,WAAW,IAAI,SAAS,YAAY;AAClC,UAAM,YAAY,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,CAAW;AACpD,UAAM,SAAS,IAAI,KAAK,UAAU,IAAI,CAAC,OAAO,MAAM,EAAE,EAAE,GAAG,OAAO;AAClE,UAAM,UAAU,IAAI,eAAe,6BAA6B;AAChE,UAAM,iBAAiB,MAAM,GAAG;AAAA,MAC9B,oBAAoB,IAAI,IAAI,WAAW,IAAI,cAAc,CAAC,CAAC,UAAU,IAAI,IAAI,WAAW,IAAI,WAAW,CAAC,CAAC,QAAQ,MAAM,IAAI,OAAO;AAAA,IACpI;AACA,UAAM,QAAQ,eAAe;AAC7B,UAAM,WAAW,CAAC,GAAG,IAAI,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,YAAY,CAAW,CAAC,CAAC;AAC7E,UAAM,WAAW,SAAS,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,GAAG,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;AACzE,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,cAAc,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,MAAM,EAAE,EAAE,GAAG,OAAO;AACtE,YAAM,gBAAgB,gBAAgB,6BAA6B;AACnE,YAAM,iBAAiB,MAAM,GAAG;AAAA,QAC9B,oBAAoB,IAAI,IAAI,WAAW,IAAI,KAAK,CAAC,CAAC,iBAAiB,WAAW,IAAI,aAAa;AAAA,MACjG;AACA,iBAAW,QAAQ,eAAe,MAAmC;AACnE,cAAM,IAAI,GAAG,IAAI,KAAK,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI;AAAA,MAC9C;AAAA,IACF;AACA,UAAM,SAAS,QAAQ,OAAO,IAAI,WAAW;AAC7C,eAAW,OAAO,OAAO;AACvB,YAAM,WAAW,OAAO,IAAI,IAAI,CAAW,KAAK,CAAC;AACjD,UAAI,SAAS,IAAI,SACd,IAAI,CAAC,OAAO,MAAM,IAAI,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,YAAY,CAAW,EAAE,CAAC,EACvE,OAAO,OAAO;AAAA,IACnB;AAAA,EACF,WAAW,IAAI,SAAS,SAAS;AAC/B,UAAM,SAAS,IAAI,cAAc;AACjC,UAAM,UAAU,MAAM,OAAO,CAAC,MAAM,kBAAkB,GAAG,IAAI,WAAW,SAAS,CAAC;AAClF,QAAI,QAAQ,WAAW,EAAG;AAE1B,UAAM,WAAW,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,SAAS,CAAW,EAAE,OAAO,OAAO;AAC9E,QAAI,SAAS,WAAW,GAAG;AACzB,iBAAW,OAAO,SAAS;AACzB,YAAI,SAAS,IAAI;AACjB,YAAI,OAAQ,QAAO,IAAI,IAAI,SAAS;AAAA,MACtC;AACA;AAAA,IACF;AACA,UAAM,SAAS,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC;AACpC,UAAM,WAAW,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,SAAS,EAAE,EAAE,CAAC;AAChE,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,SAAS,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,MAAM,EAAE,EAAE,GAAG,OAAO;AACjE,YAAM,SAAS,MAAM,GAAG,QAAQ,yCAAyC,MAAM,GAAG;AAClF,iBAAW,QAAQ,OAAO,MAAmC;AAC3D,cAAM,IAAI,SAAS,KAAK,IAAI,CAAC,IAAI,IAAI;AAAA,MACvC;AAAA,IACF;AACA,eAAW,OAAO,SAAS;AACzB,YAAM,QAAQ,IAAI,IAAI,SAAS;AAC/B,UAAI,SAAS,IAAI,QAAS,MAAM,IAAI,SAAS,KAAK,EAAE,KAAK,OAAQ;AACjE,UAAI,OAAQ,QAAO,IAAI,IAAI,SAAS;AAAA,IACtC;AAAA,EACF;AACF;AAMA,eAAsB,uBACpB,IACA,MACA,WACA,KACe;AACf,MAAI,KAAK,WAAW,EAAG;AAEvB,MAAI,IAAI,SAAS,aAAa;AAC5B,UAAM,YAAY,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,CAAW;AACnD,UAAM,SAAS,IAAI,KAAK,UAAU,IAAI,CAAC,OAAO,MAAM,EAAE,EAAE,GAAG,OAAO;AAClE,UAAM,SAAS,MAAM,GAAG;AAAA,MACtB,gCAAgC,IAAI,IAAI,WAAW,IAAI,KAAK,CAAC,CAAC,wBAAwB,MAAM,wBAAwB,SAAS;AAAA,IAC/H;AACA,UAAM,WAAW,QAAQ,OAAO,MAAmC,WAAW;AAC9E,eAAW,OAAO,MAAM;AACtB,UAAI,SAAS,KAAK,SAAS,IAAI,IAAI,CAAW,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAAA,IAC3E;AAAA,EACF,WAAW,IAAI,SAAS,YAAY;AAClC,UAAM,YAAY,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,CAAW;AACnD,UAAM,SAAS,IAAI,KAAK,UAAU,IAAI,CAAC,OAAO,MAAM,EAAE,EAAE,GAAG,OAAO;AAClE,UAAM,SAAS,MAAM,GAAG;AAAA,MACtB,aAAa,IAAI,IAAI,WAAW,IAAI,WAAW,CAAC,CAAC,gBAAgB,IAAI,IAAI,WAAW,IAAI,YAAY,CAAC,CAAC,qBAAqB,IAAI,IAAI,WAAW,IAAI,cAAc,CAAC,CAAC,UAAU,IAAI,IAAI,WAAW,IAAI,WAAW,CAAC,CAAC,QAAQ,MAAM;AAAA,IAChO;AACA,UAAM,SAAS,QAAQ,OAAO,MAAmC,SAAS;AAC1E,eAAW,OAAO,MAAM;AACtB,UAAI,SAAS,KAAK,OAAO,IAAI,IAAI,CAAW,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC;AAAA,IAC/E;AAAA,EACF;AACF;;;ACjeO,SAAS,eAAe,OAAoE;AACjG,QAAM,MAAM,MAAM;AAClB,MAAI,QAAQ,KAAM,QAAO;AACzB,MAAI,IAAI,gBAAgB,GAAI,QAAO;AACnC,MAAI,IAAI,SAAU,QAAO;AACzB,SAAO;AACT;AA4BO,SAAS,kBAAkB,QAAoC;AACpE,QAAM,gBAAgB,oBAAI,IAAoB;AAG9C,QAAM,gBAAgB,oBAAI,IAAoB;AAK9C,QAAM,cAAc,oBAAI,IAAY;AAEpC,aAAW,KAAK,QAAQ;AACtB,QAAI,CAAC,eAAe,CAAC,EAAG;AACxB,kBAAc,IAAI,EAAE,UAAU,aAAa,EAAE,IAAI;AACjD,QAAI,EAAE,YAAY,MAAM;AACtB,kBAAY,IAAI,EAAE,IAAI;AACtB,kBAAY,IAAI,EAAE,UAAU,WAAW;AACvC;AAAA,IACF;AACA,kBAAc,IAAI,EAAE,MAAM,EAAE,UAAU,WAAW;AAAA,EACnD;AAwBA,aAAW,KAAK,QAAQ;AACtB,UAAM,cAAc,cAAc,IAAI,EAAE,IAAI;AAC5C,QAAI,gBAAgB,UAAa,gBAAgB,EAAE,MAAM;AACvD,YAAM,IAAI;AAAA,QACR,4DAAuD,EAAE,IAAI,gDAC/B,WAAW;AAAA,MAE3C;AAAA,IACF;AAAA,EACF;AAIA,aAAW,KAAK,QAAQ;AACtB,QAAI,eAAe,CAAC,KAAK,EAAE,YAAY,MAAM;AAC3C,oBAAc,OAAO,EAAE,UAAU,WAAW;AAAA,IAC9C;AAAA,EACF;AAEA,QAAM,WAAW,CAAC,GAAG,aAAa,EAAE,KAAK,CAAC,CAAC,OAAO,MAAM,MAAM,UAAU,MAAM;AAE9E,WAAS,MACP,OACA,QACyB;AAUzB,UAAM,MAA+B,CAAC;AACtC,eAAW,OAAO,OAAO,KAAK,KAAK,GAAG;AACpC,UAAI,YAAY,IAAI,GAAG,EAAG;AAC1B,UAAI,OAAO,IAAI,GAAG,KAAK,GAAG,IAAI,MAAM,GAAG;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,WAAW,CAAC,UAAU,MAAM,OAAO,aAAa;AAAA,IAChD,UAAU,CAAC,QAAQ,MAAM,KAAK,aAAa;AAAA,IAC3C,WAAW,CAAC,UAAU,cAAc,IAAI,KAAK;AAAA,IAC7C,UAAU,CAAC,WAAW,cAAc,IAAI,MAAM;AAAA,IAC9C,QAAQ,CAAC,GAAG,cAAc,KAAK,CAAC;AAAA,IAChC;AAAA,EACF;AACF;","names":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
buildRelationsMap,
|
|
3
3
|
isColumnBacked,
|
|
4
4
|
resolveRelationField
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-4Y4G564P.mjs";
|
|
6
6
|
|
|
7
7
|
// src/graphql/handler.ts
|
|
8
8
|
import { createYoga, maskError } from "graphql-yoga";
|
|
@@ -618,4 +618,4 @@ function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, optio
|
|
|
618
618
|
export {
|
|
619
619
|
createGraphQLHandler
|
|
620
620
|
};
|
|
621
|
-
//# sourceMappingURL=chunk-
|
|
621
|
+
//# sourceMappingURL=chunk-HEYT5CH5.mjs.map
|