@bobbykim/manguito-cms-cli 0.11.4 → 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.
@@ -9270,7 +9270,7 @@ var require_bcryptjs = __commonJS({
9270
9270
  }
9271
9271
  });
9272
9272
 
9273
- // ../api/dist/chunk-IFE2L4VM.mjs
9273
+ // ../api/dist/chunk-EFTMAG7N.mjs
9274
9274
  init_esm_shims();
9275
9275
  import crypto from "crypto";
9276
9276
 
@@ -26681,7 +26681,7 @@ function describeSchemaChange(input) {
26681
26681
  return { from: from?.version ?? null, to: to.version, types, identical };
26682
26682
  }
26683
26683
 
26684
- // ../api/dist/chunk-IFE2L4VM.mjs
26684
+ // ../api/dist/chunk-EFTMAG7N.mjs
26685
26685
  function isColumnBacked2(field) {
26686
26686
  const col = field.db_column;
26687
26687
  if (col === null) return false;
@@ -27063,7 +27063,7 @@ async function persistJunctionField(db, itemId, junction, relatedIds) {
27063
27063
  sql`, `
27064
27064
  );
27065
27065
  await db.execute(
27066
- junction.order_column ? sql`INSERT INTO ${tbl} (${left}, ${right}, "order") VALUES ${rows}` : sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES ${rows}`
27066
+ junction.order_column ? sql`INSERT INTO ${tbl} (${left}, ${right}, "order") VALUES ${rows} ON CONFLICT DO NOTHING` : sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES ${rows} ON CONFLICT DO NOTHING`
27067
27067
  );
27068
27068
  }
27069
27069
  function junctionOrderBy(junction) {
@@ -12,7 +12,7 @@ import {
12
12
  isColumnBacked,
13
13
  relationCardinality,
14
14
  resolveRelationField
15
- } from "./chunk-44VXH5BN.js";
15
+ } from "./chunk-EVB2E4A5.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-N5IMQJBP.mjs
10790
+ // ../api/dist/handler-5OTVPIAF.mjs
10791
10791
  init_esm_shims();
10792
10792
 
10793
- // ../api/dist/chunk-IMDC3NGM.mjs
10793
+ // ../api/dist/chunk-DHOVO42E.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-IMDC3NGM.mjs
30126
+ // ../api/dist/chunk-DHOVO42E.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-IMDC3NGM.mjs
30884
+ // ../api/dist/chunk-DHOVO42E.mjs
30885
30885
  var DateTimeScalar = new GraphQLScalarType({
30886
30886
  name: "DateTime",
30887
30887
  description: "ISO-8601 date-time string",
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  validateCrossReferences,
64
64
  verifyPassword,
65
65
  walkSchemaDirectory
66
- } from "./chunk-44VXH5BN.js";
66
+ } from "./chunk-EVB2E4A5.js";
67
67
 
68
68
  // ../../node_modules/.pnpm/postgres-array@2.0.0/node_modules/postgres-array/index.js
69
69
  var require_postgres_array = __commonJS({
@@ -5272,9 +5272,10 @@ import { Command } from "commander";
5272
5272
 
5273
5273
  // src/commands/build.ts
5274
5274
  init_esm_shims();
5275
- import { mkdirSync, statSync, existsSync as existsSync3, readdirSync, writeFileSync } from "fs";
5275
+ import { mkdirSync, statSync, existsSync as existsSync3, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
5276
5276
  import { resolve as resolve3, join as join7, dirname } from "path";
5277
5277
  import { createRequire } from "module";
5278
+ import { fileURLToPath } from "url";
5278
5279
  import { build as viteBuild } from "vite";
5279
5280
  import { build as tsupBuild } from "tsup";
5280
5281
 
@@ -16721,6 +16722,7 @@ function drizzle2(...params) {
16721
16722
  })(drizzle2 || (drizzle2 = {}));
16722
16723
 
16723
16724
  // ../db/dist/index.mjs
16725
+ import { createHash } from "crypto";
16724
16726
  import { execSync, spawnSync } from "child_process";
16725
16727
  import crypto2 from "crypto";
16726
16728
  import path from "path";
@@ -16809,6 +16811,256 @@ function createPostgresAdapter(options = {}) {
16809
16811
  }
16810
16812
  };
16811
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
+ }
16812
17064
  function generateFileHeader() {
16813
17065
  return "import * as s from 'drizzle-orm/pg-core'\nimport { sql } from 'drizzle-orm'";
16814
17066
  }
@@ -16943,7 +17195,7 @@ ${colBlock2}
16943
17195
  const constraints = enumFields.map((f) => {
16944
17196
  const col = f.db_column;
16945
17197
  const values = col.check_constraint.map((v2) => `'${v2}'`).join(", ");
16946
- const cname = `${col.column_name}_check`;
17198
+ const cname = checkConstraintName(col.column_name);
16947
17199
  return [
16948
17200
  ` ${cname}: s.check(`,
16949
17201
  ` '${cname}',`,
@@ -17005,7 +17257,10 @@ function generateJunctionTables(registry) {
17005
17257
  ` .notNull()`,
17006
17258
  ` .references(() => ${rightVar}.id, { onDelete: 'cascade' }),`,
17007
17259
  ...orderLine ? [orderLine] : [],
17008
- ` }`,
17260
+ ` },`,
17261
+ ` (table) => ({`,
17262
+ ` pair: s.unique('${linkUniqueName(jt.table_name)}').on(table.${jt.left_column}, table.${jt.right_column}),`,
17263
+ ` })`,
17009
17264
  `)`
17010
17265
  ].join("\n");
17011
17266
  tables.push(def);
@@ -17244,7 +17499,14 @@ async function runGenerate(configPath, outFolder, cwd) {
17244
17499
  } catch {
17245
17500
  }
17246
17501
  const beforeSet = new Set(beforeFiles);
17247
- 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;
17248
17510
  }
17249
17511
  async function generateMigration(configPath, migrationsFolder) {
17250
17512
  return runGenerate(configPath, migrationsFolder, path.dirname(configPath));
@@ -18678,6 +18940,14 @@ function resolveSchemaConfig(cwd, config2) {
18678
18940
  };
18679
18941
  }
18680
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
+
18681
18951
  // src/utils/env.ts
18682
18952
  init_esm_shims();
18683
18953
  import { existsSync as existsSync2 } from "fs";
@@ -18765,7 +19035,7 @@ async function runBuild(options, deps) {
18765
19035
  parsedRoles
18766
19036
  );
18767
19037
  printSchemaDeprecations(findSchemaDeprecations(registry));
18768
- const crossRefErrors = validateCrossReferences(registry, config2.api.media?.max_file_size);
19038
+ const crossRefErrors = checkRegistry(registry, config2.api.media?.max_file_size);
18769
19039
  if (crossRefErrors.length > 0) {
18770
19040
  printValidationErrors(crossRefErrors, "Schema reference errors", "manguito build");
18771
19041
  process.exit(1);
@@ -18799,6 +19069,11 @@ async function runBuild(options, deps) {
18799
19069
  { adminPrefix: config2.admin.prefix, apiPrefix: config2.api.prefix },
18800
19070
  generatedDir
18801
19071
  );
19072
+ writeFileSync(
19073
+ join7(generatedDir, VERSIONS_RECORD),
19074
+ JSON.stringify(installedManguitoVersions(cwd), null, 2) + "\n",
19075
+ "utf8"
19076
+ );
18802
19077
  printSuccess("Codegen complete");
18803
19078
  try {
18804
19079
  await viteBuild({
@@ -18847,11 +19122,76 @@ async function runBuild(options, deps) {
18847
19122
  printSuccess("Server compiled");
18848
19123
  process.stdout.write("\nBuild complete \u2192 dist/\n");
18849
19124
  }
18850
- 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);
18851
19189
  const sentinel = resolve3(cwd, "dist/generated/schema.ts");
18852
19190
  if (!existsSync3(sentinel)) return true;
19191
+ const recorded = recordedVersions(cwd);
19192
+ if (recorded === null || !sameVersions(recorded, installed)) return true;
18853
19193
  const sentinelMtime = statSync(sentinel).mtimeMs;
18854
- const schemasDir = resolve3(cwd, "schemas");
19194
+ const schemasDir = options.schemaDir ?? resolve3(cwd, "schemas");
18855
19195
  if (!existsSync3(schemasDir)) return false;
18856
19196
  return walkFiles(schemasDir).some(
18857
19197
  (file) => statSync(file).mtimeMs > sentinelMtime
@@ -23044,9 +23384,9 @@ async function loadRelationsForEdit(db, registry, fields, id, row) {
23044
23384
  for (const entry of loaded) if (entry) row[entry[0]] = entry[1];
23045
23385
  }
23046
23386
  async function valuesForPublishCheck(db, registry, requiredFields, id, storedLabels, body) {
23047
- const stored = { ...storedLabels };
23048
- if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !Object.hasOwn(body, f.name)), id, stored);
23049
- 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 };
23050
23390
  }
23051
23391
  async function persistRelationFields(db, registry, fields, body, itemId, ownerTable) {
23052
23392
  const deltas = [];
@@ -24687,7 +25027,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
24687
25027
  }
24688
25028
  var API_VERSION = (() => {
24689
25029
  try {
24690
- return "0.9.3";
25030
+ return "0.9.4";
24691
25031
  } catch {
24692
25032
  return "0.0.0";
24693
25033
  }
@@ -25370,7 +25710,7 @@ function createCmsApp(options) {
25370
25710
  handler: null,
25371
25711
  error: null
25372
25712
  }));
25373
- const ready = import("./handler-N5IMQJBP-USVWSLZP.js").then(({ createGraphQLHandler }) => {
25713
+ const ready = import("./handler-5OTVPIAF-JVSJWO2S.js").then(({ createGraphQLHandler }) => {
25374
25714
  gqlPasses.forEach((pass, i) => {
25375
25715
  const mount = mounts[i];
25376
25716
  try {
@@ -25656,6 +25996,7 @@ async function runDev(options, deps) {
25656
25996
  const { registry, versionModel } = loaded.value;
25657
25997
  await generateDrizzleConfig(config2, manguitoDir);
25658
25998
  await writeFile9(join11(manguitoDir, "schema.ts"), generateSchemaFile(registry), "utf8");
25999
+ await addLinkConstraints(db.getDb(), registry);
25659
26000
  await runDevMigration(drizzleConfigPath);
25660
26001
  printSuccess(needsSeed ? "Schema tables created" : "Schema tables up to date");
25661
26002
  if (needsSeed) {
@@ -25851,6 +26192,13 @@ async function onSchemaFileChange(args) {
25851
26192
  await generateRoutes2(registry, manguitoDir);
25852
26193
  await generateForms(registry, join11(manguitoDir, "forms"));
25853
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
+ }
25854
26202
  try {
25855
26203
  await runDevMigration(drizzleConfigPath);
25856
26204
  } catch (err) {
@@ -25875,11 +26223,17 @@ async function onSchemaFileChange(args) {
25875
26223
  updateFetch((req) => newAdapter.app.fetch(req));
25876
26224
  printSuccess("Schema updated");
25877
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
+ }
25878
26232
  function loadValidatedSchema(cwd, config2) {
25879
26233
  const parsed = parseAllSchemas(cwd, config2);
25880
26234
  if (!parsed.ok) return parsed;
25881
26235
  const registry = parsed.value;
25882
- const crossRefErrors = validateCrossReferences(registry, config2.api.media?.max_file_size);
26236
+ const crossRefErrors = checkRegistry(registry, config2.api.media?.max_file_size);
25883
26237
  if (crossRefErrors.length > 0) return { ok: false, errors: crossRefErrors };
25884
26238
  const versions = loadProjectVersionModel(resolveSchemaConfig(cwd, config2), registry);
25885
26239
  if (!versions.ok) return { ok: false, errors: versions.errors };
@@ -26065,7 +26419,7 @@ function buildDeps(options, cwd) {
26065
26419
  loadEnvFile(options.env);
26066
26420
  return {
26067
26421
  buildRunner: async () => runBuild(options.env ? { env: options.env } : {}, { cwd }),
26068
- needsRebuild: () => needsRebuild(cwd),
26422
+ needsRebuild: (schemaDir) => needsRebuild(cwd, { schemaDir }),
26069
26423
  db: createPostgresAdapter(),
26070
26424
  migrationsFolder: resolve8(cwd, "./migrations"),
26071
26425
  configPath: resolve8(cwd, "dist/generated/drizzle.config.ts"),
@@ -26133,7 +26487,7 @@ async function runMigrate(options, deps) {
26133
26487
  printGuidedError("DB_URL not set or database unreachable");
26134
26488
  process.exit(1);
26135
26489
  }
26136
- if (await deps.needsRebuild()) {
26490
+ if (await deps.needsRebuild(resolve8(cwd, config2.schema.base_path))) {
26137
26491
  try {
26138
26492
  await deps.buildRunner();
26139
26493
  } catch (err) {
@@ -26319,7 +26673,7 @@ async function runValidate(options, deps) {
26319
26673
  if (allErrors.length === 0 && parsedRoles !== null && parsedRoutesDef !== null) {
26320
26674
  const registry = buildSchemaRegistry(parsedSchemas, parsedRoutesDef, parsedRoles);
26321
26675
  printSchemaDeprecations(findSchemaDeprecations(registry));
26322
- const crossRefErrors = validateCrossReferences(registry, config2.api.media?.max_file_size);
26676
+ const crossRefErrors = checkRegistry(registry, config2.api.media?.max_file_size);
26323
26677
  allErrors.push(...crossRefErrors);
26324
26678
  if (crossRefErrors.length === 0) {
26325
26679
  const versions = loadProjectVersionModel(resolveSchemaConfig(cwd, config2), registry);
@@ -26635,7 +26989,7 @@ function loadWorkingRegistry(cwd, config2, command) {
26635
26989
  process.exit(1);
26636
26990
  }
26637
26991
  const registry = buildSchemaRegistry(parsedSchemas, parsedRoutes, parsedRoles);
26638
- const crossRefErrors = validateCrossReferences(registry, config2.api.media?.max_file_size);
26992
+ const crossRefErrors = checkRegistry(registry, config2.api.media?.max_file_size);
26639
26993
  if (crossRefErrors.length > 0) {
26640
26994
  printValidationErrors(crossRefErrors, "Schema reference errors", command);
26641
26995
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbykim/manguito-cms-cli",
3
- "version": "0.11.4",
3
+ "version": "0.12.0",
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.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"
32
+ "@bobbykim/manguito-cms-admin": "0.6.3",
33
+ "@bobbykim/manguito-cms-core": "0.11.1",
34
+ "@bobbykim/manguito-cms-db": "0.3.0",
35
+ "@bobbykim/manguito-cms-api": "0.9.4"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.9.5",