@bobbykim/manguito-cms-cli 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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-N75KN2YC.js";
66
+ } from "./chunk-EVB2E4A5.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({
@@ -5264,9 +5272,10 @@ import { Command } from "commander";
5264
5272
 
5265
5273
  // src/commands/build.ts
5266
5274
  init_esm_shims();
5267
- import { mkdirSync, statSync, existsSync as existsSync3, readdirSync, writeFileSync } from "fs";
5275
+ import { mkdirSync, statSync, existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
5268
5276
  import { resolve as resolve3, join as join7, dirname } from "path";
5269
5277
  import { createRequire } from "module";
5278
+ import { fileURLToPath } from "url";
5270
5279
  import { build as viteBuild } from "vite";
5271
5280
  import { build as tsupBuild } from "tsup";
5272
5281
 
@@ -16713,6 +16722,7 @@ function drizzle2(...params) {
16713
16722
  })(drizzle2 || (drizzle2 = {}));
16714
16723
 
16715
16724
  // ../db/dist/index.mjs
16725
+ import { createHash } from "crypto";
16716
16726
  import { execSync, spawnSync } from "child_process";
16717
16727
  import crypto2 from "crypto";
16718
16728
  import path from "path";
@@ -16801,6 +16811,256 @@ function createPostgresAdapter(options = {}) {
16801
16811
  }
16802
16812
  };
16803
16813
  }
16814
+ var MAX_IDENTIFIER = 63;
16815
+ var SUFFIX = "_unique";
16816
+ function linkUniqueName(tableName) {
16817
+ const plain = `${tableName}${SUFFIX}`;
16818
+ if (plain.length <= MAX_IDENTIFIER) return plain;
16819
+ const hash = createHash("sha256").update(tableName).digest("hex").slice(0, 8);
16820
+ return `${tableName.slice(0, 47)}_${hash}${SUFFIX}`;
16821
+ }
16822
+ function linkDedupeSql(table, left, right, hasOrder) {
16823
+ const order = hasOrder ? '(a."order", a.ctid) > (b."order", b.ctid)' : "a.ctid > b.ctid";
16824
+ return `DELETE FROM "${table}" a USING "${table}" b WHERE a."${left}" = b."${left}" AND a."${right}" = b."${right}" AND ${order}`;
16825
+ }
16826
+ var ADD_LINK_UNIQUE = /ALTER TABLE "([^"]+)" ADD CONSTRAINT "([^"]+)" UNIQUE\("left_id","right_id"\);/g;
16827
+ function addLinkDedupe(sql6) {
16828
+ return sql6.replace(
16829
+ ADD_LINK_UNIQUE,
16830
+ (statement, table, name) => [
16831
+ "-- Removes only repeated (left, right) pairs, keeping the lowest order, before adding the constraint.",
16832
+ "DO $$ BEGIN",
16833
+ `IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = '"${table}"'::regclass AND conname = '${name}') THEN`,
16834
+ `LOCK TABLE "${table}" IN SHARE ROW EXCLUSIVE MODE;`,
16835
+ `${linkDedupeSql(table, "left_id", "right_id", true)};`,
16836
+ statement,
16837
+ "END IF;",
16838
+ "END $$;"
16839
+ ].join("\n")
16840
+ );
16841
+ }
16842
+ async function ensureLinkConstraints(db, registry) {
16843
+ const owners = [
16844
+ ...Object.values(registry.content_types),
16845
+ ...Object.values(registry.taxonomy_types)
16846
+ ];
16847
+ const added = [];
16848
+ for (const owner of owners) {
16849
+ for (const jt of owner.db.junction_tables) {
16850
+ const table = jt.table_name;
16851
+ const name = linkUniqueName(table);
16852
+ const exists2 = await db.execute(
16853
+ sql`SELECT to_regclass(${`"${table}"`}) IS NOT NULL AS found`
16854
+ );
16855
+ if (!exists2.rows[0].found) continue;
16856
+ const constrained = await db.execute(
16857
+ sql`SELECT 1 FROM pg_constraint WHERE conrelid = ${`"${table}"`}::regclass AND conname = ${name}`
16858
+ );
16859
+ if (constrained.rows.length > 0) continue;
16860
+ const orderColumn = await db.execute(
16861
+ sql`SELECT 1 FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ${table} AND column_name = 'order'`
16862
+ );
16863
+ const deleted = await db.execute(
16864
+ sql.raw(
16865
+ `${linkDedupeSql(table, jt.left_column, jt.right_column, orderColumn.rows.length > 0)} RETURNING 1`
16866
+ )
16867
+ );
16868
+ await db.execute(
16869
+ sql.raw(
16870
+ `ALTER TABLE "${table}" ADD CONSTRAINT "${name}" UNIQUE("${jt.left_column}", "${jt.right_column}")`
16871
+ )
16872
+ );
16873
+ added.push({ table, removed: deleted.rows.length });
16874
+ }
16875
+ }
16876
+ return added;
16877
+ }
16878
+ var LIMIT = 63;
16879
+ var stored = (name) => name.slice(0, LIMIT);
16880
+ var SYSTEM_TABLES = ["media", "base_paths", "roles", "users"];
16881
+ var SYSTEM_UNIQUES = [
16882
+ { name: "base_paths_name_unique", table: "base_paths" },
16883
+ { name: "base_paths_path_unique", table: "base_paths" },
16884
+ { name: "roles_name_unique", table: "roles" },
16885
+ { name: "roles_hierarchy_level_unique", table: "roles" },
16886
+ { name: "users_email_unique", table: "users" }
16887
+ ];
16888
+ function foreignKeyName(table, column, targetTable, targetColumn) {
16889
+ return `${table}_${column}_${targetTable}_${targetColumn}_fk`;
16890
+ }
16891
+ function checkConstraintName(column) {
16892
+ return `${column}_check`;
16893
+ }
16894
+ function primaryKeyName(table) {
16895
+ const name = `${table}_pkey`;
16896
+ return name.length <= LIMIT ? name : `${table.slice(0, LIMIT - "_pkey".length)}_pkey`;
16897
+ }
16898
+ function rawFieldPath(owner, field) {
16899
+ if (owner.schema_type !== "content-type") return `fields[${field.order}].name`;
16900
+ const t = owner.ui.tabs.findIndex((tab) => tab.fields.includes(field.name));
16901
+ const i = t < 0 ? -1 : owner.ui.tabs[t].fields.indexOf(field.name);
16902
+ return t < 0 ? `fields[${field.order}].name` : `fields[${t}].tab.fields[${i}].name`;
16903
+ }
16904
+ function declaredColumn(field) {
16905
+ const column = field.db_column?.column_name;
16906
+ return column !== void 0 && column !== field.name ? column : void 0;
16907
+ }
16908
+ function describeField(field) {
16909
+ const column = declaredColumn(field);
16910
+ return column !== void 0 ? `field "${field.name}" (column "${column}")` : `field "${field.name}"`;
16911
+ }
16912
+ function renameOf(field) {
16913
+ const column = declaredColumn(field);
16914
+ return column !== void 0 ? `change the column "${column}" of field "${field.name}"` : `rename field "${field.name}"`;
16915
+ }
16916
+ function shortenOf(field, room) {
16917
+ const column = declaredColumn(field);
16918
+ return column !== void 0 ? `shorten the column "${column}" of field "${field.name}" to at most ${room} characters` : `shorten field "${field.name}" to at most ${room} characters`;
16919
+ }
16920
+ var commonPrefixLength = (a2, b2) => {
16921
+ let i = 0;
16922
+ while (i < a2.length && i < b2.length && a2[i] === b2[i]) i++;
16923
+ return i;
16924
+ };
16925
+ function typeTableConstraints(owner) {
16926
+ const table = owner.db.table_name;
16927
+ const names = [];
16928
+ for (const field of owner.fields) {
16929
+ const col = field.db_column;
16930
+ if (field.field_type === "paragraph" || col === null || col.junction) continue;
16931
+ if (col.foreign_key) {
16932
+ const fk = col.foreign_key;
16933
+ names.push({ name: foreignKeyName(table, col.column_name, fk.table, fk.column), kind: "foreign key", field });
16934
+ }
16935
+ if ((col.check_constraint?.length ?? 0) > 0) {
16936
+ names.push({ name: checkConstraintName(col.column_name), kind: "check", field });
16937
+ }
16938
+ }
16939
+ return names;
16940
+ }
16941
+ function typeTableMessage(owner, a2, b2) {
16942
+ const table = owner.db.table_name;
16943
+ const clash = `Postgres keeps only the first ${LIMIT} characters of a name, which are the same, so the migration would fail with "constraint already exists".`;
16944
+ if (a2.kind === "foreign key" && b2.kind === "foreign key") {
16945
+ const room2 = LIMIT - 1 - table.length;
16946
+ const shared = commonPrefixLength(a2.name.slice(table.length), b2.name.slice(table.length));
16947
+ const maxType = LIMIT - 1 - shared + (owner.name.length - table.length);
16948
+ const fix = room2 < 1 ? `shorten the name of type "${owner.name}" to at most ${maxType} characters: its table name "${table}" leaves no room for a column in the first ${LIMIT} characters` : `${renameOf(a2.field)} or ${renameOf(b2.field)} so that the two differ within their first ${room2} characters, or shorten the name of type "${owner.name}"`;
16949
+ return `The ${describeField(a2.field)} and the ${describeField(b2.field)} of "${owner.name}" get foreign keys "${a2.name}" and "${b2.name}". ${clash} To fix it, ${fix}.`;
16950
+ }
16951
+ if (a2.kind === "check" && b2.kind === "check") {
16952
+ const room2 = LIMIT - "_check".length;
16953
+ return `The ${describeField(a2.field)} and the ${describeField(b2.field)} of "${owner.name}" get check constraints "${a2.name}" and "${b2.name}". ${clash} To fix it, ${renameOf(a2.field)} or ${renameOf(b2.field)} so that the two differ within their first ${room2} characters.`;
16954
+ }
16955
+ const check = a2.kind === "check" ? a2 : b2;
16956
+ const room = LIMIT - 1 - "_check".length;
16957
+ return `The ${a2.kind} "${a2.name}" of the ${describeField(a2.field)} and the ${b2.kind} "${b2.name}" of the ${describeField(b2.field)}, both on "${owner.name}", are the same in their first ${LIMIT} characters. ${clash} To fix it, ${shortenOf(check.field, room)}.`;
16958
+ }
16959
+ function checkTypeTable(owner) {
16960
+ const errors = [];
16961
+ const first = /* @__PURE__ */ new Map();
16962
+ for (const constraint of typeTableConstraints(owner)) {
16963
+ const earlier = first.get(stored(constraint.name));
16964
+ if (!earlier) {
16965
+ first.set(stored(constraint.name), constraint);
16966
+ continue;
16967
+ }
16968
+ errors.push({
16969
+ file: owner.source_file,
16970
+ code: "CONSTRAINT_NAME_COLLISION",
16971
+ message: typeTableMessage(owner, earlier, constraint),
16972
+ path: rawFieldPath(owner, constraint.field)
16973
+ });
16974
+ }
16975
+ return errors;
16976
+ }
16977
+ function checkLinkTable(link, left, right, rightTable) {
16978
+ const { owner, field, table } = link;
16979
+ const leftFk = foreignKeyName(table, left, owner.db.table_name, "id");
16980
+ const rightFk = foreignKeyName(table, right, rightTable, "id");
16981
+ const names = [leftFk, rightFk, link.unique];
16982
+ const collides = names.some((n, i) => names.some((m2, j) => i < j && stored(n) === stored(m2)));
16983
+ if (!collides) return null;
16984
+ const base = { file: owner.source_file, code: "CONSTRAINT_NAME_COLLISION", path: rawFieldPath(owner, field) };
16985
+ const truncated = `Postgres keeps only the first ${LIMIT} characters of a name`;
16986
+ if (stored(leftFk) === stored(rightFk)) {
16987
+ const prefix = `junction_${owner.db.table_name}_`.length;
16988
+ const room = LIMIT - 2 - prefix;
16989
+ const fix = 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}"`;
16990
+ return {
16991
+ ...base,
16992
+ message: `The link table of field "${field.name}" of "${owner.name}", "${table}", has ${table.length} characters, so its foreign keys "${leftFk}" and "${rightFk}" are the same in their first ${LIMIT} characters. ${truncated}, so the migration would fail with "constraint already exists". To keep the link table within ${LIMIT - 2} characters, ${fix}.`
16993
+ };
16994
+ }
16995
+ return {
16996
+ ...base,
16997
+ message: `The link table of field "${field.name}" of "${owner.name}", "${table}", gets constraints ${names.map((n) => `"${n}"`).join(", ")}, two of which are the same in their first ${LIMIT} characters. ${truncated}, so the migration would fail with "constraint already exists". To fix it, rename field "${field.name}" of type "${owner.name}".`
16998
+ };
16999
+ }
17000
+ function takenNames(registry, links) {
17001
+ const taken = [];
17002
+ for (const table of SYSTEM_TABLES) {
17003
+ taken.push({ name: table, what: `system table "${table}"` });
17004
+ taken.push({ name: primaryKeyName(table), what: `the primary key of system table "${table}"` });
17005
+ }
17006
+ for (const u of SYSTEM_UNIQUES) taken.push({ name: u.name, what: `a unique constraint of system table "${u.table}"` });
17007
+ const types3 = [
17008
+ ...Object.values(registry.taxonomy_types),
17009
+ ...Object.values(registry.paragraph_types),
17010
+ ...Object.values(registry.content_types)
17011
+ ];
17012
+ for (const t of types3) {
17013
+ taken.push({ name: t.db.table_name, what: `the table of type "${t.name}"` });
17014
+ taken.push({ name: primaryKeyName(t.db.table_name), what: `the primary key of type "${t.name}"` });
17015
+ }
17016
+ for (const l of links) taken.push({ name: l.table, what: `the link table of field "${l.field.name}" of "${l.owner.name}"` });
17017
+ return taken;
17018
+ }
17019
+ function checkLinkUniques(registry, links) {
17020
+ const first = /* @__PURE__ */ new Map();
17021
+ for (const t of takenNames(registry, links)) {
17022
+ if (!first.has(stored(t.name))) first.set(stored(t.name), t);
17023
+ }
17024
+ const errors = [];
17025
+ for (const link of links) {
17026
+ const key = stored(link.unique);
17027
+ const earlier = first.get(key);
17028
+ if (!earlier) {
17029
+ first.set(key, { name: link.unique, what: `the unique constraint of the link table of field "${link.field.name}" of "${link.owner.name}"` });
17030
+ continue;
17031
+ }
17032
+ const same = earlier.name === link.unique ? `which is also the name of ${earlier.what}` : `which is the same as ${earlier.what}, "${earlier.name}", in its first ${LIMIT} characters. Postgres keeps only the first ${LIMIT} characters of a name`;
17033
+ errors.push({
17034
+ file: link.owner.source_file,
17035
+ code: "CONSTRAINT_NAME_COLLISION",
17036
+ message: `The link table of field "${link.field.name}" of "${link.owner.name}" gets unique constraint "${link.unique}", ${same}. A unique constraint shares its name with its index, and indexes share one namespace with tables, so the migration would fail with "relation already exists". Rename field "${link.field.name}" of type "${link.owner.name}".`,
17037
+ path: rawFieldPath(link.owner, link.field)
17038
+ });
17039
+ }
17040
+ return errors;
17041
+ }
17042
+ function findConstraintNameCollisions(registry) {
17043
+ const errors = [];
17044
+ const typeTables = [
17045
+ ...Object.values(registry.taxonomy_types),
17046
+ ...Object.values(registry.paragraph_types),
17047
+ ...Object.values(registry.content_types)
17048
+ ];
17049
+ for (const owner of typeTables) errors.push(...checkTypeTable(owner));
17050
+ const links = [];
17051
+ for (const owner of [...Object.values(registry.content_types), ...Object.values(registry.taxonomy_types)]) {
17052
+ for (const jt of owner.db.junction_tables) {
17053
+ const field = owner.fields.find((f) => f.db_column?.junction?.table_name === jt.table_name);
17054
+ if (!field) continue;
17055
+ const link = { owner, field, table: jt.table_name, unique: linkUniqueName(jt.table_name) };
17056
+ links.push(link);
17057
+ const error = checkLinkTable(link, jt.left_column, jt.right_column, jt.right_table);
17058
+ if (error) errors.push(error);
17059
+ }
17060
+ }
17061
+ errors.push(...checkLinkUniques(registry, links));
17062
+ return errors;
17063
+ }
16804
17064
  function generateFileHeader() {
16805
17065
  return "import * as s from 'drizzle-orm/pg-core'\nimport { sql } from 'drizzle-orm'";
16806
17066
  }
@@ -16935,7 +17195,7 @@ ${colBlock2}
16935
17195
  const constraints = enumFields.map((f) => {
16936
17196
  const col = f.db_column;
16937
17197
  const values = col.check_constraint.map((v2) => `'${v2}'`).join(", ");
16938
- const cname = `${col.column_name}_check`;
17198
+ const cname = checkConstraintName(col.column_name);
16939
17199
  return [
16940
17200
  ` ${cname}: s.check(`,
16941
17201
  ` '${cname}',`,
@@ -16997,7 +17257,10 @@ function generateJunctionTables(registry) {
16997
17257
  ` .notNull()`,
16998
17258
  ` .references(() => ${rightVar}.id, { onDelete: 'cascade' }),`,
16999
17259
  ...orderLine ? [orderLine] : [],
17000
- ` }`,
17260
+ ` },`,
17261
+ ` (table) => ({`,
17262
+ ` pair: s.unique('${linkUniqueName(jt.table_name)}').on(table.${jt.left_column}, table.${jt.right_column}),`,
17263
+ ` })`,
17001
17264
  `)`
17002
17265
  ].join("\n");
17003
17266
  tables.push(def);
@@ -17236,7 +17499,14 @@ async function runGenerate(configPath, outFolder, cwd) {
17236
17499
  } catch {
17237
17500
  }
17238
17501
  const beforeSet = new Set(beforeFiles);
17239
- return afterFiles.filter((f) => !beforeSet.has(f));
17502
+ const added = afterFiles.filter((f) => !beforeSet.has(f));
17503
+ for (const name of added) {
17504
+ const file = path.join(outFolder, name);
17505
+ const text2 = await fs.readFile(file, "utf8");
17506
+ const rewritten = addLinkDedupe(text2);
17507
+ if (rewritten !== text2) await fs.writeFile(file, rewritten);
17508
+ }
17509
+ return added;
17240
17510
  }
17241
17511
  async function generateMigration(configPath, migrationsFolder) {
17242
17512
  return runGenerate(configPath, migrationsFolder, path.dirname(configPath));
@@ -18670,6 +18940,14 @@ function resolveSchemaConfig(cwd, config2) {
18670
18940
  };
18671
18941
  }
18672
18942
 
18943
+ // src/utils/schema-checks.ts
18944
+ init_esm_shims();
18945
+ function checkRegistry(registry, maxFileSize) {
18946
+ const crossRefErrors = validateCrossReferences(registry, maxFileSize);
18947
+ if (crossRefErrors.length > 0) return crossRefErrors;
18948
+ return findConstraintNameCollisions(registry);
18949
+ }
18950
+
18673
18951
  // src/utils/env.ts
18674
18952
  init_esm_shims();
18675
18953
  import { existsSync as existsSync2 } from "fs";
@@ -18757,7 +19035,7 @@ async function runBuild(options, deps) {
18757
19035
  parsedRoles
18758
19036
  );
18759
19037
  printSchemaDeprecations(findSchemaDeprecations(registry));
18760
- const crossRefErrors = validateCrossReferences(registry, config2.api.media?.max_file_size);
19038
+ const crossRefErrors = checkRegistry(registry, config2.api.media?.max_file_size);
18761
19039
  if (crossRefErrors.length > 0) {
18762
19040
  printValidationErrors(crossRefErrors, "Schema reference errors", "manguito build");
18763
19041
  process.exit(1);
@@ -18791,6 +19069,11 @@ async function runBuild(options, deps) {
18791
19069
  { adminPrefix: config2.admin.prefix, apiPrefix: config2.api.prefix },
18792
19070
  generatedDir
18793
19071
  );
19072
+ writeFileSync(
19073
+ join7(generatedDir, VERSIONS_RECORD),
19074
+ JSON.stringify(installedManguitoVersions(cwd), null, 2) + "\n",
19075
+ "utf8"
19076
+ );
18794
19077
  printSuccess("Codegen complete");
18795
19078
  try {
18796
19079
  await viteBuild({
@@ -18839,11 +19122,76 @@ async function runBuild(options, deps) {
18839
19122
  printSuccess("Server compiled");
18840
19123
  process.stdout.write("\nBuild complete \u2192 dist/\n");
18841
19124
  }
18842
- async function needsRebuild(cwd) {
19125
+ var VERSIONS_RECORD = "manguito-versions.json";
19126
+ var MANGUITO_PACKAGES = [
19127
+ "@bobbykim/manguito-cms-core",
19128
+ "@bobbykim/manguito-cms-db",
19129
+ "@bobbykim/manguito-cms-api",
19130
+ "@bobbykim/manguito-cms-admin",
19131
+ "@bobbykim/manguito-cms-cli"
19132
+ ];
19133
+ function installedManguitoVersions(cwd) {
19134
+ const cwdRequire = createRequire(join7(cwd, "package.json"));
19135
+ const versions = {};
19136
+ for (const name of MANGUITO_PACKAGES) {
19137
+ try {
19138
+ let dir = dirname(cwdRequire.resolve(name));
19139
+ for (; ; ) {
19140
+ const pkgPath = join7(dir, "package.json");
19141
+ if (existsSync3(pkgPath)) {
19142
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
19143
+ if (pkg.name === name && typeof pkg.version === "string") versions[name] = pkg.version;
19144
+ break;
19145
+ }
19146
+ const parent = dirname(dir);
19147
+ if (parent === dir) break;
19148
+ dir = parent;
19149
+ }
19150
+ } catch {
19151
+ }
19152
+ }
19153
+ const running = runningCliVersion();
19154
+ if (running !== void 0) versions[RUNNING_CLI_KEY] = running;
19155
+ return versions;
19156
+ }
19157
+ var RUNNING_CLI_KEY = "running-cli";
19158
+ var CLI_PACKAGE = "@bobbykim/manguito-cms-cli";
19159
+ function runningCliVersion() {
19160
+ try {
19161
+ let dir = dirname(fileURLToPath(import.meta.url));
19162
+ for (; ; ) {
19163
+ const pkgPath = join7(dir, "package.json");
19164
+ if (existsSync3(pkgPath)) {
19165
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
19166
+ if (pkg.name === CLI_PACKAGE && typeof pkg.version === "string") return pkg.version;
19167
+ }
19168
+ const parent = dirname(dir);
19169
+ if (parent === dir) return void 0;
19170
+ dir = parent;
19171
+ }
19172
+ } catch {
19173
+ return void 0;
19174
+ }
19175
+ }
19176
+ function recordedVersions(cwd) {
19177
+ try {
19178
+ return JSON.parse(readFileSync2(resolve3(cwd, "dist/generated", VERSIONS_RECORD), "utf8"));
19179
+ } catch {
19180
+ return null;
19181
+ }
19182
+ }
19183
+ function sameVersions(a2, b2) {
19184
+ const keys = Object.keys(a2);
19185
+ return keys.length === Object.keys(b2).length && keys.every((k) => a2[k] === b2[k]);
19186
+ }
19187
+ async function needsRebuild(cwd, options = {}) {
19188
+ const installed = options.installed ?? installedManguitoVersions(cwd);
18843
19189
  const sentinel = resolve3(cwd, "dist/generated/schema.ts");
18844
19190
  if (!existsSync3(sentinel)) return true;
19191
+ const recorded = recordedVersions(cwd);
19192
+ if (recorded === null || !sameVersions(recorded, installed)) return true;
18845
19193
  const sentinelMtime = statSync(sentinel).mtimeMs;
18846
- const schemasDir = resolve3(cwd, "schemas");
19194
+ const schemasDir = options.schemaDir ?? resolve3(cwd, "schemas");
18847
19195
  if (!existsSync3(schemasDir)) return false;
18848
19196
  return walkFiles(schemasDir).some(
18849
19197
  (file) => statSync(file).mtimeMs > sentinelMtime
@@ -21901,7 +22249,7 @@ var ERROR_STATUS_MAP = {
21901
22249
  var errorHandler2 = (err, c) => {
21902
22250
  console.error(err.stack ?? err.message);
21903
22251
  const raw2 = err.code;
21904
- const deliberate = raw2 !== void 0 && raw2 !== "INTERNAL_ERROR" && raw2 in ERROR_STATUS_MAP;
22252
+ const deliberate = raw2 !== void 0 && raw2 !== "INTERNAL_ERROR" && Object.hasOwn(ERROR_STATUS_MAP, raw2);
21905
22253
  if (!deliberate) {
21906
22254
  return c.json({ ok: false, error: { code: "INTERNAL_ERROR", message: "Internal server error" } }, 500);
21907
22255
  }
@@ -22464,16 +22812,16 @@ function parseFilters(url, validFields, columnFor) {
22464
22812
  }
22465
22813
  return { ok: true, filters };
22466
22814
  }
22815
+ function pickProgrammatic(resolved, names) {
22816
+ const out = {};
22817
+ for (const name of names) if (Object.hasOwn(resolved, name)) out[name] = resolved[name];
22818
+ return out;
22819
+ }
22467
22820
  function isPublished(item) {
22468
22821
  return item["published"] === true;
22469
22822
  }
22470
22823
  function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
22471
22824
  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
22825
  async function respondItem(row, typeName, programmatic) {
22478
22826
  const data = projectRow(row, typeName, projectors);
22479
22827
  if (!resolver?.hasSchema(typeName)) return data;
@@ -22785,25 +23133,42 @@ function quoteIdent(name) {
22785
23133
  function isRequiredSingleReferenceTo(f, targetType) {
22786
23134
  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
23135
  }
23136
+ function requiredReferencesTo(owners, targetType) {
23137
+ return owners.flatMap(
23138
+ (owner) => owner.fields.filter((f) => isRequiredSingleReferenceTo(f, targetType)).map((f) => ({ owner, f }))
23139
+ );
23140
+ }
22788
23141
  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
- }
23142
+ const targetFields = registry.content_types[targetType]?.fields ?? registry.taxonomy_types[targetType]?.fields ?? [];
23143
+ const itemOwners = [...Object.values(registry.content_types), ...Object.values(registry.taxonomy_types)];
23144
+ const itemFields = requiredReferencesTo(itemOwners, targetType);
23145
+ const paragraphFields = requiredReferencesTo(Object.values(registry.paragraph_types), targetType);
23146
+ const ownIds = paragraphFields.length > 0 ? await ownParagraphRowIds(db, registry, targetFields, id) : [];
23147
+ const [itemCounts, paragraphCounts] = await Promise.all([
23148
+ Promise.all(
23149
+ itemFields.map(async ({ owner, f }) => {
23150
+ const notSelf = owner.name === targetType ? sql` AND id <> ${id}` : sql``;
23151
+ const r = await db.execute(
23152
+ 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}`
23153
+ );
23154
+ return { owner, f, n: r.rows[0].n, own: 0 };
23155
+ })
23156
+ ),
23157
+ Promise.all(
23158
+ paragraphFields.map(async ({ owner, f }) => {
23159
+ const r = await db.execute(
23160
+ 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}`
23161
+ );
23162
+ const { n, own } = r.rows[0];
23163
+ return { owner, f, n, own };
23164
+ })
23165
+ )
23166
+ ]);
23167
+ const others = [];
23168
+ for (const { owner, f, n, own } of [...itemCounts, ...paragraphCounts]) {
23169
+ if (n - own > 0) others.push({ type_label: owner.label, field_label: f.label, count: n - own });
22805
23170
  }
22806
- return out;
23171
+ return { others, fromOwnParagraphs: paragraphCounts.some((c) => c.own > 0) };
22807
23172
  }
22808
23173
  function inUseMessage(referrers) {
22809
23174
  const total = referrers.reduce((sum, r) => sum + r.count, 0);
@@ -22828,7 +23193,7 @@ function topLevelMediaDelta(mediaFields, before, after) {
22828
23193
  for (const f of mediaFields) {
22829
23194
  const key = f.db_column?.column_name;
22830
23195
  if (key === void 0 || key === "") continue;
22831
- if (after !== null && !(key in after)) continue;
23196
+ if (after !== null && !Object.hasOwn(after, key)) continue;
22832
23197
  const oldId = before ? extractMediaId(before[key]) : null;
22833
23198
  const newId = after ? extractMediaId(after[key]) : null;
22834
23199
  if (newId === oldId) continue;
@@ -22851,127 +23216,6 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
22851
23216
  if (netAdd.length > 0) await mediaRepo.incrementReferenceCount(netAdd);
22852
23217
  if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
22853
23218
  }
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
23219
  var VARCHAR_DEFAULT_LIMIT = 255;
22976
23220
  var INTEGER_MIN = -2147483648;
22977
23221
  var INTEGER_MAX = 2147483647;
@@ -22982,7 +23226,7 @@ var EMPTY_MEANS_NULL = /* @__PURE__ */ new Set(["date", "integer", "float", "enu
22982
23226
  function emptyValuesToNull(fields, body, registry) {
22983
23227
  const out = { ...body };
22984
23228
  for (const f of fields) {
22985
- if (!(f.name in out)) continue;
23229
+ if (!Object.hasOwn(out, f.name)) continue;
22986
23230
  const value = out[f.name];
22987
23231
  if (value === "" && EMPTY_MEANS_NULL.has(f.field_type)) {
22988
23232
  out[f.name] = null;
@@ -23087,7 +23331,7 @@ function rangeError(name, value, min, max) {
23087
23331
  function checkFieldValues(fields, body, registry, path5 = "") {
23088
23332
  const errors = [];
23089
23333
  for (const f of fields) {
23090
- if (!(f.name in body) || f.removed === true) continue;
23334
+ if (!Object.hasOwn(body, f.name) || f.removed === true) continue;
23091
23335
  const name = `${path5}${f.name}`;
23092
23336
  const message = valueError(f, body[f.name], name);
23093
23337
  if (message) errors.push({ field: name, message });
@@ -23097,20 +23341,18 @@ function checkFieldValues(fields, body, registry, path5 = "") {
23097
23341
  }
23098
23342
  return errors;
23099
23343
  }
23100
- function quoteIdent3(name) {
23344
+ function quoteIdent2(name) {
23101
23345
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
23102
23346
  return `"${name}"`;
23103
23347
  }
23104
23348
  async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
23105
23349
  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`
23350
+ 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
23351
  );
23108
23352
  const rows = result.rows;
23109
23353
  for (const nf of pType.fields) {
23110
23354
  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];
23355
+ const nType = paragraphTypeOf(nf, registry);
23114
23356
  if (!nType) continue;
23115
23357
  for (const row of rows) {
23116
23358
  row[nf.name] = oneOrMany(
@@ -23122,35 +23364,36 @@ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
23122
23364
  return rows;
23123
23365
  }
23124
23366
  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
- }
23367
+ const loaded = await Promise.all(
23368
+ fields.map(async (f) => {
23369
+ if (f.db_column === null) {
23370
+ const pType = paragraphTypeOf(f, registry);
23371
+ if (!pType) return null;
23372
+ return [f.name, oneOrMany(relationCardinality(f) ?? "many", await loadParagraphRows(db, registry, pType, id, f.name))];
23373
+ }
23374
+ if (f.db_column.junction) {
23375
+ const j = f.db_column.junction;
23376
+ const r = await db.execute(
23377
+ 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)}`
23378
+ );
23379
+ return [f.name, [...new Set(r.rows.map((linked) => linked[j.right_column]))]];
23380
+ }
23381
+ return null;
23382
+ })
23383
+ );
23384
+ for (const entry of loaded) if (entry) row[entry[0]] = entry[1];
23140
23385
  }
23141
23386
  async function valuesForPublishCheck(db, registry, requiredFields, id, storedLabels, body) {
23142
- const stored = { ...storedLabels };
23143
- if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !(f.name in body)), id, stored);
23144
- return { ...stored, ...body };
23387
+ const stored2 = { ...storedLabels };
23388
+ if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !Object.hasOwn(body, f.name)), id, stored2);
23389
+ return { ...stored2, ...body };
23145
23390
  }
23146
23391
  async function persistRelationFields(db, registry, fields, body, itemId, ownerTable) {
23147
23392
  const deltas = [];
23148
23393
  for (const f of fields) {
23149
- if (!(f.name in body)) continue;
23394
+ if (!Object.hasOwn(body, f.name)) continue;
23150
23395
  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];
23396
+ const pType = paragraphTypeOf(f, registry);
23154
23397
  if (!pType) continue;
23155
23398
  deltas.push(await persistParagraphField(db, itemId, ownerTable, f.name, pType, paragraphItems(f, body[f.name]), registry));
23156
23399
  } else if (f.db_column.junction) {
@@ -23161,6 +23404,46 @@ async function persistRelationFields(db, registry, fields, body, itemId, ownerTa
23161
23404
  }
23162
23405
  return deltas;
23163
23406
  }
23407
+ async function deleteOwnParagraphs(db, registry, fields, itemId) {
23408
+ const deltas = [];
23409
+ for (const f of fields) {
23410
+ const pType = paragraphTypeOf(f, registry);
23411
+ if (!pType) continue;
23412
+ deltas.push(await deleteParagraphField(db, itemId, f.name, pType, registry));
23413
+ }
23414
+ return deltas;
23415
+ }
23416
+ function releasedFromParagraphsOnly(paragraphDeltas, topLevel) {
23417
+ const stillHeld = new Set(topLevel.removed);
23418
+ return { added: [], removed: mergeMediaDeltas(...paragraphDeltas).removed.filter((mediaId) => !stillHeld.has(mediaId)) };
23419
+ }
23420
+ async function deleteItemCascade(db, registry, mediaRepo, target) {
23421
+ const { typeName, fields, mediaFields, repo, id, row } = target;
23422
+ const referrers = db ? await findRequiredReferrers(db, registry, typeName, id) : { others: [], fromOwnParagraphs: false };
23423
+ if (referrers.others.length > 0) return { ok: false, message: inUseMessage(referrers.others) };
23424
+ const topLevel = topLevelMediaDelta(mediaFields, row, null);
23425
+ if (!db || !referrers.fromOwnParagraphs) {
23426
+ try {
23427
+ await repo.delete(id);
23428
+ } catch (err) {
23429
+ if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
23430
+ throw err;
23431
+ }
23432
+ const paragraphDeltas2 = db ? await deleteOwnParagraphs(db, registry, fields, id) : [];
23433
+ await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas2), mediaRepo);
23434
+ return { ok: true };
23435
+ }
23436
+ const paragraphDeltas = await deleteOwnParagraphs(db, registry, fields, id);
23437
+ try {
23438
+ await repo.delete(id);
23439
+ } catch (err) {
23440
+ await applyMediaReferenceDelta(releasedFromParagraphsOnly(paragraphDeltas, topLevel), mediaRepo);
23441
+ if (isForeignKeyViolation(err)) return { ok: false, message: IN_USE_RACE_MESSAGE };
23442
+ throw err;
23443
+ }
23444
+ await applyMediaReferenceDelta(mergeMediaDeltas(topLevel, ...paragraphDeltas), mediaRepo);
23445
+ return { ok: true };
23446
+ }
23164
23447
  async function lookupBasePathId(db, pathOrName) {
23165
23448
  const r = await db.execute(
23166
23449
  sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
@@ -23270,7 +23553,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23270
23553
  const mediaFields = contentType.fields.filter(
23271
23554
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
23272
23555
  );
23273
- const paragraphFieldDefs = contentType.fields.filter((f) => f.db_column === null);
23274
23556
  const searchableColumns = [
23275
23557
  ...contentType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
23276
23558
  ...contentType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
@@ -23420,7 +23702,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23420
23702
  }
23421
23703
  for (const f of columnBackedFields) {
23422
23704
  const key = f.db_column.column_name;
23423
- if (!(key in storageBody)) continue;
23705
+ if (!Object.hasOwn(storageBody, key)) continue;
23424
23706
  columnData[key] = storageBody[key];
23425
23707
  }
23426
23708
  if (db) {
@@ -23462,7 +23744,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23462
23744
  404
23463
23745
  );
23464
23746
  }
23465
- if (!contentType.only_one && "slug" in body) {
23747
+ if (!contentType.only_one && Object.hasOwn(body, "slug")) {
23466
23748
  const slug = body["slug"];
23467
23749
  if (typeof slug !== "string" || slug.trim() === "") {
23468
23750
  return c.json(
@@ -23532,11 +23814,11 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23532
23814
  }
23533
23815
  const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
23534
23816
  const patchData = {};
23535
- if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
23536
- if ("published" in body) patchData["published"] = body["published"];
23817
+ if (!contentType.only_one && Object.hasOwn(body, "slug")) patchData["slug"] = body["slug"];
23818
+ if (Object.hasOwn(body, "published")) patchData["published"] = body["published"];
23537
23819
  for (const f of patchColumnBackedFields) {
23538
23820
  const key = f.db_column.column_name;
23539
- if (!(key in storageBody)) continue;
23821
+ if (!Object.hasOwn(storageBody, key)) continue;
23540
23822
  patchData[key] = storageBody[key];
23541
23823
  }
23542
23824
  const updated = await repo.update(id, patchData);
@@ -23586,31 +23868,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23586
23868
  404
23587
23869
  );
23588
23870
  }
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
- }
23871
+ const outcome = await deleteItemCascade(db, registry, mediaRepo, {
23872
+ typeName,
23873
+ fields: contentType.fields,
23874
+ mediaFields,
23875
+ repo,
23876
+ id,
23877
+ row: item
23878
+ });
23879
+ if (!outcome.ok) {
23880
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
23612
23881
  }
23613
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
23614
23882
  return c.json({ ok: true });
23615
23883
  }
23616
23884
  );
@@ -23631,7 +23899,6 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23631
23899
  const mediaFields = taxonomyType.fields.filter(
23632
23900
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
23633
23901
  );
23634
- const paragraphFieldDefs = taxonomyType.fields.filter((f) => f.db_column === null);
23635
23902
  const searchableColumns = [
23636
23903
  ...taxonomyType.fields.filter((f) => f.field_type === "text/plain" && f.db_column !== null).map((f) => f.db_column.column_name),
23637
23904
  ...taxonomyType.system_fields.some((f) => f.name === "slug") ? ["slug"] : []
@@ -23717,7 +23984,7 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23717
23984
  };
23718
23985
  for (const f of taxColumnBackedFields) {
23719
23986
  const key = f.db_column.column_name;
23720
- if (!(key in storageBody)) continue;
23987
+ if (!Object.hasOwn(storageBody, key)) continue;
23721
23988
  taxColumnData[key] = storageBody[key];
23722
23989
  }
23723
23990
  const item = await repo.create(taxColumnData);
@@ -23769,10 +24036,10 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23769
24036
  }
23770
24037
  const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
23771
24038
  const taxPatchData = {};
23772
- if ("published" in body) taxPatchData["published"] = body["published"];
24039
+ if (Object.hasOwn(body, "published")) taxPatchData["published"] = body["published"];
23773
24040
  for (const f of taxPatchColumnBackedFields) {
23774
24041
  const key = f.db_column.column_name;
23775
- if (!(key in storageBody)) continue;
24042
+ if (!Object.hasOwn(storageBody, key)) continue;
23776
24043
  taxPatchData[key] = storageBody[key];
23777
24044
  }
23778
24045
  const updated = await repo.update(id, taxPatchData);
@@ -23805,31 +24072,17 @@ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo,
23805
24072
  404
23806
24073
  );
23807
24074
  }
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
- }
24075
+ const outcome = await deleteItemCascade(db, registry, mediaRepo, {
24076
+ typeName,
24077
+ fields: taxonomyType.fields,
24078
+ mediaFields,
24079
+ repo,
24080
+ id,
24081
+ row: item
24082
+ });
24083
+ if (!outcome.ok) {
24084
+ return c.json({ ok: false, error: { code: "ITEM_IN_USE", message: outcome.message } }, 409);
23831
24085
  }
23832
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
23833
24086
  return c.json({ ok: true });
23834
24087
  }
23835
24088
  );
@@ -24774,7 +25027,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
24774
25027
  }
24775
25028
  var API_VERSION = (() => {
24776
25029
  try {
24777
- return "0.9.2";
25030
+ return "0.9.4";
24778
25031
  } catch {
24779
25032
  return "0.0.0";
24780
25033
  }
@@ -24883,7 +25136,7 @@ function codeError(code, message) {
24883
25136
  err.code = code;
24884
25137
  return err;
24885
25138
  }
24886
- function quoteIdent4(name) {
25139
+ function quoteIdent3(name) {
24887
25140
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
24888
25141
  throw new Error(`Unsafe SQL identifier: ${name}`);
24889
25142
  }
@@ -24934,7 +25187,7 @@ function whereFragment(conds) {
24934
25187
  function createDrizzleContentRepository(db, tableName, options = {}) {
24935
25188
  const { relations = {}, publishedRelations = false, sortableColumns } = options;
24936
25189
  function tableRaw() {
24937
- return sql.raw(quoteIdent4(tableName));
25190
+ return sql.raw(quoteIdent3(tableName));
24938
25191
  }
24939
25192
  async function resolveRows(rows, include, cache) {
24940
25193
  if (rows.length === 0) return rows;
@@ -24977,7 +25230,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
24977
25230
  const offset = (page - 1) * per_page;
24978
25231
  const conds = buildConditions(published_only, filters, search);
24979
25232
  const where = whereFragment(conds);
24980
- const sortCol = sql.raw(quoteIdent4(sort_by));
25233
+ const sortCol = sql.raw(quoteIdent3(sort_by));
24981
25234
  const sortDir = sql.raw(sort_order === "desc" ? "DESC" : "ASC");
24982
25235
  const countResult = await db.execute(
24983
25236
  sql`SELECT COUNT(*) AS total FROM ${tableRaw()}${where}`
@@ -25031,7 +25284,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
25031
25284
  updated_at: /* @__PURE__ */ new Date()
25032
25285
  };
25033
25286
  const entries = Object.entries(record).filter(([, v2]) => v2 !== void 0);
25034
- const colsSql = sql.join(entries.map(([k]) => sql.raw(quoteIdent4(k))), sql`, `);
25287
+ const colsSql = sql.join(entries.map(([k]) => sql.raw(quoteIdent3(k))), sql`, `);
25035
25288
  const valsSql = sql.join(entries.map(([, v2]) => sql`${v2}`), sql`, `);
25036
25289
  const result = await db.execute(
25037
25290
  sql`INSERT INTO ${tableRaw()} (${colsSql}) VALUES (${valsSql}) RETURNING *`
@@ -25048,7 +25301,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
25048
25301
  return this.findOne(id);
25049
25302
  }
25050
25303
  const setClauses = sql.join(
25051
- entries.map(([k, v2]) => sql`${sql.raw(quoteIdent4(k))} = ${v2}`),
25304
+ entries.map(([k, v2]) => sql`${sql.raw(quoteIdent3(k))} = ${v2}`),
25052
25305
  sql`, `
25053
25306
  );
25054
25307
  const result = await db.execute(
@@ -25457,7 +25710,7 @@ function createCmsApp(options) {
25457
25710
  handler: null,
25458
25711
  error: null
25459
25712
  }));
25460
- const ready = import("./handler-MFF32JB4-NUVEJZXC.js").then(({ createGraphQLHandler }) => {
25713
+ const ready = import("./handler-5OTVPIAF-JVSJWO2S.js").then(({ createGraphQLHandler }) => {
25461
25714
  gqlPasses.forEach((pass, i) => {
25462
25715
  const mount = mounts[i];
25463
25716
  try {
@@ -25743,6 +25996,7 @@ async function runDev(options, deps) {
25743
25996
  const { registry, versionModel } = loaded.value;
25744
25997
  await generateDrizzleConfig(config2, manguitoDir);
25745
25998
  await writeFile9(join11(manguitoDir, "schema.ts"), generateSchemaFile(registry), "utf8");
25999
+ await addLinkConstraints(db.getDb(), registry);
25746
26000
  await runDevMigration(drizzleConfigPath);
25747
26001
  printSuccess(needsSeed ? "Schema tables created" : "Schema tables up to date");
25748
26002
  if (needsSeed) {
@@ -25938,6 +26192,13 @@ async function onSchemaFileChange(args) {
25938
26192
  await generateRoutes2(registry, manguitoDir);
25939
26193
  await generateForms(registry, join11(manguitoDir, "forms"));
25940
26194
  await generateNav(registry, manguitoDir);
26195
+ try {
26196
+ await addLinkConstraints(db.getDb(), registry);
26197
+ } catch (err) {
26198
+ process.stderr.write(`\u26A0 Adding link constraints failed: ${err instanceof Error ? err.message : String(err)}
26199
+ `);
26200
+ return;
26201
+ }
25941
26202
  try {
25942
26203
  await runDevMigration(drizzleConfigPath);
25943
26204
  } catch (err) {
@@ -25962,11 +26223,17 @@ async function onSchemaFileChange(args) {
25962
26223
  updateFetch((req) => newAdapter.app.fetch(req));
25963
26224
  printSuccess("Schema updated");
25964
26225
  }
26226
+ async function addLinkConstraints(database, registry) {
26227
+ const results = await ensureLinkConstraints(database, registry);
26228
+ for (const { table, removed } of results) {
26229
+ if (removed > 0) printSuccess(`Removed ${removed} duplicate links from ${table}`);
26230
+ }
26231
+ }
25965
26232
  function loadValidatedSchema(cwd, config2) {
25966
26233
  const parsed = parseAllSchemas(cwd, config2);
25967
26234
  if (!parsed.ok) return parsed;
25968
26235
  const registry = parsed.value;
25969
- const crossRefErrors = validateCrossReferences(registry, config2.api.media?.max_file_size);
26236
+ const crossRefErrors = checkRegistry(registry, config2.api.media?.max_file_size);
25970
26237
  if (crossRefErrors.length > 0) return { ok: false, errors: crossRefErrors };
25971
26238
  const versions = loadProjectVersionModel(resolveSchemaConfig(cwd, config2), registry);
25972
26239
  if (!versions.ok) return { ok: false, errors: versions.errors };
@@ -26152,7 +26419,7 @@ function buildDeps(options, cwd) {
26152
26419
  loadEnvFile(options.env);
26153
26420
  return {
26154
26421
  buildRunner: async () => runBuild(options.env ? { env: options.env } : {}, { cwd }),
26155
- needsRebuild: () => needsRebuild(cwd),
26422
+ needsRebuild: (schemaDir) => needsRebuild(cwd, { schemaDir }),
26156
26423
  db: createPostgresAdapter(),
26157
26424
  migrationsFolder: resolve8(cwd, "./migrations"),
26158
26425
  configPath: resolve8(cwd, "dist/generated/drizzle.config.ts"),
@@ -26220,7 +26487,7 @@ async function runMigrate(options, deps) {
26220
26487
  printGuidedError("DB_URL not set or database unreachable");
26221
26488
  process.exit(1);
26222
26489
  }
26223
- if (await deps.needsRebuild()) {
26490
+ if (await deps.needsRebuild(resolve8(cwd, config2.schema.base_path))) {
26224
26491
  try {
26225
26492
  await deps.buildRunner();
26226
26493
  } catch (err) {
@@ -26406,7 +26673,7 @@ async function runValidate(options, deps) {
26406
26673
  if (allErrors.length === 0 && parsedRoles !== null && parsedRoutesDef !== null) {
26407
26674
  const registry = buildSchemaRegistry(parsedSchemas, parsedRoutesDef, parsedRoles);
26408
26675
  printSchemaDeprecations(findSchemaDeprecations(registry));
26409
- const crossRefErrors = validateCrossReferences(registry, config2.api.media?.max_file_size);
26676
+ const crossRefErrors = checkRegistry(registry, config2.api.media?.max_file_size);
26410
26677
  allErrors.push(...crossRefErrors);
26411
26678
  if (crossRefErrors.length === 0) {
26412
26679
  const versions = loadProjectVersionModel(resolveSchemaConfig(cwd, config2), registry);
@@ -26722,7 +26989,7 @@ function loadWorkingRegistry(cwd, config2, command) {
26722
26989
  process.exit(1);
26723
26990
  }
26724
26991
  const registry = buildSchemaRegistry(parsedSchemas, parsedRoutes, parsedRoles);
26725
- const crossRefErrors = validateCrossReferences(registry, config2.api.media?.max_file_size);
26992
+ const crossRefErrors = checkRegistry(registry, config2.api.media?.max_file_size);
26726
26993
  if (crossRefErrors.length > 0) {
26727
26994
  printValidationErrors(crossRefErrors, "Schema reference errors", command);
26728
26995
  process.exit(1);