@drzl/validation-core 3.17.0 → 3.20.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.cjs +368 -98
- package/dist/index.d.cts +266 -16
- package/dist/index.d.ts +266 -16
- package/dist/index.js +364 -98
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -44,7 +44,9 @@ __export(index_exports, {
|
|
|
44
44
|
NAME_MODES: () => NAME_MODES,
|
|
45
45
|
NESTED_PREFIX: () => NESTED_PREFIX,
|
|
46
46
|
applyTableCase: () => applyTableCase,
|
|
47
|
+
buildBrandPlan: () => buildBrandPlan,
|
|
47
48
|
buildNestedPlan: () => buildNestedPlan,
|
|
49
|
+
columnMetaFacts: () => columnMetaFacts,
|
|
48
50
|
describeSet: () => describeSet,
|
|
49
51
|
formatCode: () => formatCode,
|
|
50
52
|
importSpecifier: () => importSpecifier,
|
|
@@ -63,10 +65,12 @@ __export(index_exports, {
|
|
|
63
65
|
pascalCase: () => pascalCase,
|
|
64
66
|
renderDuplicateFinder: () => renderDuplicateFinder,
|
|
65
67
|
resolveAffix: () => resolveAffix,
|
|
68
|
+
resolveBranding: () => resolveBranding,
|
|
66
69
|
resolveConfiguredImport: () => resolveConfiguredImport,
|
|
67
70
|
resolveNestedDepth: () => resolveNestedDepth,
|
|
68
71
|
schemaName: () => schemaName,
|
|
69
72
|
selectColumns: () => selectColumns,
|
|
73
|
+
tableMetaFacts: () => tableMetaFacts,
|
|
70
74
|
typeName: () => typeName,
|
|
71
75
|
updateColumns: () => updateColumns,
|
|
72
76
|
validateAffix: () => validateAffix
|
|
@@ -78,6 +82,259 @@ var import_node_module = require("module");
|
|
|
78
82
|
var import_node_path2 = __toESM(require("path"), 1);
|
|
79
83
|
var import_node_url = require("url");
|
|
80
84
|
|
|
85
|
+
// src/naming.ts
|
|
86
|
+
var NAME_MODES = ["insert", "update", "select"];
|
|
87
|
+
var DEFAULT_MODE_PREFIX = {
|
|
88
|
+
insert: "Insert",
|
|
89
|
+
update: "Update",
|
|
90
|
+
select: "Select"
|
|
91
|
+
};
|
|
92
|
+
var DEFAULT_TYPE_SUFFIX = {
|
|
93
|
+
insert: "Input",
|
|
94
|
+
update: "Input",
|
|
95
|
+
select: "Output"
|
|
96
|
+
};
|
|
97
|
+
var DEFAULT_SCHEMA_SUFFIX = "Schema";
|
|
98
|
+
var AFFIX_PROBE_TABLE = "users";
|
|
99
|
+
function spread(value, fallback) {
|
|
100
|
+
if (value === void 0) return { ...fallback };
|
|
101
|
+
if (typeof value === "string") return { insert: value, update: value, select: value };
|
|
102
|
+
return {
|
|
103
|
+
insert: value.insert ?? fallback.insert,
|
|
104
|
+
update: value.update ?? fallback.update,
|
|
105
|
+
select: value.select ?? fallback.select
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function pascalCase(s) {
|
|
109
|
+
return s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[\s_-]+/).filter(Boolean).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join("");
|
|
110
|
+
}
|
|
111
|
+
function applyTableCase(tsName, tableCase) {
|
|
112
|
+
return tableCase === "pascal" ? pascalCase(tsName) : tsName;
|
|
113
|
+
}
|
|
114
|
+
function resolveAffix(opts) {
|
|
115
|
+
const affix = opts?.affix;
|
|
116
|
+
const legacy = opts?.schemaSuffix ?? DEFAULT_SCHEMA_SUFFIX;
|
|
117
|
+
const legacyMap = {
|
|
118
|
+
insert: legacy,
|
|
119
|
+
update: legacy,
|
|
120
|
+
select: legacy
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
tableCase: affix?.tableCase ?? "preserve",
|
|
124
|
+
schema: {
|
|
125
|
+
prefix: spread(affix?.schema?.prefix, DEFAULT_MODE_PREFIX),
|
|
126
|
+
suffix: spread(affix?.schema?.suffix, legacyMap)
|
|
127
|
+
},
|
|
128
|
+
type: {
|
|
129
|
+
prefix: spread(affix?.type?.prefix, DEFAULT_MODE_PREFIX),
|
|
130
|
+
suffix: spread(affix?.type?.suffix, DEFAULT_TYPE_SUFFIX)
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function schemaName(mode, tsName, affix) {
|
|
135
|
+
return affix.schema.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.schema.suffix[mode];
|
|
136
|
+
}
|
|
137
|
+
function typeName(mode, tsName, affix) {
|
|
138
|
+
return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
|
|
139
|
+
}
|
|
140
|
+
var PREFIX_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
141
|
+
var SUFFIX_RE = /^[A-Za-z0-9_$]+$/;
|
|
142
|
+
function validateAffix(affix, schemaSuffix) {
|
|
143
|
+
const issues = [];
|
|
144
|
+
if (!affix) return issues;
|
|
145
|
+
const checkOne = (value, path2, kind) => {
|
|
146
|
+
if (value === "") return;
|
|
147
|
+
const ok = kind === "prefix" ? PREFIX_RE.test(value) : SUFFIX_RE.test(value);
|
|
148
|
+
if (ok) return;
|
|
149
|
+
issues.push({
|
|
150
|
+
path: path2,
|
|
151
|
+
message: `${JSON.stringify(value)} cannot appear in a TypeScript identifier. Use only letters, digits, "_" and "$"` + (kind === "prefix" ? ", and do not start with a digit." : ".")
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
const checkValue = (value, base, kind) => {
|
|
155
|
+
if (value === void 0) return;
|
|
156
|
+
if (typeof value === "string") {
|
|
157
|
+
checkOne(value, base, kind);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
for (const mode of NAME_MODES) {
|
|
161
|
+
const v = value[mode];
|
|
162
|
+
if (v !== void 0) checkOne(v, [...base, mode], kind);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
checkValue(affix.schema?.prefix, ["schema", "prefix"], "prefix");
|
|
166
|
+
checkValue(affix.schema?.suffix, ["schema", "suffix"], "suffix");
|
|
167
|
+
checkValue(affix.type?.prefix, ["type", "prefix"], "prefix");
|
|
168
|
+
checkValue(affix.type?.suffix, ["type", "suffix"], "suffix");
|
|
169
|
+
if (issues.length) return issues;
|
|
170
|
+
const resolved = resolveAffix({ affix, schemaSuffix });
|
|
171
|
+
const collisions = (space, build) => {
|
|
172
|
+
const seen = /* @__PURE__ */ new Map();
|
|
173
|
+
for (const mode of NAME_MODES) {
|
|
174
|
+
const name = build(mode);
|
|
175
|
+
const first = seen.get(name);
|
|
176
|
+
if (first) {
|
|
177
|
+
issues.push({
|
|
178
|
+
path: [space],
|
|
179
|
+
message: `The ${space} names for "${first}" and "${mode}" collide: both resolve to "${name}". All three are emitted into the same file, so at least one prefix or suffix has to differ.`
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
seen.set(name, mode);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
collisions("schema", (mode) => schemaName(mode, AFFIX_PROBE_TABLE, resolved));
|
|
187
|
+
collisions("type", (mode) => typeName(mode, AFFIX_PROBE_TABLE, resolved));
|
|
188
|
+
return issues;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/branding.ts
|
|
192
|
+
function resolveBranding(opt) {
|
|
193
|
+
if (!opt) return void 0;
|
|
194
|
+
if (opt === true) return { foreignKeys: true, aliases: true };
|
|
195
|
+
if (opt.enabled === false) return void 0;
|
|
196
|
+
return { foreignKeys: opt.foreignKeys !== false, aliases: opt.aliases !== false };
|
|
197
|
+
}
|
|
198
|
+
var at = (tsName, column) => `${tsName}\0${column}`;
|
|
199
|
+
function referenceOf(table, columnName) {
|
|
200
|
+
const col = table.columns.find((c) => c.name === columnName);
|
|
201
|
+
if (col?.references) return { sqlTable: col.references.table, column: col.references.column };
|
|
202
|
+
for (const fk of table.foreignKeys ?? []) {
|
|
203
|
+
const i = fk.columns.indexOf(columnName);
|
|
204
|
+
if (i === -1) continue;
|
|
205
|
+
const target = fk.foreignColumns[i];
|
|
206
|
+
if (target === void 0) continue;
|
|
207
|
+
return { sqlTable: fk.foreignTable, column: target };
|
|
208
|
+
}
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
function isKeyColumn(table, columnName) {
|
|
212
|
+
return (table.primaryKey?.columns ?? []).includes(columnName);
|
|
213
|
+
}
|
|
214
|
+
function buildBrandPlan(tables, opt) {
|
|
215
|
+
const resolved = resolveBranding(opt);
|
|
216
|
+
if (!resolved) return void 0;
|
|
217
|
+
const notes = [];
|
|
218
|
+
const byTs = /* @__PURE__ */ new Map();
|
|
219
|
+
const dupTs = /* @__PURE__ */ new Set();
|
|
220
|
+
for (const t of tables) {
|
|
221
|
+
if (byTs.has(t.tsName)) dupTs.add(t.tsName);
|
|
222
|
+
byTs.set(t.tsName, t);
|
|
223
|
+
}
|
|
224
|
+
for (const name of dupTs) {
|
|
225
|
+
byTs.delete(name);
|
|
226
|
+
notes.push(
|
|
227
|
+
`two tables are exported as "${name}", so nothing on either is branded: a brand token is built from the export name and the two would be indistinguishable.`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const bySqlName = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const t of byTs.values()) {
|
|
232
|
+
const list = bySqlName.get(t.name) ?? [];
|
|
233
|
+
list.push(t);
|
|
234
|
+
bySqlName.set(t.name, list);
|
|
235
|
+
}
|
|
236
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
237
|
+
const cache = /* @__PURE__ */ new Map();
|
|
238
|
+
const resolveToken = (tsName, columnName) => {
|
|
239
|
+
const start = at(tsName, columnName);
|
|
240
|
+
if (cache.has(start)) return cache.get(start);
|
|
241
|
+
const origin = byTs.get(tsName)?.columns.find((c) => c.name === columnName);
|
|
242
|
+
if (!origin || origin.arrayDimensions) {
|
|
243
|
+
cache.set(start, void 0);
|
|
244
|
+
return void 0;
|
|
245
|
+
}
|
|
246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
247
|
+
let curTable = tsName;
|
|
248
|
+
let curColumn = columnName;
|
|
249
|
+
let answer;
|
|
250
|
+
let terminal;
|
|
251
|
+
for (; ; ) {
|
|
252
|
+
const key = at(curTable, curColumn);
|
|
253
|
+
if (seen.has(key)) {
|
|
254
|
+
notes.push(
|
|
255
|
+
`${tsName}.${columnName} references a cycle of foreign keys, so it is not branded.`
|
|
256
|
+
);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
seen.add(key);
|
|
260
|
+
const table = byTs.get(curTable);
|
|
261
|
+
const col = table?.columns.find((c) => c.name === curColumn);
|
|
262
|
+
if (!table || !col) break;
|
|
263
|
+
const ref = resolved.foreignKeys ? referenceOf(table, curColumn) : void 0;
|
|
264
|
+
if (ref) {
|
|
265
|
+
const candidates = bySqlName.get(ref.sqlTable) ?? [];
|
|
266
|
+
if (candidates.length !== 1) {
|
|
267
|
+
notes.push(
|
|
268
|
+
candidates.length === 0 ? `${tsName}.${columnName} references a table "${ref.sqlTable}" that is not in this analysis, so it is not branded.` : `${tsName}.${columnName} references "${ref.sqlTable}", which names more than one table here, so it is not branded.`
|
|
269
|
+
);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
const next = candidates[0];
|
|
273
|
+
if (!next.columns.some((c) => c.name === ref.column)) {
|
|
274
|
+
notes.push(
|
|
275
|
+
`${tsName}.${columnName} references ${next.tsName}.${ref.column}, which is not a column of that table, so it is not branded.`
|
|
276
|
+
);
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
curTable = next.tsName;
|
|
280
|
+
curColumn = ref.column;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (isKeyColumn(table, curColumn)) {
|
|
284
|
+
answer = `${curTable}.${curColumn}`;
|
|
285
|
+
terminal = col;
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
if (answer && terminal && terminal.tsType !== origin.tsType) {
|
|
290
|
+
notes.push(
|
|
291
|
+
`${tsName}.${columnName} is a ${origin.tsType} and ${answer} is a ${terminal.tsType}, so it is not branded.`
|
|
292
|
+
);
|
|
293
|
+
answer = void 0;
|
|
294
|
+
}
|
|
295
|
+
cache.set(start, answer);
|
|
296
|
+
return answer;
|
|
297
|
+
};
|
|
298
|
+
for (const t of byTs.values()) {
|
|
299
|
+
for (const c of t.columns) {
|
|
300
|
+
const token = resolveToken(t.tsName, c.name);
|
|
301
|
+
if (token) tokens.set(at(t.tsName, c.name), token);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
305
|
+
if (resolved.aliases) {
|
|
306
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
307
|
+
const draft = [];
|
|
308
|
+
for (const t of byTs.values()) {
|
|
309
|
+
for (const c of t.columns) {
|
|
310
|
+
const token = tokens.get(at(t.tsName, c.name));
|
|
311
|
+
if (token !== `${t.tsName}.${c.name}`) continue;
|
|
312
|
+
const alias = pascalCase(t.tsName) + pascalCase(c.name);
|
|
313
|
+
draft.push({ tsName: t.tsName, entry: { alias, column: c.name, token } });
|
|
314
|
+
claimed.set(alias, [...claimed.get(alias) ?? [], `${t.tsName}.${c.name}`]);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const { tsName, entry } of draft) {
|
|
318
|
+
const owners = claimed.get(entry.alias) ?? [];
|
|
319
|
+
if (owners.length > 1) continue;
|
|
320
|
+
aliases.set(tsName, [...aliases.get(tsName) ?? [], entry]);
|
|
321
|
+
}
|
|
322
|
+
for (const [alias, owners] of claimed) {
|
|
323
|
+
if (owners.length > 1) {
|
|
324
|
+
notes.push(
|
|
325
|
+
`${owners.join(" and ")} both name their brand alias "${alias}", so neither is exported. The schemas are unaffected; refer to the type as the select type's property instead.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
brandOf: (tsName, columnName) => tokens.get(at(tsName, columnName)),
|
|
332
|
+
aliasesFor: (tsName) => aliases.get(tsName) ?? [],
|
|
333
|
+
any: tokens.size > 0,
|
|
334
|
+
notes
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
81
338
|
// src/checks.ts
|
|
82
339
|
var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
|
|
83
340
|
var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
|
|
@@ -351,113 +608,120 @@ function withTsExtension(p) {
|
|
|
351
608
|
return `${p}.ts`;
|
|
352
609
|
}
|
|
353
610
|
|
|
354
|
-
// src/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
insert: "Insert",
|
|
358
|
-
update: "Update",
|
|
359
|
-
select: "Select"
|
|
360
|
-
};
|
|
361
|
-
var DEFAULT_TYPE_SUFFIX = {
|
|
362
|
-
insert: "Input",
|
|
363
|
-
update: "Input",
|
|
364
|
-
select: "Output"
|
|
365
|
-
};
|
|
366
|
-
var DEFAULT_SCHEMA_SUFFIX = "Schema";
|
|
367
|
-
var AFFIX_PROBE_TABLE = "users";
|
|
368
|
-
function spread(value, fallback) {
|
|
369
|
-
if (value === void 0) return { ...fallback };
|
|
370
|
-
if (typeof value === "string") return { insert: value, update: value, select: value };
|
|
371
|
-
return {
|
|
372
|
-
insert: value.insert ?? fallback.insert,
|
|
373
|
-
update: value.update ?? fallback.update,
|
|
374
|
-
select: value.select ?? fallback.select
|
|
375
|
-
};
|
|
611
|
+
// src/meta.ts
|
|
612
|
+
function labelled(name, text) {
|
|
613
|
+
return name ? `${name}: ${text}` : text;
|
|
376
614
|
}
|
|
377
|
-
function
|
|
378
|
-
return
|
|
615
|
+
function literalText(value, kind) {
|
|
616
|
+
return kind === "string" ? `'${value}'` : value;
|
|
379
617
|
}
|
|
380
|
-
function
|
|
381
|
-
return
|
|
618
|
+
function columnCheckText(k) {
|
|
619
|
+
return labelled(k.name, `${k.column} ${k.operator} ${literalText(k.value, k.kind)}`);
|
|
382
620
|
}
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
update: legacy,
|
|
389
|
-
select: legacy
|
|
390
|
-
};
|
|
391
|
-
return {
|
|
392
|
-
tableCase: affix?.tableCase ?? "preserve",
|
|
393
|
-
schema: {
|
|
394
|
-
prefix: spread(affix?.schema?.prefix, DEFAULT_MODE_PREFIX),
|
|
395
|
-
suffix: spread(affix?.schema?.suffix, legacyMap)
|
|
396
|
-
},
|
|
397
|
-
type: {
|
|
398
|
-
prefix: spread(affix?.type?.prefix, DEFAULT_MODE_PREFIX),
|
|
399
|
-
suffix: spread(affix?.type?.suffix, DEFAULT_TYPE_SUFFIX)
|
|
400
|
-
}
|
|
401
|
-
};
|
|
621
|
+
function setText(k) {
|
|
622
|
+
return labelled(
|
|
623
|
+
k.name,
|
|
624
|
+
`${k.column} IN (${k.values.map((v) => literalText(v, k.kind)).join(", ")})`
|
|
625
|
+
);
|
|
402
626
|
}
|
|
403
|
-
function
|
|
404
|
-
return
|
|
627
|
+
function lengthText(k) {
|
|
628
|
+
return labelled(k.name, `length(${k.column}) ${k.operator} ${k.value}`);
|
|
405
629
|
}
|
|
406
|
-
function
|
|
407
|
-
return
|
|
630
|
+
function cardinalityText(k) {
|
|
631
|
+
return labelled(k.name, `cardinality(${k.column}) ${k.operator} ${k.value}`);
|
|
408
632
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (value === void 0) return;
|
|
425
|
-
if (typeof value === "string") {
|
|
426
|
-
checkOne(value, base, kind);
|
|
633
|
+
function rowText(k) {
|
|
634
|
+
return labelled(k.name, `${k.left} ${k.operator} ${k.right}`);
|
|
635
|
+
}
|
|
636
|
+
function takesScalarChecks(c) {
|
|
637
|
+
return !c.arrayDimensions && !c.shape;
|
|
638
|
+
}
|
|
639
|
+
function classifyChecks(table) {
|
|
640
|
+
const perColumn = /* @__PURE__ */ new Map();
|
|
641
|
+
const rows = [];
|
|
642
|
+
const unenforced = [];
|
|
643
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
644
|
+
const add = (column, text, guard) => {
|
|
645
|
+
const c = byName.get(column);
|
|
646
|
+
if (!c || !guard(c)) {
|
|
647
|
+
unenforced.push(text);
|
|
427
648
|
return;
|
|
428
649
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
650
|
+
const list = perColumn.get(column) ?? [];
|
|
651
|
+
list.push(text);
|
|
652
|
+
perColumn.set(column, list);
|
|
433
653
|
};
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
for (const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
message: `The ${space} names for "${first}" and "${mode}" collide: both resolve to "${name}". All three are emitted into the same file, so at least one prefix or suffix has to differ.`
|
|
449
|
-
});
|
|
450
|
-
} else {
|
|
451
|
-
seen.set(name, mode);
|
|
452
|
-
}
|
|
654
|
+
for (const k of table.checks ?? []) {
|
|
655
|
+
const parsed = parseCheck(k.expression, k.name);
|
|
656
|
+
if (!parsed.ok) {
|
|
657
|
+
unenforced.push(labelled(k.name, (k.expression ?? "").trim()));
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
for (const c of parsed.checks) add(c.column, columnCheckText(c), takesScalarChecks);
|
|
661
|
+
for (const s of parsed.sets ?? []) add(s.column, setText(s), takesScalarChecks);
|
|
662
|
+
for (const l of parsed.lengths ?? []) add(l.column, lengthText(l), takesScalarChecks);
|
|
663
|
+
for (const a of parsed.cardinalities ?? [])
|
|
664
|
+
add(a.column, cardinalityText(a), (c) => !!c.arrayDimensions);
|
|
665
|
+
for (const r of parsed.rows ?? []) {
|
|
666
|
+
if (byName.has(r.left) && byName.has(r.right)) rows.push(rowText(r));
|
|
667
|
+
else unenforced.push(rowText(r));
|
|
453
668
|
}
|
|
669
|
+
}
|
|
670
|
+
return { perColumn, rows, unenforced };
|
|
671
|
+
}
|
|
672
|
+
function columnDescription(facts) {
|
|
673
|
+
const parts = [];
|
|
674
|
+
if (facts.maxLength !== void 0) parts.push(`at most ${facts.maxLength} characters`);
|
|
675
|
+
if (facts.maxBytes !== void 0) parts.push(`at most ${facts.maxBytes} bytes`);
|
|
676
|
+
for (const c of facts.checks ?? []) parts.push(`CHECK ${c}`);
|
|
677
|
+
return parts.length ? parts.join(". ") : void 0;
|
|
678
|
+
}
|
|
679
|
+
function tableDescription(facts) {
|
|
680
|
+
const parts = [];
|
|
681
|
+
for (const c of facts.checks ?? []) parts.push(`CHECK ${c}`);
|
|
682
|
+
if (facts.unenforcedChecks?.length) {
|
|
683
|
+
parts.push(
|
|
684
|
+
`not enforced by this schema, the database also checks: ${facts.unenforcedChecks.join("; ")}`
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return parts.length ? parts.join(". ") : void 0;
|
|
688
|
+
}
|
|
689
|
+
function columnMetaFacts(column, table, opts = {}) {
|
|
690
|
+
const checks = classifyChecks(table).perColumn.get(column.name);
|
|
691
|
+
const facts = {
|
|
692
|
+
...column.sqlType ? { sqlType: column.sqlType } : {},
|
|
693
|
+
...column.maxLength !== void 0 ? { maxLength: column.maxLength } : {},
|
|
694
|
+
...column.maxBytes !== void 0 ? { maxBytes: column.maxBytes } : {},
|
|
695
|
+
...column.hasDefault ? { hasDefault: true } : {},
|
|
696
|
+
...column.isGenerated ? { generated: true } : {},
|
|
697
|
+
...checks?.length ? { checks } : {}
|
|
454
698
|
};
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return
|
|
699
|
+
if (!opts.description) return facts;
|
|
700
|
+
const description = columnDescription(facts);
|
|
701
|
+
return description ? { ...facts, description } : facts;
|
|
702
|
+
}
|
|
703
|
+
function tableMetaFacts(table, opts) {
|
|
704
|
+
const { rows, unenforced } = classifyChecks(table);
|
|
705
|
+
const pk = table.primaryKey?.columns ?? [];
|
|
706
|
+
const unique = (table.unique ?? []).map((k) => k.columns).filter((c) => c.length > 0);
|
|
707
|
+
const facts = {
|
|
708
|
+
table: table.name,
|
|
709
|
+
...table.schema ? { schema: table.schema } : {},
|
|
710
|
+
...opts.dialect ? { dialect: opts.dialect } : {},
|
|
711
|
+
mode: opts.mode,
|
|
712
|
+
...pk.length ? { primaryKey: pk } : {},
|
|
713
|
+
...unique.length ? { unique } : {},
|
|
714
|
+
...table.readOnly ? { readOnly: true } : {},
|
|
715
|
+
...rows.length ? { checks: rows } : {},
|
|
716
|
+
...unenforced.length ? { unenforcedChecks: unenforced } : {}
|
|
717
|
+
};
|
|
718
|
+
if (!opts.description) return facts;
|
|
719
|
+
const description = tableDescription(facts);
|
|
720
|
+
return description ? { ...facts, description } : facts;
|
|
458
721
|
}
|
|
459
722
|
|
|
460
723
|
// src/nested.ts
|
|
724
|
+
var import_analyzer = require("@drzl/analyzer");
|
|
461
725
|
var NESTED_PREFIX = "Nested";
|
|
462
726
|
function nestedSchemaName(mode, tsName, affix) {
|
|
463
727
|
return NESTED_PREFIX + schemaName(mode, tsName, affix);
|
|
@@ -485,13 +749,14 @@ var KINDS_BY_MODE = {
|
|
|
485
749
|
};
|
|
486
750
|
var KIND_ORDER = { many: 0, manyToMany: 1, one: 2 };
|
|
487
751
|
function omittedColumnsFor(parent, child) {
|
|
488
|
-
const
|
|
752
|
+
const parentName = (0, import_analyzer.qualifiedTableName)(parent);
|
|
753
|
+
const back = (child.foreignKeys ?? []).filter((fk) => (0, import_analyzer.qualifiedForeignTable)(fk) === parentName);
|
|
489
754
|
if (back.length === 1) return { omitted: [...back[0].columns] };
|
|
490
755
|
if (back.length === 0) return { omitted: [] };
|
|
491
756
|
const named = back.map((fk) => fk.columns.join("+")).join(", ");
|
|
492
757
|
return {
|
|
493
758
|
omitted: [],
|
|
494
|
-
note: `${child.tsName} has ${back.length} foreign keys to ${
|
|
759
|
+
note: `${child.tsName} has ${back.length} foreign keys to ${parentName} (${named}), so which one this relation uses is not stated. None were omitted: supply them yourself.`
|
|
495
760
|
};
|
|
496
761
|
}
|
|
497
762
|
function buildNestedPlan(root, tables, relations, mode, depth) {
|
|
@@ -500,14 +765,15 @@ function buildNestedPlan(root, tables, relations, mode, depth) {
|
|
|
500
765
|
}
|
|
501
766
|
function buildNode(table, omitted, tables, relations, mode, depth) {
|
|
502
767
|
if (depth <= 0) return { table, omitted, arms: [] };
|
|
503
|
-
const
|
|
768
|
+
const byName = new Map(tables.map((t) => [(0, import_analyzer.qualifiedTableName)(t), t]));
|
|
504
769
|
const allowed = KINDS_BY_MODE[mode];
|
|
505
770
|
const columnNames = new Set(table.columns.map((c) => c.name));
|
|
506
771
|
const taken = /* @__PURE__ */ new Set();
|
|
507
772
|
const arms = [];
|
|
508
|
-
const
|
|
773
|
+
const self = (0, import_analyzer.qualifiedTableName)(table);
|
|
774
|
+
const candidates = relations.filter((r) => r.from === self && allowed.has(r.kind)).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
|
|
509
775
|
for (const rel of candidates) {
|
|
510
|
-
const child =
|
|
776
|
+
const child = byName.get(rel.to);
|
|
511
777
|
if (!child) continue;
|
|
512
778
|
const key = child.tsName;
|
|
513
779
|
if (columnNames.has(key)) continue;
|
|
@@ -733,7 +999,9 @@ async function formatCode(code, filePath, fmt) {
|
|
|
733
999
|
NAME_MODES,
|
|
734
1000
|
NESTED_PREFIX,
|
|
735
1001
|
applyTableCase,
|
|
1002
|
+
buildBrandPlan,
|
|
736
1003
|
buildNestedPlan,
|
|
1004
|
+
columnMetaFacts,
|
|
737
1005
|
describeSet,
|
|
738
1006
|
formatCode,
|
|
739
1007
|
importSpecifier,
|
|
@@ -752,10 +1020,12 @@ async function formatCode(code, filePath, fmt) {
|
|
|
752
1020
|
pascalCase,
|
|
753
1021
|
renderDuplicateFinder,
|
|
754
1022
|
resolveAffix,
|
|
1023
|
+
resolveBranding,
|
|
755
1024
|
resolveConfiguredImport,
|
|
756
1025
|
resolveNestedDepth,
|
|
757
1026
|
schemaName,
|
|
758
1027
|
selectColumns,
|
|
1028
|
+
tableMetaFacts,
|
|
759
1029
|
typeName,
|
|
760
1030
|
updateColumns,
|
|
761
1031
|
validateAffix
|