@drzl/validation-core 3.18.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.js CHANGED
@@ -5,6 +5,259 @@ import { createRequire } from "module";
5
5
  import path from "path";
6
6
  import { pathToFileURL } from "url";
7
7
 
8
+ // src/naming.ts
9
+ var NAME_MODES = ["insert", "update", "select"];
10
+ var DEFAULT_MODE_PREFIX = {
11
+ insert: "Insert",
12
+ update: "Update",
13
+ select: "Select"
14
+ };
15
+ var DEFAULT_TYPE_SUFFIX = {
16
+ insert: "Input",
17
+ update: "Input",
18
+ select: "Output"
19
+ };
20
+ var DEFAULT_SCHEMA_SUFFIX = "Schema";
21
+ var AFFIX_PROBE_TABLE = "users";
22
+ function spread(value, fallback) {
23
+ if (value === void 0) return { ...fallback };
24
+ if (typeof value === "string") return { insert: value, update: value, select: value };
25
+ return {
26
+ insert: value.insert ?? fallback.insert,
27
+ update: value.update ?? fallback.update,
28
+ select: value.select ?? fallback.select
29
+ };
30
+ }
31
+ function pascalCase(s) {
32
+ 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("");
33
+ }
34
+ function applyTableCase(tsName, tableCase) {
35
+ return tableCase === "pascal" ? pascalCase(tsName) : tsName;
36
+ }
37
+ function resolveAffix(opts) {
38
+ const affix = opts?.affix;
39
+ const legacy = opts?.schemaSuffix ?? DEFAULT_SCHEMA_SUFFIX;
40
+ const legacyMap = {
41
+ insert: legacy,
42
+ update: legacy,
43
+ select: legacy
44
+ };
45
+ return {
46
+ tableCase: affix?.tableCase ?? "preserve",
47
+ schema: {
48
+ prefix: spread(affix?.schema?.prefix, DEFAULT_MODE_PREFIX),
49
+ suffix: spread(affix?.schema?.suffix, legacyMap)
50
+ },
51
+ type: {
52
+ prefix: spread(affix?.type?.prefix, DEFAULT_MODE_PREFIX),
53
+ suffix: spread(affix?.type?.suffix, DEFAULT_TYPE_SUFFIX)
54
+ }
55
+ };
56
+ }
57
+ function schemaName(mode, tsName, affix) {
58
+ return affix.schema.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.schema.suffix[mode];
59
+ }
60
+ function typeName(mode, tsName, affix) {
61
+ return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
62
+ }
63
+ var PREFIX_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
64
+ var SUFFIX_RE = /^[A-Za-z0-9_$]+$/;
65
+ function validateAffix(affix, schemaSuffix) {
66
+ const issues = [];
67
+ if (!affix) return issues;
68
+ const checkOne = (value, path2, kind) => {
69
+ if (value === "") return;
70
+ const ok = kind === "prefix" ? PREFIX_RE.test(value) : SUFFIX_RE.test(value);
71
+ if (ok) return;
72
+ issues.push({
73
+ path: path2,
74
+ message: `${JSON.stringify(value)} cannot appear in a TypeScript identifier. Use only letters, digits, "_" and "$"` + (kind === "prefix" ? ", and do not start with a digit." : ".")
75
+ });
76
+ };
77
+ const checkValue = (value, base, kind) => {
78
+ if (value === void 0) return;
79
+ if (typeof value === "string") {
80
+ checkOne(value, base, kind);
81
+ return;
82
+ }
83
+ for (const mode of NAME_MODES) {
84
+ const v = value[mode];
85
+ if (v !== void 0) checkOne(v, [...base, mode], kind);
86
+ }
87
+ };
88
+ checkValue(affix.schema?.prefix, ["schema", "prefix"], "prefix");
89
+ checkValue(affix.schema?.suffix, ["schema", "suffix"], "suffix");
90
+ checkValue(affix.type?.prefix, ["type", "prefix"], "prefix");
91
+ checkValue(affix.type?.suffix, ["type", "suffix"], "suffix");
92
+ if (issues.length) return issues;
93
+ const resolved = resolveAffix({ affix, schemaSuffix });
94
+ const collisions = (space, build) => {
95
+ const seen = /* @__PURE__ */ new Map();
96
+ for (const mode of NAME_MODES) {
97
+ const name = build(mode);
98
+ const first = seen.get(name);
99
+ if (first) {
100
+ issues.push({
101
+ path: [space],
102
+ 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.`
103
+ });
104
+ } else {
105
+ seen.set(name, mode);
106
+ }
107
+ }
108
+ };
109
+ collisions("schema", (mode) => schemaName(mode, AFFIX_PROBE_TABLE, resolved));
110
+ collisions("type", (mode) => typeName(mode, AFFIX_PROBE_TABLE, resolved));
111
+ return issues;
112
+ }
113
+
114
+ // src/branding.ts
115
+ function resolveBranding(opt) {
116
+ if (!opt) return void 0;
117
+ if (opt === true) return { foreignKeys: true, aliases: true };
118
+ if (opt.enabled === false) return void 0;
119
+ return { foreignKeys: opt.foreignKeys !== false, aliases: opt.aliases !== false };
120
+ }
121
+ var at = (tsName, column) => `${tsName}\0${column}`;
122
+ function referenceOf(table, columnName) {
123
+ const col = table.columns.find((c) => c.name === columnName);
124
+ if (col?.references) return { sqlTable: col.references.table, column: col.references.column };
125
+ for (const fk of table.foreignKeys ?? []) {
126
+ const i = fk.columns.indexOf(columnName);
127
+ if (i === -1) continue;
128
+ const target = fk.foreignColumns[i];
129
+ if (target === void 0) continue;
130
+ return { sqlTable: fk.foreignTable, column: target };
131
+ }
132
+ return void 0;
133
+ }
134
+ function isKeyColumn(table, columnName) {
135
+ return (table.primaryKey?.columns ?? []).includes(columnName);
136
+ }
137
+ function buildBrandPlan(tables, opt) {
138
+ const resolved = resolveBranding(opt);
139
+ if (!resolved) return void 0;
140
+ const notes = [];
141
+ const byTs = /* @__PURE__ */ new Map();
142
+ const dupTs = /* @__PURE__ */ new Set();
143
+ for (const t of tables) {
144
+ if (byTs.has(t.tsName)) dupTs.add(t.tsName);
145
+ byTs.set(t.tsName, t);
146
+ }
147
+ for (const name of dupTs) {
148
+ byTs.delete(name);
149
+ notes.push(
150
+ `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.`
151
+ );
152
+ }
153
+ const bySqlName = /* @__PURE__ */ new Map();
154
+ for (const t of byTs.values()) {
155
+ const list = bySqlName.get(t.name) ?? [];
156
+ list.push(t);
157
+ bySqlName.set(t.name, list);
158
+ }
159
+ const tokens = /* @__PURE__ */ new Map();
160
+ const cache = /* @__PURE__ */ new Map();
161
+ const resolveToken = (tsName, columnName) => {
162
+ const start = at(tsName, columnName);
163
+ if (cache.has(start)) return cache.get(start);
164
+ const origin = byTs.get(tsName)?.columns.find((c) => c.name === columnName);
165
+ if (!origin || origin.arrayDimensions) {
166
+ cache.set(start, void 0);
167
+ return void 0;
168
+ }
169
+ const seen = /* @__PURE__ */ new Set();
170
+ let curTable = tsName;
171
+ let curColumn = columnName;
172
+ let answer;
173
+ let terminal;
174
+ for (; ; ) {
175
+ const key = at(curTable, curColumn);
176
+ if (seen.has(key)) {
177
+ notes.push(
178
+ `${tsName}.${columnName} references a cycle of foreign keys, so it is not branded.`
179
+ );
180
+ break;
181
+ }
182
+ seen.add(key);
183
+ const table = byTs.get(curTable);
184
+ const col = table?.columns.find((c) => c.name === curColumn);
185
+ if (!table || !col) break;
186
+ const ref = resolved.foreignKeys ? referenceOf(table, curColumn) : void 0;
187
+ if (ref) {
188
+ const candidates = bySqlName.get(ref.sqlTable) ?? [];
189
+ if (candidates.length !== 1) {
190
+ notes.push(
191
+ 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.`
192
+ );
193
+ break;
194
+ }
195
+ const next = candidates[0];
196
+ if (!next.columns.some((c) => c.name === ref.column)) {
197
+ notes.push(
198
+ `${tsName}.${columnName} references ${next.tsName}.${ref.column}, which is not a column of that table, so it is not branded.`
199
+ );
200
+ break;
201
+ }
202
+ curTable = next.tsName;
203
+ curColumn = ref.column;
204
+ continue;
205
+ }
206
+ if (isKeyColumn(table, curColumn)) {
207
+ answer = `${curTable}.${curColumn}`;
208
+ terminal = col;
209
+ }
210
+ break;
211
+ }
212
+ if (answer && terminal && terminal.tsType !== origin.tsType) {
213
+ notes.push(
214
+ `${tsName}.${columnName} is a ${origin.tsType} and ${answer} is a ${terminal.tsType}, so it is not branded.`
215
+ );
216
+ answer = void 0;
217
+ }
218
+ cache.set(start, answer);
219
+ return answer;
220
+ };
221
+ for (const t of byTs.values()) {
222
+ for (const c of t.columns) {
223
+ const token = resolveToken(t.tsName, c.name);
224
+ if (token) tokens.set(at(t.tsName, c.name), token);
225
+ }
226
+ }
227
+ const aliases = /* @__PURE__ */ new Map();
228
+ if (resolved.aliases) {
229
+ const claimed = /* @__PURE__ */ new Map();
230
+ const draft = [];
231
+ for (const t of byTs.values()) {
232
+ for (const c of t.columns) {
233
+ const token = tokens.get(at(t.tsName, c.name));
234
+ if (token !== `${t.tsName}.${c.name}`) continue;
235
+ const alias = pascalCase(t.tsName) + pascalCase(c.name);
236
+ draft.push({ tsName: t.tsName, entry: { alias, column: c.name, token } });
237
+ claimed.set(alias, [...claimed.get(alias) ?? [], `${t.tsName}.${c.name}`]);
238
+ }
239
+ }
240
+ for (const { tsName, entry } of draft) {
241
+ const owners = claimed.get(entry.alias) ?? [];
242
+ if (owners.length > 1) continue;
243
+ aliases.set(tsName, [...aliases.get(tsName) ?? [], entry]);
244
+ }
245
+ for (const [alias, owners] of claimed) {
246
+ if (owners.length > 1) {
247
+ notes.push(
248
+ `${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.`
249
+ );
250
+ }
251
+ }
252
+ }
253
+ return {
254
+ brandOf: (tsName, columnName) => tokens.get(at(tsName, columnName)),
255
+ aliasesFor: (tsName) => aliases.get(tsName) ?? [],
256
+ any: tokens.size > 0,
257
+ notes
258
+ };
259
+ }
260
+
8
261
  // src/checks.ts
9
262
  var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
10
263
  var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
@@ -278,113 +531,120 @@ function withTsExtension(p) {
278
531
  return `${p}.ts`;
279
532
  }
280
533
 
281
- // src/naming.ts
282
- var NAME_MODES = ["insert", "update", "select"];
283
- var DEFAULT_MODE_PREFIX = {
284
- insert: "Insert",
285
- update: "Update",
286
- select: "Select"
287
- };
288
- var DEFAULT_TYPE_SUFFIX = {
289
- insert: "Input",
290
- update: "Input",
291
- select: "Output"
292
- };
293
- var DEFAULT_SCHEMA_SUFFIX = "Schema";
294
- var AFFIX_PROBE_TABLE = "users";
295
- function spread(value, fallback) {
296
- if (value === void 0) return { ...fallback };
297
- if (typeof value === "string") return { insert: value, update: value, select: value };
298
- return {
299
- insert: value.insert ?? fallback.insert,
300
- update: value.update ?? fallback.update,
301
- select: value.select ?? fallback.select
302
- };
534
+ // src/meta.ts
535
+ function labelled(name, text) {
536
+ return name ? `${name}: ${text}` : text;
303
537
  }
304
- function pascalCase(s) {
305
- 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("");
538
+ function literalText(value, kind) {
539
+ return kind === "string" ? `'${value}'` : value;
306
540
  }
307
- function applyTableCase(tsName, tableCase) {
308
- return tableCase === "pascal" ? pascalCase(tsName) : tsName;
541
+ function columnCheckText(k) {
542
+ return labelled(k.name, `${k.column} ${k.operator} ${literalText(k.value, k.kind)}`);
309
543
  }
310
- function resolveAffix(opts) {
311
- const affix = opts?.affix;
312
- const legacy = opts?.schemaSuffix ?? DEFAULT_SCHEMA_SUFFIX;
313
- const legacyMap = {
314
- insert: legacy,
315
- update: legacy,
316
- select: legacy
317
- };
318
- return {
319
- tableCase: affix?.tableCase ?? "preserve",
320
- schema: {
321
- prefix: spread(affix?.schema?.prefix, DEFAULT_MODE_PREFIX),
322
- suffix: spread(affix?.schema?.suffix, legacyMap)
323
- },
324
- type: {
325
- prefix: spread(affix?.type?.prefix, DEFAULT_MODE_PREFIX),
326
- suffix: spread(affix?.type?.suffix, DEFAULT_TYPE_SUFFIX)
327
- }
328
- };
544
+ function setText(k) {
545
+ return labelled(
546
+ k.name,
547
+ `${k.column} IN (${k.values.map((v) => literalText(v, k.kind)).join(", ")})`
548
+ );
329
549
  }
330
- function schemaName(mode, tsName, affix) {
331
- return affix.schema.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.schema.suffix[mode];
550
+ function lengthText(k) {
551
+ return labelled(k.name, `length(${k.column}) ${k.operator} ${k.value}`);
332
552
  }
333
- function typeName(mode, tsName, affix) {
334
- return affix.type.prefix[mode] + applyTableCase(tsName, affix.tableCase) + affix.type.suffix[mode];
553
+ function cardinalityText(k) {
554
+ return labelled(k.name, `cardinality(${k.column}) ${k.operator} ${k.value}`);
335
555
  }
336
- var PREFIX_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
337
- var SUFFIX_RE = /^[A-Za-z0-9_$]+$/;
338
- function validateAffix(affix, schemaSuffix) {
339
- const issues = [];
340
- if (!affix) return issues;
341
- const checkOne = (value, path2, kind) => {
342
- if (value === "") return;
343
- const ok = kind === "prefix" ? PREFIX_RE.test(value) : SUFFIX_RE.test(value);
344
- if (ok) return;
345
- issues.push({
346
- path: path2,
347
- message: `${JSON.stringify(value)} cannot appear in a TypeScript identifier. Use only letters, digits, "_" and "$"` + (kind === "prefix" ? ", and do not start with a digit." : ".")
348
- });
349
- };
350
- const checkValue = (value, base, kind) => {
351
- if (value === void 0) return;
352
- if (typeof value === "string") {
353
- checkOne(value, base, kind);
556
+ function rowText(k) {
557
+ return labelled(k.name, `${k.left} ${k.operator} ${k.right}`);
558
+ }
559
+ function takesScalarChecks(c) {
560
+ return !c.arrayDimensions && !c.shape;
561
+ }
562
+ function classifyChecks(table) {
563
+ const perColumn = /* @__PURE__ */ new Map();
564
+ const rows = [];
565
+ const unenforced = [];
566
+ const byName = new Map(table.columns.map((c) => [c.name, c]));
567
+ const add = (column, text, guard) => {
568
+ const c = byName.get(column);
569
+ if (!c || !guard(c)) {
570
+ unenforced.push(text);
354
571
  return;
355
572
  }
356
- for (const mode of NAME_MODES) {
357
- const v = value[mode];
358
- if (v !== void 0) checkOne(v, [...base, mode], kind);
359
- }
573
+ const list = perColumn.get(column) ?? [];
574
+ list.push(text);
575
+ perColumn.set(column, list);
360
576
  };
361
- checkValue(affix.schema?.prefix, ["schema", "prefix"], "prefix");
362
- checkValue(affix.schema?.suffix, ["schema", "suffix"], "suffix");
363
- checkValue(affix.type?.prefix, ["type", "prefix"], "prefix");
364
- checkValue(affix.type?.suffix, ["type", "suffix"], "suffix");
365
- if (issues.length) return issues;
366
- const resolved = resolveAffix({ affix, schemaSuffix });
367
- const collisions = (space, build) => {
368
- const seen = /* @__PURE__ */ new Map();
369
- for (const mode of NAME_MODES) {
370
- const name = build(mode);
371
- const first = seen.get(name);
372
- if (first) {
373
- issues.push({
374
- path: [space],
375
- 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.`
376
- });
377
- } else {
378
- seen.set(name, mode);
379
- }
577
+ for (const k of table.checks ?? []) {
578
+ const parsed = parseCheck(k.expression, k.name);
579
+ if (!parsed.ok) {
580
+ unenforced.push(labelled(k.name, (k.expression ?? "").trim()));
581
+ continue;
582
+ }
583
+ for (const c of parsed.checks) add(c.column, columnCheckText(c), takesScalarChecks);
584
+ for (const s of parsed.sets ?? []) add(s.column, setText(s), takesScalarChecks);
585
+ for (const l of parsed.lengths ?? []) add(l.column, lengthText(l), takesScalarChecks);
586
+ for (const a of parsed.cardinalities ?? [])
587
+ add(a.column, cardinalityText(a), (c) => !!c.arrayDimensions);
588
+ for (const r of parsed.rows ?? []) {
589
+ if (byName.has(r.left) && byName.has(r.right)) rows.push(rowText(r));
590
+ else unenforced.push(rowText(r));
380
591
  }
592
+ }
593
+ return { perColumn, rows, unenforced };
594
+ }
595
+ function columnDescription(facts) {
596
+ const parts = [];
597
+ if (facts.maxLength !== void 0) parts.push(`at most ${facts.maxLength} characters`);
598
+ if (facts.maxBytes !== void 0) parts.push(`at most ${facts.maxBytes} bytes`);
599
+ for (const c of facts.checks ?? []) parts.push(`CHECK ${c}`);
600
+ return parts.length ? parts.join(". ") : void 0;
601
+ }
602
+ function tableDescription(facts) {
603
+ const parts = [];
604
+ for (const c of facts.checks ?? []) parts.push(`CHECK ${c}`);
605
+ if (facts.unenforcedChecks?.length) {
606
+ parts.push(
607
+ `not enforced by this schema, the database also checks: ${facts.unenforcedChecks.join("; ")}`
608
+ );
609
+ }
610
+ return parts.length ? parts.join(". ") : void 0;
611
+ }
612
+ function columnMetaFacts(column, table, opts = {}) {
613
+ const checks = classifyChecks(table).perColumn.get(column.name);
614
+ const facts = {
615
+ ...column.sqlType ? { sqlType: column.sqlType } : {},
616
+ ...column.maxLength !== void 0 ? { maxLength: column.maxLength } : {},
617
+ ...column.maxBytes !== void 0 ? { maxBytes: column.maxBytes } : {},
618
+ ...column.hasDefault ? { hasDefault: true } : {},
619
+ ...column.isGenerated ? { generated: true } : {},
620
+ ...checks?.length ? { checks } : {}
381
621
  };
382
- collisions("schema", (mode) => schemaName(mode, AFFIX_PROBE_TABLE, resolved));
383
- collisions("type", (mode) => typeName(mode, AFFIX_PROBE_TABLE, resolved));
384
- return issues;
622
+ if (!opts.description) return facts;
623
+ const description = columnDescription(facts);
624
+ return description ? { ...facts, description } : facts;
625
+ }
626
+ function tableMetaFacts(table, opts) {
627
+ const { rows, unenforced } = classifyChecks(table);
628
+ const pk = table.primaryKey?.columns ?? [];
629
+ const unique = (table.unique ?? []).map((k) => k.columns).filter((c) => c.length > 0);
630
+ const facts = {
631
+ table: table.name,
632
+ ...table.schema ? { schema: table.schema } : {},
633
+ ...opts.dialect ? { dialect: opts.dialect } : {},
634
+ mode: opts.mode,
635
+ ...pk.length ? { primaryKey: pk } : {},
636
+ ...unique.length ? { unique } : {},
637
+ ...table.readOnly ? { readOnly: true } : {},
638
+ ...rows.length ? { checks: rows } : {},
639
+ ...unenforced.length ? { unenforcedChecks: unenforced } : {}
640
+ };
641
+ if (!opts.description) return facts;
642
+ const description = tableDescription(facts);
643
+ return description ? { ...facts, description } : facts;
385
644
  }
386
645
 
387
646
  // src/nested.ts
647
+ import { qualifiedForeignTable, qualifiedTableName } from "@drzl/analyzer";
388
648
  var NESTED_PREFIX = "Nested";
389
649
  function nestedSchemaName(mode, tsName, affix) {
390
650
  return NESTED_PREFIX + schemaName(mode, tsName, affix);
@@ -412,13 +672,14 @@ var KINDS_BY_MODE = {
412
672
  };
413
673
  var KIND_ORDER = { many: 0, manyToMany: 1, one: 2 };
414
674
  function omittedColumnsFor(parent, child) {
415
- const back = (child.foreignKeys ?? []).filter((fk) => fk.foreignTable === parent.name);
675
+ const parentName = qualifiedTableName(parent);
676
+ const back = (child.foreignKeys ?? []).filter((fk) => qualifiedForeignTable(fk) === parentName);
416
677
  if (back.length === 1) return { omitted: [...back[0].columns] };
417
678
  if (back.length === 0) return { omitted: [] };
418
679
  const named = back.map((fk) => fk.columns.join("+")).join(", ");
419
680
  return {
420
681
  omitted: [],
421
- note: `${child.tsName} has ${back.length} foreign keys to ${parent.name} (${named}), so which one this relation uses is not stated. None were omitted: supply them yourself.`
682
+ 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.`
422
683
  };
423
684
  }
424
685
  function buildNestedPlan(root, tables, relations, mode, depth) {
@@ -427,14 +688,15 @@ function buildNestedPlan(root, tables, relations, mode, depth) {
427
688
  }
428
689
  function buildNode(table, omitted, tables, relations, mode, depth) {
429
690
  if (depth <= 0) return { table, omitted, arms: [] };
430
- const byDbName = new Map(tables.map((t) => [t.name, t]));
691
+ const byName = new Map(tables.map((t) => [qualifiedTableName(t), t]));
431
692
  const allowed = KINDS_BY_MODE[mode];
432
693
  const columnNames = new Set(table.columns.map((c) => c.name));
433
694
  const taken = /* @__PURE__ */ new Set();
434
695
  const arms = [];
435
- const candidates = relations.filter((r) => r.from === table.name && allowed.has(r.kind)).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
696
+ const self = qualifiedTableName(table);
697
+ const candidates = relations.filter((r) => r.from === self && allowed.has(r.kind)).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
436
698
  for (const rel of candidates) {
437
- const child = byDbName.get(rel.to);
699
+ const child = byName.get(rel.to);
438
700
  if (!child) continue;
439
701
  const key = child.tsName;
440
702
  if (columnNames.has(key)) continue;
@@ -659,7 +921,9 @@ export {
659
921
  NAME_MODES,
660
922
  NESTED_PREFIX,
661
923
  applyTableCase,
924
+ buildBrandPlan,
662
925
  buildNestedPlan,
926
+ columnMetaFacts,
663
927
  describeSet,
664
928
  formatCode,
665
929
  importSpecifier,
@@ -678,10 +942,12 @@ export {
678
942
  pascalCase,
679
943
  renderDuplicateFinder,
680
944
  resolveAffix,
945
+ resolveBranding,
681
946
  resolveConfiguredImport,
682
947
  resolveNestedDepth,
683
948
  schemaName,
684
949
  selectColumns,
950
+ tableMetaFacts,
685
951
  typeName,
686
952
  updateColumns,
687
953
  validateAffix
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/validation-core",
3
- "version": "3.18.0",
3
+ "version": "3.20.0",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "sideEffects": false,
26
26
  "dependencies": {
27
- "@drzl/analyzer": "^1.18.0"
27
+ "@drzl/analyzer": "^1.20.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "prettier": ">=3"