@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-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
22790
|
-
|
|
22791
|
-
|
|
22792
|
-
|
|
22793
|
-
];
|
|
22794
|
-
const
|
|
22795
|
-
|
|
22796
|
-
|
|
22797
|
-
|
|
22798
|
-
|
|
22799
|
-
|
|
22800
|
-
|
|
22801
|
-
|
|
22802
|
-
|
|
22803
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
23126
|
-
|
|
23127
|
-
|
|
23128
|
-
|
|
23129
|
-
|
|
23130
|
-
|
|
23131
|
-
|
|
23132
|
-
|
|
23133
|
-
|
|
23134
|
-
|
|
23135
|
-
|
|
23136
|
-
|
|
23137
|
-
|
|
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
|
|
23143
|
-
if (db) await loadRelationsForEdit(db, registry, requiredFields.filter((f) => !(f.name
|
|
23144
|
-
return { ...
|
|
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
|
|
23394
|
+
if (!Object.hasOwn(body, f.name)) continue;
|
|
23150
23395
|
if (f.db_column === null) {
|
|
23151
|
-
const
|
|
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
|
|
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"
|
|
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"
|
|
23536
|
-
if ("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
|
|
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
|
|
23590
|
-
|
|
23591
|
-
|
|
23592
|
-
|
|
23593
|
-
|
|
23594
|
-
|
|
23595
|
-
|
|
23596
|
-
|
|
23597
|
-
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
23809
|
-
|
|
23810
|
-
|
|
23811
|
-
|
|
23812
|
-
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
|
|
23816
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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);
|