@bobbykim/manguito-cms-api 0.6.0 → 0.8.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/index.js CHANGED
@@ -38,7 +38,7 @@ function isColumnBacked(field) {
38
38
  if (col.junction) return false;
39
39
  return true;
40
40
  }
41
- function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
41
+ function buildFieldKeyMap(pairs, allLabels, droppedColumns, droppedLabels) {
42
42
  const labelToColumn = /* @__PURE__ */ new Map();
43
43
  const columnToLabel = /* @__PURE__ */ new Map();
44
44
  for (const { label, column } of pairs) {
@@ -46,7 +46,7 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
46
46
  columnToLabel.set(column, label);
47
47
  }
48
48
  for (const { label, ownColumn } of allLabels) {
49
- if (ownColumn === label) continue;
49
+ if (ownColumn !== void 0) continue;
50
50
  const columnOwner = columnToLabel.get(label);
51
51
  if (columnOwner !== void 0 && columnOwner !== label) {
52
52
  throw new Error(
@@ -55,23 +55,33 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
55
55
  }
56
56
  }
57
57
  for (const label of labelToColumn.keys()) {
58
- if (droppedKeys.has(label)) labelToColumn.delete(label);
58
+ if (droppedLabels.has(label)) labelToColumn.delete(label);
59
59
  }
60
60
  for (const column of columnToLabel.keys()) {
61
- if (droppedKeys.has(column)) columnToLabel.delete(column);
61
+ if (droppedColumns.has(column)) columnToLabel.delete(column);
62
62
  }
63
63
  const diverges = [...labelToColumn].some(([label, column]) => label !== column);
64
- function remap(input, lookup) {
64
+ function remap(input, lookup, dropped, droppedIfUnmapped) {
65
65
  const out = {};
66
66
  for (const key of Object.keys(input)) {
67
- if (droppedKeys.has(key)) continue;
68
- out[lookup.get(key) ?? key] = input[key];
67
+ const mapped = lookup.get(key);
68
+ if (mapped !== void 0) {
69
+ out[mapped] = input[key];
70
+ continue;
71
+ }
72
+ if (dropped.has(key) || droppedIfUnmapped?.has(key)) continue;
73
+ out[key] = input[key];
69
74
  }
70
75
  return out;
71
76
  }
72
77
  return {
73
- toStorage: (input) => remap(input, labelToColumn),
74
- toLabels: (row) => remap(row, columnToLabel),
78
+ // toStorage reads LABEL-keyed input: a live label maps and writes
79
+ // regardless of either drop set; an unmapped key is refused if it is
80
+ // EITHER the tombstone's own name (droppedLabels) or a raw, unexposed
81
+ // column (droppedColumns) — see remap's comment.
82
+ // toLabels reads COLUMN-keyed input, so it consults only droppedColumns.
83
+ toStorage: (input) => remap(input, labelToColumn, droppedLabels, droppedColumns),
84
+ toLabels: (row) => remap(row, columnToLabel, droppedColumns),
75
85
  columnFor: (label) => labelToColumn.get(label),
76
86
  labelFor: (column) => columnToLabel.get(column),
77
87
  labels: [...labelToColumn.keys()],
@@ -79,14 +89,15 @@ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
79
89
  };
80
90
  }
81
91
  function createFieldKeyMap(fields) {
82
- const droppedKeys = /* @__PURE__ */ new Set();
92
+ const droppedColumns = /* @__PURE__ */ new Set();
93
+ const droppedLabels = /* @__PURE__ */ new Set();
83
94
  const pairs = [];
84
95
  for (const f of fields) {
85
96
  if (!isColumnBacked(f)) continue;
86
97
  pairs.push({ label: f.name, column: f.db_column.column_name });
87
98
  if (f.removed === true) {
88
- droppedKeys.add(f.name);
89
- droppedKeys.add(f.db_column.column_name);
99
+ droppedLabels.add(f.name);
100
+ droppedColumns.add(f.db_column.column_name);
90
101
  }
91
102
  }
92
103
  return buildFieldKeyMap(
@@ -97,30 +108,33 @@ function createFieldKeyMap(fields) {
97
108
  // forbids present-and-undefined.
98
109
  ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
99
110
  })),
100
- droppedKeys
111
+ droppedColumns,
112
+ droppedLabels
101
113
  );
102
114
  }
103
115
  function createFieldKeyMapFromProjection(projectionType, allFields) {
104
116
  const pairs = projectionType.fields.map((f) => ({ label: f.exposed_as, column: f.column_name }));
105
- const projected = new Set(pairs.map((p) => p.label));
106
- const dropped = /* @__PURE__ */ new Set();
117
+ const projectedColumns = new Set(pairs.map((p) => p.column));
118
+ const projectedLabels = new Set(pairs.map((p) => p.label));
119
+ const droppedColumns = /* @__PURE__ */ new Set();
120
+ const droppedLabels = /* @__PURE__ */ new Set();
107
121
  for (const f of allFields) {
108
122
  if (!isColumnBacked(f)) continue;
109
- dropped.add(f.name);
110
- dropped.add(f.db_column.column_name);
123
+ if (!projectedColumns.has(f.db_column.column_name)) droppedColumns.add(f.db_column.column_name);
124
+ if (!projectedLabels.has(f.name)) droppedLabels.add(f.name);
111
125
  }
112
- for (const p of pairs) dropped.delete(p.column);
113
- for (const label of projected) dropped.delete(label);
114
126
  return buildFieldKeyMap(
115
127
  pairs,
116
128
  allFields.map((f) => ({
117
129
  label: f.name,
118
- // See buildFieldKeyMap's collision check: this is what lets it tell a
119
- // version exposing a field's own column under a new label (fine) from a
120
- // label that reuses another field's column (not fine).
130
+ // See buildFieldKeyMap's collision check: `ownColumn` marks every
131
+ // column-backed field so the check skips it entirely — only a
132
+ // paragraph/junction/programmatic label (no column of its own) can
133
+ // still collide.
121
134
  ...isColumnBacked(f) && { ownColumn: f.db_column.column_name }
122
135
  })),
123
- dropped
136
+ droppedColumns,
137
+ droppedLabels
124
138
  );
125
139
  }
126
140
  var init_field_keys = __esm({
@@ -130,7 +144,7 @@ var init_field_keys = __esm({
130
144
  });
131
145
 
132
146
  // src/relations.ts
133
- function quoteIdent(name) {
147
+ function quoteIdent2(name) {
134
148
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
135
149
  return `"${name}"`;
136
150
  }
@@ -142,10 +156,10 @@ function paragraphMediaFieldNames(pType) {
142
156
  async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
143
157
  const mediaColumns = pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.db_column.column_name);
144
158
  if (mediaColumns.length === 0) return [];
145
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
146
- const cols = import_drizzle_orm2.sql.join(mediaColumns.map((c) => import_drizzle_orm2.sql.raw(quoteIdent(c))), import_drizzle_orm2.sql`, `);
159
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
160
+ const cols = import_drizzle_orm3.sql.join(mediaColumns.map((c) => import_drizzle_orm3.sql.raw(quoteIdent2(c))), import_drizzle_orm3.sql`, `);
147
161
  const result = await db.execute(
148
- import_drizzle_orm2.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
162
+ import_drizzle_orm3.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
149
163
  );
150
164
  const ids = [];
151
165
  for (const row of result.rows) {
@@ -164,14 +178,14 @@ function nestedParagraphFields(pType, registry) {
164
178
  const comp = f.ui_component;
165
179
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
166
180
  const nType = registry.paragraph_types[comp.ref];
167
- if (nType) out.push({ fieldName: f.name, nType });
181
+ if (nType) out.push({ fieldName: f.name, nType, field: f });
168
182
  }
169
183
  return out;
170
184
  }
171
185
  async function fetchParagraphRowIds(db, pType, itemId, fieldName) {
172
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
186
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
173
187
  const result = await db.execute(
174
- import_drizzle_orm2.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
188
+ import_drizzle_orm3.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
175
189
  );
176
190
  return result.rows.map((r) => r["id"]).filter((id) => typeof id === "string");
177
191
  }
@@ -187,8 +201,8 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
187
201
  }
188
202
  }
189
203
  }
190
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
191
- await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
204
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
205
+ await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
192
206
  const mediaFieldNames = paragraphMediaFieldNames(pType);
193
207
  const added = [];
194
208
  for (let i = 0; i < items.length; i++) {
@@ -207,15 +221,15 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
207
221
  if (!pf.db_column || pf.db_column.junction) continue;
208
222
  data[pf.db_column.column_name] = pItem[pf.name] ?? null;
209
223
  }
210
- const cols = import_drizzle_orm2.sql.join(Object.keys(data).map((k) => import_drizzle_orm2.sql.raw(quoteIdent(k))), import_drizzle_orm2.sql`, `);
211
- const vals = import_drizzle_orm2.sql.join(Object.values(data).map((v) => import_drizzle_orm2.sql`${v}`), import_drizzle_orm2.sql`, `);
212
- await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
224
+ const cols = import_drizzle_orm3.sql.join(Object.keys(data).map((k) => import_drizzle_orm3.sql.raw(quoteIdent2(k))), import_drizzle_orm3.sql`, `);
225
+ const vals = import_drizzle_orm3.sql.join(Object.values(data).map((v) => import_drizzle_orm3.sql`${v}`), import_drizzle_orm3.sql`, `);
226
+ await db.execute(import_drizzle_orm3.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
213
227
  for (const name of mediaFieldNames) {
214
228
  const val = pItem[name];
215
229
  if (typeof val === "string" && val !== "") added.push(val);
216
230
  }
217
231
  for (const n of nested) {
218
- const nestedItems = Array.isArray(pItem[n.fieldName]) ? pItem[n.fieldName] : [];
232
+ const nestedItems = paragraphItems(n.field, pItem[n.fieldName]);
219
233
  const d = await persistParagraphField(
220
234
  db,
221
235
  rowId,
@@ -243,19 +257,28 @@ async function deleteParagraphField(db, itemId, fieldName, pType, registry) {
243
257
  }
244
258
  }
245
259
  }
246
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
247
- await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
260
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
261
+ await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
248
262
  return { added: [], removed };
249
263
  }
250
264
  async function persistJunctionField(db, itemId, junction, relatedIds) {
251
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(junction.table_name));
252
- const left = import_drizzle_orm2.sql.raw(quoteIdent(junction.left_column));
253
- const right = import_drizzle_orm2.sql.raw(quoteIdent(junction.right_column));
254
- await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
265
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(junction.table_name));
266
+ const left = import_drizzle_orm3.sql.raw(quoteIdent2(junction.left_column));
267
+ const right = import_drizzle_orm3.sql.raw(quoteIdent2(junction.right_column));
268
+ await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
255
269
  for (const rid of relatedIds) {
256
- await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
270
+ await db.execute(import_drizzle_orm3.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
257
271
  }
258
272
  }
273
+ function oneOrMany(cardinality, list) {
274
+ return cardinality === "one" ? list[0] ?? null : list;
275
+ }
276
+ function paragraphItems(field, value) {
277
+ if ((0, import_manguito_cms_core2.relationCardinality)(field) === "one") {
278
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? [value] : [];
279
+ }
280
+ return Array.isArray(value) ? value : [];
281
+ }
259
282
  function buildRelationsMap(fields, registry) {
260
283
  const relations = {};
261
284
  for (const field of fields) {
@@ -266,7 +289,11 @@ function buildRelationsMap(fields, registry) {
266
289
  const ref = field.ui_component.ref;
267
290
  const pType = ref ? registry.paragraph_types[ref] : void 0;
268
291
  if (!pType) continue;
269
- relations[field.name] = { type: "paragraph", table: pType.db.table_name };
292
+ relations[field.name] = {
293
+ type: "paragraph",
294
+ table: pType.db.table_name,
295
+ cardinality: (0, import_manguito_cms_core2.relationCardinality)(field) ?? "many"
296
+ };
270
297
  } else if (field.field_type === "reference") {
271
298
  if (!field.db_column) continue;
272
299
  if (field.db_column.junction) {
@@ -299,9 +326,8 @@ function groupBy(items, key) {
299
326
  }
300
327
  return result;
301
328
  }
302
- function needsFkResolution(row, fkColumn, fieldName) {
303
- if (fkColumn !== fieldName && !(fkColumn in row)) return false;
304
- const current = row[fieldName];
329
+ function needsFkResolution(row, fkColumn) {
330
+ const current = row[fkColumn];
305
331
  return !(typeof current === "object" && current !== null);
306
332
  }
307
333
  async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
@@ -309,33 +335,29 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
309
335
  const batch = [...new Set(rows)];
310
336
  if (rel.type === "paragraph") {
311
337
  const parentIds = batch.map((r) => r["id"]);
312
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
338
+ const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
313
339
  const result = await db.execute(
314
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
340
+ import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
315
341
  );
316
342
  const byParent = groupBy(result.rows, "parent_id");
317
343
  for (const row of batch) {
318
- row[fieldName] = byParent[row["id"]] ?? [];
344
+ row[fieldName] = oneOrMany(rel.cardinality, byParent[row["id"]] ?? []);
319
345
  }
320
346
  } else if (rel.type === "reference") {
321
- const dropFk = rel.fk_column !== fieldName;
322
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
347
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
323
348
  if (pending.length === 0) return;
324
349
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
325
350
  if (fkValues.length === 0) {
326
- for (const row of pending) {
327
- row[fieldName] = null;
328
- if (dropFk) delete row[rel.fk_column];
329
- }
351
+ for (const row of pending) row[rel.fk_column] = null;
330
352
  return;
331
353
  }
332
354
  const unique = [...new Set(fkValues)];
333
355
  const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
334
356
  if (uncached.length > 0) {
335
- const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
336
- const publishedCond = publishedOnly ? import_drizzle_orm2.sql` AND published = true` : import_drizzle_orm2.sql``;
357
+ const inList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
358
+ const publishedCond = publishedOnly ? import_drizzle_orm3.sql` AND published = true` : import_drizzle_orm3.sql``;
337
359
  const result = await db.execute(
338
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})${publishedCond}`
360
+ import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE id IN (${inList})${publishedCond}`
339
361
  );
340
362
  for (const item of result.rows) {
341
363
  cache.set(`${rel.table}:${item["id"]}`, item);
@@ -343,24 +365,23 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
343
365
  }
344
366
  for (const row of pending) {
345
367
  const fkVal = row[rel.fk_column];
346
- row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
347
- if (dropFk) delete row[rel.fk_column];
368
+ row[rel.fk_column] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
348
369
  }
349
370
  } else if (rel.type === "junction") {
350
371
  const parentIds = batch.map((r) => r["id"]);
351
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
352
- const orderBy = rel.order_column ? import_drizzle_orm2.sql` ORDER BY "order" ASC` : import_drizzle_orm2.sql``;
372
+ const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
373
+ const orderBy = rel.order_column ? import_drizzle_orm3.sql` ORDER BY "order" ASC` : import_drizzle_orm3.sql``;
353
374
  const junctionResult = await db.execute(
354
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`
375
+ import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.junction_table))} WHERE ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.left_column))} IN (${inList})${orderBy}`
355
376
  );
356
377
  const jRows = junctionResult.rows;
357
378
  const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
358
379
  const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
359
380
  if (uncached.length > 0) {
360
- const rightInList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
361
- const publishedCond = publishedOnly ? import_drizzle_orm2.sql` AND published = true` : import_drizzle_orm2.sql``;
381
+ const rightInList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
382
+ const publishedCond = publishedOnly ? import_drizzle_orm3.sql` AND published = true` : import_drizzle_orm3.sql``;
362
383
  const entitiesResult = await db.execute(
363
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
384
+ import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
364
385
  );
365
386
  for (const item of entitiesResult.rows) {
366
387
  cache.set(`${rel.table}:${item["id"]}`, item);
@@ -372,30 +393,25 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
372
393
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
373
394
  }
374
395
  } else if (rel.type === "media") {
375
- const dropFk = rel.fk_column !== fieldName;
376
- const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
396
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column));
377
397
  if (pending.length === 0) return;
378
398
  const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
379
399
  if (fkValues.length === 0) {
380
- for (const row of pending) {
381
- row[fieldName] = null;
382
- if (dropFk) delete row[rel.fk_column];
383
- }
400
+ for (const row of pending) row[rel.fk_column] = null;
384
401
  return;
385
402
  }
386
403
  const unique = [...new Set(fkValues)];
387
404
  const uncached = unique.filter((id) => !cache.has(`media:${id}`));
388
405
  if (uncached.length > 0) {
389
- const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
390
- const result = await db.execute(import_drizzle_orm2.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
406
+ const inList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
407
+ const result = await db.execute(import_drizzle_orm3.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
391
408
  for (const item of result.rows) {
392
409
  cache.set(`media:${item["id"]}`, item);
393
410
  }
394
411
  }
395
412
  for (const row of pending) {
396
413
  const fkVal = row[rel.fk_column];
397
- row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
398
- if (dropFk) delete row[rel.fk_column];
414
+ row[rel.fk_column] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
399
415
  }
400
416
  }
401
417
  }
@@ -403,19 +419,19 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
403
419
  if (rows.length === 0) return;
404
420
  if (rel.type === "paragraph") {
405
421
  const parentIds = rows.map((r) => r["id"]);
406
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
422
+ const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
407
423
  const result = await db.execute(
408
- import_drizzle_orm2.sql`SELECT id, parent_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
424
+ import_drizzle_orm3.sql`SELECT id, parent_id FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
409
425
  );
410
426
  const byParent = groupBy(result.rows, "parent_id");
411
427
  for (const row of rows) {
412
- row[fieldName] = (byParent[row["id"]] ?? []).map((r) => r["id"]);
428
+ row[fieldName] = oneOrMany(rel.cardinality, (byParent[row["id"]] ?? []).map((r) => r["id"]));
413
429
  }
414
430
  } else if (rel.type === "junction") {
415
431
  const parentIds = rows.map((r) => r["id"]);
416
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
432
+ const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
417
433
  const result = await db.execute(
418
- import_drizzle_orm2.sql`SELECT ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} AS left_id, ${import_drizzle_orm2.sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})`
434
+ import_drizzle_orm3.sql`SELECT ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.left_column))} AS left_id, ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.right_column))} AS right_id FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.junction_table))} WHERE ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.left_column))} IN (${inList})`
419
435
  );
420
436
  const byLeft = groupBy(result.rows, "left_id");
421
437
  for (const row of rows) {
@@ -423,12 +439,13 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
423
439
  }
424
440
  }
425
441
  }
426
- var import_node_crypto, import_drizzle_orm2;
442
+ var import_node_crypto, import_drizzle_orm3, import_manguito_cms_core2;
427
443
  var init_relations = __esm({
428
444
  "src/relations.ts"() {
429
445
  "use strict";
430
446
  import_node_crypto = __toESM(require("crypto"));
431
- import_drizzle_orm2 = require("drizzle-orm");
447
+ import_drizzle_orm3 = require("drizzle-orm");
448
+ import_manguito_cms_core2 = require("@bobbykim/manguito-cms-core");
432
449
  }
433
450
  });
434
451
 
@@ -677,29 +694,22 @@ function scalarFieldResolver(field, fallback) {
677
694
  function relationFieldResolver(typeName, schemaFieldName) {
678
695
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
679
696
  }
680
- function programmaticFieldResolver(typeName, schemaFieldName, mediaFields = [], fieldKeys) {
697
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = [], fieldKeys) {
681
698
  return async (parent, _args, ctx) => {
682
699
  let p = ctx.programmaticMemo.get(parent);
683
700
  if (!p) {
684
- p = resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys);
701
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys);
685
702
  ctx.programmaticMemo.set(parent, p);
686
703
  }
687
704
  return (await p)[schemaFieldName];
688
705
  };
689
706
  }
690
- async function resolveProgrammaticRow(typeName, parent, ctx, mediaFields, fieldKeys) {
707
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys) {
691
708
  const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
692
- if (mediaFields.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
709
+ if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
693
710
  const enriched = { ...parent };
694
- await Promise.all(mediaFields.map((m) => ctx.loaders.load(typeName, m.name, enriched)));
695
- return ctx.resolver.resolveItem(typeName, relabelMedia(toLabels(enriched), enriched, mediaFields));
696
- }
697
- function relabelMedia(record, enriched, mediaFields) {
698
- for (const { name, exposedAs } of mediaFields) {
699
- if (!(name in enriched)) continue;
700
- record[exposedAs] = enriched[name];
701
- }
702
- return record;
711
+ await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
712
+ return ctx.resolver.resolveItem(typeName, toLabels(enriched));
703
713
  }
704
714
  function collectionResolver(typeName, nameMap, fieldKeys) {
705
715
  return async (_root, args, ctx) => {
@@ -757,7 +767,7 @@ var init_resolvers = __esm({
757
767
  });
758
768
 
759
769
  // src/graphql/schema.ts
760
- function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
770
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view, programmaticKeyMaps = fieldKeyMaps) {
761
771
  const objectTypes = /* @__PURE__ */ new Map();
762
772
  const enumTypes = /* @__PURE__ */ new Map();
763
773
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -797,7 +807,8 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
797
807
  `
798
808
  );
799
809
  }
800
- return new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(target ?? MEDIA));
810
+ const t = target ?? MEDIA;
811
+ return (0, import_manguito_cms_core6.relationCardinality)(field) === "one" ? t : new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(t));
801
812
  }
802
813
  if (field.field_type === "reference") {
803
814
  const ref = field.ui_component.component === "typeahead-select" ? field.ui_component.ref : void 0;
@@ -806,8 +817,7 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
806
817
  process.stderr.write(`\u26A0 reference field '${field.name}' targets unknown type '${ref}'; exposing as Media
807
818
  `);
808
819
  }
809
- const rel = field.ui_component.component === "typeahead-select" ? field.ui_component.rel : void 0;
810
- const isMany = rel === "many-to-many" || rel === "one-to-many";
820
+ const isMany = (0, import_manguito_cms_core6.relationCardinality)(field) === "many";
811
821
  const t = target ?? MEDIA;
812
822
  return isMany ? new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(t)) : t;
813
823
  }
@@ -821,9 +831,9 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
821
831
  }
822
832
  function buildObjectType(machineName, type) {
823
833
  const visible = viewFieldsFor(machineName, type);
824
- const mediaFields = visible.filter(
825
- (v) => v.field.field_type === "image" || v.field.field_type === "video" || v.field.field_type === "file"
826
- ).map((v) => ({ name: v.field.name, exposedAs: v.exposedAs }));
834
+ const mediaFieldNames = type.fields.filter(
835
+ (f) => f.removed !== true && (f.field_type === "image" || f.field_type === "video" || f.field_type === "file")
836
+ ).map((f) => f.name);
827
837
  return new import_graphql5.GraphQLObjectType({
828
838
  name: graphqlTypeName(machineName),
829
839
  fields: () => {
@@ -843,7 +853,7 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
843
853
  const outType = outputTypeForField(field, vf.required);
844
854
  let resolve;
845
855
  if (field.field_type === "programmatic") {
846
- resolve = programmaticFieldResolver(machineName, field.name, mediaFields, fieldKeyMaps[machineName]);
856
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames, programmaticKeyMaps[machineName]);
847
857
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
848
858
  resolve = relationFieldResolver(machineName, field.name);
849
859
  } else {
@@ -888,7 +898,10 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
888
898
  for (const [name, ct] of Object.entries(registry.content_types)) {
889
899
  const objType = objectTypes.get(name);
890
900
  const visible = viewFieldsFor(name, ct);
891
- const nameMap = buildFieldNameMap(visible.map((v) => v.exposedAs));
901
+ const nameMap = buildFieldNameMap([
902
+ ...visible.map((v) => v.exposedAs),
903
+ ...ct.system_fields.map((s) => s.name)
904
+ ]);
892
905
  if (ct.only_one) {
893
906
  queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
894
907
  continue;
@@ -946,11 +959,12 @@ function buildGraphQLSchema(registry, fieldKeyMaps = {}, view) {
946
959
  query: new import_graphql5.GraphQLObjectType({ name: "Query", fields: queryFields })
947
960
  });
948
961
  }
949
- var import_graphql5, PAGE_META, MEDIA;
962
+ var import_graphql5, import_manguito_cms_core6, PAGE_META, MEDIA;
950
963
  var init_schema = __esm({
951
964
  "src/graphql/schema.ts"() {
952
965
  "use strict";
953
966
  import_graphql5 = require("graphql");
967
+ import_manguito_cms_core6 = require("@bobbykim/manguito-cms-core");
954
968
  init_scalars();
955
969
  init_type_mapping();
956
970
  init_naming();
@@ -1076,7 +1090,8 @@ function createRelationLoaders(db, registry) {
1076
1090
  async (parents) => {
1077
1091
  const rows = parents;
1078
1092
  await resolveRelationField(db, rows, fieldName, rel, cache, true);
1079
- return rows.map((r) => r[fieldName]);
1093
+ const resultKey = rel.type === "reference" || rel.type === "media" ? rel.fk_column : fieldName;
1094
+ return rows.map((r) => r[resultKey]);
1080
1095
  },
1081
1096
  // Batch within a tick; do not memoize by parent identity (rows are mutated
1082
1097
  // and may recur across nesting levels).
@@ -1161,7 +1176,7 @@ function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, optio
1161
1176
  currentVersion: versioning.currentVersion
1162
1177
  });
1163
1178
  }
1164
- const schema = buildGraphQLSchema(registry, fieldKeyMaps, view);
1179
+ const schema = buildGraphQLSchema(registry, fieldKeyMaps, view, versioning.currentFieldKeyMaps ?? fieldKeyMaps);
1165
1180
  const { plugins } = buildArmorPlugin({
1166
1181
  maxDepth: options.maxDepth,
1167
1182
  maxComplexity: options.maxComplexity
@@ -1240,7 +1255,7 @@ function createVersionedPaths(prefix, version) {
1240
1255
 
1241
1256
  // src/app.ts
1242
1257
  var import_hono = require("hono");
1243
- var import_drizzle_orm10 = require("drizzle-orm");
1258
+ var import_drizzle_orm11 = require("drizzle-orm");
1244
1259
 
1245
1260
  // src/middleware/cors.ts
1246
1261
  function createCorsMiddleware(corsConfig) {
@@ -1334,16 +1349,20 @@ var ERROR_STATUS_MAP = {
1334
1349
  UNSUPPORTED_MIME_TYPE: 415,
1335
1350
  STORAGE_ERROR: 502,
1336
1351
  MEDIA_IN_USE: 409,
1352
+ ITEM_IN_USE: 409,
1337
1353
  PRESIGNED_URL_EXPIRED: 410,
1338
1354
  RATE_LIMITED: 429,
1339
1355
  INTERNAL_ERROR: 500
1340
1356
  };
1341
1357
  var errorHandler = (err, c) => {
1342
1358
  console.error(err.stack ?? err.message);
1343
- const apiErr = err;
1344
- const code = apiErr.code ?? "INTERNAL_ERROR";
1345
- const status = ERROR_STATUS_MAP[code] ?? 500;
1346
- return c.json({ ok: false, error: { code, message: err.message } }, status);
1359
+ const raw = err.code;
1360
+ const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && raw in ERROR_STATUS_MAP;
1361
+ if (!deliberate) {
1362
+ return c.json({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
1363
+ }
1364
+ const status = ERROR_STATUS_MAP[raw] ?? 500;
1365
+ return c.json({ ok: false, error: { code: raw, message: err.message } }, status);
1347
1366
  };
1348
1367
 
1349
1368
  // src/middleware/auth.ts
@@ -1817,18 +1836,21 @@ function createProgrammaticResolver(resolvers, options) {
1817
1836
  }
1818
1837
 
1819
1838
  // src/projector.ts
1839
+ init_field_keys();
1820
1840
  function isPlainRow(v) {
1821
1841
  return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
1822
1842
  }
1823
1843
  var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
1824
- function nestedTargets(fields) {
1844
+ function nestedTargets(fields, map) {
1825
1845
  const out = [];
1826
1846
  for (const f of fields) {
1827
1847
  if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
1828
1848
  if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
1829
1849
  const ref = f.ui_component.ref;
1830
1850
  if (!ref) continue;
1831
- out.push({ label: f.name, target: ref });
1851
+ const label = isColumnBacked(f) ? map.labelFor(f.db_column.column_name) : f.name;
1852
+ if (label === void 0) continue;
1853
+ out.push({ label, target: ref });
1832
1854
  }
1833
1855
  return out;
1834
1856
  }
@@ -1846,7 +1868,7 @@ function buildProjectors(registry, fieldKeyMaps, fallbacks) {
1846
1868
  const typeFallbacks = fallbacks?.[typeName];
1847
1869
  projectors[typeName] = {
1848
1870
  map,
1849
- nested: nestedTargets(type.fields),
1871
+ nested: nestedTargets(type.fields, map),
1850
1872
  // Spread rather than assign: with exactOptionalPropertyTypes, an optional
1851
1873
  // property may be ABSENT but not present-and-undefined.
1852
1874
  ...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
@@ -1933,7 +1955,28 @@ function parseFilters(url, validFields, columnFor) {
1933
1955
  function isPublished(item) {
1934
1956
  return item["published"] === true;
1935
1957
  }
1936
- function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver) {
1958
+ function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
1959
+ const forResolver = resolverProjectors ?? projectors;
1960
+ function pickProgrammatic(resolved, names) {
1961
+ const out = {};
1962
+ for (const name of names) if (name in resolved) out[name] = resolved[name];
1963
+ return out;
1964
+ }
1965
+ async function respondItem(row, typeName, programmatic) {
1966
+ const data = projectRow(row, typeName, projectors);
1967
+ if (!resolver?.hasSchema(typeName)) return data;
1968
+ const resolved = await resolver.resolveItem(typeName, projectRow(row, typeName, forResolver));
1969
+ return { ...data, ...pickProgrammatic(resolved, programmatic) };
1970
+ }
1971
+ async function respondList(rows, typeName, programmatic) {
1972
+ const data = rows.map((row) => projectRow(row, typeName, projectors));
1973
+ if (!resolver?.hasSchema(typeName)) return data;
1974
+ const resolved = await resolver.resolveList(
1975
+ typeName,
1976
+ rows.map((row) => projectRow(row, typeName, forResolver))
1977
+ );
1978
+ return data.map((d, i) => ({ ...d, ...pickProgrammatic(resolved[i], programmatic) }));
1979
+ }
1937
1980
  function registerListRoute(path, handler) {
1938
1981
  if (listRateLimit) {
1939
1982
  app.get(path, listRateLimit, handler);
@@ -1957,18 +2000,29 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1957
2000
  return c.json({ ok: true, data });
1958
2001
  });
1959
2002
  for (const [typeName, contentType] of Object.entries(registry.content_types)) {
2003
+ let translateInclude2 = function(requested) {
2004
+ const include = [];
2005
+ for (const field of requested) {
2006
+ const registryName = includeNames.get(field);
2007
+ if (registryName === void 0) return { ok: false, field };
2008
+ include.push(registryName);
2009
+ }
2010
+ return { ok: true, include };
2011
+ };
2012
+ var translateInclude = translateInclude2;
1960
2013
  const basePath = contentType.default_base_path;
1961
2014
  const repo = repos[typeName];
1962
2015
  if (!repo) continue;
1963
2016
  const fieldKeys = projectors[typeName].map;
1964
- const relationFieldNames = /* @__PURE__ */ new Set();
2017
+ const programmatic = contentType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
2018
+ const includeNames = /* @__PURE__ */ new Map();
1965
2019
  for (const f of contentType.fields) {
1966
2020
  if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
1967
2021
  if (isColumnBacked(f)) {
1968
2022
  const label = fieldKeys.labelFor(f.db_column.column_name);
1969
- if (label !== void 0) relationFieldNames.add(label);
2023
+ if (label !== void 0) includeNames.set(label, f.name);
1970
2024
  } else {
1971
- relationFieldNames.add(f.name);
2025
+ includeNames.set(f.name, f.name);
1972
2026
  }
1973
2027
  }
1974
2028
  const filterableFieldNames = /* @__PURE__ */ new Set([
@@ -1987,8 +2041,11 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
1987
2041
  404
1988
2042
  );
1989
2043
  }
1990
- let data = projectRow(result.data[0], typeName, projectors);
1991
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
2044
+ const data = await respondItem(
2045
+ result.data[0],
2046
+ typeName,
2047
+ programmatic
2048
+ );
1992
2049
  return c.json({ ok: true, data });
1993
2050
  });
1994
2051
  } else {
@@ -2045,21 +2102,20 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2045
2102
  400
2046
2103
  );
2047
2104
  }
2048
- const include = parseInclude(c.req.query("include"));
2049
- for (const field of include) {
2050
- if (!relationFieldNames.has(field)) {
2051
- return c.json(
2052
- {
2053
- ok: false,
2054
- error: {
2055
- code: "INVALID_INCLUDE_FIELD",
2056
- message: `'${field}' is not a valid relation field`
2057
- }
2058
- },
2059
- 400
2060
- );
2061
- }
2105
+ const translated = translateInclude2(parseInclude(c.req.query("include")));
2106
+ if (!translated.ok) {
2107
+ return c.json(
2108
+ {
2109
+ ok: false,
2110
+ error: {
2111
+ code: "INVALID_INCLUDE_FIELD",
2112
+ message: `'${translated.field}' is not a valid relation field`
2113
+ }
2114
+ },
2115
+ 400
2116
+ );
2062
2117
  }
2118
+ const include = translated.include;
2063
2119
  const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
2064
2120
  const result = await repo.findMany({
2065
2121
  published_only: true,
@@ -2070,29 +2126,25 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2070
2126
  filters: filtersResult.filters,
2071
2127
  include
2072
2128
  });
2073
- const labeled = result.data.map(
2074
- (row) => projectRow(row, typeName, projectors)
2075
- );
2076
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
2129
+ const data = await respondList(result.data, typeName, programmatic);
2077
2130
  return c.json({ ...result, data });
2078
2131
  });
2079
2132
  app.get(paths.item(basePath), async (c) => {
2080
2133
  const slug = c.req.param("slug");
2081
- const include = parseInclude(c.req.query("include"));
2082
- for (const field of include) {
2083
- if (!relationFieldNames.has(field)) {
2084
- return c.json(
2085
- {
2086
- ok: false,
2087
- error: {
2088
- code: "INVALID_INCLUDE_FIELD",
2089
- message: `'${field}' is not a valid relation field`
2090
- }
2091
- },
2092
- 400
2093
- );
2094
- }
2134
+ const translated = translateInclude2(parseInclude(c.req.query("include")));
2135
+ if (!translated.ok) {
2136
+ return c.json(
2137
+ {
2138
+ ok: false,
2139
+ error: {
2140
+ code: "INVALID_INCLUDE_FIELD",
2141
+ message: `'${translated.field}' is not a valid relation field`
2142
+ }
2143
+ },
2144
+ 400
2145
+ );
2095
2146
  }
2147
+ const include = translated.include;
2096
2148
  const item = await repo.findBySlug(slug, include);
2097
2149
  if (!item || !isPublished(item)) {
2098
2150
  return c.json(
@@ -2103,15 +2155,15 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2103
2155
  404
2104
2156
  );
2105
2157
  }
2106
- let data = projectRow(item, typeName, projectors);
2107
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
2158
+ const data = await respondItem(item, typeName, programmatic);
2108
2159
  return c.json({ ok: true, data });
2109
2160
  });
2110
2161
  }
2111
2162
  }
2112
- for (const [typeName] of Object.entries(registry.taxonomy_types)) {
2163
+ for (const [typeName, taxonomyType] of Object.entries(registry.taxonomy_types)) {
2113
2164
  const repo = repos[typeName];
2114
2165
  if (!repo) continue;
2166
+ const programmatic = taxonomyType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
2115
2167
  registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
2116
2168
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
2117
2169
  if (!pagination.ok) {
@@ -2131,10 +2183,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2131
2183
  page: pagination.page,
2132
2184
  per_page: pagination.per_page
2133
2185
  });
2134
- const labeled = result.data.map(
2135
- (row) => projectRow(row, typeName, projectors)
2136
- );
2137
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
2186
+ const data = await respondList(result.data, typeName, programmatic);
2138
2187
  return c.json({ ...result, data });
2139
2188
  });
2140
2189
  app.get(paths.taxonomyItem(typeName), async (c) => {
@@ -2149,8 +2198,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
2149
2198
  404
2150
2199
  );
2151
2200
  }
2152
- let data = projectRow(item, typeName, projectors);
2153
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
2201
+ const data = await respondItem(item, typeName, programmatic);
2154
2202
  return c.json({ ok: true, data });
2155
2203
  });
2156
2204
  }
@@ -2222,7 +2270,55 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
2222
2270
  }
2223
2271
 
2224
2272
  // src/routes/admin/content.ts
2225
- var import_drizzle_orm3 = require("drizzle-orm");
2273
+ var import_drizzle_orm4 = require("drizzle-orm");
2274
+
2275
+ // src/references-in-use.ts
2276
+ var import_drizzle_orm2 = require("drizzle-orm");
2277
+ var import_manguito_cms_core = require("@bobbykim/manguito-cms-core");
2278
+ function quoteIdent(name) {
2279
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
2280
+ return `"${name}"`;
2281
+ }
2282
+ function isRequiredSingleReferenceTo(f, targetType) {
2283
+ return f.field_type === "reference" && f.required && (0, import_manguito_cms_core.relationCardinality)(f) === "one" && f.db_column?.foreign_key !== void 0 && f.ui_component.component === "typeahead-select" && f.ui_component.ref === targetType;
2284
+ }
2285
+ async function findRequiredReferrers(db, registry, targetType, id) {
2286
+ const owners = [
2287
+ ...Object.values(registry.content_types),
2288
+ ...Object.values(registry.taxonomy_types),
2289
+ ...Object.values(registry.paragraph_types)
2290
+ ];
2291
+ const out = [];
2292
+ for (const owner of owners) {
2293
+ for (const f of owner.fields) {
2294
+ if (!isRequiredSingleReferenceTo(f, targetType)) continue;
2295
+ const notSelf = owner.name === targetType ? import_drizzle_orm2.sql` AND id <> ${id}` : import_drizzle_orm2.sql``;
2296
+ const r = await db.execute(
2297
+ import_drizzle_orm2.sql`SELECT count(*)::int AS n FROM ${import_drizzle_orm2.sql.raw(quoteIdent(owner.db.table_name))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(f.db_column.column_name))} = ${id}${notSelf}`
2298
+ );
2299
+ const n = r.rows[0].n;
2300
+ if (n > 0) out.push({ type_label: owner.label, field_label: f.label, count: n });
2301
+ }
2302
+ }
2303
+ return out;
2304
+ }
2305
+ function inUseMessage(referrers) {
2306
+ const total = referrers.reduce((sum, r) => sum + r.count, 0);
2307
+ const where = referrers.map((r) => `${r.count} in ${r.type_label} (${r.field_label})`).join(", ");
2308
+ return `This item is still used by ${total} item${total === 1 ? "" : "s"}: ${where}. Remove it from those first.`;
2309
+ }
2310
+ var IN_USE_RACE_MESSAGE = "This item is still in use. Remove it from the items that use it first.";
2311
+ function isForeignKeyViolation(err) {
2312
+ let e = err;
2313
+ for (let depth = 0; depth < 3 && typeof e === "object" && e !== null; depth++) {
2314
+ if (e.code === "23503") return true;
2315
+ e = e.cause;
2316
+ }
2317
+ return false;
2318
+ }
2319
+
2320
+ // src/routes/admin/content.ts
2321
+ var import_manguito_cms_core4 = require("@bobbykim/manguito-cms-core");
2226
2322
 
2227
2323
  // src/media-references.ts
2228
2324
  function extractMediaId(value) {
@@ -2260,14 +2356,62 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
2260
2356
 
2261
2357
  // src/routes/admin/content.ts
2262
2358
  init_relations();
2359
+
2360
+ // src/relation-input.ts
2361
+ var import_manguito_cms_core3 = require("@bobbykim/manguito-cms-core");
2362
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2363
+ function isUuid(v) {
2364
+ return typeof v === "string" && UUID_RE.test(v);
2365
+ }
2366
+ function isPlainObject(v) {
2367
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2368
+ }
2369
+ function checkRelationInput(fields, body, registry, path = "") {
2370
+ const errors = [];
2371
+ for (const field of fields) {
2372
+ if (!(field.name in body)) continue;
2373
+ const cardinality = (0, import_manguito_cms_core3.relationCardinality)(field);
2374
+ if (cardinality === null) continue;
2375
+ const value = body[field.name];
2376
+ const name = `${path}${field.name}`;
2377
+ if (field.field_type === "reference") {
2378
+ const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
2379
+ if (!ok) {
2380
+ errors.push({
2381
+ field: name,
2382
+ message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
2383
+ });
2384
+ }
2385
+ continue;
2386
+ }
2387
+ const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
2388
+ if (items === void 0) {
2389
+ errors.push({
2390
+ field: name,
2391
+ message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
2392
+ });
2393
+ continue;
2394
+ }
2395
+ const comp = field.ui_component;
2396
+ const pType = comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
2397
+ if (!pType) continue;
2398
+ items.forEach((item, i) => {
2399
+ const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
2400
+ errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
2401
+ });
2402
+ }
2403
+ return errors;
2404
+ }
2405
+
2406
+ // src/routes/admin/content.ts
2263
2407
  init_field_keys();
2264
- function quoteIdent2(name) {
2408
+ function quoteIdent3(name) {
2265
2409
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
2266
2410
  return `"${name}"`;
2267
2411
  }
2268
2412
  async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
2269
2413
  const result = await db.execute(
2270
- import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
2414
+ import_drizzle_orm4.sql`SELECT * FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
2271
2415
  );
2272
2416
  const rows = result.rows;
2273
2417
  for (const nf of pType.fields) {
@@ -2277,14 +2421,17 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
2277
2421
  const nType = registry.paragraph_types[comp.ref];
2278
2422
  if (!nType) continue;
2279
2423
  for (const row of rows) {
2280
- row[nf.name] = await loadParagraphRows(db, registry, nType, row["id"], nf.name);
2424
+ row[nf.name] = oneOrMany(
2425
+ (0, import_manguito_cms_core4.relationCardinality)(nf) ?? "many",
2426
+ await loadParagraphRows(db, registry, nType, row["id"], nf.name)
2427
+ );
2281
2428
  }
2282
2429
  }
2283
2430
  return rows;
2284
2431
  }
2285
2432
  async function lookupBasePathId(db, pathOrName) {
2286
2433
  const r = await db.execute(
2287
- import_drizzle_orm3.sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
2434
+ import_drizzle_orm4.sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
2288
2435
  );
2289
2436
  return r.rows[0]?.id ?? null;
2290
2437
  }
@@ -2298,6 +2445,14 @@ function isEmpty(value) {
2298
2445
  function checkRequiredFields(fields, data) {
2299
2446
  return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
2300
2447
  }
2448
+ function relationShapeError(c, fields, body, registry) {
2449
+ const details = checkRelationInput(fields, body, registry);
2450
+ if (details.length === 0) return null;
2451
+ return c.json(
2452
+ { ok: false, error: { code: "VALIDATION_ERROR", message: "Relation fields have the wrong shape", details } },
2453
+ 422
2454
+ );
2455
+ }
2301
2456
  function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
2302
2457
  const searchParams = new URL(url).searchParams;
2303
2458
  const pagination = parsePagination(
@@ -2439,11 +2594,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2439
2594
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2440
2595
  const pType = registry.paragraph_types[comp.ref];
2441
2596
  if (!pType) continue;
2442
- row[f.name] = await loadParagraphRows(db, registry, pType, id, f.name);
2597
+ row[f.name] = oneOrMany((0, import_manguito_cms_core4.relationCardinality)(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name));
2443
2598
  } else if (f.db_column.junction) {
2444
2599
  const j = f.db_column.junction;
2445
2600
  const r = await db.execute(
2446
- import_drizzle_orm3.sql`SELECT ${import_drizzle_orm3.sql.raw(quoteIdent2(j.right_column))} FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(j.table_name))} WHERE ${import_drizzle_orm3.sql.raw(quoteIdent2(j.left_column))} = ${id}`
2601
+ import_drizzle_orm4.sql`SELECT ${import_drizzle_orm4.sql.raw(quoteIdent3(j.right_column))} FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(j.table_name))} WHERE ${import_drizzle_orm4.sql.raw(quoteIdent3(j.left_column))} = ${id}`
2447
2602
  );
2448
2603
  row[f.name] = r.rows.map(
2449
2604
  (row2) => row2[j.right_column]
@@ -2519,6 +2674,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2519
2674
  }
2520
2675
  );
2521
2676
  const writeNewItem = async (c, body) => {
2677
+ const shapeError = relationShapeError(c, contentType.fields, body, registry);
2678
+ if (shapeError) return shapeError;
2522
2679
  const storageBody = fieldKeys.toStorage(body);
2523
2680
  const fieldErrors = checkRequiredFields(requiredFields, body);
2524
2681
  if (fieldErrors.length > 0) {
@@ -2582,7 +2739,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2582
2739
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2583
2740
  const pType = registry.paragraph_types[comp.ref];
2584
2741
  if (!pType) continue;
2585
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2742
+ const items = paragraphItems(f, body[f.name]);
2586
2743
  mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
2587
2744
  }
2588
2745
  for (const f of junctionFields) {
@@ -2653,6 +2810,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2653
2810
  }
2654
2811
  );
2655
2812
  const writeExistingItem = async (c, id, existing, body) => {
2813
+ const shapeError = relationShapeError(c, contentType.fields, body, registry);
2814
+ if (shapeError) return shapeError;
2656
2815
  const storageBody = fieldKeys.toStorage(body);
2657
2816
  if (body["published"] === true) {
2658
2817
  const publishDeny = await requirePermission("content:edit")(c, async () => {
@@ -2699,14 +2858,16 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2699
2858
  ];
2700
2859
  if (db) {
2701
2860
  for (const f of patchParagraphFields) {
2861
+ if (!(f.name in body)) continue;
2702
2862
  const comp = f.ui_component;
2703
2863
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2704
2864
  const pType = registry.paragraph_types[comp.ref];
2705
2865
  if (!pType) continue;
2706
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2866
+ const items = paragraphItems(f, body[f.name]);
2707
2867
  mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
2708
2868
  }
2709
2869
  for (const f of patchJunctionFields) {
2870
+ if (!(f.name in body)) continue;
2710
2871
  const junction = f.db_column.junction;
2711
2872
  const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
2712
2873
  await persistJunctionField(db, id, junction, relatedIds);
@@ -2746,6 +2907,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2746
2907
  404
2747
2908
  );
2748
2909
  }
2910
+ const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
2911
+ if (referrers.length > 0) {
2912
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
2913
+ }
2914
+ try {
2915
+ await repo.delete(id);
2916
+ } catch (err) {
2917
+ if (isForeignKeyViolation(err)) {
2918
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
2919
+ }
2920
+ throw err;
2921
+ }
2749
2922
  const mediaDeltas = [
2750
2923
  topLevelMediaDelta(mediaFields, item, null)
2751
2924
  ];
@@ -2759,7 +2932,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2759
2932
  }
2760
2933
  }
2761
2934
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2762
- await repo.delete(id);
2763
2935
  return c.json({ ok: true });
2764
2936
  }
2765
2937
  );
@@ -2837,6 +3009,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2837
3009
  requirePermission("content:create"),
2838
3010
  async (c) => {
2839
3011
  const body = await c.req.json();
3012
+ const shapeError = relationShapeError(c, taxonomyType.fields, body, registry);
3013
+ if (shapeError) return shapeError;
2840
3014
  const storageBody = fieldKeys.toStorage(body);
2841
3015
  const fieldErrors = checkRequiredFields(requiredFields, body);
2842
3016
  if (fieldErrors.length > 0) {
@@ -2876,7 +3050,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2876
3050
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2877
3051
  const pType = registry.paragraph_types[comp.ref];
2878
3052
  if (!pType) continue;
2879
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
3053
+ const items = paragraphItems(f, body[f.name]);
2880
3054
  mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
2881
3055
  }
2882
3056
  }
@@ -2899,6 +3073,8 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2899
3073
  404
2900
3074
  );
2901
3075
  }
3076
+ const shapeError = relationShapeError(c, taxonomyType.fields, body, registry);
3077
+ if (shapeError) return shapeError;
2902
3078
  if (body["published"] === true) {
2903
3079
  const publishDeny = await requirePermission("content:edit")(c, async () => {
2904
3080
  });
@@ -2940,11 +3116,12 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2940
3116
  ];
2941
3117
  if (db) {
2942
3118
  for (const f of taxPatchParagraphFields) {
3119
+ if (!(f.name in body)) continue;
2943
3120
  const comp = f.ui_component;
2944
3121
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2945
3122
  const pType = registry.paragraph_types[comp.ref];
2946
3123
  if (!pType) continue;
2947
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
3124
+ const items = paragraphItems(f, body[f.name]);
2948
3125
  mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
2949
3126
  }
2950
3127
  }
@@ -2965,6 +3142,18 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2965
3142
  404
2966
3143
  );
2967
3144
  }
3145
+ const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
3146
+ if (referrers.length > 0) {
3147
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
3148
+ }
3149
+ try {
3150
+ await repo.delete(id);
3151
+ } catch (err) {
3152
+ if (isForeignKeyViolation(err)) {
3153
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
3154
+ }
3155
+ throw err;
3156
+ }
2968
3157
  const mediaDeltas = [
2969
3158
  topLevelMediaDelta(mediaFields, item, null)
2970
3159
  ];
@@ -2978,7 +3167,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2978
3167
  }
2979
3168
  }
2980
3169
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2981
- await repo.delete(id);
2982
3170
  return c.json({ ok: true });
2983
3171
  }
2984
3172
  );
@@ -3440,10 +3628,10 @@ function registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, ma
3440
3628
 
3441
3629
  // src/routes/admin/auth.ts
3442
3630
  var import_cookie3 = require("hono/cookie");
3443
- var import_drizzle_orm4 = require("drizzle-orm");
3631
+ var import_drizzle_orm5 = require("drizzle-orm");
3444
3632
 
3445
3633
  // src/auth/password.ts
3446
- var import_manguito_cms_core = require("@bobbykim/manguito-cms-core");
3634
+ var import_manguito_cms_core5 = require("@bobbykim/manguito-cms-core");
3447
3635
 
3448
3636
  // src/routes/admin/auth.ts
3449
3637
  var loginAttempts = /* @__PURE__ */ new Map();
@@ -3506,20 +3694,20 @@ function registerAuthRoutes(app, db) {
3506
3694
  );
3507
3695
  }
3508
3696
  const userResult = await db.execute(
3509
- import_drizzle_orm4.sql`SELECT id, email, password_hash, role_id, token_version
3697
+ import_drizzle_orm5.sql`SELECT id, email, password_hash, role_id, token_version
3510
3698
  FROM users WHERE email = ${email} LIMIT 1`
3511
3699
  );
3512
3700
  const user = userResult.rows[0];
3513
3701
  if (!user) {
3514
- await (0, import_manguito_cms_core.verifyPassword)(password, DUMMY_PASSWORD_HASH);
3702
+ await (0, import_manguito_cms_core5.verifyPassword)(password, DUMMY_PASSWORD_HASH);
3515
3703
  return c.json(invalidCredentials, 401);
3516
3704
  }
3517
- const passwordValid = await (0, import_manguito_cms_core.verifyPassword)(password, user.password_hash);
3705
+ const passwordValid = await (0, import_manguito_cms_core5.verifyPassword)(password, user.password_hash);
3518
3706
  if (!passwordValid) {
3519
3707
  return c.json(invalidCredentials, 401);
3520
3708
  }
3521
3709
  const roleResult = await db.execute(
3522
- import_drizzle_orm4.sql`SELECT r.name AS name, u.must_change_password
3710
+ import_drizzle_orm5.sql`SELECT r.name AS name, u.must_change_password
3523
3711
  FROM roles r
3524
3712
  JOIN users u ON u.role_id = r.id
3525
3713
  WHERE r.id = ${user.role_id} AND u.id = ${user.id}
@@ -3579,7 +3767,7 @@ function registerAuthRoutes(app, db) {
3579
3767
  );
3580
3768
  }
3581
3769
  const rowResult = await db.execute(
3582
- import_drizzle_orm4.sql`SELECT u.token_version, r.name AS role
3770
+ import_drizzle_orm5.sql`SELECT u.token_version, r.name AS role
3583
3771
  FROM users u
3584
3772
  JOIN roles r ON r.id = u.role_id
3585
3773
  WHERE u.id = ${payload.user_id}
@@ -3603,7 +3791,7 @@ function registerAuthRoutes(app, db) {
3603
3791
  app.post("/admin/api/auth/logout", authMiddleware, async (c) => {
3604
3792
  const user = c.get("user");
3605
3793
  await db.execute(
3606
- import_drizzle_orm4.sql`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
3794
+ import_drizzle_orm5.sql`UPDATE users SET token_version = token_version + 1 WHERE id = ${user.id}`
3607
3795
  );
3608
3796
  clearAuthCookie(c, "auth_token");
3609
3797
  clearAuthCookie(c, "refresh_token", "/admin/api/auth");
@@ -3612,7 +3800,7 @@ function registerAuthRoutes(app, db) {
3612
3800
  }
3613
3801
 
3614
3802
  // src/routes/admin/users.ts
3615
- var import_drizzle_orm5 = require("drizzle-orm");
3803
+ var import_drizzle_orm6 = require("drizzle-orm");
3616
3804
  var import_node_crypto2 = require("crypto");
3617
3805
  function getActingUser(c) {
3618
3806
  return c.get("user");
@@ -3647,18 +3835,18 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3647
3835
  );
3648
3836
  }
3649
3837
  const hashResult = await db.execute(
3650
- import_drizzle_orm5.sql`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
3838
+ import_drizzle_orm6.sql`SELECT password_hash FROM users WHERE id = ${actingUser.id} LIMIT 1`
3651
3839
  );
3652
3840
  const hashRow = hashResult.rows[0];
3653
- if (!hashRow || !await (0, import_manguito_cms_core.verifyPassword)(currentPassword, hashRow.password_hash)) {
3841
+ if (!hashRow || !await (0, import_manguito_cms_core5.verifyPassword)(currentPassword, hashRow.password_hash)) {
3654
3842
  return c.json(
3655
3843
  { ok: false, error: { code: "INVALID_CREDENTIALS", message: "Invalid credentials." } },
3656
3844
  401
3657
3845
  );
3658
3846
  }
3659
- const newHash = await (0, import_manguito_cms_core.hashPassword)(newPassword);
3847
+ const newHash = await (0, import_manguito_cms_core5.hashPassword)(newPassword);
3660
3848
  await db.execute(
3661
- import_drizzle_orm5.sql`UPDATE users
3849
+ import_drizzle_orm6.sql`UPDATE users
3662
3850
  SET password_hash = ${newHash},
3663
3851
  must_change_password = false,
3664
3852
  token_version = token_version + 1,
@@ -3669,7 +3857,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3669
3857
  });
3670
3858
  app.get("/admin/api/users", requirePermission("users:read"), async (c) => {
3671
3859
  const result = await db.execute(
3672
- import_drizzle_orm5.sql`SELECT u.id, u.email, r.name AS role, u.must_change_password,
3860
+ import_drizzle_orm6.sql`SELECT u.id, u.email, r.name AS role, u.must_change_password,
3673
3861
  u.created_at, u.updated_at
3674
3862
  FROM users u
3675
3863
  JOIN roles r ON r.id = u.role_id
@@ -3680,7 +3868,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3680
3868
  app.get("/admin/api/users/:id", requirePermission("users:read"), async (c) => {
3681
3869
  const id = c.req.param("id");
3682
3870
  const result = await db.execute(
3683
- import_drizzle_orm5.sql`SELECT u.id, u.email, r.name AS role, u.must_change_password,
3871
+ import_drizzle_orm6.sql`SELECT u.id, u.email, r.name AS role, u.must_change_password,
3684
3872
  u.created_at, u.updated_at
3685
3873
  FROM users u
3686
3874
  JOIN roles r ON r.id = u.role_id
@@ -3725,7 +3913,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3725
3913
  );
3726
3914
  }
3727
3915
  const roleResult = await db.execute(
3728
- import_drizzle_orm5.sql`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
3916
+ import_drizzle_orm6.sql`SELECT id FROM roles WHERE name = ${roleName} LIMIT 1`
3729
3917
  );
3730
3918
  const roleRow = roleResult.rows[0];
3731
3919
  if (!roleRow) {
@@ -3738,9 +3926,9 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3738
3926
  );
3739
3927
  }
3740
3928
  const tempPassword = generateTempPassword();
3741
- const passwordHash = await (0, import_manguito_cms_core.hashPassword)(tempPassword);
3929
+ const passwordHash = await (0, import_manguito_cms_core5.hashPassword)(tempPassword);
3742
3930
  const insertResult = await db.execute(
3743
- import_drizzle_orm5.sql`INSERT INTO users
3931
+ import_drizzle_orm6.sql`INSERT INTO users
3744
3932
  (id, email, password_hash, role_id, must_change_password, token_version,
3745
3933
  created_at, updated_at)
3746
3934
  VALUES
@@ -3795,7 +3983,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3795
3983
  );
3796
3984
  }
3797
3985
  const existingResult = await db.execute(
3798
- import_drizzle_orm5.sql`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
3986
+ import_drizzle_orm6.sql`SELECT id, email, role_id FROM users WHERE id = ${id} LIMIT 1`
3799
3987
  );
3800
3988
  const existing = existingResult.rows[0];
3801
3989
  if (!existing) {
@@ -3809,7 +3997,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3809
3997
  let changeRole = false;
3810
3998
  if (typeof body.role === "string" && body.role) {
3811
3999
  const roleResult = await db.execute(
3812
- import_drizzle_orm5.sql`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
4000
+ import_drizzle_orm6.sql`SELECT id FROM roles WHERE name = ${body.role} LIMIT 1`
3813
4001
  );
3814
4002
  const roleRow = roleResult.rows[0];
3815
4003
  if (!roleRow) {
@@ -3829,7 +4017,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3829
4017
  }
3830
4018
  if (changeRole) {
3831
4019
  await db.execute(
3832
- import_drizzle_orm5.sql`UPDATE users
4020
+ import_drizzle_orm6.sql`UPDATE users
3833
4021
  SET email = ${newEmail},
3834
4022
  role_id = ${newRoleId},
3835
4023
  token_version = token_version + 1,
@@ -3838,7 +4026,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3838
4026
  );
3839
4027
  } else {
3840
4028
  await db.execute(
3841
- import_drizzle_orm5.sql`UPDATE users
4029
+ import_drizzle_orm6.sql`UPDATE users
3842
4030
  SET email = ${newEmail},
3843
4031
  role_id = ${newRoleId},
3844
4032
  updated_at = NOW()
@@ -3846,7 +4034,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3846
4034
  );
3847
4035
  }
3848
4036
  const updatedResult = await db.execute(
3849
- import_drizzle_orm5.sql`SELECT u.id, u.email, r.name AS role, u.must_change_password,
4037
+ import_drizzle_orm6.sql`SELECT u.id, u.email, r.name AS role, u.must_change_password,
3850
4038
  u.created_at, u.updated_at
3851
4039
  FROM users u
3852
4040
  JOIN roles r ON r.id = u.role_id
@@ -3883,7 +4071,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3883
4071
  );
3884
4072
  }
3885
4073
  const result = await db.execute(
3886
- import_drizzle_orm5.sql`DELETE FROM users WHERE id = ${id} RETURNING id`
4074
+ import_drizzle_orm6.sql`DELETE FROM users WHERE id = ${id} RETURNING id`
3887
4075
  );
3888
4076
  if (result.rows.length === 0) {
3889
4077
  return c.json(
@@ -3914,7 +4102,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3914
4102
  );
3915
4103
  }
3916
4104
  const existingResult = await db.execute(
3917
- import_drizzle_orm5.sql`SELECT id FROM users WHERE id = ${id} LIMIT 1`
4105
+ import_drizzle_orm6.sql`SELECT id FROM users WHERE id = ${id} LIMIT 1`
3918
4106
  );
3919
4107
  const existing = existingResult.rows[0];
3920
4108
  if (!existing) {
@@ -3924,9 +4112,9 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3924
4112
  );
3925
4113
  }
3926
4114
  const tempPassword = generateTempPassword();
3927
- const passwordHash = await (0, import_manguito_cms_core.hashPassword)(tempPassword);
4115
+ const passwordHash = await (0, import_manguito_cms_core5.hashPassword)(tempPassword);
3928
4116
  await db.execute(
3929
- import_drizzle_orm5.sql`UPDATE users
4117
+ import_drizzle_orm6.sql`UPDATE users
3930
4118
  SET password_hash = ${passwordHash},
3931
4119
  must_change_password = true,
3932
4120
  token_version = token_version + 1,
@@ -3939,10 +4127,10 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3939
4127
  }
3940
4128
 
3941
4129
  // src/routes/admin/config.ts
3942
- var import_drizzle_orm6 = require("drizzle-orm");
4130
+ var import_drizzle_orm7 = require("drizzle-orm");
3943
4131
  var API_VERSION = (() => {
3944
4132
  try {
3945
- return "0.6.0";
4133
+ return "0.8.0";
3946
4134
  } catch {
3947
4135
  return "0.0.0";
3948
4136
  }
@@ -3961,7 +4149,7 @@ function registerConfigRoute(app, config, registry, db) {
3961
4149
  permissions: r.permissions
3962
4150
  }));
3963
4151
  const userResult = await db.execute(
3964
- import_drizzle_orm6.sql`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
4152
+ import_drizzle_orm7.sql`SELECT email, must_change_password FROM users WHERE id = ${actingUser.id} LIMIT 1`
3965
4153
  );
3966
4154
  const userRow = userResult.rows[0];
3967
4155
  return c.json({
@@ -3987,7 +4175,7 @@ function registerConfigRoute(app, config, registry, db) {
3987
4175
  }
3988
4176
 
3989
4177
  // src/routes/admin/schema.ts
3990
- var import_drizzle_orm7 = require("drizzle-orm");
4178
+ var import_drizzle_orm8 = require("drizzle-orm");
3991
4179
  function liveFields(fields) {
3992
4180
  return fields.filter((f) => f.removed !== true);
3993
4181
  }
@@ -4027,7 +4215,7 @@ function registerSchemaRoute(app, registry, db) {
4027
4215
  const data = await Promise.all(
4028
4216
  Object.values(registry.content_types).map(async (ct) => {
4029
4217
  const result = await db.execute(
4030
- import_drizzle_orm7.sql`SELECT COUNT(*)::int AS count FROM ${import_drizzle_orm7.sql.raw(`"${ct.db.table_name}"`)}`
4218
+ import_drizzle_orm8.sql`SELECT COUNT(*)::int AS count FROM ${import_drizzle_orm8.sql.raw(`"${ct.db.table_name}"`)}`
4031
4219
  );
4032
4220
  const count = Number(result.rows[0].count);
4033
4221
  return { name: ct.name, label: ct.label, only_one: ct.only_one, count };
@@ -4039,7 +4227,7 @@ function registerSchemaRoute(app, registry, db) {
4039
4227
  const data = await Promise.all(
4040
4228
  Object.values(registry.taxonomy_types).map(async (tt) => {
4041
4229
  const result = await db.execute(
4042
- import_drizzle_orm7.sql`SELECT COUNT(*)::int AS count FROM ${import_drizzle_orm7.sql.raw(`"${tt.db.table_name}"`)}`
4230
+ import_drizzle_orm8.sql`SELECT COUNT(*)::int AS count FROM ${import_drizzle_orm8.sql.raw(`"${tt.db.table_name}"`)}`
4043
4231
  );
4044
4232
  const count = Number(result.rows[0].count);
4045
4233
  return { name: tt.name, label: tt.label, count };
@@ -4051,7 +4239,7 @@ function registerSchemaRoute(app, registry, db) {
4051
4239
 
4052
4240
  // src/repositories/content.ts
4053
4241
  var import_node_crypto3 = __toESM(require("crypto"));
4054
- var import_drizzle_orm8 = require("drizzle-orm");
4242
+ var import_drizzle_orm9 = require("drizzle-orm");
4055
4243
  init_relations();
4056
4244
  var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
4057
4245
  function codeError(code, message) {
@@ -4059,7 +4247,7 @@ function codeError(code, message) {
4059
4247
  err.code = code;
4060
4248
  return err;
4061
4249
  }
4062
- function quoteIdent3(name) {
4250
+ function quoteIdent4(name) {
4063
4251
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
4064
4252
  throw new Error(`Unsafe SQL identifier: ${name}`);
4065
4253
  }
@@ -4074,43 +4262,43 @@ function quoteField(name) {
4074
4262
  function buildConditions(published_only, filters, search) {
4075
4263
  const conds = [];
4076
4264
  if (published_only) {
4077
- conds.push(import_drizzle_orm8.sql`published = true`);
4265
+ conds.push(import_drizzle_orm9.sql`published = true`);
4078
4266
  }
4079
4267
  if (search && search.term.trim() !== "" && search.columns.length > 0) {
4080
4268
  const pattern = `%${search.term.trim()}%`;
4081
- const orConds = search.columns.map((col) => import_drizzle_orm8.sql`${import_drizzle_orm8.sql.raw(quoteField(col))} ILIKE ${pattern}`);
4082
- conds.push(import_drizzle_orm8.sql`(${import_drizzle_orm8.sql.join(orConds, import_drizzle_orm8.sql` OR `)})`);
4269
+ const orConds = search.columns.map((col) => import_drizzle_orm9.sql`${import_drizzle_orm9.sql.raw(quoteField(col))} ILIKE ${pattern}`);
4270
+ conds.push(import_drizzle_orm9.sql`(${import_drizzle_orm9.sql.join(orConds, import_drizzle_orm9.sql` OR `)})`);
4083
4271
  }
4084
4272
  if (filters) {
4085
4273
  for (const [field, value] of Object.entries(filters)) {
4086
- const col = import_drizzle_orm8.sql.raw(quoteField(field));
4274
+ const col = import_drizzle_orm9.sql.raw(quoteField(field));
4087
4275
  if (Array.isArray(value)) {
4088
4276
  if (value.length === 0) continue;
4089
- const inList = import_drizzle_orm8.sql.join(
4090
- value.map((v) => import_drizzle_orm8.sql`${v}`),
4091
- import_drizzle_orm8.sql`, `
4277
+ const inList = import_drizzle_orm9.sql.join(
4278
+ value.map((v) => import_drizzle_orm9.sql`${v}`),
4279
+ import_drizzle_orm9.sql`, `
4092
4280
  );
4093
- conds.push(import_drizzle_orm8.sql`${col} IN (${inList})`);
4281
+ conds.push(import_drizzle_orm9.sql`${col} IN (${inList})`);
4094
4282
  } else if (typeof value === "object" && value !== null) {
4095
4283
  const op = value;
4096
- if (op.gt !== void 0) conds.push(import_drizzle_orm8.sql`${col} > ${op.gt}`);
4097
- if (op.gte !== void 0) conds.push(import_drizzle_orm8.sql`${col} >= ${op.gte}`);
4098
- if (op.lt !== void 0) conds.push(import_drizzle_orm8.sql`${col} < ${op.lt}`);
4099
- if (op.lte !== void 0) conds.push(import_drizzle_orm8.sql`${col} <= ${op.lte}`);
4284
+ if (op.gt !== void 0) conds.push(import_drizzle_orm9.sql`${col} > ${op.gt}`);
4285
+ if (op.gte !== void 0) conds.push(import_drizzle_orm9.sql`${col} >= ${op.gte}`);
4286
+ if (op.lt !== void 0) conds.push(import_drizzle_orm9.sql`${col} < ${op.lt}`);
4287
+ if (op.lte !== void 0) conds.push(import_drizzle_orm9.sql`${col} <= ${op.lte}`);
4100
4288
  } else {
4101
- conds.push(import_drizzle_orm8.sql`${col} = ${value}`);
4289
+ conds.push(import_drizzle_orm9.sql`${col} = ${value}`);
4102
4290
  }
4103
4291
  }
4104
4292
  }
4105
4293
  return conds;
4106
4294
  }
4107
4295
  function whereFragment(conds) {
4108
- return conds.length > 0 ? import_drizzle_orm8.sql` WHERE ${import_drizzle_orm8.sql.join(conds, import_drizzle_orm8.sql` AND `)}` : import_drizzle_orm8.sql``;
4296
+ return conds.length > 0 ? import_drizzle_orm9.sql` WHERE ${import_drizzle_orm9.sql.join(conds, import_drizzle_orm9.sql` AND `)}` : import_drizzle_orm9.sql``;
4109
4297
  }
4110
4298
  function createDrizzleContentRepository(db, tableName, options = {}) {
4111
4299
  const { relations = {}, publishedRelations = false, sortableColumns } = options;
4112
4300
  function tableRaw() {
4113
- return import_drizzle_orm8.sql.raw(quoteIdent3(tableName));
4301
+ return import_drizzle_orm9.sql.raw(quoteIdent4(tableName));
4114
4302
  }
4115
4303
  async function resolveRows(rows, include, cache) {
4116
4304
  if (rows.length === 0) return rows;
@@ -4153,14 +4341,14 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
4153
4341
  const offset = (page - 1) * per_page;
4154
4342
  const conds = buildConditions(published_only, filters, search);
4155
4343
  const where = whereFragment(conds);
4156
- const sortCol = import_drizzle_orm8.sql.raw(quoteIdent3(sort_by));
4157
- const sortDir = import_drizzle_orm8.sql.raw(sort_order === "desc" ? "DESC" : "ASC");
4344
+ const sortCol = import_drizzle_orm9.sql.raw(quoteIdent4(sort_by));
4345
+ const sortDir = import_drizzle_orm9.sql.raw(sort_order === "desc" ? "DESC" : "ASC");
4158
4346
  const countResult = await db.execute(
4159
- import_drizzle_orm8.sql`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
4347
+ import_drizzle_orm9.sql`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
4160
4348
  );
4161
4349
  const total = Number(countResult.rows[0]?.["total"] ?? 0);
4162
4350
  const dataResult = await db.execute(
4163
- import_drizzle_orm8.sql`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
4351
+ import_drizzle_orm9.sql`SELECT * FROM ${tableRaw()}${where} ORDER BY ${sortCol} ${sortDir} LIMIT ${per_page} OFFSET ${offset}`
4164
4352
  );
4165
4353
  const rows = dataResult.rows;
4166
4354
  const cache = /* @__PURE__ */ new Map();
@@ -4181,7 +4369,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
4181
4369
  },
4182
4370
  async findOne(id, include = []) {
4183
4371
  const result = await db.execute(
4184
- import_drizzle_orm8.sql`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
4372
+ import_drizzle_orm9.sql`SELECT * FROM ${tableRaw()} WHERE id = ${id} LIMIT 1`
4185
4373
  );
4186
4374
  if (result.rows.length === 0) return null;
4187
4375
  const rows = result.rows;
@@ -4191,7 +4379,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
4191
4379
  },
4192
4380
  async findBySlug(slug, include = []) {
4193
4381
  const result = await db.execute(
4194
- import_drizzle_orm8.sql`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
4382
+ import_drizzle_orm9.sql`SELECT * FROM ${tableRaw()} WHERE slug = ${slug} LIMIT 1`
4195
4383
  );
4196
4384
  if (result.rows.length === 0) return null;
4197
4385
  const rows = result.rows;
@@ -4207,10 +4395,10 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
4207
4395
  updated_at: /* @__PURE__ */ new Date()
4208
4396
  };
4209
4397
  const entries = Object.entries(record).filter(([, v]) => v !== void 0);
4210
- const colsSql = import_drizzle_orm8.sql.join(entries.map(([k]) => import_drizzle_orm8.sql.raw(quoteIdent3(k))), import_drizzle_orm8.sql`, `);
4211
- const valsSql = import_drizzle_orm8.sql.join(entries.map(([, v]) => import_drizzle_orm8.sql`${v}`), import_drizzle_orm8.sql`, `);
4398
+ const colsSql = import_drizzle_orm9.sql.join(entries.map(([k]) => import_drizzle_orm9.sql.raw(quoteIdent4(k))), import_drizzle_orm9.sql`, `);
4399
+ const valsSql = import_drizzle_orm9.sql.join(entries.map(([, v]) => import_drizzle_orm9.sql`${v}`), import_drizzle_orm9.sql`, `);
4212
4400
  const result = await db.execute(
4213
- import_drizzle_orm8.sql`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
4401
+ import_drizzle_orm9.sql`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
4214
4402
  );
4215
4403
  return result.rows[0];
4216
4404
  },
@@ -4223,25 +4411,25 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
4223
4411
  if (entries.length === 0) {
4224
4412
  return this.findOne(id);
4225
4413
  }
4226
- const setClauses = import_drizzle_orm8.sql.join(
4227
- entries.map(([k, v]) => import_drizzle_orm8.sql`${import_drizzle_orm8.sql.raw(quoteIdent3(k))} = ${v}`),
4228
- import_drizzle_orm8.sql`, `
4414
+ const setClauses = import_drizzle_orm9.sql.join(
4415
+ entries.map(([k, v]) => import_drizzle_orm9.sql`${import_drizzle_orm9.sql.raw(quoteIdent4(k))} = ${v}`),
4416
+ import_drizzle_orm9.sql`, `
4229
4417
  );
4230
4418
  const result = await db.execute(
4231
- import_drizzle_orm8.sql`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
4419
+ import_drizzle_orm9.sql`UPDATE ${tableRaw()} SET ${setClauses} WHERE id = ${id} RETURNING *`
4232
4420
  );
4233
4421
  if (result.rows.length === 0) return null;
4234
4422
  return result.rows[0];
4235
4423
  },
4236
4424
  async delete(id) {
4237
- await db.execute(import_drizzle_orm8.sql`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
4425
+ await db.execute(import_drizzle_orm9.sql`DELETE FROM ${tableRaw()} WHERE id = ${id}`);
4238
4426
  },
4239
4427
  async findAll(opts = {}) {
4240
4428
  const { include = [], published_only } = opts;
4241
4429
  const conds = buildConditions(published_only, void 0, void 0);
4242
4430
  const where = whereFragment(conds);
4243
4431
  const result = await db.execute(
4244
- import_drizzle_orm8.sql`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
4432
+ import_drizzle_orm9.sql`SELECT * FROM ${tableRaw()}${where} ORDER BY created_at ASC`
4245
4433
  );
4246
4434
  const rows = result.rows;
4247
4435
  const cache = /* @__PURE__ */ new Map();
@@ -4256,23 +4444,23 @@ init_relations();
4256
4444
 
4257
4445
  // src/repositories/media.ts
4258
4446
  var import_node_crypto4 = __toESM(require("crypto"));
4259
- var import_drizzle_orm9 = require("drizzle-orm");
4447
+ var import_drizzle_orm10 = require("drizzle-orm");
4260
4448
  function createMediaRepository(db) {
4261
- const TABLE = import_drizzle_orm9.sql.raw('"media"');
4449
+ const TABLE = import_drizzle_orm10.sql.raw('"media"');
4262
4450
  return {
4263
4451
  async findMany(opts = {}) {
4264
4452
  const { page = 1, per_page = 10, type, orphaned } = opts;
4265
4453
  const offset = (page - 1) * per_page;
4266
4454
  const conds = [];
4267
- if (type) conds.push(import_drizzle_orm9.sql`type = ${type}`);
4268
- if (orphaned) conds.push(import_drizzle_orm9.sql`reference_count = 0`);
4269
- const where = conds.length > 0 ? import_drizzle_orm9.sql` WHERE ${import_drizzle_orm9.sql.join(conds, import_drizzle_orm9.sql` AND `)}` : import_drizzle_orm9.sql``;
4270
- const countResult = await db.execute(import_drizzle_orm9.sql`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
4455
+ if (type) conds.push(import_drizzle_orm10.sql`type = ${type}`);
4456
+ if (orphaned) conds.push(import_drizzle_orm10.sql`reference_count = 0`);
4457
+ const where = conds.length > 0 ? import_drizzle_orm10.sql` WHERE ${import_drizzle_orm10.sql.join(conds, import_drizzle_orm10.sql` AND `)}` : import_drizzle_orm10.sql``;
4458
+ const countResult = await db.execute(import_drizzle_orm10.sql`SELECT COUNT(*) AS total FROM ${TABLE}${where}`);
4271
4459
  const total = Number(
4272
4460
  countResult.rows[0]?.["total"] ?? 0
4273
4461
  );
4274
4462
  const dataResult = await db.execute(
4275
- import_drizzle_orm9.sql`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
4463
+ import_drizzle_orm10.sql`SELECT * FROM ${TABLE}${where} ORDER BY created_at DESC LIMIT ${per_page} OFFSET ${offset}`
4276
4464
  );
4277
4465
  const total_pages = Math.ceil(total / per_page);
4278
4466
  return {
@@ -4290,7 +4478,7 @@ function createMediaRepository(db) {
4290
4478
  },
4291
4479
  async findOne(id) {
4292
4480
  const result = await db.execute(
4293
- import_drizzle_orm9.sql`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
4481
+ import_drizzle_orm10.sql`SELECT * FROM ${TABLE} WHERE id = ${id} LIMIT 1`
4294
4482
  );
4295
4483
  if (result.rows.length === 0) return null;
4296
4484
  return result.rows[0];
@@ -4304,10 +4492,10 @@ function createMediaRepository(db) {
4304
4492
  updated_at: /* @__PURE__ */ new Date()
4305
4493
  };
4306
4494
  const entries = Object.entries(record).filter(([, v]) => v !== void 0);
4307
- const colsSql = import_drizzle_orm9.sql.join(entries.map(([k]) => import_drizzle_orm9.sql.raw(`"${k}"`)), import_drizzle_orm9.sql`, `);
4308
- const valsSql = import_drizzle_orm9.sql.join(entries.map(([, v]) => import_drizzle_orm9.sql`${v}`), import_drizzle_orm9.sql`, `);
4495
+ const colsSql = import_drizzle_orm10.sql.join(entries.map(([k]) => import_drizzle_orm10.sql.raw(`"${k}"`)), import_drizzle_orm10.sql`, `);
4496
+ const valsSql = import_drizzle_orm10.sql.join(entries.map(([, v]) => import_drizzle_orm10.sql`${v}`), import_drizzle_orm10.sql`, `);
4309
4497
  const result = await db.execute(
4310
- import_drizzle_orm9.sql`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
4498
+ import_drizzle_orm10.sql`INSERT INTO ${TABLE} (${colsSql}) VALUES (${valsSql}) RETURNING *`
4311
4499
  );
4312
4500
  return result.rows[0];
4313
4501
  },
@@ -4317,31 +4505,31 @@ function createMediaRepository(db) {
4317
4505
  ([k, v]) => k !== "id" && v !== void 0
4318
4506
  );
4319
4507
  if (entries.length === 0) return this.findOne(id);
4320
- const setClauses = import_drizzle_orm9.sql.join(
4321
- entries.map(([k, v]) => import_drizzle_orm9.sql`${import_drizzle_orm9.sql.raw(`"${k}"`)} = ${v}`),
4322
- import_drizzle_orm9.sql`, `
4508
+ const setClauses = import_drizzle_orm10.sql.join(
4509
+ entries.map(([k, v]) => import_drizzle_orm10.sql`${import_drizzle_orm10.sql.raw(`"${k}"`)} = ${v}`),
4510
+ import_drizzle_orm10.sql`, `
4323
4511
  );
4324
4512
  const result = await db.execute(
4325
- import_drizzle_orm9.sql`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
4513
+ import_drizzle_orm10.sql`UPDATE ${TABLE} SET ${setClauses} WHERE id = ${id} RETURNING *`
4326
4514
  );
4327
4515
  if (result.rows.length === 0) return null;
4328
4516
  return result.rows[0];
4329
4517
  },
4330
4518
  async delete(id) {
4331
- await db.execute(import_drizzle_orm9.sql`DELETE FROM ${TABLE} WHERE id = ${id}`);
4519
+ await db.execute(import_drizzle_orm10.sql`DELETE FROM ${TABLE} WHERE id = ${id}`);
4332
4520
  },
4333
4521
  async incrementReferenceCount(ids) {
4334
4522
  if (ids.length === 0) return;
4335
- const inList = import_drizzle_orm9.sql.join(ids.map((id) => import_drizzle_orm9.sql`${id}`), import_drizzle_orm9.sql`, `);
4523
+ const inList = import_drizzle_orm10.sql.join(ids.map((id) => import_drizzle_orm10.sql`${id}`), import_drizzle_orm10.sql`, `);
4336
4524
  await db.execute(
4337
- import_drizzle_orm9.sql`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
4525
+ import_drizzle_orm10.sql`UPDATE ${TABLE} SET reference_count = reference_count + 1, updated_at = NOW() WHERE id IN (${inList})`
4338
4526
  );
4339
4527
  },
4340
4528
  async decrementReferenceCount(ids) {
4341
4529
  if (ids.length === 0) return;
4342
- const inList = import_drizzle_orm9.sql.join(ids.map((id) => import_drizzle_orm9.sql`${id}`), import_drizzle_orm9.sql`, `);
4530
+ const inList = import_drizzle_orm10.sql.join(ids.map((id) => import_drizzle_orm10.sql`${id}`), import_drizzle_orm10.sql`, `);
4343
4531
  await db.execute(
4344
- import_drizzle_orm9.sql`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
4532
+ import_drizzle_orm10.sql`UPDATE ${TABLE} SET reference_count = GREATEST(reference_count - 1, 0), updated_at = NOW() WHERE id IN (${inList})`
4345
4533
  );
4346
4534
  }
4347
4535
  };
@@ -4478,7 +4666,7 @@ function createCmsApp(options) {
4478
4666
  const requirePermission = createPermissionMiddleware(rolesRegistry);
4479
4667
  const getUserRole = async (userId) => {
4480
4668
  const result = await db.execute(
4481
- import_drizzle_orm10.sql`SELECT r.name AS role
4669
+ import_drizzle_orm11.sql`SELECT r.name AS role
4482
4670
  FROM users u
4483
4671
  JOIN roles r ON r.id = u.role_id
4484
4672
  WHERE u.id = ${userId}
@@ -4606,7 +4794,7 @@ function createCmsApp(options) {
4606
4794
  app.use(path, attachDeprecationHeaders);
4607
4795
  }
4608
4796
  }
4609
- registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
4797
+ registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver, projectors);
4610
4798
  }
4611
4799
  if (options.versions !== void 0) {
4612
4800
  app.all(`${prefix}/:version/*`, async (c, next) => {
@@ -4672,7 +4860,8 @@ function createCmsApp(options) {
4672
4860
  endpoint: mount.path,
4673
4861
  currentVersion: model.current,
4674
4862
  ...projection !== void 0 && { projection },
4675
- ...currentProjection !== void 0 && { currentProjection }
4863
+ ...currentProjection !== void 0 && { currentProjection },
4864
+ currentFieldKeyMaps: fieldKeyMaps
4676
4865
  }
4677
4866
  );
4678
4867
  } catch (err) {