@bobbykim/manguito-cms-api 0.9.2 → 0.9.3

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
@@ -143,8 +143,139 @@ var init_field_keys = __esm({
143
143
  }
144
144
  });
145
145
 
146
+ // src/relation-input.ts
147
+ function isUuid(v) {
148
+ return typeof v === "string" && UUID_RE.test(v);
149
+ }
150
+ function isPlainObject(v) {
151
+ return typeof v === "object" && v !== null && !Array.isArray(v);
152
+ }
153
+ function checkRelationInput(fields, body, registry, path = "") {
154
+ const errors = [];
155
+ for (const field of fields) {
156
+ if (!Object.hasOwn(body, field.name)) continue;
157
+ const cardinality = (0, import_manguito_cms_core.relationCardinality)(field);
158
+ if (cardinality === null) continue;
159
+ const value = body[field.name];
160
+ const name = `${path}${field.name}`;
161
+ if (field.field_type === "reference") {
162
+ const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
163
+ if (!ok) {
164
+ errors.push({
165
+ field: name,
166
+ message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
167
+ });
168
+ }
169
+ continue;
170
+ }
171
+ const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
172
+ if (items === void 0) {
173
+ errors.push({
174
+ field: name,
175
+ message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
176
+ });
177
+ continue;
178
+ }
179
+ const pType = paragraphTypeOf(field, registry);
180
+ if (!pType) continue;
181
+ items.forEach((item, i) => {
182
+ const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
183
+ errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
184
+ });
185
+ }
186
+ return errors;
187
+ }
188
+ function paragraphTypeOf(field, registry) {
189
+ const comp = field.ui_component;
190
+ return comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
191
+ }
192
+ function paragraphItemsIn(fields, body, registry, path) {
193
+ const out = [];
194
+ for (const field of fields) {
195
+ if (field.field_type !== "paragraph" || !Object.hasOwn(body, field.name)) continue;
196
+ const pType = paragraphTypeOf(field, registry);
197
+ if (!pType) continue;
198
+ const value = body[field.name];
199
+ const name = `${path}${field.name}`;
200
+ if ((0, import_manguito_cms_core.relationCardinality)(field) === "one") {
201
+ if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
202
+ } else if (Array.isArray(value)) {
203
+ value.forEach((item, i) => {
204
+ if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
205
+ });
206
+ }
207
+ }
208
+ return out;
209
+ }
210
+ function isMissing(value) {
211
+ if (value === null || value === void 0) return true;
212
+ if (typeof value === "string" && value.trim() === "") return true;
213
+ if (Array.isArray(value) && value.length === 0) return true;
214
+ return false;
215
+ }
216
+ function checkRequiredInParagraphItems(fields, body, registry, path = "") {
217
+ const errors = [];
218
+ for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
219
+ for (const f of itemFields) {
220
+ if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
221
+ if (isMissing(item[f.name])) {
222
+ errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
223
+ }
224
+ }
225
+ errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
226
+ }
227
+ return errors;
228
+ }
229
+ function quoteIdent(name) {
230
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
231
+ return `"${name}"`;
232
+ }
233
+ function collectReferences(fields, body, registry, path, out) {
234
+ for (const f of fields) {
235
+ if (!Object.hasOwn(body, f.name) || !f.db_column) continue;
236
+ const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
237
+ if (!table) continue;
238
+ const value = body[f.name];
239
+ const name = `${path}${f.name}`;
240
+ if (typeof value === "string") {
241
+ out.push({ table, id: value, field: name });
242
+ } else if (Array.isArray(value)) {
243
+ value.forEach((v, i) => {
244
+ if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
245
+ });
246
+ }
247
+ }
248
+ for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
249
+ collectReferences(itemFields, item, registry, prefix, out);
250
+ }
251
+ }
252
+ async function findMissingReferences(db, fields, body, registry) {
253
+ const uses = [];
254
+ collectReferences(fields, body, registry, "", uses);
255
+ if (uses.length === 0) return [];
256
+ const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
257
+ const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
258
+ if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
259
+ const result = await db.execute(
260
+ import_drizzle_orm2.sql`SELECT id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(table))} WHERE id IN (${import_drizzle_orm2.sql.join(ids.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `)})`
261
+ );
262
+ return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
263
+ });
264
+ const found = new Map(await Promise.all(lookups));
265
+ return uses.filter((u) => !found.get(u.table).has(u.id.toLowerCase())).map((u) => ({ field: u.field, message: `${u.field} refers to an item that does not exist.` }));
266
+ }
267
+ var import_drizzle_orm2, import_manguito_cms_core, UUID_RE;
268
+ var init_relation_input = __esm({
269
+ "src/relation-input.ts"() {
270
+ "use strict";
271
+ import_drizzle_orm2 = require("drizzle-orm");
272
+ import_manguito_cms_core = require("@bobbykim/manguito-cms-core");
273
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
274
+ }
275
+ });
276
+
146
277
  // src/relations.ts
147
- function quoteIdent3(name) {
278
+ function quoteIdent2(name) {
148
279
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
149
280
  return `"${name}"`;
150
281
  }
@@ -156,10 +287,10 @@ function paragraphMediaFieldNames(pType) {
156
287
  async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
157
288
  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);
158
289
  if (mediaColumns.length === 0) return [];
159
- const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(pType.db.table_name));
160
- const cols = import_drizzle_orm4.sql.join(mediaColumns.map((c) => import_drizzle_orm4.sql.raw(quoteIdent3(c))), import_drizzle_orm4.sql`, `);
290
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
291
+ const cols = import_drizzle_orm3.sql.join(mediaColumns.map((c) => import_drizzle_orm3.sql.raw(quoteIdent2(c))), import_drizzle_orm3.sql`, `);
161
292
  const result = await db.execute(
162
- import_drizzle_orm4.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
293
+ import_drizzle_orm3.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
163
294
  );
164
295
  const ids = [];
165
296
  for (const row of result.rows) {
@@ -175,20 +306,37 @@ function nestedParagraphFields(pType, registry) {
175
306
  const out = [];
176
307
  for (const f of pType.fields) {
177
308
  if (f.db_column !== null) continue;
178
- const comp = f.ui_component;
179
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
180
- const nType = registry.paragraph_types[comp.ref];
309
+ const nType = paragraphTypeOf(f, registry);
181
310
  if (nType) out.push({ fieldName: f.name, nType, field: f });
182
311
  }
183
312
  return out;
184
313
  }
185
314
  async function fetchParagraphRowIds(db, pType, itemId, fieldName) {
186
- const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(pType.db.table_name));
315
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
187
316
  const result = await db.execute(
188
- import_drizzle_orm4.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
317
+ import_drizzle_orm3.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
189
318
  );
190
319
  return result.rows.map((r) => r["id"]).filter((id) => typeof id === "string");
191
320
  }
321
+ async function ownParagraphRowIds(db, registry, fields, itemId) {
322
+ const ids = [];
323
+ for (const f of fields) {
324
+ const pType = paragraphTypeOf(f, registry);
325
+ if (!pType) continue;
326
+ const rowIds = await fetchParagraphRowIds(db, pType, itemId, f.name);
327
+ ids.push(...rowIds);
328
+ if (rowIds.length === 0) continue;
329
+ for (const n of nestedParagraphFields(pType, registry)) {
330
+ const result = await db.execute(
331
+ import_drizzle_orm3.sql`SELECT id FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(n.nType.db.table_name))} WHERE parent_id = ANY(${import_drizzle_orm3.sql.param(rowIds)}::uuid[]) AND parent_field = ${n.fieldName}`
332
+ );
333
+ for (const r of result.rows) {
334
+ if (typeof r["id"] === "string") ids.push(r["id"]);
335
+ }
336
+ }
337
+ }
338
+ return ids;
339
+ }
192
340
  async function persistParagraphField(db, itemId, parentType, fieldName, pType, items, registry) {
193
341
  const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
194
342
  const nested = nestedParagraphFields(pType, registry);
@@ -201,8 +349,8 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
201
349
  }
202
350
  }
203
351
  }
204
- const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(pType.db.table_name));
205
- await db.execute(import_drizzle_orm4.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
352
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
353
+ await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
206
354
  const mediaFieldNames = paragraphMediaFieldNames(pType);
207
355
  const added = [];
208
356
  for (let i = 0; i < items.length; i++) {
@@ -221,9 +369,9 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
221
369
  if (!pf.db_column || pf.db_column.junction) continue;
222
370
  data[pf.db_column.column_name] = pItem[pf.name] ?? null;
223
371
  }
224
- const cols = import_drizzle_orm4.sql.join(Object.keys(data).map((k) => import_drizzle_orm4.sql.raw(quoteIdent3(k))), import_drizzle_orm4.sql`, `);
225
- const vals = import_drizzle_orm4.sql.join(Object.values(data).map((v) => import_drizzle_orm4.sql`${v}`), import_drizzle_orm4.sql`, `);
226
- await db.execute(import_drizzle_orm4.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
372
+ const cols = import_drizzle_orm3.sql.join(Object.keys(data).map((k) => import_drizzle_orm3.sql.raw(quoteIdent2(k))), import_drizzle_orm3.sql`, `);
373
+ const vals = import_drizzle_orm3.sql.join(Object.values(data).map((v) => import_drizzle_orm3.sql`${v}`), import_drizzle_orm3.sql`, `);
374
+ await db.execute(import_drizzle_orm3.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
227
375
  for (const name of mediaFieldNames) {
228
376
  const val = pItem[name];
229
377
  if (typeof val === "string" && val !== "") added.push(val);
@@ -257,14 +405,14 @@ async function deleteParagraphField(db, itemId, fieldName, pType, registry) {
257
405
  }
258
406
  }
259
407
  }
260
- const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(pType.db.table_name));
261
- await db.execute(import_drizzle_orm4.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
408
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name));
409
+ await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
262
410
  return { added: [], removed };
263
411
  }
264
412
  async function persistJunctionField(db, itemId, junction, relatedIds) {
265
- const tbl = import_drizzle_orm4.sql.raw(quoteIdent3(junction.table_name));
266
- const left = import_drizzle_orm4.sql.raw(quoteIdent3(junction.left_column));
267
- const right = import_drizzle_orm4.sql.raw(quoteIdent3(junction.right_column));
413
+ const tbl = import_drizzle_orm3.sql.raw(quoteIdent2(junction.table_name));
414
+ const left = import_drizzle_orm3.sql.raw(quoteIdent2(junction.left_column));
415
+ const right = import_drizzle_orm3.sql.raw(quoteIdent2(junction.right_column));
268
416
  const seen = /* @__PURE__ */ new Set();
269
417
  const ids = relatedIds.filter((id) => {
270
418
  const key = id.toLowerCase();
@@ -272,27 +420,36 @@ async function persistJunctionField(db, itemId, junction, relatedIds) {
272
420
  seen.add(key);
273
421
  return true;
274
422
  });
275
- await db.execute(import_drizzle_orm4.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
423
+ await db.execute(import_drizzle_orm3.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
276
424
  if (ids.length === 0) return;
277
- const rows = import_drizzle_orm4.sql.join(
425
+ const rows = import_drizzle_orm3.sql.join(
278
426
  ids.map(
279
- (rid, position) => junction.order_column ? import_drizzle_orm4.sql`(${itemId}, ${rid}, ${position})` : import_drizzle_orm4.sql`(${itemId}, ${rid})`
427
+ (rid, position) => junction.order_column ? import_drizzle_orm3.sql`(${itemId}, ${rid}, ${position})` : import_drizzle_orm3.sql`(${itemId}, ${rid})`
280
428
  ),
281
- import_drizzle_orm4.sql`, `
429
+ import_drizzle_orm3.sql`, `
282
430
  );
283
431
  await db.execute(
284
- junction.order_column ? import_drizzle_orm4.sql`INSERT INTO ${tbl} (${left}, ${right}, "order") VALUES ${rows}` : import_drizzle_orm4.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES ${rows}`
432
+ junction.order_column ? import_drizzle_orm3.sql`INSERT INTO ${tbl} (${left}, ${right}, "order") VALUES ${rows}` : import_drizzle_orm3.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES ${rows}`
285
433
  );
286
434
  }
287
435
  function junctionOrderBy(junction) {
288
- const right = import_drizzle_orm4.sql.raw(quoteIdent3(junction.right_column));
289
- return junction.order_column ? import_drizzle_orm4.sql` ORDER BY "order" ASC, ${right} ASC` : import_drizzle_orm4.sql` ORDER BY ${right} ASC`;
436
+ const right = import_drizzle_orm3.sql.raw(quoteIdent2(junction.right_column));
437
+ return junction.order_column ? import_drizzle_orm3.sql` ORDER BY "order" ASC, ${right} ASC` : import_drizzle_orm3.sql` ORDER BY ${right} ASC`;
438
+ }
439
+ function firstOfEach(items, key) {
440
+ const seen = /* @__PURE__ */ new Set();
441
+ return items.filter((item) => {
442
+ const k = key(item);
443
+ if (seen.has(k)) return false;
444
+ seen.add(k);
445
+ return true;
446
+ });
290
447
  }
291
448
  function oneOrMany(cardinality, list) {
292
449
  return cardinality === "one" ? list[0] ?? null : list;
293
450
  }
294
451
  function paragraphItems(field, value) {
295
- if ((0, import_manguito_cms_core3.relationCardinality)(field) === "one") {
452
+ if ((0, import_manguito_cms_core2.relationCardinality)(field) === "one") {
296
453
  return typeof value === "object" && value !== null && !Array.isArray(value) ? [value] : [];
297
454
  }
298
455
  return Array.isArray(value) ? value : [];
@@ -310,7 +467,7 @@ function buildRelationsMap(fields, registry) {
310
467
  relations[field.name] = {
311
468
  type: "paragraph",
312
469
  table: pType.db.table_name,
313
- cardinality: (0, import_manguito_cms_core3.relationCardinality)(field) ?? "many"
470
+ cardinality: (0, import_manguito_cms_core2.relationCardinality)(field) ?? "many"
314
471
  };
315
472
  } else if (field.field_type === "reference") {
316
473
  if (!field.db_column) continue;
@@ -353,9 +510,9 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
353
510
  const batch = [...new Set(rows)];
354
511
  if (rel.type === "paragraph") {
355
512
  const parentIds = batch.map((r) => r["id"]);
356
- const inList = import_drizzle_orm4.sql.join(parentIds.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
513
+ const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
357
514
  const result = await db.execute(
358
- import_drizzle_orm4.sql`SELECT * FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
515
+ 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, id ASC`
359
516
  );
360
517
  const byParent = groupBy(result.rows, "parent_id");
361
518
  for (const row of batch) {
@@ -372,10 +529,10 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
372
529
  const unique = [...new Set(fkValues)];
373
530
  const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
374
531
  if (uncached.length > 0) {
375
- const inList = import_drizzle_orm4.sql.join(uncached.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
376
- const publishedCond = publishedOnly ? import_drizzle_orm4.sql` AND published = true` : import_drizzle_orm4.sql``;
532
+ const inList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
533
+ const publishedCond = publishedOnly ? import_drizzle_orm3.sql` AND published = true` : import_drizzle_orm3.sql``;
377
534
  const result = await db.execute(
378
- import_drizzle_orm4.sql`SELECT * FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.table))} WHERE id IN (${inList})${publishedCond}`
535
+ import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE id IN (${inList})${publishedCond}`
379
536
  );
380
537
  for (const item of result.rows) {
381
538
  cache.set(`${rel.table}:${item["id"]}`, item);
@@ -387,18 +544,18 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
387
544
  }
388
545
  } else if (rel.type === "junction") {
389
546
  const parentIds = batch.map((r) => r["id"]);
390
- const inList = import_drizzle_orm4.sql.join(parentIds.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
547
+ const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
391
548
  const junctionResult = await db.execute(
392
- import_drizzle_orm4.sql`SELECT * FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.junction_table))} WHERE ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
549
+ 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})${junctionOrderBy(rel)}`
393
550
  );
394
551
  const jRows = junctionResult.rows;
395
552
  const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
396
553
  const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
397
554
  if (uncached.length > 0) {
398
- const rightInList = import_drizzle_orm4.sql.join(uncached.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
399
- const publishedCond = publishedOnly ? import_drizzle_orm4.sql` AND published = true` : import_drizzle_orm4.sql``;
555
+ const rightInList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
556
+ const publishedCond = publishedOnly ? import_drizzle_orm3.sql` AND published = true` : import_drizzle_orm3.sql``;
400
557
  const entitiesResult = await db.execute(
401
- import_drizzle_orm4.sql`SELECT * FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
558
+ import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
402
559
  );
403
560
  for (const item of entitiesResult.rows) {
404
561
  cache.set(`${rel.table}:${item["id"]}`, item);
@@ -406,7 +563,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
406
563
  }
407
564
  const byLeft = groupBy(jRows, rel.left_column);
408
565
  for (const row of batch) {
409
- const jEntries = byLeft[row["id"]] ?? [];
566
+ const jEntries = firstOfEach(byLeft[row["id"]] ?? [], (jr) => jr[rel.right_column]);
410
567
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
411
568
  }
412
569
  } else if (rel.type === "media") {
@@ -420,8 +577,8 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
420
577
  const unique = [...new Set(fkValues)];
421
578
  const uncached = unique.filter((id) => !cache.has(`media:${id}`));
422
579
  if (uncached.length > 0) {
423
- const inList = import_drizzle_orm4.sql.join(uncached.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
424
- const result = await db.execute(import_drizzle_orm4.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
580
+ const inList = import_drizzle_orm3.sql.join(uncached.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
581
+ const result = await db.execute(import_drizzle_orm3.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
425
582
  for (const item of result.rows) {
426
583
  cache.set(`media:${item["id"]}`, item);
427
584
  }
@@ -436,9 +593,9 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
436
593
  if (rows.length === 0) return;
437
594
  if (rel.type === "paragraph") {
438
595
  const parentIds = rows.map((r) => r["id"]);
439
- const inList = import_drizzle_orm4.sql.join(parentIds.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
596
+ const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
440
597
  const result = await db.execute(
441
- import_drizzle_orm4.sql`SELECT id, parent_id FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
598
+ 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, id ASC`
442
599
  );
443
600
  const byParent = groupBy(result.rows, "parent_id");
444
601
  for (const row of rows) {
@@ -446,23 +603,24 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
446
603
  }
447
604
  } else if (rel.type === "junction") {
448
605
  const parentIds = rows.map((r) => r["id"]);
449
- const inList = import_drizzle_orm4.sql.join(parentIds.map((id) => import_drizzle_orm4.sql`${id}`), import_drizzle_orm4.sql`, `);
606
+ const inList = import_drizzle_orm3.sql.join(parentIds.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `);
450
607
  const result = await db.execute(
451
- import_drizzle_orm4.sql`SELECT ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.left_column))} AS left_id, ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.right_column))} AS right_id FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.junction_table))} WHERE ${import_drizzle_orm4.sql.raw(quoteIdent3(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
608
+ 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})${junctionOrderBy(rel)}`
452
609
  );
453
610
  const byLeft = groupBy(result.rows, "left_id");
454
611
  for (const row of rows) {
455
- row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
612
+ row[fieldName] = firstOfEach(byLeft[row["id"]] ?? [], (r) => r["right_id"]).map((r) => r["right_id"]);
456
613
  }
457
614
  }
458
615
  }
459
- var import_node_crypto, import_drizzle_orm4, import_manguito_cms_core3;
616
+ var import_node_crypto, import_drizzle_orm3, import_manguito_cms_core2;
460
617
  var init_relations = __esm({
461
618
  "src/relations.ts"() {
462
619
  "use strict";
463
620
  import_node_crypto = __toESM(require("crypto"));
464
- import_drizzle_orm4 = require("drizzle-orm");
465
- import_manguito_cms_core3 = require("@bobbykim/manguito-cms-core");
621
+ import_drizzle_orm3 = require("drizzle-orm");
622
+ import_manguito_cms_core2 = require("@bobbykim/manguito-cms-core");
623
+ init_relation_input();
466
624
  }
467
625
  });
468
626
 
@@ -1374,7 +1532,7 @@ var ERROR_STATUS_MAP = {
1374
1532
  var errorHandler = (err, c) => {
1375
1533
  console.error(err.stack ?? err.message);
1376
1534
  const raw = err.code;
1377
- const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && raw in ERROR_STATUS_MAP;
1535
+ const deliberate = raw !== void 0 && raw !== "INTERNAL_ERROR" && Object.hasOwn(ERROR_STATUS_MAP, raw);
1378
1536
  if (!deliberate) {
1379
1537
  return c.json({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
1380
1538
  }
@@ -1969,16 +2127,16 @@ function parseFilters(url, validFields, columnFor) {
1969
2127
  }
1970
2128
 
1971
2129
  // src/routes/content.ts
2130
+ function pickProgrammatic(resolved, names) {
2131
+ const out = {};
2132
+ for (const name of names) if (Object.hasOwn(resolved, name)) out[name] = resolved[name];
2133
+ return out;
2134
+ }
1972
2135
  function isPublished(item) {
1973
2136
  return item["published"] === true;
1974
2137
  }
1975
2138
  function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
1976
2139
  const forResolver = resolverProjectors ?? projectors;
1977
- function pickProgrammatic(resolved, names) {
1978
- const out = {};
1979
- for (const name of names) if (name in resolved) out[name] = resolved[name];
1980
- return out;
1981
- }
1982
2140
  async function respondItem(row, typeName, programmatic) {
1983
2141
  const data = projectRow(row, typeName, projectors);
1984
2142
  if (!resolver?.hasSchema(typeName)) return data;
@@ -2290,34 +2448,52 @@ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
2290
2448
  var import_drizzle_orm5 = require("drizzle-orm");
2291
2449
 
2292
2450
  // src/references-in-use.ts
2293
- var import_drizzle_orm2 = require("drizzle-orm");
2294
- var import_manguito_cms_core = require("@bobbykim/manguito-cms-core");
2295
- function quoteIdent(name) {
2451
+ var import_drizzle_orm4 = require("drizzle-orm");
2452
+ var import_manguito_cms_core3 = require("@bobbykim/manguito-cms-core");
2453
+ init_relations();
2454
+ function quoteIdent3(name) {
2296
2455
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
2297
2456
  return `"${name}"`;
2298
2457
  }
2299
2458
  function isRequiredSingleReferenceTo(f, targetType) {
2300
- 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;
2459
+ return f.field_type === "reference" && f.required && (0, import_manguito_cms_core3.relationCardinality)(f) === "one" && f.db_column?.foreign_key !== void 0 && f.ui_component.component === "typeahead-select" && f.ui_component.ref === targetType;
2460
+ }
2461
+ function requiredReferencesTo(owners, targetType) {
2462
+ return owners.flatMap(
2463
+ (owner) => owner.fields.filter((f) => isRequiredSingleReferenceTo(f, targetType)).map((f) => ({ owner, f }))
2464
+ );
2301
2465
  }
2302
2466
  async function findRequiredReferrers(db, registry, targetType, id) {
2303
- const owners = [
2304
- ...Object.values(registry.content_types),
2305
- ...Object.values(registry.taxonomy_types),
2306
- ...Object.values(registry.paragraph_types)
2307
- ];
2308
- const out = [];
2309
- for (const owner of owners) {
2310
- for (const f of owner.fields) {
2311
- if (!isRequiredSingleReferenceTo(f, targetType)) continue;
2312
- const notSelf = owner.name === targetType ? import_drizzle_orm2.sql` AND id <> ${id}` : import_drizzle_orm2.sql``;
2313
- const r = await db.execute(
2314
- 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}`
2315
- );
2316
- const n = r.rows[0].n;
2317
- if (n > 0) out.push({ type_label: owner.label, field_label: f.label, count: n });
2318
- }
2467
+ const targetFields = registry.content_types[targetType]?.fields ?? registry.taxonomy_types[targetType]?.fields ?? [];
2468
+ const itemOwners = [...Object.values(registry.content_types), ...Object.values(registry.taxonomy_types)];
2469
+ const itemFields = requiredReferencesTo(itemOwners, targetType);
2470
+ const paragraphFields = requiredReferencesTo(Object.values(registry.paragraph_types), targetType);
2471
+ const ownIds = paragraphFields.length > 0 ? await ownParagraphRowIds(db, registry, targetFields, id) : [];
2472
+ const [itemCounts, paragraphCounts] = await Promise.all([
2473
+ Promise.all(
2474
+ itemFields.map(async ({ owner, f }) => {
2475
+ const notSelf = owner.name === targetType ? import_drizzle_orm4.sql` AND id <> ${id}` : import_drizzle_orm4.sql``;
2476
+ const r = await db.execute(
2477
+ import_drizzle_orm4.sql`SELECT count(*)::int AS n FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(owner.db.table_name))} WHERE ${import_drizzle_orm4.sql.raw(quoteIdent3(f.db_column.column_name))} = ${id}${notSelf}`
2478
+ );
2479
+ return { owner, f, n: r.rows[0].n, own: 0 };
2480
+ })
2481
+ ),
2482
+ Promise.all(
2483
+ paragraphFields.map(async ({ owner, f }) => {
2484
+ const r = await db.execute(
2485
+ import_drizzle_orm4.sql`SELECT count(*)::int AS n, count(*) FILTER (WHERE id = ANY(${import_drizzle_orm4.sql.param(ownIds)}::uuid[]))::int AS own FROM ${import_drizzle_orm4.sql.raw(quoteIdent3(owner.db.table_name))} WHERE ${import_drizzle_orm4.sql.raw(quoteIdent3(f.db_column.column_name))} = ${id}`
2486
+ );
2487
+ const { n, own } = r.rows[0];
2488
+ return { owner, f, n, own };
2489
+ })
2490
+ )
2491
+ ]);
2492
+ const others = [];
2493
+ for (const { owner, f, n, own } of [...itemCounts, ...paragraphCounts]) {
2494
+ if (n - own > 0) others.push({ type_label: owner.label, field_label: f.label, count: n - own });
2319
2495
  }
2320
- return out;
2496
+ return { others, fromOwnParagraphs: paragraphCounts.some((c) => c.own > 0) };
2321
2497
  }
2322
2498
  function inUseMessage(referrers) {
2323
2499
  const total = referrers.reduce((sum, r) => sum + r.count, 0);
@@ -2347,7 +2523,7 @@ function topLevelMediaDelta(mediaFields, before, after) {
2347
2523
  for (const f of mediaFields) {
2348
2524
  const key = f.db_column?.column_name;
2349
2525
  if (key === void 0 || key === "") continue;
2350
- if (after !== null && !(key in after)) continue;
2526
+ if (after !== null && !Object.hasOwn(after, key)) continue;
2351
2527
  const oldId = before ? extractMediaId(before[key]) : null;
2352
2528
  const newId = after ? extractMediaId(after[key]) : null;
2353
2529
  if (newId === oldId) continue;
@@ -2371,132 +2547,8 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
2371
2547
  if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
2372
2548
  }
2373
2549
 
2374
- // src/relation-input.ts
2375
- var import_drizzle_orm3 = require("drizzle-orm");
2376
- var import_manguito_cms_core2 = require("@bobbykim/manguito-cms-core");
2377
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2378
- function isUuid(v) {
2379
- return typeof v === "string" && UUID_RE.test(v);
2380
- }
2381
- function isPlainObject(v) {
2382
- return typeof v === "object" && v !== null && !Array.isArray(v);
2383
- }
2384
- function checkRelationInput(fields, body, registry, path = "") {
2385
- const errors = [];
2386
- for (const field of fields) {
2387
- if (!(field.name in body)) continue;
2388
- const cardinality = (0, import_manguito_cms_core2.relationCardinality)(field);
2389
- if (cardinality === null) continue;
2390
- const value = body[field.name];
2391
- const name = `${path}${field.name}`;
2392
- if (field.field_type === "reference") {
2393
- const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
2394
- if (!ok) {
2395
- errors.push({
2396
- field: name,
2397
- message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
2398
- });
2399
- }
2400
- continue;
2401
- }
2402
- const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
2403
- if (items === void 0) {
2404
- errors.push({
2405
- field: name,
2406
- message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
2407
- });
2408
- continue;
2409
- }
2410
- const pType = paragraphTypeOf(field, registry);
2411
- if (!pType) continue;
2412
- items.forEach((item, i) => {
2413
- const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
2414
- errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
2415
- });
2416
- }
2417
- return errors;
2418
- }
2419
- function paragraphTypeOf(field, registry) {
2420
- const comp = field.ui_component;
2421
- return comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
2422
- }
2423
- function paragraphItemsIn(fields, body, registry, path) {
2424
- const out = [];
2425
- for (const field of fields) {
2426
- if (field.field_type !== "paragraph" || !(field.name in body)) continue;
2427
- const pType = paragraphTypeOf(field, registry);
2428
- if (!pType) continue;
2429
- const value = body[field.name];
2430
- const name = `${path}${field.name}`;
2431
- if ((0, import_manguito_cms_core2.relationCardinality)(field) === "one") {
2432
- if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
2433
- } else if (Array.isArray(value)) {
2434
- value.forEach((item, i) => {
2435
- if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
2436
- });
2437
- }
2438
- }
2439
- return out;
2440
- }
2441
- function isMissing(value) {
2442
- if (value === null || value === void 0) return true;
2443
- if (typeof value === "string" && value.trim() === "") return true;
2444
- if (Array.isArray(value) && value.length === 0) return true;
2445
- return false;
2446
- }
2447
- function checkRequiredInParagraphItems(fields, body, registry, path = "") {
2448
- const errors = [];
2449
- for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
2450
- for (const f of itemFields) {
2451
- if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
2452
- if (isMissing(item[f.name])) {
2453
- errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
2454
- }
2455
- }
2456
- errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
2457
- }
2458
- return errors;
2459
- }
2460
- function quoteIdent2(name) {
2461
- if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
2462
- return `"${name}"`;
2463
- }
2464
- function collectReferences(fields, body, registry, path, out) {
2465
- for (const f of fields) {
2466
- if (!(f.name in body) || !f.db_column) continue;
2467
- const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
2468
- if (!table) continue;
2469
- const value = body[f.name];
2470
- const name = `${path}${f.name}`;
2471
- if (typeof value === "string") {
2472
- out.push({ table, id: value, field: name });
2473
- } else if (Array.isArray(value)) {
2474
- value.forEach((v, i) => {
2475
- if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
2476
- });
2477
- }
2478
- }
2479
- for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path)) {
2480
- collectReferences(itemFields, item, registry, prefix, out);
2481
- }
2482
- }
2483
- async function findMissingReferences(db, fields, body, registry) {
2484
- const uses = [];
2485
- collectReferences(fields, body, registry, "", uses);
2486
- if (uses.length === 0) return [];
2487
- const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
2488
- const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
2489
- if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
2490
- const result = await db.execute(
2491
- import_drizzle_orm3.sql`SELECT id FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(table))} WHERE id IN (${import_drizzle_orm3.sql.join(ids.map((id) => import_drizzle_orm3.sql`${id}`), import_drizzle_orm3.sql`, `)})`
2492
- );
2493
- return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
2494
- });
2495
- const found = new Map(await Promise.all(lookups));
2496
- return uses.filter((u) => !found.get(u.table).has(u.id.toLowerCase())).map((u) => ({ field: u.field, message: `${u.field} refers to an item that does not exist.` }));
2497
- }
2498
-
2499
2550
  // src/field-values.ts
2551
+ init_relation_input();
2500
2552
  var VARCHAR_DEFAULT_LIMIT = 255;
2501
2553
  var INTEGER_MIN = -2147483648;
2502
2554
  var INTEGER_MAX = 2147483647;
@@ -2507,7 +2559,7 @@ var EMPTY_MEANS_NULL = /* @__PURE__ */ new Set(["date", "integer", "float", "enu
2507
2559
  function emptyValuesToNull(fields, body, registry) {
2508
2560
  const out = { ...body };
2509
2561
  for (const f of fields) {
2510
- if (!(f.name in out)) continue;
2562
+ if (!Object.hasOwn(out, f.name)) continue;
2511
2563
  const value = out[f.name];
2512
2564
  if (value === "" && EMPTY_MEANS_NULL.has(f.field_type)) {
2513
2565
  out[f.name] = null;
@@ -2612,7 +2664,7 @@ function rangeError(name, value, min, max) {
2612
2664
  function checkFieldValues(fields, body, registry, path = "") {
2613
2665
  const errors = [];
2614
2666
  for (const f of fields) {
2615
- if (!(f.name in body) || f.removed === true) continue;
2667
+ if (!Object.hasOwn(body, f.name) || f.removed === true) continue;
2616
2668
  const name = `${path}${f.name}`;
2617
2669
  const message = valueError(f, body[f.name], name);
2618
2670
  if (message) errors.push({ field: name, message });
@@ -2625,6 +2677,7 @@ function checkFieldValues(fields, body, registry, path = "") {
2625
2677
 
2626
2678
  // src/routes/admin/content.ts
2627
2679
  init_relations();
2680
+ init_relation_input();
2628
2681
  init_field_keys();
2629
2682
  function quoteIdent4(name) {
2630
2683
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
@@ -2632,14 +2685,12 @@ function quoteIdent4(name) {
2632
2685
  }
2633
2686
  async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
2634
2687
  const result = await db.execute(
2635
- import_drizzle_orm5.sql`SELECT * FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
2688
+ import_drizzle_orm5.sql`SELECT * FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC, id ASC`
2636
2689
  );
2637
2690
  const rows = result.rows;
2638
2691
  for (const nf of pType.fields) {
2639
2692
  if (nf.db_column !== null) continue;
2640
- const comp = nf.ui_component;
2641
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2642
- const nType = registry.paragraph_types[comp.ref];
2693
+ const nType = paragraphTypeOf(nf, registry);
2643
2694
  if (!nType) continue;
2644
2695
  for (const row of rows) {
2645
2696
  row[nf.name] = oneOrMany(
@@ -2651,35 +2702,36 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
2651
2702
  return rows;
2652
2703
  }
2653
2704
  async function loadRelationsForEdit(db, registry, fields, id, row) {
2654
- for (const f of fields) {
2655
- if (f.db_column === null) {
2656
- const comp = f.ui_component;
2657
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2658
- const pType = registry.paragraph_types[comp.ref];
2659
- if (!pType) continue;
2660
- row[f.name] = oneOrMany((0, import_manguito_cms_core4.relationCardinality)(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name));
2661
- } else if (f.db_column.junction) {
2662
- const j = f.db_column.junction;
2663
- const r = await db.execute(
2664
- import_drizzle_orm5.sql`SELECT ${import_drizzle_orm5.sql.raw(quoteIdent4(j.right_column))} FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(j.table_name))} WHERE ${import_drizzle_orm5.sql.raw(quoteIdent4(j.left_column))} = ${id}${junctionOrderBy(j)}`
2665
- );
2666
- row[f.name] = r.rows.map((linked) => linked[j.right_column]);
2667
- }
2668
- }
2705
+ const loaded = await Promise.all(
2706
+ fields.map(async (f) => {
2707
+ if (f.db_column === null) {
2708
+ const pType = paragraphTypeOf(f, registry);
2709
+ if (!pType) return null;
2710
+ return [f.name, oneOrMany((0, import_manguito_cms_core4.relationCardinality)(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name))];
2711
+ }
2712
+ if (f.db_column.junction) {
2713
+ const j = f.db_column.junction;
2714
+ const r = await db.execute(
2715
+ import_drizzle_orm5.sql`SELECT ${import_drizzle_orm5.sql.raw(quoteIdent4(j.right_column))} FROM ${import_drizzle_orm5.sql.raw(quoteIdent4(j.table_name))} WHERE ${import_drizzle_orm5.sql.raw(quoteIdent4(j.left_column))} = ${id}${junctionOrderBy(j)}`
2716
+ );
2717
+ return [f.name, [...new Set(r.rows.map((linked) => linked[j.right_column]))]];
2718
+ }
2719
+ return null;
2720
+ })
2721
+ );
2722
+ for (const entry of loaded) if (entry) row[entry[0]] = entry[1];
2669
2723
  }
2670
2724
  async function valuesForPublishCheck(db, registry, requiredFields, id, storedLabels, body) {
2671
2725
  const stored = { ...storedLabels };
2672
- if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !(f.name in body)), id, stored);
2726
+ if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !Object.hasOwn(body, f.name)), id, stored);
2673
2727
  return { ...stored, ...body };
2674
2728
  }
2675
2729
  async function persistRelationFields(db, registry, fields, body, itemId, ownerTable) {
2676
2730
  const deltas = [];
2677
2731
  for (const f of fields) {
2678
- if (!(f.name in body)) continue;
2732
+ if (!Object.hasOwn(body, f.name)) continue;
2679
2733
  if (f.db_column === null) {
2680
- const comp = f.ui_component;
2681
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2682
- const pType = registry.paragraph_types[comp.ref];
2734
+ const pType = paragraphTypeOf(f, registry);
2683
2735
  if (!pType) continue;
2684
2736
  deltas.push(await persistParagraphField(db, itemId, ownerTable, f.name, pType, paragraphItems(f, body[f.name]), registry));
2685
2737
  } else if (f.db_column.junction) {
@@ -2690,6 +2742,46 @@ async function persistRelationFields(db, registry, fields, body, itemId, ownerTa
2690
2742
  }
2691
2743
  return deltas;
2692
2744
  }
2745
+ async function deleteOwnParagraphs(db, registry, fields, itemId) {
2746
+ const deltas = [];
2747
+ for (const f of fields) {
2748
+ const pType = paragraphTypeOf(f, registry);
2749
+ if (!pType) continue;
2750
+ deltas.push(await deleteParagraphField(db, itemId, f.name, pType, registry));
2751
+ }
2752
+ return deltas;
2753
+ }
2754
+ function releasedFromParagraphsOnly(paragraphDeltas, topLevel) {
2755
+ const stillHeld = new Set(topLevel.removed);
2756
+ return { added: [], removed: mergeMediaDeltas(...paragraphDeltas).removed.filter((mediaId) => !stillHeld.has(mediaId)) };
2757
+ }
2758
+ async function deleteItemCascade(db, registry, mediaRepo, target) {
2759
+ const { typeName, fields, mediaFields, repo, id, row } = target;
2760
+ const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : { others: [], fromOwnParagraphs: false };
2761
+ if (referrers.others.length > 0) return { ok: false, message: inUseMessage(referrers.others) };
2762
+ const topLevel = topLevelMediaDelta(mediaFields, row, null);
2763
+ if (!db || !referrers.fromOwnParagraphs) {
2764
+ try {
2765
+ await repo.delete(id);
2766
+ } catch (err) {
2767
+ if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
2768
+ throw err;
2769
+ }
2770
+ const paragraphDeltas2 = db ? await deleteOwnParagraphs(db, registry, fields, id) : [];
2771
+ await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas2), mediaRepo);
2772
+ return { ok: true };
2773
+ }
2774
+ const paragraphDeltas = await deleteOwnParagraphs(db, registry, fields, id);
2775
+ try {
2776
+ await repo.delete(id);
2777
+ } catch (err) {
2778
+ await applyMediaReferenceDelta(releasedFromParagraphsOnly(paragraphDeltas, topLevel), mediaRepo);
2779
+ if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
2780
+ throw err;
2781
+ }
2782
+ await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas), mediaRepo);
2783
+ return { ok: true };
2784
+ }
2693
2785
  async function lookupBasePathId(db, pathOrName) {
2694
2786
  const r = await db.execute(
2695
2787
  import_drizzle_orm5.sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
@@ -2799,7 +2891,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2799
2891
  const mediaFields = contentType.fields.filter(
2800
2892
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
2801
2893
  );
2802
- const paragraphFieldDefs = contentType.fields.filter((f) => f.db_column === null);
2803
2894
  const searchableColumns = [
2804
2895
  ...contentType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
2805
2896
  ...contentType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
@@ -2949,7 +3040,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2949
3040
  }
2950
3041
  for (const f of columnBackedFields) {
2951
3042
  const key = f.db_column.column_name;
2952
- if (!(key in storageBody)) continue;
3043
+ if (!Object.hasOwn(storageBody, key)) continue;
2953
3044
  columnData[key] = storageBody[key];
2954
3045
  }
2955
3046
  if (db) {
@@ -2991,7 +3082,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
2991
3082
  404
2992
3083
  );
2993
3084
  }
2994
- if (!contentType.only_one && "slug" in body) {
3085
+ if (!contentType.only_one && Object.hasOwn(body, "slug")) {
2995
3086
  const slug = body["slug"];
2996
3087
  if (typeof slug !== "string" || slug.trim() === "") {
2997
3088
  return c.json(
@@ -3061,11 +3152,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
3061
3152
  }
3062
3153
  const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
3063
3154
  const patchData = {};
3064
- if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
3065
- if ("published" in body) patchData["published"] = body["published"];
3155
+ if (!contentType.only_one && Object.hasOwn(body, "slug")) patchData["slug"] = body["slug"];
3156
+ if (Object.hasOwn(body, "published")) patchData["published"] = body["published"];
3066
3157
  for (const f of patchColumnBackedFields) {
3067
3158
  const key = f.db_column.column_name;
3068
- if (!(key in storageBody)) continue;
3159
+ if (!Object.hasOwn(storageBody, key)) continue;
3069
3160
  patchData[key] = storageBody[key];
3070
3161
  }
3071
3162
  const updated = await repo.update(id, patchData);
@@ -3115,31 +3206,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
3115
3206
  404
3116
3207
  );
3117
3208
  }
3118
- const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
3119
- if (referrers.length > 0) {
3120
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
3121
- }
3122
- try {
3123
- await repo.delete(id);
3124
- } catch (err) {
3125
- if (isForeignKeyViolation(err)) {
3126
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
3127
- }
3128
- throw err;
3129
- }
3130
- const mediaDeltas = [
3131
- topLevelMediaDelta(mediaFields, item, null)
3132
- ];
3133
- if (db) {
3134
- for (const f of paragraphFieldDefs) {
3135
- const comp = f.ui_component;
3136
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
3137
- const pType = registry.paragraph_types[comp.ref];
3138
- if (!pType) continue;
3139
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
3140
- }
3209
+ const outcome = await deleteItemCascade(db, registry, mediaRepo, {
3210
+ typeName,
3211
+ fields: contentType.fields,
3212
+ mediaFields,
3213
+ repo,
3214
+ id,
3215
+ row: item
3216
+ });
3217
+ if (!outcome.ok) {
3218
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
3141
3219
  }
3142
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
3143
3220
  return c.json({ ok: true });
3144
3221
  }
3145
3222
  );
@@ -3160,7 +3237,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
3160
3237
  const mediaFields = taxonomyType.fields.filter(
3161
3238
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
3162
3239
  );
3163
- const paragraphFieldDefs = taxonomyType.fields.filter((f) => f.db_column === null);
3164
3240
  const searchableColumns = [
3165
3241
  ...taxonomyType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
3166
3242
  ...taxonomyType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
@@ -3246,7 +3322,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
3246
3322
  };
3247
3323
  for (const f of taxColumnBackedFields) {
3248
3324
  const key = f.db_column.column_name;
3249
- if (!(key in storageBody)) continue;
3325
+ if (!Object.hasOwn(storageBody, key)) continue;
3250
3326
  taxColumnData[key] = storageBody[key];
3251
3327
  }
3252
3328
  const item = await repo.create(taxColumnData);
@@ -3298,10 +3374,10 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
3298
3374
  }
3299
3375
  const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
3300
3376
  const taxPatchData = {};
3301
- if ("published" in body) taxPatchData["published"] = body["published"];
3377
+ if (Object.hasOwn(body, "published")) taxPatchData["published"] = body["published"];
3302
3378
  for (const f of taxPatchColumnBackedFields) {
3303
3379
  const key = f.db_column.column_name;
3304
- if (!(key in storageBody)) continue;
3380
+ if (!Object.hasOwn(storageBody, key)) continue;
3305
3381
  taxPatchData[key] = storageBody[key];
3306
3382
  }
3307
3383
  const updated = await repo.update(id, taxPatchData);
@@ -3334,31 +3410,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
3334
3410
  404
3335
3411
  );
3336
3412
  }
3337
- const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
3338
- if (referrers.length > 0) {
3339
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
3340
- }
3341
- try {
3342
- await repo.delete(id);
3343
- } catch (err) {
3344
- if (isForeignKeyViolation(err)) {
3345
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
3346
- }
3347
- throw err;
3348
- }
3349
- const mediaDeltas = [
3350
- topLevelMediaDelta(mediaFields, item, null)
3351
- ];
3352
- if (db) {
3353
- for (const f of paragraphFieldDefs) {
3354
- const comp = f.ui_component;
3355
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
3356
- const pType = registry.paragraph_types[comp.ref];
3357
- if (!pType) continue;
3358
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
3359
- }
3413
+ const outcome = await deleteItemCascade(db, registry, mediaRepo, {
3414
+ typeName,
3415
+ fields: taxonomyType.fields,
3416
+ mediaFields,
3417
+ repo,
3418
+ id,
3419
+ row: item
3420
+ });
3421
+ if (!outcome.ok) {
3422
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
3360
3423
  }
3361
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
3362
3424
  return c.json({ ok: true });
3363
3425
  }
3364
3426
  );
@@ -4322,7 +4384,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
4322
4384
  var import_drizzle_orm8 = require("drizzle-orm");
4323
4385
  var API_VERSION = (() => {
4324
4386
  try {
4325
- return "0.9.2";
4387
+ return "0.9.3";
4326
4388
  } catch {
4327
4389
  return "0.0.0";
4328
4390
  }