@bobbykim/manguito-cms-cli 0.11.2 → 0.11.4

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.
@@ -9270,7 +9270,7 @@ var require_bcryptjs = __commonJS({
9270
9270
  }
9271
9271
  });
9272
9272
 
9273
- // ../api/dist/chunk-GUZZQZSN.mjs
9273
+ // ../api/dist/chunk-IFE2L4VM.mjs
9274
9274
  init_esm_shims();
9275
9275
  import crypto from "crypto";
9276
9276
 
@@ -25308,6 +25308,7 @@ function validateCrossReferences(registry2, globalMaxFileSize) {
25308
25308
  errors.push(...checkFieldRefs(registry2));
25309
25309
  errors.push(...checkCircularParagraphRefs(registry2));
25310
25310
  errors.push(...checkParagraphRelations(registry2));
25311
+ errors.push(...checkTableNameCollisions(registry2));
25311
25312
  if (globalMaxFileSize !== void 0) {
25312
25313
  errors.push(...checkMaxSizeLimit(registry2, globalMaxFileSize));
25313
25314
  }
@@ -25328,6 +25329,58 @@ function checkParagraphRelations(registry2) {
25328
25329
  }
25329
25330
  return errors;
25330
25331
  }
25332
+ var POSTGRES_IDENTIFIER_LIMIT = 63;
25333
+ function rawFieldPath(owner, field) {
25334
+ if (owner.schema_type !== "content-type") return `fields[${field.order}].name`;
25335
+ const t = owner.ui.tabs.findIndex((tab) => tab.fields.includes(field.name));
25336
+ const i = t < 0 ? -1 : owner.ui.tabs[t].fields.indexOf(field.name);
25337
+ return t < 0 ? `fields[${field.order}].name` : `fields[${t}].tab.fields[${i}].name`;
25338
+ }
25339
+ function checkTableNameCollisions(registry2) {
25340
+ const uses = [];
25341
+ for (const p of Object.values(registry2.paragraph_types)) {
25342
+ uses.push({ table: p.db.table_name, file: p.source_file, what: `type "${p.name}"`, shorten: `shorten the name of type "${p.name}"` });
25343
+ }
25344
+ for (const owner of [...Object.values(registry2.content_types), ...Object.values(registry2.taxonomy_types)]) {
25345
+ uses.push({
25346
+ table: owner.db.table_name,
25347
+ file: owner.source_file,
25348
+ what: `type "${owner.name}"`,
25349
+ shorten: `shorten the name of type "${owner.name}"`
25350
+ });
25351
+ for (const jt of owner.db.junction_tables) {
25352
+ const field = owner.fields.find((f) => f.db_column?.junction?.table_name === jt.table_name);
25353
+ if (!field) continue;
25354
+ const prefix = `junction_${owner.db.table_name}_`.length;
25355
+ const room = POSTGRES_IDENTIFIER_LIMIT - prefix;
25356
+ uses.push({
25357
+ table: jt.table_name,
25358
+ file: owner.source_file,
25359
+ what: `field "${field.name}" of "${owner.name}"`,
25360
+ shorten: room < 1 ? `shorten the name of type "${owner.name}": its link table names already have ${prefix} characters before the field name` : `shorten field "${field.name}" to at most ${room} characters, or the name of type "${owner.name}"`,
25361
+ path: rawFieldPath(owner, field)
25362
+ });
25363
+ }
25364
+ }
25365
+ const errors = [];
25366
+ const first = /* @__PURE__ */ new Map();
25367
+ for (const use of uses) {
25368
+ const stored = use.table.slice(0, POSTGRES_IDENTIFIER_LIMIT);
25369
+ const earlier = first.get(stored);
25370
+ if (!earlier) {
25371
+ first.set(stored, use);
25372
+ continue;
25373
+ }
25374
+ const tooLong = use.table.length > POSTGRES_IDENTIFIER_LIMIT ? use : earlier;
25375
+ errors.push({
25376
+ file: use.file,
25377
+ code: "TABLE_NAME_COLLISION",
25378
+ message: earlier.table === use.table ? `The ${use.what} would store its data in table "${use.table}", the same table as the ${earlier.what}. Rename one of the two so their tables differ.` : `The ${use.what} and the ${earlier.what} would get tables "${use.table}" and "${earlier.table}". Postgres keeps only the first ${POSTGRES_IDENTIFIER_LIMIT} characters of a name, which are the same, so they would be one table. To keep "${tooLong.table}" within ${POSTGRES_IDENTIFIER_LIMIT} characters, ${tooLong.shorten}.`,
25379
+ ...use.path !== void 0 && { path: use.path }
25380
+ });
25381
+ }
25382
+ return errors;
25383
+ }
25331
25384
  function checkDuplicateSchemaNames(allSchemas) {
25332
25385
  const errors = [];
25333
25386
  const seen = /* @__PURE__ */ new Map();
@@ -25904,7 +25957,17 @@ function checkDuplicateColumns(fields, sourceFile, schemaName) {
25904
25957
  for (const f of fields) {
25905
25958
  if (!hasStorageColumn(f)) continue;
25906
25959
  const column = f.column ?? f.name;
25907
- const first = owner.get(column);
25960
+ const stored = column.slice(0, 63);
25961
+ const firstUse = owner.get(stored);
25962
+ if (firstUse !== void 0 && firstUse.column !== column) {
25963
+ errors.push({
25964
+ file: sourceFile,
25965
+ code: "DUPLICATE_COLUMN",
25966
+ message: `Fields "${firstUse.field}" and "${f.name}" in schema "${schemaName}" resolve to columns "${firstUse.column}" and "${column}". Postgres keeps only the first 63 characters of a column name, which are the same, so they would be one column. Shorten one of the field names to at most 63 characters.`
25967
+ });
25968
+ continue;
25969
+ }
25970
+ const first = firstUse?.field;
25908
25971
  if (first !== void 0) {
25909
25972
  errors.push({
25910
25973
  file: sourceFile,
@@ -25913,7 +25976,7 @@ function checkDuplicateColumns(fields, sourceFile, schemaName) {
25913
25976
  });
25914
25977
  continue;
25915
25978
  }
25916
- owner.set(column, f.name);
25979
+ owner.set(stored, { field: f.name, column });
25917
25980
  }
25918
25981
  return errors;
25919
25982
  }
@@ -26618,7 +26681,7 @@ function describeSchemaChange(input) {
26618
26681
  return { from: from?.version ?? null, to: to.version, types, identical };
26619
26682
  }
26620
26683
 
26621
- // ../api/dist/chunk-GUZZQZSN.mjs
26684
+ // ../api/dist/chunk-IFE2L4VM.mjs
26622
26685
  function isColumnBacked2(field) {
26623
26686
  const col = field.db_column;
26624
26687
  if (col === null) return false;
@@ -26725,10 +26788,131 @@ function createFieldKeyMapFromProjection(projectionType, allFields) {
26725
26788
  droppedLabels
26726
26789
  );
26727
26790
  }
26791
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
26792
+ function isUuid(v) {
26793
+ return typeof v === "string" && UUID_RE.test(v);
26794
+ }
26795
+ function isPlainObject2(v) {
26796
+ return typeof v === "object" && v !== null && !Array.isArray(v);
26797
+ }
26798
+ function checkRelationInput(fields, body, registry2, path4 = "") {
26799
+ const errors = [];
26800
+ for (const field of fields) {
26801
+ if (!Object.hasOwn(body, field.name)) continue;
26802
+ const cardinality = relationCardinality(field);
26803
+ if (cardinality === null) continue;
26804
+ const value = body[field.name];
26805
+ const name = `${path4}${field.name}`;
26806
+ if (field.field_type === "reference") {
26807
+ const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
26808
+ if (!ok) {
26809
+ errors.push({
26810
+ field: name,
26811
+ message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
26812
+ });
26813
+ }
26814
+ continue;
26815
+ }
26816
+ const items = cardinality === "one" ? value === null ? [] : isPlainObject2(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject2) ? value : void 0;
26817
+ if (items === void 0) {
26818
+ errors.push({
26819
+ field: name,
26820
+ message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
26821
+ });
26822
+ continue;
26823
+ }
26824
+ const pType = paragraphTypeOf(field, registry2);
26825
+ if (!pType) continue;
26826
+ items.forEach((item, i) => {
26827
+ const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
26828
+ errors.push(...checkRelationInput(pType.fields, item, registry2, prefix));
26829
+ });
26830
+ }
26831
+ return errors;
26832
+ }
26833
+ function paragraphTypeOf(field, registry2) {
26834
+ const comp = field.ui_component;
26835
+ return comp.component === "paragraph-embed" ? registry2.paragraph_types[comp.ref] : void 0;
26836
+ }
26837
+ function paragraphItemsIn(fields, body, registry2, path4) {
26838
+ const out = [];
26839
+ for (const field of fields) {
26840
+ if (field.field_type !== "paragraph" || !Object.hasOwn(body, field.name)) continue;
26841
+ const pType = paragraphTypeOf(field, registry2);
26842
+ if (!pType) continue;
26843
+ const value = body[field.name];
26844
+ const name = `${path4}${field.name}`;
26845
+ if (relationCardinality(field) === "one") {
26846
+ if (isPlainObject2(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
26847
+ } else if (Array.isArray(value)) {
26848
+ value.forEach((item, i) => {
26849
+ if (isPlainObject2(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
26850
+ });
26851
+ }
26852
+ }
26853
+ return out;
26854
+ }
26855
+ function isMissing(value) {
26856
+ if (value === null || value === void 0) return true;
26857
+ if (typeof value === "string" && value.trim() === "") return true;
26858
+ if (Array.isArray(value) && value.length === 0) return true;
26859
+ return false;
26860
+ }
26861
+ function checkRequiredInParagraphItems(fields, body, registry2, path4 = "") {
26862
+ const errors = [];
26863
+ for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry2, path4)) {
26864
+ for (const f of itemFields) {
26865
+ if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
26866
+ if (isMissing(item[f.name])) {
26867
+ errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
26868
+ }
26869
+ }
26870
+ errors.push(...checkRequiredInParagraphItems(itemFields, item, registry2, prefix));
26871
+ }
26872
+ return errors;
26873
+ }
26728
26874
  function quoteIdent(name) {
26729
26875
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
26730
26876
  return `"${name}"`;
26731
26877
  }
26878
+ function collectReferences(fields, body, registry2, path4, out) {
26879
+ for (const f of fields) {
26880
+ if (!Object.hasOwn(body, f.name) || !f.db_column) continue;
26881
+ const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
26882
+ if (!table) continue;
26883
+ const value = body[f.name];
26884
+ const name = `${path4}${f.name}`;
26885
+ if (typeof value === "string") {
26886
+ out.push({ table, id: value, field: name });
26887
+ } else if (Array.isArray(value)) {
26888
+ value.forEach((v, i) => {
26889
+ if (typeof v === "string") out.push({ table, id: v, field: `${name}[${i}]` });
26890
+ });
26891
+ }
26892
+ }
26893
+ for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry2, path4)) {
26894
+ collectReferences(itemFields, item, registry2, prefix, out);
26895
+ }
26896
+ }
26897
+ async function findMissingReferences(db, fields, body, registry2) {
26898
+ const uses = [];
26899
+ collectReferences(fields, body, registry2, "", uses);
26900
+ if (uses.length === 0) return [];
26901
+ const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
26902
+ const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
26903
+ if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
26904
+ const result = await db.execute(
26905
+ sql`SELECT id FROM ${sql.raw(quoteIdent(table))} WHERE id IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`
26906
+ );
26907
+ return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
26908
+ });
26909
+ const found = new Map(await Promise.all(lookups));
26910
+ 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.` }));
26911
+ }
26912
+ function quoteIdent2(name) {
26913
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
26914
+ return `"${name}"`;
26915
+ }
26732
26916
  function paragraphMediaFieldNames(pType) {
26733
26917
  return new Set(
26734
26918
  pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name)
@@ -26737,8 +26921,8 @@ function paragraphMediaFieldNames(pType) {
26737
26921
  async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
26738
26922
  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);
26739
26923
  if (mediaColumns.length === 0) return [];
26740
- const tbl = sql.raw(quoteIdent(pType.db.table_name));
26741
- const cols = sql.join(mediaColumns.map((c) => sql.raw(quoteIdent(c))), sql`, `);
26924
+ const tbl = sql.raw(quoteIdent2(pType.db.table_name));
26925
+ const cols = sql.join(mediaColumns.map((c) => sql.raw(quoteIdent2(c))), sql`, `);
26742
26926
  const result = await db.execute(
26743
26927
  sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
26744
26928
  );
@@ -26756,20 +26940,37 @@ function nestedParagraphFields(pType, registry2) {
26756
26940
  const out = [];
26757
26941
  for (const f of pType.fields) {
26758
26942
  if (f.db_column !== null) continue;
26759
- const comp = f.ui_component;
26760
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
26761
- const nType = registry2.paragraph_types[comp.ref];
26943
+ const nType = paragraphTypeOf(f, registry2);
26762
26944
  if (nType) out.push({ fieldName: f.name, nType, field: f });
26763
26945
  }
26764
26946
  return out;
26765
26947
  }
26766
26948
  async function fetchParagraphRowIds(db, pType, itemId, fieldName) {
26767
- const tbl = sql.raw(quoteIdent(pType.db.table_name));
26949
+ const tbl = sql.raw(quoteIdent2(pType.db.table_name));
26768
26950
  const result = await db.execute(
26769
26951
  sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
26770
26952
  );
26771
26953
  return result.rows.map((r) => r["id"]).filter((id) => typeof id === "string");
26772
26954
  }
26955
+ async function ownParagraphRowIds(db, registry2, fields, itemId) {
26956
+ const ids = [];
26957
+ for (const f of fields) {
26958
+ const pType = paragraphTypeOf(f, registry2);
26959
+ if (!pType) continue;
26960
+ const rowIds = await fetchParagraphRowIds(db, pType, itemId, f.name);
26961
+ ids.push(...rowIds);
26962
+ if (rowIds.length === 0) continue;
26963
+ for (const n of nestedParagraphFields(pType, registry2)) {
26964
+ const result = await db.execute(
26965
+ sql`SELECT id FROM ${sql.raw(quoteIdent2(n.nType.db.table_name))} WHERE parent_id = ANY(${sql.param(rowIds)}::uuid[]) AND parent_field = ${n.fieldName}`
26966
+ );
26967
+ for (const r of result.rows) {
26968
+ if (typeof r["id"] === "string") ids.push(r["id"]);
26969
+ }
26970
+ }
26971
+ }
26972
+ return ids;
26973
+ }
26773
26974
  async function persistParagraphField(db, itemId, parentType, fieldName, pType, items, registry2) {
26774
26975
  const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
26775
26976
  const nested = nestedParagraphFields(pType, registry2);
@@ -26782,7 +26983,7 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
26782
26983
  }
26783
26984
  }
26784
26985
  }
26785
- const tbl = sql.raw(quoteIdent(pType.db.table_name));
26986
+ const tbl = sql.raw(quoteIdent2(pType.db.table_name));
26786
26987
  await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
26787
26988
  const mediaFieldNames = paragraphMediaFieldNames(pType);
26788
26989
  const added = [];
@@ -26802,7 +27003,7 @@ async function persistParagraphField(db, itemId, parentType, fieldName, pType, i
26802
27003
  if (!pf.db_column || pf.db_column.junction) continue;
26803
27004
  data[pf.db_column.column_name] = pItem[pf.name] ?? null;
26804
27005
  }
26805
- const cols = sql.join(Object.keys(data).map((k) => sql.raw(quoteIdent(k))), sql`, `);
27006
+ const cols = sql.join(Object.keys(data).map((k) => sql.raw(quoteIdent2(k))), sql`, `);
26806
27007
  const vals = sql.join(Object.values(data).map((v) => sql`${v}`), sql`, `);
26807
27008
  await db.execute(sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
26808
27009
  for (const name of mediaFieldNames) {
@@ -26838,14 +27039,14 @@ async function deleteParagraphField(db, itemId, fieldName, pType, registry2) {
26838
27039
  }
26839
27040
  }
26840
27041
  }
26841
- const tbl = sql.raw(quoteIdent(pType.db.table_name));
27042
+ const tbl = sql.raw(quoteIdent2(pType.db.table_name));
26842
27043
  await db.execute(sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
26843
27044
  return { added: [], removed };
26844
27045
  }
26845
27046
  async function persistJunctionField(db, itemId, junction, relatedIds) {
26846
- const tbl = sql.raw(quoteIdent(junction.table_name));
26847
- const left = sql.raw(quoteIdent(junction.left_column));
26848
- const right = sql.raw(quoteIdent(junction.right_column));
27047
+ const tbl = sql.raw(quoteIdent2(junction.table_name));
27048
+ const left = sql.raw(quoteIdent2(junction.left_column));
27049
+ const right = sql.raw(quoteIdent2(junction.right_column));
26849
27050
  const seen = /* @__PURE__ */ new Set();
26850
27051
  const ids = relatedIds.filter((id) => {
26851
27052
  const key = id.toLowerCase();
@@ -26866,9 +27067,18 @@ async function persistJunctionField(db, itemId, junction, relatedIds) {
26866
27067
  );
26867
27068
  }
26868
27069
  function junctionOrderBy(junction) {
26869
- const right = sql.raw(quoteIdent(junction.right_column));
27070
+ const right = sql.raw(quoteIdent2(junction.right_column));
26870
27071
  return junction.order_column ? sql` ORDER BY "order" ASC, ${right} ASC` : sql` ORDER BY ${right} ASC`;
26871
27072
  }
27073
+ function firstOfEach(items, key) {
27074
+ const seen = /* @__PURE__ */ new Set();
27075
+ return items.filter((item) => {
27076
+ const k = key(item);
27077
+ if (seen.has(k)) return false;
27078
+ seen.add(k);
27079
+ return true;
27080
+ });
27081
+ }
26872
27082
  function oneOrMany(cardinality, list) {
26873
27083
  return cardinality === "one" ? list[0] ?? null : list;
26874
27084
  }
@@ -26936,7 +27146,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
26936
27146
  const parentIds = batch.map((r) => r["id"]);
26937
27147
  const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `);
26938
27148
  const result = await db.execute(
26939
- sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
27149
+ sql`SELECT * FROM ${sql.raw(quoteIdent2(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC, id ASC`
26940
27150
  );
26941
27151
  const byParent = groupBy(result.rows, "parent_id");
26942
27152
  for (const row of batch) {
@@ -26956,7 +27166,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
26956
27166
  const inList = sql.join(uncached.map((id) => sql`${id}`), sql`, `);
26957
27167
  const publishedCond = publishedOnly ? sql` AND published = true` : sql``;
26958
27168
  const result = await db.execute(
26959
- sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})${publishedCond}`
27169
+ sql`SELECT * FROM ${sql.raw(quoteIdent2(rel.table))} WHERE id IN (${inList})${publishedCond}`
26960
27170
  );
26961
27171
  for (const item of result.rows) {
26962
27172
  cache.set(`${rel.table}:${item["id"]}`, item);
@@ -26970,7 +27180,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
26970
27180
  const parentIds = batch.map((r) => r["id"]);
26971
27181
  const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `);
26972
27182
  const junctionResult = await db.execute(
26973
- sql`SELECT * FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
27183
+ sql`SELECT * FROM ${sql.raw(quoteIdent2(rel.junction_table))} WHERE ${sql.raw(quoteIdent2(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
26974
27184
  );
26975
27185
  const jRows = junctionResult.rows;
26976
27186
  const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
@@ -26979,7 +27189,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
26979
27189
  const rightInList = sql.join(uncached.map((id) => sql`${id}`), sql`, `);
26980
27190
  const publishedCond = publishedOnly ? sql` AND published = true` : sql``;
26981
27191
  const entitiesResult = await db.execute(
26982
- sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
27192
+ sql`SELECT * FROM ${sql.raw(quoteIdent2(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
26983
27193
  );
26984
27194
  for (const item of entitiesResult.rows) {
26985
27195
  cache.set(`${rel.table}:${item["id"]}`, item);
@@ -26987,7 +27197,7 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
26987
27197
  }
26988
27198
  const byLeft = groupBy(jRows, rel.left_column);
26989
27199
  for (const row of batch) {
26990
- const jEntries = byLeft[row["id"]] ?? [];
27200
+ const jEntries = firstOfEach(byLeft[row["id"]] ?? [], (jr) => jr[rel.right_column]);
26991
27201
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
26992
27202
  }
26993
27203
  } else if (rel.type === "media") {
@@ -27019,7 +27229,7 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
27019
27229
  const parentIds = rows.map((r) => r["id"]);
27020
27230
  const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `);
27021
27231
  const result = await db.execute(
27022
- sql`SELECT id, parent_id FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
27232
+ sql`SELECT id, parent_id FROM ${sql.raw(quoteIdent2(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC, id ASC`
27023
27233
  );
27024
27234
  const byParent = groupBy(result.rows, "parent_id");
27025
27235
  for (const row of rows) {
@@ -27029,11 +27239,11 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
27029
27239
  const parentIds = rows.map((r) => r["id"]);
27030
27240
  const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `);
27031
27241
  const result = await db.execute(
27032
- sql`SELECT ${sql.raw(quoteIdent(rel.left_column))} AS left_id, ${sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${sql.raw(quoteIdent(rel.junction_table))} WHERE ${sql.raw(quoteIdent(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
27242
+ sql`SELECT ${sql.raw(quoteIdent2(rel.left_column))} AS left_id, ${sql.raw(quoteIdent2(rel.right_column))} AS right_id FROM ${sql.raw(quoteIdent2(rel.junction_table))} WHERE ${sql.raw(quoteIdent2(rel.left_column))} IN (${inList})${junctionOrderBy(rel)}`
27033
27243
  );
27034
27244
  const byLeft = groupBy(result.rows, "left_id");
27035
27245
  for (const row of rows) {
27036
- row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
27246
+ row[fieldName] = firstOfEach(byLeft[row["id"]] ?? [], (r) => r["right_id"]).map((r) => r["right_id"]);
27037
27247
  }
27038
27248
  }
27039
27249
  }
@@ -27086,6 +27296,14 @@ export {
27086
27296
  isColumnBacked2 as isColumnBacked,
27087
27297
  createFieldKeyMap,
27088
27298
  createFieldKeyMapFromProjection,
27299
+ isPlainObject2 as isPlainObject,
27300
+ checkRelationInput,
27301
+ paragraphTypeOf,
27302
+ paragraphItemsIn,
27303
+ isMissing,
27304
+ checkRequiredInParagraphItems,
27305
+ findMissingReferences,
27306
+ ownParagraphRowIds,
27089
27307
  persistParagraphField,
27090
27308
  deleteParagraphField,
27091
27309
  persistJunctionField,
@@ -12,7 +12,7 @@ import {
12
12
  isColumnBacked,
13
13
  relationCardinality,
14
14
  resolveRelationField
15
- } from "./chunk-WTZBHFCV.js";
15
+ } from "./chunk-44VXH5BN.js";
16
16
 
17
17
  // ../../node_modules/.pnpm/@whatwg-node+fetch@0.10.13/node_modules/@whatwg-node/fetch/dist/shouldSkipPonyfill.js
18
18
  var require_shouldSkipPonyfill = __commonJS({
@@ -10787,10 +10787,10 @@ var require_parser = __commonJS({
10787
10787
  }
10788
10788
  });
10789
10789
 
10790
- // ../api/dist/handler-MFF32JB4.mjs
10790
+ // ../api/dist/handler-N5IMQJBP.mjs
10791
10791
  init_esm_shims();
10792
10792
 
10793
- // ../api/dist/chunk-ITOSMGDT.mjs
10793
+ // ../api/dist/chunk-IMDC3NGM.mjs
10794
10794
  init_esm_shims();
10795
10795
 
10796
10796
  // ../../node_modules/.pnpm/graphql-yoga@5.21.2_graphql@16.14.2/node_modules/graphql-yoga/esm/index.js
@@ -30123,7 +30123,7 @@ function createYoga(options) {
30123
30123
  });
30124
30124
  }
30125
30125
 
30126
- // ../api/dist/chunk-ITOSMGDT.mjs
30126
+ // ../api/dist/chunk-IMDC3NGM.mjs
30127
30127
  var import_dataloader = __toESM(require_dataloader(), 1);
30128
30128
 
30129
30129
  // ../../node_modules/.pnpm/@escape.tech+graphql-armor@3.2.0_@envelop+core@5.5.1_@escape.tech+graphql-armor-types@0.7.0/node_modules/@escape.tech/graphql-armor/dist/graphql-armor.esm.js
@@ -30881,7 +30881,7 @@ var EnvelopArmor = class {
30881
30881
  }
30882
30882
  };
30883
30883
 
30884
- // ../api/dist/chunk-ITOSMGDT.mjs
30884
+ // ../api/dist/chunk-IMDC3NGM.mjs
30885
30885
  var DateTimeScalar = new GraphQLScalarType({
30886
30886
  name: "DateTime",
30887
30887
  description: "ISO-8601 date-time string",
package/dist/index.js CHANGED
@@ -21,6 +21,8 @@ import {
21
21
  __toESM,
22
22
  buildRelationsMap,
23
23
  buildSchemaRegistry,
24
+ checkRelationInput,
25
+ checkRequiredInParagraphItems,
24
26
  computeVersionModel,
25
27
  createFieldKeyMap,
26
28
  createFieldKeyMapFromProjection,
@@ -28,6 +30,7 @@ import {
28
30
  describeSchemaChange,
29
31
  entityKind,
30
32
  fillPlaceholders,
33
+ findMissingReferences,
31
34
  findSchemaDeprecations,
32
35
  getTableName,
33
36
  getTableUniqueName,
@@ -36,12 +39,17 @@ import {
36
39
  is,
37
40
  isColumnBacked,
38
41
  isDriverValueEncoder,
42
+ isMissing,
43
+ isPlainObject,
39
44
  isSQLWrapper,
40
45
  junctionOrderBy,
41
46
  loadSchemaFile,
42
47
  loadVersionSnapshots,
43
48
  oneOrMany,
49
+ ownParagraphRowIds,
44
50
  paragraphItems,
51
+ paragraphItemsIn,
52
+ paragraphTypeOf,
45
53
  parseRoles,
46
54
  parseRoutes,
47
55
  parseSchema,
@@ -55,7 +63,7 @@ import {
55
63
  validateCrossReferences,
56
64
  verifyPassword,
57
65
  walkSchemaDirectory
58
- } from "./chunk-WTZBHFCV.js";
66
+ } from "./chunk-44VXH5BN.js";
59
67
 
60
68
  // ../../node_modules/.pnpm/postgres-array@2.0.0/node_modules/postgres-array/index.js
61
69
  var require_postgres_array = __commonJS({
@@ -21901,7 +21909,7 @@ var ERROR_STATUS_MAP = {
21901
21909
  var errorHandler2 = (err, c) => {
21902
21910
  console.error(err.stack ?? err.message);
21903
21911
  const raw2 = err.code;
21904
- const deliberate = raw2 !== void 0 && raw2 !== "INTERNAL_ERROR" && raw2 in ERROR_STATUS_MAP;
21912
+ const deliberate = raw2 !== void 0 && raw2 !== "INTERNAL_ERROR" && Object.hasOwn(ERROR_STATUS_MAP, raw2);
21905
21913
  if (!deliberate) {
21906
21914
  return c.json({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
21907
21915
  }
@@ -22464,16 +22472,16 @@ function parseFilters(url, validFields, columnFor) {
22464
22472
  }
22465
22473
  return { ok: true, filters };
22466
22474
  }
22475
+ function pickProgrammatic(resolved, names) {
22476
+ const out = {};
22477
+ for (const name of names) if (Object.hasOwn(resolved, name)) out[name] = resolved[name];
22478
+ return out;
22479
+ }
22467
22480
  function isPublished(item) {
22468
22481
  return item["published"] === true;
22469
22482
  }
22470
22483
  function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
22471
22484
  const forResolver = resolverProjectors ?? projectors;
22472
- function pickProgrammatic(resolved, names) {
22473
- const out = {};
22474
- for (const name of names) if (name in resolved) out[name] = resolved[name];
22475
- return out;
22476
- }
22477
22485
  async function respondItem(row, typeName, programmatic) {
22478
22486
  const data = projectRow(row, typeName, projectors);
22479
22487
  if (!resolver?.hasSchema(typeName)) return data;
@@ -22785,25 +22793,42 @@ function quoteIdent(name) {
22785
22793
  function isRequiredSingleReferenceTo(f, targetType) {
22786
22794
  return f.field_type === "reference" && f.required && relationCardinality(f) === "one" && f.db_column?.foreign_key !== void 0 && f.ui_component.component === "typeahead-select" && f.ui_component.ref === targetType;
22787
22795
  }
22796
+ function requiredReferencesTo(owners, targetType) {
22797
+ return owners.flatMap(
22798
+ (owner) => owner.fields.filter((f) => isRequiredSingleReferenceTo(f, targetType)).map((f) => ({ owner, f }))
22799
+ );
22800
+ }
22788
22801
  async function findRequiredReferrers(db, registry, targetType, id) {
22789
- const owners = [
22790
- ...Object.values(registry.content_types),
22791
- ...Object.values(registry.taxonomy_types),
22792
- ...Object.values(registry.paragraph_types)
22793
- ];
22794
- const out = [];
22795
- for (const owner of owners) {
22796
- for (const f of owner.fields) {
22797
- if (!isRequiredSingleReferenceTo(f, targetType)) continue;
22798
- const notSelf = owner.name === targetType ? sql` AND id <> ${id}` : sql``;
22799
- const r = await db.execute(
22800
- sql`SELECT count(*)::int AS n FROM ${sql.raw(quoteIdent(owner.db.table_name))} WHERE ${sql.raw(quoteIdent(f.db_column.column_name))} = ${id}${notSelf}`
22801
- );
22802
- const n = r.rows[0].n;
22803
- if (n > 0) out.push({ type_label: owner.label, field_label: f.label, count: n });
22804
- }
22802
+ const targetFields = registry.content_types[targetType]?.fields ?? registry.taxonomy_types[targetType]?.fields ?? [];
22803
+ const itemOwners = [...Object.values(registry.content_types), ...Object.values(registry.taxonomy_types)];
22804
+ const itemFields = requiredReferencesTo(itemOwners, targetType);
22805
+ const paragraphFields = requiredReferencesTo(Object.values(registry.paragraph_types), targetType);
22806
+ const ownIds = paragraphFields.length > 0 ? await ownParagraphRowIds(db, registry, targetFields, id) : [];
22807
+ const [itemCounts, paragraphCounts] = await Promise.all([
22808
+ Promise.all(
22809
+ itemFields.map(async ({ owner, f }) => {
22810
+ const notSelf = owner.name === targetType ? sql` AND id <> ${id}` : sql``;
22811
+ const r = await db.execute(
22812
+ sql`SELECT count(*)::int AS n FROM ${sql.raw(quoteIdent(owner.db.table_name))} WHERE ${sql.raw(quoteIdent(f.db_column.column_name))} = ${id}${notSelf}`
22813
+ );
22814
+ return { owner, f, n: r.rows[0].n, own: 0 };
22815
+ })
22816
+ ),
22817
+ Promise.all(
22818
+ paragraphFields.map(async ({ owner, f }) => {
22819
+ const r = await db.execute(
22820
+ sql`SELECT count(*)::int AS n, count(*) FILTER (WHERE id = ANY(${sql.param(ownIds)}::uuid[]))::int AS own FROM ${sql.raw(quoteIdent(owner.db.table_name))} WHERE ${sql.raw(quoteIdent(f.db_column.column_name))} = ${id}`
22821
+ );
22822
+ const { n, own } = r.rows[0];
22823
+ return { owner, f, n, own };
22824
+ })
22825
+ )
22826
+ ]);
22827
+ const others = [];
22828
+ for (const { owner, f, n, own } of [...itemCounts, ...paragraphCounts]) {
22829
+ if (n - own > 0) others.push({ type_label: owner.label, field_label: f.label, count: n - own });
22805
22830
  }
22806
- return out;
22831
+ return { others, fromOwnParagraphs: paragraphCounts.some((c) => c.own > 0) };
22807
22832
  }
22808
22833
  function inUseMessage(referrers) {
22809
22834
  const total = referrers.reduce((sum, r) => sum + r.count, 0);
@@ -22828,7 +22853,7 @@ function topLevelMediaDelta(mediaFields, before, after) {
22828
22853
  for (const f of mediaFields) {
22829
22854
  const key = f.db_column?.column_name;
22830
22855
  if (key === void 0 || key === "") continue;
22831
- if (after !== null && !(key in after)) continue;
22856
+ if (after !== null && !Object.hasOwn(after, key)) continue;
22832
22857
  const oldId = before ? extractMediaId(before[key]) : null;
22833
22858
  const newId = after ? extractMediaId(after[key]) : null;
22834
22859
  if (newId === oldId) continue;
@@ -22851,127 +22876,6 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
22851
22876
  if (netAdd.length > 0) await mediaRepo.incrementReferenceCount(netAdd);
22852
22877
  if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
22853
22878
  }
22854
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22855
- function isUuid(v2) {
22856
- return typeof v2 === "string" && UUID_RE.test(v2);
22857
- }
22858
- function isPlainObject(v2) {
22859
- return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
22860
- }
22861
- function checkRelationInput(fields, body, registry, path5 = "") {
22862
- const errors = [];
22863
- for (const field of fields) {
22864
- if (!(field.name in body)) continue;
22865
- const cardinality = relationCardinality(field);
22866
- if (cardinality === null) continue;
22867
- const value = body[field.name];
22868
- const name = `${path5}${field.name}`;
22869
- if (field.field_type === "reference") {
22870
- const ok = cardinality === "one" ? value === null || isUuid(value) : Array.isArray(value) && value.every(isUuid);
22871
- if (!ok) {
22872
- errors.push({
22873
- field: name,
22874
- message: cardinality === "one" ? `${name} holds one item: send a UUID string or null.` : `${name} holds a list: send an array of UUID strings.`
22875
- });
22876
- }
22877
- continue;
22878
- }
22879
- const items = cardinality === "one" ? value === null ? [] : isPlainObject(value) ? [value] : void 0 : Array.isArray(value) && value.every(isPlainObject) ? value : void 0;
22880
- if (items === void 0) {
22881
- errors.push({
22882
- field: name,
22883
- message: cardinality === "one" ? `${name} holds one item: send an object or null.` : `${name} holds a list: send an array of objects.`
22884
- });
22885
- continue;
22886
- }
22887
- const pType = paragraphTypeOf(field, registry);
22888
- if (!pType) continue;
22889
- items.forEach((item, i) => {
22890
- const prefix = cardinality === "one" ? `${name}.` : `${name}[${i}].`;
22891
- errors.push(...checkRelationInput(pType.fields, item, registry, prefix));
22892
- });
22893
- }
22894
- return errors;
22895
- }
22896
- function paragraphTypeOf(field, registry) {
22897
- const comp = field.ui_component;
22898
- return comp.component === "paragraph-embed" ? registry.paragraph_types[comp.ref] : void 0;
22899
- }
22900
- function paragraphItemsIn(fields, body, registry, path5) {
22901
- const out = [];
22902
- for (const field of fields) {
22903
- if (field.field_type !== "paragraph" || !(field.name in body)) continue;
22904
- const pType = paragraphTypeOf(field, registry);
22905
- if (!pType) continue;
22906
- const value = body[field.name];
22907
- const name = `${path5}${field.name}`;
22908
- if (relationCardinality(field) === "one") {
22909
- if (isPlainObject(value)) out.push({ item: value, fields: pType.fields, prefix: `${name}.` });
22910
- } else if (Array.isArray(value)) {
22911
- value.forEach((item, i) => {
22912
- if (isPlainObject(item)) out.push({ item, fields: pType.fields, prefix: `${name}[${i}].` });
22913
- });
22914
- }
22915
- }
22916
- return out;
22917
- }
22918
- function isMissing(value) {
22919
- if (value === null || value === void 0) return true;
22920
- if (typeof value === "string" && value.trim() === "") return true;
22921
- if (Array.isArray(value) && value.length === 0) return true;
22922
- return false;
22923
- }
22924
- function checkRequiredInParagraphItems(fields, body, registry, path5 = "") {
22925
- const errors = [];
22926
- for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path5)) {
22927
- for (const f of itemFields) {
22928
- if (!f.required || f.removed === true || !f.db_column || f.db_column.junction) continue;
22929
- if (isMissing(item[f.name])) {
22930
- errors.push({ field: `${prefix}${f.name}`, message: `${prefix}${f.name} is required.` });
22931
- }
22932
- }
22933
- errors.push(...checkRequiredInParagraphItems(itemFields, item, registry, prefix));
22934
- }
22935
- return errors;
22936
- }
22937
- function quoteIdent2(name) {
22938
- if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
22939
- return `"${name}"`;
22940
- }
22941
- function collectReferences(fields, body, registry, path5, out) {
22942
- for (const f of fields) {
22943
- if (!(f.name in body) || !f.db_column) continue;
22944
- const table = f.db_column.junction ? f.db_column.junction.right_table : f.db_column.foreign_key?.table;
22945
- if (!table) continue;
22946
- const value = body[f.name];
22947
- const name = `${path5}${f.name}`;
22948
- if (typeof value === "string") {
22949
- out.push({ table, id: value, field: name });
22950
- } else if (Array.isArray(value)) {
22951
- value.forEach((v2, i) => {
22952
- if (typeof v2 === "string") out.push({ table, id: v2, field: `${name}[${i}]` });
22953
- });
22954
- }
22955
- }
22956
- for (const { item, fields: itemFields, prefix } of paragraphItemsIn(fields, body, registry, path5)) {
22957
- collectReferences(itemFields, item, registry, prefix, out);
22958
- }
22959
- }
22960
- async function findMissingReferences(db, fields, body, registry) {
22961
- const uses = [];
22962
- collectReferences(fields, body, registry, "", uses);
22963
- if (uses.length === 0) return [];
22964
- const lookups = [...new Set(uses.map((u) => u.table))].map(async (table) => {
22965
- const ids = [...new Set(uses.filter((u) => u.table === table && isUuid(u.id)).map((u) => u.id.toLowerCase()))];
22966
- if (ids.length === 0) return [table, /* @__PURE__ */ new Set()];
22967
- const result = await db.execute(
22968
- sql`SELECT id FROM ${sql.raw(quoteIdent2(table))} WHERE id IN (${sql.join(ids.map((id) => sql`${id}`), sql`, `)})`
22969
- );
22970
- return [table, new Set(result.rows.map((r) => String(r.id).toLowerCase()))];
22971
- });
22972
- const found = new Map(await Promise.all(lookups));
22973
- 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.` }));
22974
- }
22975
22879
  var VARCHAR_DEFAULT_LIMIT = 255;
22976
22880
  var INTEGER_MIN = -2147483648;
22977
22881
  var INTEGER_MAX = 2147483647;
@@ -22982,7 +22886,7 @@ var EMPTY_MEANS_NULL = /* @__PURE__ */ new Set(["date", "integer", "float", "enu
22982
22886
  function emptyValuesToNull(fields, body, registry) {
22983
22887
  const out = { ...body };
22984
22888
  for (const f of fields) {
22985
- if (!(f.name in out)) continue;
22889
+ if (!Object.hasOwn(out, f.name)) continue;
22986
22890
  const value = out[f.name];
22987
22891
  if (value === "" && EMPTY_MEANS_NULL.has(f.field_type)) {
22988
22892
  out[f.name] = null;
@@ -23087,7 +22991,7 @@ function rangeError(name, value, min, max) {
23087
22991
  function checkFieldValues(fields, body, registry, path5 = "") {
23088
22992
  const errors = [];
23089
22993
  for (const f of fields) {
23090
- if (!(f.name in body) || f.removed === true) continue;
22994
+ if (!Object.hasOwn(body, f.name) || f.removed === true) continue;
23091
22995
  const name = `${path5}${f.name}`;
23092
22996
  const message = valueError(f, body[f.name], name);
23093
22997
  if (message) errors.push({ field: name, message });
@@ -23097,20 +23001,18 @@ function checkFieldValues(fields, body, registry, path5 = "") {
23097
23001
  }
23098
23002
  return errors;
23099
23003
  }
23100
- function quoteIdent3(name) {
23004
+ function quoteIdent2(name) {
23101
23005
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
23102
23006
  return `"${name}"`;
23103
23007
  }
23104
23008
  async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
23105
23009
  const result = await db.execute(
23106
- sql`SELECT * FROM ${sql.raw(quoteIdent3(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
23010
+ sql`SELECT * FROM ${sql.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC, id ASC`
23107
23011
  );
23108
23012
  const rows = result.rows;
23109
23013
  for (const nf of pType.fields) {
23110
23014
  if (nf.db_column !== null) continue;
23111
- const comp = nf.ui_component;
23112
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
23113
- const nType = registry.paragraph_types[comp.ref];
23015
+ const nType = paragraphTypeOf(nf, registry);
23114
23016
  if (!nType) continue;
23115
23017
  for (const row of rows) {
23116
23018
  row[nf.name] = oneOrMany(
@@ -23122,35 +23024,36 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
23122
23024
  return rows;
23123
23025
  }
23124
23026
  async function loadRelationsForEdit(db, registry, fields, id, row) {
23125
- for (const f of fields) {
23126
- if (f.db_column === null) {
23127
- const comp = f.ui_component;
23128
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
23129
- const pType = registry.paragraph_types[comp.ref];
23130
- if (!pType) continue;
23131
- row[f.name] = oneOrMany(relationCardinality(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name));
23132
- } else if (f.db_column.junction) {
23133
- const j = f.db_column.junction;
23134
- const r = await db.execute(
23135
- sql`SELECT ${sql.raw(quoteIdent3(j.right_column))} FROM ${sql.raw(quoteIdent3(j.table_name))} WHERE ${sql.raw(quoteIdent3(j.left_column))} = ${id}${junctionOrderBy(j)}`
23136
- );
23137
- row[f.name] = r.rows.map((linked) => linked[j.right_column]);
23138
- }
23139
- }
23027
+ const loaded = await Promise.all(
23028
+ fields.map(async (f) => {
23029
+ if (f.db_column === null) {
23030
+ const pType = paragraphTypeOf(f, registry);
23031
+ if (!pType) return null;
23032
+ return [f.name, oneOrMany(relationCardinality(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name))];
23033
+ }
23034
+ if (f.db_column.junction) {
23035
+ const j = f.db_column.junction;
23036
+ const r = await db.execute(
23037
+ sql`SELECT ${sql.raw(quoteIdent2(j.right_column))} FROM ${sql.raw(quoteIdent2(j.table_name))} WHERE ${sql.raw(quoteIdent2(j.left_column))} = ${id}${junctionOrderBy(j)}`
23038
+ );
23039
+ return [f.name, [...new Set(r.rows.map((linked) => linked[j.right_column]))]];
23040
+ }
23041
+ return null;
23042
+ })
23043
+ );
23044
+ for (const entry of loaded) if (entry) row[entry[0]] = entry[1];
23140
23045
  }
23141
23046
  async function valuesForPublishCheck(db, registry, requiredFields, id, storedLabels, body) {
23142
23047
  const stored = { ...storedLabels };
23143
- if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !(f.name in body)), id, stored);
23048
+ if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !Object.hasOwn(body, f.name)), id, stored);
23144
23049
  return { ...stored, ...body };
23145
23050
  }
23146
23051
  async function persistRelationFields(db, registry, fields, body, itemId, ownerTable) {
23147
23052
  const deltas = [];
23148
23053
  for (const f of fields) {
23149
- if (!(f.name in body)) continue;
23054
+ if (!Object.hasOwn(body, f.name)) continue;
23150
23055
  if (f.db_column === null) {
23151
- const comp = f.ui_component;
23152
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
23153
- const pType = registry.paragraph_types[comp.ref];
23056
+ const pType = paragraphTypeOf(f, registry);
23154
23057
  if (!pType) continue;
23155
23058
  deltas.push(await persistParagraphField(db, itemId, ownerTable, f.name, pType, paragraphItems(f, body[f.name]), registry));
23156
23059
  } else if (f.db_column.junction) {
@@ -23161,6 +23064,46 @@ async function persistRelationFields(db, registry, fields, body, itemId, ownerTa
23161
23064
  }
23162
23065
  return deltas;
23163
23066
  }
23067
+ async function deleteOwnParagraphs(db, registry, fields, itemId) {
23068
+ const deltas = [];
23069
+ for (const f of fields) {
23070
+ const pType = paragraphTypeOf(f, registry);
23071
+ if (!pType) continue;
23072
+ deltas.push(await deleteParagraphField(db, itemId, f.name, pType, registry));
23073
+ }
23074
+ return deltas;
23075
+ }
23076
+ function releasedFromParagraphsOnly(paragraphDeltas, topLevel) {
23077
+ const stillHeld = new Set(topLevel.removed);
23078
+ return { added: [], removed: mergeMediaDeltas(...paragraphDeltas).removed.filter((mediaId) => !stillHeld.has(mediaId)) };
23079
+ }
23080
+ async function deleteItemCascade(db, registry, mediaRepo, target) {
23081
+ const { typeName, fields, mediaFields, repo, id, row } = target;
23082
+ const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : { others: [], fromOwnParagraphs: false };
23083
+ if (referrers.others.length > 0) return { ok: false, message: inUseMessage(referrers.others) };
23084
+ const topLevel = topLevelMediaDelta(mediaFields, row, null);
23085
+ if (!db || !referrers.fromOwnParagraphs) {
23086
+ try {
23087
+ await repo.delete(id);
23088
+ } catch (err) {
23089
+ if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
23090
+ throw err;
23091
+ }
23092
+ const paragraphDeltas2 = db ? await deleteOwnParagraphs(db, registry, fields, id) : [];
23093
+ await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas2), mediaRepo);
23094
+ return { ok: true };
23095
+ }
23096
+ const paragraphDeltas = await deleteOwnParagraphs(db, registry, fields, id);
23097
+ try {
23098
+ await repo.delete(id);
23099
+ } catch (err) {
23100
+ await applyMediaReferenceDelta(releasedFromParagraphsOnly(paragraphDeltas, topLevel), mediaRepo);
23101
+ if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
23102
+ throw err;
23103
+ }
23104
+ await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas), mediaRepo);
23105
+ return { ok: true };
23106
+ }
23164
23107
  async function lookupBasePathId(db, pathOrName) {
23165
23108
  const r = await db.execute(
23166
23109
  sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
@@ -23270,7 +23213,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23270
23213
  const mediaFields = contentType.fields.filter(
23271
23214
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
23272
23215
  );
23273
- const paragraphFieldDefs = contentType.fields.filter((f) => f.db_column === null);
23274
23216
  const searchableColumns = [
23275
23217
  ...contentType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
23276
23218
  ...contentType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
@@ -23420,7 +23362,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23420
23362
  }
23421
23363
  for (const f of columnBackedFields) {
23422
23364
  const key = f.db_column.column_name;
23423
- if (!(key in storageBody)) continue;
23365
+ if (!Object.hasOwn(storageBody, key)) continue;
23424
23366
  columnData[key] = storageBody[key];
23425
23367
  }
23426
23368
  if (db) {
@@ -23462,7 +23404,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23462
23404
  404
23463
23405
  );
23464
23406
  }
23465
- if (!contentType.only_one && "slug" in body) {
23407
+ if (!contentType.only_one && Object.hasOwn(body, "slug")) {
23466
23408
  const slug = body["slug"];
23467
23409
  if (typeof slug !== "string" || slug.trim() === "") {
23468
23410
  return c.json(
@@ -23532,11 +23474,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23532
23474
  }
23533
23475
  const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
23534
23476
  const patchData = {};
23535
- if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
23536
- if ("published" in body) patchData["published"] = body["published"];
23477
+ if (!contentType.only_one && Object.hasOwn(body, "slug")) patchData["slug"] = body["slug"];
23478
+ if (Object.hasOwn(body, "published")) patchData["published"] = body["published"];
23537
23479
  for (const f of patchColumnBackedFields) {
23538
23480
  const key = f.db_column.column_name;
23539
- if (!(key in storageBody)) continue;
23481
+ if (!Object.hasOwn(storageBody, key)) continue;
23540
23482
  patchData[key] = storageBody[key];
23541
23483
  }
23542
23484
  const updated = await repo.update(id, patchData);
@@ -23586,31 +23528,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23586
23528
  404
23587
23529
  );
23588
23530
  }
23589
- const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
23590
- if (referrers.length > 0) {
23591
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
23592
- }
23593
- try {
23594
- await repo.delete(id);
23595
- } catch (err) {
23596
- if (isForeignKeyViolation(err)) {
23597
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
23598
- }
23599
- throw err;
23600
- }
23601
- const mediaDeltas = [
23602
- topLevelMediaDelta(mediaFields, item, null)
23603
- ];
23604
- if (db) {
23605
- for (const f of paragraphFieldDefs) {
23606
- const comp = f.ui_component;
23607
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
23608
- const pType = registry.paragraph_types[comp.ref];
23609
- if (!pType) continue;
23610
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
23611
- }
23531
+ const outcome = await deleteItemCascade(db, registry, mediaRepo, {
23532
+ typeName,
23533
+ fields: contentType.fields,
23534
+ mediaFields,
23535
+ repo,
23536
+ id,
23537
+ row: item
23538
+ });
23539
+ if (!outcome.ok) {
23540
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
23612
23541
  }
23613
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
23614
23542
  return c.json({ ok: true });
23615
23543
  }
23616
23544
  );
@@ -23631,7 +23559,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23631
23559
  const mediaFields = taxonomyType.fields.filter(
23632
23560
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
23633
23561
  );
23634
- const paragraphFieldDefs = taxonomyType.fields.filter((f) => f.db_column === null);
23635
23562
  const searchableColumns = [
23636
23563
  ...taxonomyType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
23637
23564
  ...taxonomyType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
@@ -23717,7 +23644,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23717
23644
  };
23718
23645
  for (const f of taxColumnBackedFields) {
23719
23646
  const key = f.db_column.column_name;
23720
- if (!(key in storageBody)) continue;
23647
+ if (!Object.hasOwn(storageBody, key)) continue;
23721
23648
  taxColumnData[key] = storageBody[key];
23722
23649
  }
23723
23650
  const item = await repo.create(taxColumnData);
@@ -23769,10 +23696,10 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23769
23696
  }
23770
23697
  const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
23771
23698
  const taxPatchData = {};
23772
- if ("published" in body) taxPatchData["published"] = body["published"];
23699
+ if (Object.hasOwn(body, "published")) taxPatchData["published"] = body["published"];
23773
23700
  for (const f of taxPatchColumnBackedFields) {
23774
23701
  const key = f.db_column.column_name;
23775
- if (!(key in storageBody)) continue;
23702
+ if (!Object.hasOwn(storageBody, key)) continue;
23776
23703
  taxPatchData[key] = storageBody[key];
23777
23704
  }
23778
23705
  const updated = await repo.update(id, taxPatchData);
@@ -23805,31 +23732,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23805
23732
  404
23806
23733
  );
23807
23734
  }
23808
- const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : [];
23809
- if (referrers.length > 0) {
23810
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: inUseMessage(referrers) } }, 409);
23811
- }
23812
- try {
23813
- await repo.delete(id);
23814
- } catch (err) {
23815
- if (isForeignKeyViolation(err)) {
23816
- return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: IN_USE_RACE_MESSAGE } }, 409);
23817
- }
23818
- throw err;
23819
- }
23820
- const mediaDeltas = [
23821
- topLevelMediaDelta(mediaFields, item, null)
23822
- ];
23823
- if (db) {
23824
- for (const f of paragraphFieldDefs) {
23825
- const comp = f.ui_component;
23826
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
23827
- const pType = registry.paragraph_types[comp.ref];
23828
- if (!pType) continue;
23829
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
23830
- }
23735
+ const outcome = await deleteItemCascade(db, registry, mediaRepo, {
23736
+ typeName,
23737
+ fields: taxonomyType.fields,
23738
+ mediaFields,
23739
+ repo,
23740
+ id,
23741
+ row: item
23742
+ });
23743
+ if (!outcome.ok) {
23744
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
23831
23745
  }
23832
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
23833
23746
  return c.json({ ok: true });
23834
23747
  }
23835
23748
  );
@@ -24774,7 +24687,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
24774
24687
  }
24775
24688
  var API_VERSION = (() => {
24776
24689
  try {
24777
- return "0.9.1";
24690
+ return "0.9.3";
24778
24691
  } catch {
24779
24692
  return "0.0.0";
24780
24693
  }
@@ -24883,7 +24796,7 @@ function codeError(code, message) {
24883
24796
  err.code = code;
24884
24797
  return err;
24885
24798
  }
24886
- function quoteIdent4(name) {
24799
+ function quoteIdent3(name) {
24887
24800
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
24888
24801
  throw new Error(`Unsafe SQL identifier: ${name}`);
24889
24802
  }
@@ -24934,7 +24847,7 @@ function whereFragment(conds) {
24934
24847
  function createDrizzleContentRepository(db, tableName, options = {}) {
24935
24848
  const { relations = {}, publishedRelations = false, sortableColumns } = options;
24936
24849
  function tableRaw() {
24937
- return sql.raw(quoteIdent4(tableName));
24850
+ return sql.raw(quoteIdent3(tableName));
24938
24851
  }
24939
24852
  async function resolveRows(rows, include, cache) {
24940
24853
  if (rows.length === 0) return rows;
@@ -24977,7 +24890,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
24977
24890
  const offset = (page - 1) * per_page;
24978
24891
  const conds = buildConditions(published_only, filters, search);
24979
24892
  const where = whereFragment(conds);
24980
- const sortCol = sql.raw(quoteIdent4(sort_by));
24893
+ const sortCol = sql.raw(quoteIdent3(sort_by));
24981
24894
  const sortDir = sql.raw(sort_order === "desc" ? "DESC" : "ASC");
24982
24895
  const countResult = await db.execute(
24983
24896
  sql`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
@@ -25031,7 +24944,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
25031
24944
  updated_at: /* @__PURE__ */ new Date()
25032
24945
  };
25033
24946
  const entries = Object.entries(record).filter(([, v2]) => v2 !== void 0);
25034
- const colsSql = sql.join(entries.map(([k]) => sql.raw(quoteIdent4(k))), sql`, `);
24947
+ const colsSql = sql.join(entries.map(([k]) => sql.raw(quoteIdent3(k))), sql`, `);
25035
24948
  const valsSql = sql.join(entries.map(([, v2]) => sql`${v2}`), sql`, `);
25036
24949
  const result = await db.execute(
25037
24950
  sql`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
@@ -25048,7 +24961,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
25048
24961
  return this.findOne(id);
25049
24962
  }
25050
24963
  const setClauses = sql.join(
25051
- entries.map(([k, v2]) => sql`${sql.raw(quoteIdent4(k))} = ${v2}`),
24964
+ entries.map(([k, v2]) => sql`${sql.raw(quoteIdent3(k))} = ${v2}`),
25052
24965
  sql`, `
25053
24966
  );
25054
24967
  const result = await db.execute(
@@ -25457,7 +25370,7 @@ function createCmsApp(options) {
25457
25370
  handler: null,
25458
25371
  error: null
25459
25372
  }));
25460
- const ready = import("./handler-MFF32JB4-MEMKPHHQ.js").then(({ createGraphQLHandler }) => {
25373
+ const ready = import("./handler-N5IMQJBP-USVWSLZP.js").then(({ createGraphQLHandler }) => {
25461
25374
  gqlPasses.forEach((pass, i) => {
25462
25375
  const mount = mounts[i];
25463
25376
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbykim/manguito-cms-cli",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "license": "MIT",
5
5
  "author": "Bobby Kim <bobby.sihun.kim@gmail.com>",
6
6
  "homepage": "https://github.com/bobbykim89/manguito-cms",
@@ -29,10 +29,10 @@
29
29
  "tsup": "^8.5.1",
30
30
  "typescript": "^6.0.3",
31
31
  "vite": "^8.1.3",
32
- "@bobbykim/manguito-cms-admin": "0.6.0",
33
- "@bobbykim/manguito-cms-db": "0.2.0",
34
- "@bobbykim/manguito-cms-core": "0.10.0",
35
- "@bobbykim/manguito-cms-api": "0.9.1"
32
+ "@bobbykim/manguito-cms-admin": "0.6.2",
33
+ "@bobbykim/manguito-cms-api": "0.9.3",
34
+ "@bobbykim/manguito-cms-db": "0.2.1",
35
+ "@bobbykim/manguito-cms-core": "0.11.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.9.5",