@drzl/validation-core 3.16.4 → 3.18.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 +102 -0
- package/dist/index.d.cts +171 -14
- package/dist/index.d.ts +171 -14
- package/dist/index.js +93 -0
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -36,11 +36,15 @@ __export(index_exports, {
|
|
|
36
36
|
COLUMN_FORMATS: () => COLUMN_FORMATS,
|
|
37
37
|
DEFAULT_IMPORT_EXTENSION: () => DEFAULT_IMPORT_EXTENSION,
|
|
38
38
|
DEFAULT_MODE_PREFIX: () => DEFAULT_MODE_PREFIX,
|
|
39
|
+
DEFAULT_NESTED_DEPTH: () => DEFAULT_NESTED_DEPTH,
|
|
39
40
|
DEFAULT_SCHEMA_SUFFIX: () => DEFAULT_SCHEMA_SUFFIX,
|
|
40
41
|
DEFAULT_TYPE_SUFFIX: () => DEFAULT_TYPE_SUFFIX,
|
|
41
42
|
IMPORT_EXTENSIONS: () => IMPORT_EXTENSIONS,
|
|
43
|
+
MAX_NESTED_DEPTH: () => MAX_NESTED_DEPTH,
|
|
42
44
|
NAME_MODES: () => NAME_MODES,
|
|
45
|
+
NESTED_PREFIX: () => NESTED_PREFIX,
|
|
43
46
|
applyTableCase: () => applyTableCase,
|
|
47
|
+
buildNestedPlan: () => buildNestedPlan,
|
|
44
48
|
describeSet: () => describeSet,
|
|
45
49
|
formatCode: () => formatCode,
|
|
46
50
|
importSpecifier: () => importSpecifier,
|
|
@@ -49,6 +53,10 @@ __export(index_exports, {
|
|
|
49
53
|
isIntegerColumn: () => isIntegerColumn,
|
|
50
54
|
moduleFileName: () => moduleFileName,
|
|
51
55
|
moduleSpecifier: () => moduleSpecifier,
|
|
56
|
+
nestedArmNotes: () => nestedArmNotes,
|
|
57
|
+
nestedNodeColumns: () => nestedNodeColumns,
|
|
58
|
+
nestedSchemaName: () => nestedSchemaName,
|
|
59
|
+
nestedTypeName: () => nestedTypeName,
|
|
52
60
|
nonFiniteAccepted: () => nonFiniteAccepted,
|
|
53
61
|
parseCheck: () => parseCheck,
|
|
54
62
|
parsesToADate: () => parsesToADate,
|
|
@@ -56,6 +64,7 @@ __export(index_exports, {
|
|
|
56
64
|
renderDuplicateFinder: () => renderDuplicateFinder,
|
|
57
65
|
resolveAffix: () => resolveAffix,
|
|
58
66
|
resolveConfiguredImport: () => resolveConfiguredImport,
|
|
67
|
+
resolveNestedDepth: () => resolveNestedDepth,
|
|
59
68
|
schemaName: () => schemaName,
|
|
60
69
|
selectColumns: () => selectColumns,
|
|
61
70
|
typeName: () => typeName,
|
|
@@ -448,6 +457,90 @@ function validateAffix(affix, schemaSuffix) {
|
|
|
448
457
|
return issues;
|
|
449
458
|
}
|
|
450
459
|
|
|
460
|
+
// src/nested.ts
|
|
461
|
+
var NESTED_PREFIX = "Nested";
|
|
462
|
+
function nestedSchemaName(mode, tsName, affix) {
|
|
463
|
+
return NESTED_PREFIX + schemaName(mode, tsName, affix);
|
|
464
|
+
}
|
|
465
|
+
function nestedTypeName(mode, tsName, affix) {
|
|
466
|
+
return NESTED_PREFIX + typeName(mode, tsName, affix);
|
|
467
|
+
}
|
|
468
|
+
var DEFAULT_NESTED_DEPTH = 1;
|
|
469
|
+
var MAX_NESTED_DEPTH = 3;
|
|
470
|
+
function resolveNestedDepth(depth, warn) {
|
|
471
|
+
if (depth === void 0) return DEFAULT_NESTED_DEPTH;
|
|
472
|
+
if (!Number.isFinite(depth)) return DEFAULT_NESTED_DEPTH;
|
|
473
|
+
const whole = Math.trunc(depth);
|
|
474
|
+
const clamped = Math.min(Math.max(whole, 1), MAX_NESTED_DEPTH);
|
|
475
|
+
if (clamped !== depth && warn) {
|
|
476
|
+
warn(
|
|
477
|
+
`[drzl] nestedDepth ${depth} is outside 1..${MAX_NESTED_DEPTH} and was read as ${clamped}.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return clamped;
|
|
481
|
+
}
|
|
482
|
+
var KINDS_BY_MODE = {
|
|
483
|
+
insert: /* @__PURE__ */ new Set(["many", "manyToMany"]),
|
|
484
|
+
select: /* @__PURE__ */ new Set(["one", "many", "manyToMany"])
|
|
485
|
+
};
|
|
486
|
+
var KIND_ORDER = { many: 0, manyToMany: 1, one: 2 };
|
|
487
|
+
function omittedColumnsFor(parent, child) {
|
|
488
|
+
const back = (child.foreignKeys ?? []).filter((fk) => fk.foreignTable === parent.name);
|
|
489
|
+
if (back.length === 1) return { omitted: [...back[0].columns] };
|
|
490
|
+
if (back.length === 0) return { omitted: [] };
|
|
491
|
+
const named = back.map((fk) => fk.columns.join("+")).join(", ");
|
|
492
|
+
return {
|
|
493
|
+
omitted: [],
|
|
494
|
+
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.`
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
function buildNestedPlan(root, tables, relations, mode, depth) {
|
|
498
|
+
const node = buildNode(root, [], tables, relations, mode, depth);
|
|
499
|
+
return node.arms.length ? node : void 0;
|
|
500
|
+
}
|
|
501
|
+
function buildNode(table, omitted, tables, relations, mode, depth) {
|
|
502
|
+
if (depth <= 0) return { table, omitted, arms: [] };
|
|
503
|
+
const byDbName = new Map(tables.map((t) => [t.name, t]));
|
|
504
|
+
const allowed = KINDS_BY_MODE[mode];
|
|
505
|
+
const columnNames = new Set(table.columns.map((c) => c.name));
|
|
506
|
+
const taken = /* @__PURE__ */ new Set();
|
|
507
|
+
const arms = [];
|
|
508
|
+
const candidates = relations.filter((r) => r.from === table.name && allowed.has(r.kind)).sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
|
|
509
|
+
for (const rel of candidates) {
|
|
510
|
+
const child = byDbName.get(rel.to);
|
|
511
|
+
if (!child) continue;
|
|
512
|
+
const key = child.tsName;
|
|
513
|
+
if (columnNames.has(key)) continue;
|
|
514
|
+
if (taken.has(key)) continue;
|
|
515
|
+
taken.add(key);
|
|
516
|
+
const { omitted: childOmitted, note } = rel.kind === "many" && mode === "insert" ? omittedColumnsFor(table, child) : { omitted: [], note: void 0 };
|
|
517
|
+
arms.push({
|
|
518
|
+
key,
|
|
519
|
+
kind: rel.kind,
|
|
520
|
+
via: rel.via,
|
|
521
|
+
single: rel.kind === "one",
|
|
522
|
+
note,
|
|
523
|
+
child: buildNode(child, childOmitted, tables, relations, mode, depth - 1)
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return { table, omitted, arms };
|
|
527
|
+
}
|
|
528
|
+
function nestedArmNotes(arm) {
|
|
529
|
+
const out = [];
|
|
530
|
+
if (arm.kind === "manyToMany") {
|
|
531
|
+
out.push(
|
|
532
|
+
`Through ${arm.via ?? "a join table"}. The join row is not described: its columns are the two foreign keys, and the two ends of this payload supply both.`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (arm.note) out.push(arm.note);
|
|
536
|
+
return out;
|
|
537
|
+
}
|
|
538
|
+
function nestedNodeColumns(columnsForMode, node) {
|
|
539
|
+
if (!node.omitted.length) return [...columnsForMode];
|
|
540
|
+
const drop = new Set(node.omitted);
|
|
541
|
+
return columnsForMode.filter((c) => !drop.has(c.name));
|
|
542
|
+
}
|
|
543
|
+
|
|
451
544
|
// src/duplicates.ts
|
|
452
545
|
function usableKeys(table) {
|
|
453
546
|
return (table.unique ?? []).filter((k) => k.columns.length > 0);
|
|
@@ -632,11 +725,15 @@ async function formatCode(code, filePath, fmt) {
|
|
|
632
725
|
COLUMN_FORMATS,
|
|
633
726
|
DEFAULT_IMPORT_EXTENSION,
|
|
634
727
|
DEFAULT_MODE_PREFIX,
|
|
728
|
+
DEFAULT_NESTED_DEPTH,
|
|
635
729
|
DEFAULT_SCHEMA_SUFFIX,
|
|
636
730
|
DEFAULT_TYPE_SUFFIX,
|
|
637
731
|
IMPORT_EXTENSIONS,
|
|
732
|
+
MAX_NESTED_DEPTH,
|
|
638
733
|
NAME_MODES,
|
|
734
|
+
NESTED_PREFIX,
|
|
639
735
|
applyTableCase,
|
|
736
|
+
buildNestedPlan,
|
|
640
737
|
describeSet,
|
|
641
738
|
formatCode,
|
|
642
739
|
importSpecifier,
|
|
@@ -645,6 +742,10 @@ async function formatCode(code, filePath, fmt) {
|
|
|
645
742
|
isIntegerColumn,
|
|
646
743
|
moduleFileName,
|
|
647
744
|
moduleSpecifier,
|
|
745
|
+
nestedArmNotes,
|
|
746
|
+
nestedNodeColumns,
|
|
747
|
+
nestedSchemaName,
|
|
748
|
+
nestedTypeName,
|
|
648
749
|
nonFiniteAccepted,
|
|
649
750
|
parseCheck,
|
|
650
751
|
parsesToADate,
|
|
@@ -652,6 +753,7 @@ async function formatCode(code, filePath, fmt) {
|
|
|
652
753
|
renderDuplicateFinder,
|
|
653
754
|
resolveAffix,
|
|
654
755
|
resolveConfiguredImport,
|
|
756
|
+
resolveNestedDepth,
|
|
655
757
|
schemaName,
|
|
656
758
|
selectColumns,
|
|
657
759
|
typeName,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Table as Table$1, Column, Analysis } from '@drzl/analyzer';
|
|
1
|
+
import { Relation, Table as Table$1, Column, Analysis } from '@drzl/analyzer';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* One place that decides what a generated module is called, on disk and in an import.
|
|
@@ -284,6 +284,127 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
|
|
|
284
284
|
*/
|
|
285
285
|
declare function describeSet(set: ColumnSet): string;
|
|
286
286
|
|
|
287
|
+
/**
|
|
288
|
+
* What a relations-aware nested schema describes, decided once for all five generators.
|
|
289
|
+
*
|
|
290
|
+
* A caller who inserts a parent and its children in one payload, `{ ...user, posts: [...] }`, has
|
|
291
|
+
* nothing to validate it against. Every first-party Drizzle validator emits columns only: measured
|
|
292
|
+
* at drizzle-orm 1.0.0-rc.4 and at the 0.4x `drizzle-zod`/`drizzle-valibot`/`drizzle-typebox`/
|
|
293
|
+
* `drizzle-arktype` packages, `createInsertSchema(users)` yields exactly `['id', 'name']` and never
|
|
294
|
+
* a `posts` key, in every mode and every library. Handing the relations object in as the second
|
|
295
|
+
* argument does not change that either: it lands in the refine slot and is dropped, because its
|
|
296
|
+
* keys are not column names. Handing it in first throws inside `getColumns`.
|
|
297
|
+
*
|
|
298
|
+
* And the payload is not merely unvalidated, it is silently discarded. `db.insert(users).values({
|
|
299
|
+
* name: 'a', posts: [{ title: 't' }] })` emits `insert into "users" ("id", "name") values
|
|
300
|
+
* (default, $1)` on both majors: the relation key is dropped, no error is raised, and the children
|
|
301
|
+
* are simply never written.
|
|
302
|
+
*
|
|
303
|
+
* This module states the *shape* of such a payload from the analysis. It renders nothing: each
|
|
304
|
+
* generator walks the plan and emits it in its own library, so the four cannot disagree about
|
|
305
|
+
* which relations appear or which columns a child carries.
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Which modes get a nested variant.
|
|
310
|
+
*
|
|
311
|
+
* `update` is deliberately absent, and it is the one decision here that is a refusal rather than a
|
|
312
|
+
* design. A nested update payload has no single meaning: `{ name: 'x', posts: [...] }` could mean
|
|
313
|
+
* replace every child, upsert them, or patch the ones that match, and choosing needs an operation
|
|
314
|
+
* vocabulary (Prisma spells it `create` / `connect` / `set` / `deleteMany`) that neither DRZL nor
|
|
315
|
+
* Drizzle has. Worse, it could not be acted on even if the meaning were fixed: `updateColumns`
|
|
316
|
+
* drops the primary key, so a child inside an update payload carries nothing that identifies which
|
|
317
|
+
* row it patches. A schema whose meaning the tool emitting it cannot state is worse than no schema.
|
|
318
|
+
*/
|
|
319
|
+
type NestedMode = 'insert' | 'select';
|
|
320
|
+
/** Prefix in front of the ordinary resolved name, so affixes keep working underneath it. */
|
|
321
|
+
declare const NESTED_PREFIX = "Nested";
|
|
322
|
+
/** e.g. `NestedInsertusersSchema`. */
|
|
323
|
+
declare function nestedSchemaName(mode: NestedMode, tsName: string, affix: ResolvedAffix): string;
|
|
324
|
+
/** e.g. `NestedInsertusersInput`. */
|
|
325
|
+
declare function nestedTypeName(mode: NestedMode, tsName: string, affix: ResolvedAffix): string;
|
|
326
|
+
/**
|
|
327
|
+
* How many levels of children a nested schema describes, when nothing says.
|
|
328
|
+
*
|
|
329
|
+
* One, because one is what the payload in the plan item is: a parent and its children. Deeper is
|
|
330
|
+
* available and is not the default, because the output grows multiplicatively in this number and
|
|
331
|
+
* generated code ships in the consumer's bundle.
|
|
332
|
+
*/
|
|
333
|
+
declare const DEFAULT_NESTED_DEPTH = 1;
|
|
334
|
+
/**
|
|
335
|
+
* The most levels `nestedDepth` will honour.
|
|
336
|
+
*
|
|
337
|
+
* Not a taste limit. Nesting is expanded inline rather than by reference, so a schema whose tables
|
|
338
|
+
* average R relations emits R^depth child shapes per root table, and both directions of a
|
|
339
|
+
* many-to-many count. At 3 that is already an eightfold expansion of every column list in the
|
|
340
|
+
* schema; past it the emitted file stops being something a person can open.
|
|
341
|
+
*/
|
|
342
|
+
declare const MAX_NESTED_DEPTH = 3;
|
|
343
|
+
/** Clamp a configured depth into the range that is actually emitted, saying so if it moved. */
|
|
344
|
+
declare function resolveNestedDepth(depth: number | undefined, warn?: (msg: string) => void): number;
|
|
345
|
+
/** One relation as it appears in a payload. */
|
|
346
|
+
interface NestedArm {
|
|
347
|
+
/** The key the payload uses. The child table's Drizzle export name, since nothing else names it. */
|
|
348
|
+
key: string;
|
|
349
|
+
kind: Relation['kind'];
|
|
350
|
+
/** Join table of a many-to-many, carried only so the emitted comment can name it. */
|
|
351
|
+
via?: string;
|
|
352
|
+
/** `true` when the value is one object rather than an array of them. Only a `one` relation. */
|
|
353
|
+
single: boolean;
|
|
354
|
+
/** The child payload, itself possibly carrying relations. */
|
|
355
|
+
child: NestedNode;
|
|
356
|
+
/**
|
|
357
|
+
* Something true about this arm that the shape alone does not say, emitted as a comment beside
|
|
358
|
+
* it. Absent when there is nothing to say.
|
|
359
|
+
*/
|
|
360
|
+
note?: string;
|
|
361
|
+
}
|
|
362
|
+
/** One object in a nested payload: a table, minus what the enclosing parent supplies. */
|
|
363
|
+
interface NestedNode {
|
|
364
|
+
table: Table$1;
|
|
365
|
+
/**
|
|
366
|
+
* Columns dropped from this object because the row that encloses it supplies them.
|
|
367
|
+
*
|
|
368
|
+
* Always empty at the root and always empty in `select`, where every column of the row really is
|
|
369
|
+
* returned. On `insert` it holds the child's foreign key to its parent; see `armFor`.
|
|
370
|
+
*/
|
|
371
|
+
omitted: string[];
|
|
372
|
+
arms: NestedArm[];
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The nested payload for one table, expanded `depth` levels.
|
|
376
|
+
*
|
|
377
|
+
* Recursion is bounded rather than expressed. All four libraries can state a cyclic schema, and
|
|
378
|
+
* each does it differently: zod through a property getter, valibot through `v.lazy`, ArkType only
|
|
379
|
+
* inside a `scope` (a plain forward reference throws `Cannot access 'Post' before initialization`
|
|
380
|
+
* at module load), TypeBox only inside a `Type.Module` (a bare `Type.Ref` constructs happily and
|
|
381
|
+
* then throws `Unable to dereference schema with $id` the first time anything checks a value).
|
|
382
|
+
* Every one of those was run.
|
|
383
|
+
*
|
|
384
|
+
* None is used. Expanding inline to a fixed depth needs no recursion API, so no emitted module can
|
|
385
|
+
* throw on import from an unresolvable reference, no zod or valibot schema needs the explicit type
|
|
386
|
+
* annotation a self-referential one demands from TypeScript, and the four outputs stay structurally
|
|
387
|
+
* identical instead of diverging into four different recursion mechanisms. A cycle in the relations
|
|
388
|
+
* simply stops at the depth: `users -> posts -> users` at depth 1 emits the posts and no more.
|
|
389
|
+
*/
|
|
390
|
+
declare function buildNestedPlan(root: Table$1, tables: readonly Table$1[], relations: readonly Relation[], mode: NestedMode, depth: number): NestedNode | undefined;
|
|
391
|
+
/**
|
|
392
|
+
* The comment lines that go above an arm, without their `//`.
|
|
393
|
+
*
|
|
394
|
+
* Shared so the five generators say the same thing about the same relation, and returned unmarked
|
|
395
|
+
* because each of them indents its object literal differently.
|
|
396
|
+
*/
|
|
397
|
+
declare function nestedArmNotes(arm: NestedArm): string[];
|
|
398
|
+
/**
|
|
399
|
+
* Which columns a node contributes, given the mode's column list for its table.
|
|
400
|
+
*
|
|
401
|
+
* The list is passed in rather than derived, so this cannot drift from `insertColumns` and
|
|
402
|
+
* `selectColumns` and so `validation-core`'s entry point stays free of a cycle back into itself.
|
|
403
|
+
*/
|
|
404
|
+
declare function nestedNodeColumns<T extends {
|
|
405
|
+
name: string;
|
|
406
|
+
}>(columnsForMode: readonly T[], node: NestedNode): T[];
|
|
407
|
+
|
|
287
408
|
/**
|
|
288
409
|
* A duplicate finder for a batch of rows, emitted beside the schemas.
|
|
289
410
|
*
|
|
@@ -297,7 +418,7 @@ declare function describeSet(set: ColumnSet): string;
|
|
|
297
418
|
* is worth checking, because it is the half a user can fix before sending anything.
|
|
298
419
|
*
|
|
299
420
|
* The emitted function is plain TypeScript with no reference to any validation library, so all
|
|
300
|
-
*
|
|
421
|
+
* five generators emit the same thing. It is rendered from one place for that reason.
|
|
301
422
|
*/
|
|
302
423
|
|
|
303
424
|
/**
|
|
@@ -316,7 +437,7 @@ interface Table {
|
|
|
316
437
|
columns: string[];
|
|
317
438
|
};
|
|
318
439
|
}
|
|
319
|
-
type ValidationLibrary = 'zod' | 'valibot' | 'arktype' | 'typebox';
|
|
440
|
+
type ValidationLibrary = 'zod' | 'valibot' | 'arktype' | 'typebox' | 'effect';
|
|
320
441
|
interface FormatOptions {
|
|
321
442
|
enabled?: boolean;
|
|
322
443
|
engine?: 'auto' | 'prettier' | 'biome';
|
|
@@ -403,6 +524,29 @@ interface ValidationGenerateOptions {
|
|
|
403
524
|
*/
|
|
404
525
|
affix?: AffixOptions;
|
|
405
526
|
coerceDates?: 'input' | 'all' | 'none';
|
|
527
|
+
/**
|
|
528
|
+
* Also emit `NestedInsert<Table>` and `NestedSelect<Table>`: the table's own schema plus one key
|
|
529
|
+
* per relation, so `{ ...user, posts: [...] }` can be validated whole.
|
|
530
|
+
*
|
|
531
|
+
* Nothing in the Drizzle validator ecosystem describes this payload. Measured across both majors
|
|
532
|
+
* and all four libraries, `createInsertSchema(users)` emits column keys only, and
|
|
533
|
+
* `db.insert(users).values({ name, posts: [...] })` drops the `posts` key without a word rather
|
|
534
|
+
* than refusing it, so the children are silently never written.
|
|
535
|
+
*
|
|
536
|
+
* Off by default, like every other option that adds bytes to the consumer's bundle. See
|
|
537
|
+
* `NestedMode` in `@drzl/validation-core` for why there is no nested update schema, and
|
|
538
|
+
* `KINDS_BY_MODE` for why a `one` relation appears on select and not on insert.
|
|
539
|
+
*/
|
|
540
|
+
nestedSchemas?: boolean;
|
|
541
|
+
/**
|
|
542
|
+
* How many levels of children a nested schema describes. Defaults to 1, capped at
|
|
543
|
+
* `MAX_NESTED_DEPTH`.
|
|
544
|
+
*
|
|
545
|
+
* Nesting is expanded inline rather than by reference, so this multiplies the emitted size: a
|
|
546
|
+
* schema whose tables average R relations emits R^depth child shapes per root table. It is also
|
|
547
|
+
* what terminates a cycle, since `users -> posts -> users` simply stops here.
|
|
548
|
+
*/
|
|
549
|
+
nestedDepth?: number;
|
|
406
550
|
emit?: {
|
|
407
551
|
select?: boolean;
|
|
408
552
|
insert?: boolean;
|
|
@@ -411,6 +555,13 @@ interface ValidationGenerateOptions {
|
|
|
411
555
|
}
|
|
412
556
|
interface ValidationRenderer<TOptions extends ValidationGenerateOptions = ValidationGenerateOptions> {
|
|
413
557
|
readonly library: ValidationLibrary;
|
|
558
|
+
/**
|
|
559
|
+
* One table's schemas, without the nested variants even under `nestedSchemas`.
|
|
560
|
+
*
|
|
561
|
+
* Structural rather than an omission: relations live on the `Analysis` and this is handed a
|
|
562
|
+
* `Table`, so there is nothing here to read them from. `typedJson` is absent for the same
|
|
563
|
+
* reason. Nothing but the generators' own tests calls this; `generate` is the path that emits.
|
|
564
|
+
*/
|
|
414
565
|
renderTable(table: Table, opts?: TOptions): string;
|
|
415
566
|
renderIndex?(analysis: Analysis, opts?: TOptions): string;
|
|
416
567
|
generate(opts: TOptions): Promise<string[]>;
|
|
@@ -550,11 +701,11 @@ declare function parsesToADate(expr: string): string;
|
|
|
550
701
|
* Refusing a user's emoji is the failure mode this avoids, and it is the same rule applied
|
|
551
702
|
* everywhere else here: never reject what the database accepts.
|
|
552
703
|
*
|
|
553
|
-
* All
|
|
554
|
-
* declarative forms, so
|
|
555
|
-
* kind onto the field
|
|
556
|
-
*
|
|
557
|
-
* different measurement is not a better trade.
|
|
704
|
+
* All five generators count code points. `@sinclair/typebox`, ArkType and Effect cannot say it in
|
|
705
|
+
* their declarative forms, so none of them uses `maxLength` or `string <= n`: TypeBox intersects a
|
|
706
|
+
* registered kind onto the field, ArkType puts a Type carrying a narrow there, and Effect pipes a
|
|
707
|
+
* `Schema.filter`. Each costs the same thing, the cap no longer serialising into a JSON Schema, and
|
|
708
|
+
* emitting a number that means a different measurement is not a better trade.
|
|
558
709
|
*
|
|
559
710
|
* MySQL's TEXT family is a byte budget rather than a character count, carried separately as
|
|
560
711
|
* `maxBytes`. Two measurements on string columns in the same database, verified against a real
|
|
@@ -566,18 +717,24 @@ declare function isIntegerColumn(c: Column): boolean;
|
|
|
566
717
|
* The non-finite doubles the emitted schema must admit beside the column's range, as the analyzer
|
|
567
718
|
* stated them.
|
|
568
719
|
*
|
|
569
|
-
* One reading of two flags, shared so that
|
|
570
|
-
* a shared *rendering*: the
|
|
720
|
+
* One reading of two flags, shared so that five generators cannot drift on what they mean, and not
|
|
721
|
+
* a shared *rendering*: the five libraries do not need the same repair. `z.number()` and
|
|
571
722
|
* `Type.Number()` refuse `NaN` and both infinities outright, `v.number()` and ArkType's `number`
|
|
572
|
-
* refuse only `NaN`, and any bound at all makes all
|
|
573
|
-
* add depends on the library and on whether the column carries a range.
|
|
723
|
+
* refuse only `NaN`, and any bound at all makes all of those refuse the infinities, so what each
|
|
724
|
+
* has to add depends on the library and on whether the column carries a range.
|
|
725
|
+
*
|
|
726
|
+
* Effect is the one that runs the other way, measured on 3.22.1: `Schema.Number` *accepts* `NaN`
|
|
727
|
+
* and both infinities, so the flags being false is what makes that generator emit something. It
|
|
728
|
+
* builds on `Schema.Finite` rather than `Schema.Number` for exactly that reason, and does so
|
|
729
|
+
* unconditionally rather than leaning on the range, since `Infinity >= 0` is true and a lower bound
|
|
730
|
+
* alone therefore excludes nothing.
|
|
574
731
|
*
|
|
575
732
|
* Guarded on `tsType` so an enum, a shape or a string can never pick these up from a stale
|
|
576
733
|
* analysis. `@drzl/generator-json-schema` deliberately does not call this at all: JSON has no `NaN`
|
|
577
734
|
* and no `Infinity`, so there is nothing for a JSON Schema to admit.
|
|
578
735
|
*
|
|
579
736
|
* A numeric CHECK folded into one end of the column's range does not take a branch away, in any of
|
|
580
|
-
* the
|
|
737
|
+
* the five. That is a decision rather than an oversight, and it is deliberately the loose one: what
|
|
581
738
|
* Postgres does with `CHECK (c >= 0)` and a `NaN` was not measured for this change, and dropping
|
|
582
739
|
* the branch on a column that carries a CHECK would put back, for that column, exactly the
|
|
583
740
|
* read-path failure this exists to remove.
|
|
@@ -629,4 +786,4 @@ declare function selectColumns(table: Table): Column[];
|
|
|
629
786
|
*/
|
|
630
787
|
declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
|
|
631
788
|
|
|
632
|
-
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, type CardinalityCheck, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, NAME_MODES, type NameMode, type ParsedCheck, type ResolvedAffix, type RowCheck, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, nonFiniteAccepted, parseCheck, parsesToADate, pascalCase, renderDuplicateFinder, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
|
789
|
+
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, type CardinalityCheck, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, MAX_NESTED_DEPTH, NAME_MODES, NESTED_PREFIX, type NameMode, type NestedArm, type NestedMode, type NestedNode, type ParsedCheck, type ResolvedAffix, type RowCheck, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, buildNestedPlan, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, parseCheck, parsesToADate, pascalCase, renderDuplicateFinder, resolveAffix, resolveConfiguredImport, resolveNestedDepth, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Table as Table$1, Column, Analysis } from '@drzl/analyzer';
|
|
1
|
+
import { Relation, Table as Table$1, Column, Analysis } from '@drzl/analyzer';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* One place that decides what a generated module is called, on disk and in an import.
|
|
@@ -284,6 +284,127 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
|
|
|
284
284
|
*/
|
|
285
285
|
declare function describeSet(set: ColumnSet): string;
|
|
286
286
|
|
|
287
|
+
/**
|
|
288
|
+
* What a relations-aware nested schema describes, decided once for all five generators.
|
|
289
|
+
*
|
|
290
|
+
* A caller who inserts a parent and its children in one payload, `{ ...user, posts: [...] }`, has
|
|
291
|
+
* nothing to validate it against. Every first-party Drizzle validator emits columns only: measured
|
|
292
|
+
* at drizzle-orm 1.0.0-rc.4 and at the 0.4x `drizzle-zod`/`drizzle-valibot`/`drizzle-typebox`/
|
|
293
|
+
* `drizzle-arktype` packages, `createInsertSchema(users)` yields exactly `['id', 'name']` and never
|
|
294
|
+
* a `posts` key, in every mode and every library. Handing the relations object in as the second
|
|
295
|
+
* argument does not change that either: it lands in the refine slot and is dropped, because its
|
|
296
|
+
* keys are not column names. Handing it in first throws inside `getColumns`.
|
|
297
|
+
*
|
|
298
|
+
* And the payload is not merely unvalidated, it is silently discarded. `db.insert(users).values({
|
|
299
|
+
* name: 'a', posts: [{ title: 't' }] })` emits `insert into "users" ("id", "name") values
|
|
300
|
+
* (default, $1)` on both majors: the relation key is dropped, no error is raised, and the children
|
|
301
|
+
* are simply never written.
|
|
302
|
+
*
|
|
303
|
+
* This module states the *shape* of such a payload from the analysis. It renders nothing: each
|
|
304
|
+
* generator walks the plan and emits it in its own library, so the four cannot disagree about
|
|
305
|
+
* which relations appear or which columns a child carries.
|
|
306
|
+
*/
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Which modes get a nested variant.
|
|
310
|
+
*
|
|
311
|
+
* `update` is deliberately absent, and it is the one decision here that is a refusal rather than a
|
|
312
|
+
* design. A nested update payload has no single meaning: `{ name: 'x', posts: [...] }` could mean
|
|
313
|
+
* replace every child, upsert them, or patch the ones that match, and choosing needs an operation
|
|
314
|
+
* vocabulary (Prisma spells it `create` / `connect` / `set` / `deleteMany`) that neither DRZL nor
|
|
315
|
+
* Drizzle has. Worse, it could not be acted on even if the meaning were fixed: `updateColumns`
|
|
316
|
+
* drops the primary key, so a child inside an update payload carries nothing that identifies which
|
|
317
|
+
* row it patches. A schema whose meaning the tool emitting it cannot state is worse than no schema.
|
|
318
|
+
*/
|
|
319
|
+
type NestedMode = 'insert' | 'select';
|
|
320
|
+
/** Prefix in front of the ordinary resolved name, so affixes keep working underneath it. */
|
|
321
|
+
declare const NESTED_PREFIX = "Nested";
|
|
322
|
+
/** e.g. `NestedInsertusersSchema`. */
|
|
323
|
+
declare function nestedSchemaName(mode: NestedMode, tsName: string, affix: ResolvedAffix): string;
|
|
324
|
+
/** e.g. `NestedInsertusersInput`. */
|
|
325
|
+
declare function nestedTypeName(mode: NestedMode, tsName: string, affix: ResolvedAffix): string;
|
|
326
|
+
/**
|
|
327
|
+
* How many levels of children a nested schema describes, when nothing says.
|
|
328
|
+
*
|
|
329
|
+
* One, because one is what the payload in the plan item is: a parent and its children. Deeper is
|
|
330
|
+
* available and is not the default, because the output grows multiplicatively in this number and
|
|
331
|
+
* generated code ships in the consumer's bundle.
|
|
332
|
+
*/
|
|
333
|
+
declare const DEFAULT_NESTED_DEPTH = 1;
|
|
334
|
+
/**
|
|
335
|
+
* The most levels `nestedDepth` will honour.
|
|
336
|
+
*
|
|
337
|
+
* Not a taste limit. Nesting is expanded inline rather than by reference, so a schema whose tables
|
|
338
|
+
* average R relations emits R^depth child shapes per root table, and both directions of a
|
|
339
|
+
* many-to-many count. At 3 that is already an eightfold expansion of every column list in the
|
|
340
|
+
* schema; past it the emitted file stops being something a person can open.
|
|
341
|
+
*/
|
|
342
|
+
declare const MAX_NESTED_DEPTH = 3;
|
|
343
|
+
/** Clamp a configured depth into the range that is actually emitted, saying so if it moved. */
|
|
344
|
+
declare function resolveNestedDepth(depth: number | undefined, warn?: (msg: string) => void): number;
|
|
345
|
+
/** One relation as it appears in a payload. */
|
|
346
|
+
interface NestedArm {
|
|
347
|
+
/** The key the payload uses. The child table's Drizzle export name, since nothing else names it. */
|
|
348
|
+
key: string;
|
|
349
|
+
kind: Relation['kind'];
|
|
350
|
+
/** Join table of a many-to-many, carried only so the emitted comment can name it. */
|
|
351
|
+
via?: string;
|
|
352
|
+
/** `true` when the value is one object rather than an array of them. Only a `one` relation. */
|
|
353
|
+
single: boolean;
|
|
354
|
+
/** The child payload, itself possibly carrying relations. */
|
|
355
|
+
child: NestedNode;
|
|
356
|
+
/**
|
|
357
|
+
* Something true about this arm that the shape alone does not say, emitted as a comment beside
|
|
358
|
+
* it. Absent when there is nothing to say.
|
|
359
|
+
*/
|
|
360
|
+
note?: string;
|
|
361
|
+
}
|
|
362
|
+
/** One object in a nested payload: a table, minus what the enclosing parent supplies. */
|
|
363
|
+
interface NestedNode {
|
|
364
|
+
table: Table$1;
|
|
365
|
+
/**
|
|
366
|
+
* Columns dropped from this object because the row that encloses it supplies them.
|
|
367
|
+
*
|
|
368
|
+
* Always empty at the root and always empty in `select`, where every column of the row really is
|
|
369
|
+
* returned. On `insert` it holds the child's foreign key to its parent; see `armFor`.
|
|
370
|
+
*/
|
|
371
|
+
omitted: string[];
|
|
372
|
+
arms: NestedArm[];
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The nested payload for one table, expanded `depth` levels.
|
|
376
|
+
*
|
|
377
|
+
* Recursion is bounded rather than expressed. All four libraries can state a cyclic schema, and
|
|
378
|
+
* each does it differently: zod through a property getter, valibot through `v.lazy`, ArkType only
|
|
379
|
+
* inside a `scope` (a plain forward reference throws `Cannot access 'Post' before initialization`
|
|
380
|
+
* at module load), TypeBox only inside a `Type.Module` (a bare `Type.Ref` constructs happily and
|
|
381
|
+
* then throws `Unable to dereference schema with $id` the first time anything checks a value).
|
|
382
|
+
* Every one of those was run.
|
|
383
|
+
*
|
|
384
|
+
* None is used. Expanding inline to a fixed depth needs no recursion API, so no emitted module can
|
|
385
|
+
* throw on import from an unresolvable reference, no zod or valibot schema needs the explicit type
|
|
386
|
+
* annotation a self-referential one demands from TypeScript, and the four outputs stay structurally
|
|
387
|
+
* identical instead of diverging into four different recursion mechanisms. A cycle in the relations
|
|
388
|
+
* simply stops at the depth: `users -> posts -> users` at depth 1 emits the posts and no more.
|
|
389
|
+
*/
|
|
390
|
+
declare function buildNestedPlan(root: Table$1, tables: readonly Table$1[], relations: readonly Relation[], mode: NestedMode, depth: number): NestedNode | undefined;
|
|
391
|
+
/**
|
|
392
|
+
* The comment lines that go above an arm, without their `//`.
|
|
393
|
+
*
|
|
394
|
+
* Shared so the five generators say the same thing about the same relation, and returned unmarked
|
|
395
|
+
* because each of them indents its object literal differently.
|
|
396
|
+
*/
|
|
397
|
+
declare function nestedArmNotes(arm: NestedArm): string[];
|
|
398
|
+
/**
|
|
399
|
+
* Which columns a node contributes, given the mode's column list for its table.
|
|
400
|
+
*
|
|
401
|
+
* The list is passed in rather than derived, so this cannot drift from `insertColumns` and
|
|
402
|
+
* `selectColumns` and so `validation-core`'s entry point stays free of a cycle back into itself.
|
|
403
|
+
*/
|
|
404
|
+
declare function nestedNodeColumns<T extends {
|
|
405
|
+
name: string;
|
|
406
|
+
}>(columnsForMode: readonly T[], node: NestedNode): T[];
|
|
407
|
+
|
|
287
408
|
/**
|
|
288
409
|
* A duplicate finder for a batch of rows, emitted beside the schemas.
|
|
289
410
|
*
|
|
@@ -297,7 +418,7 @@ declare function describeSet(set: ColumnSet): string;
|
|
|
297
418
|
* is worth checking, because it is the half a user can fix before sending anything.
|
|
298
419
|
*
|
|
299
420
|
* The emitted function is plain TypeScript with no reference to any validation library, so all
|
|
300
|
-
*
|
|
421
|
+
* five generators emit the same thing. It is rendered from one place for that reason.
|
|
301
422
|
*/
|
|
302
423
|
|
|
303
424
|
/**
|
|
@@ -316,7 +437,7 @@ interface Table {
|
|
|
316
437
|
columns: string[];
|
|
317
438
|
};
|
|
318
439
|
}
|
|
319
|
-
type ValidationLibrary = 'zod' | 'valibot' | 'arktype' | 'typebox';
|
|
440
|
+
type ValidationLibrary = 'zod' | 'valibot' | 'arktype' | 'typebox' | 'effect';
|
|
320
441
|
interface FormatOptions {
|
|
321
442
|
enabled?: boolean;
|
|
322
443
|
engine?: 'auto' | 'prettier' | 'biome';
|
|
@@ -403,6 +524,29 @@ interface ValidationGenerateOptions {
|
|
|
403
524
|
*/
|
|
404
525
|
affix?: AffixOptions;
|
|
405
526
|
coerceDates?: 'input' | 'all' | 'none';
|
|
527
|
+
/**
|
|
528
|
+
* Also emit `NestedInsert<Table>` and `NestedSelect<Table>`: the table's own schema plus one key
|
|
529
|
+
* per relation, so `{ ...user, posts: [...] }` can be validated whole.
|
|
530
|
+
*
|
|
531
|
+
* Nothing in the Drizzle validator ecosystem describes this payload. Measured across both majors
|
|
532
|
+
* and all four libraries, `createInsertSchema(users)` emits column keys only, and
|
|
533
|
+
* `db.insert(users).values({ name, posts: [...] })` drops the `posts` key without a word rather
|
|
534
|
+
* than refusing it, so the children are silently never written.
|
|
535
|
+
*
|
|
536
|
+
* Off by default, like every other option that adds bytes to the consumer's bundle. See
|
|
537
|
+
* `NestedMode` in `@drzl/validation-core` for why there is no nested update schema, and
|
|
538
|
+
* `KINDS_BY_MODE` for why a `one` relation appears on select and not on insert.
|
|
539
|
+
*/
|
|
540
|
+
nestedSchemas?: boolean;
|
|
541
|
+
/**
|
|
542
|
+
* How many levels of children a nested schema describes. Defaults to 1, capped at
|
|
543
|
+
* `MAX_NESTED_DEPTH`.
|
|
544
|
+
*
|
|
545
|
+
* Nesting is expanded inline rather than by reference, so this multiplies the emitted size: a
|
|
546
|
+
* schema whose tables average R relations emits R^depth child shapes per root table. It is also
|
|
547
|
+
* what terminates a cycle, since `users -> posts -> users` simply stops here.
|
|
548
|
+
*/
|
|
549
|
+
nestedDepth?: number;
|
|
406
550
|
emit?: {
|
|
407
551
|
select?: boolean;
|
|
408
552
|
insert?: boolean;
|
|
@@ -411,6 +555,13 @@ interface ValidationGenerateOptions {
|
|
|
411
555
|
}
|
|
412
556
|
interface ValidationRenderer<TOptions extends ValidationGenerateOptions = ValidationGenerateOptions> {
|
|
413
557
|
readonly library: ValidationLibrary;
|
|
558
|
+
/**
|
|
559
|
+
* One table's schemas, without the nested variants even under `nestedSchemas`.
|
|
560
|
+
*
|
|
561
|
+
* Structural rather than an omission: relations live on the `Analysis` and this is handed a
|
|
562
|
+
* `Table`, so there is nothing here to read them from. `typedJson` is absent for the same
|
|
563
|
+
* reason. Nothing but the generators' own tests calls this; `generate` is the path that emits.
|
|
564
|
+
*/
|
|
414
565
|
renderTable(table: Table, opts?: TOptions): string;
|
|
415
566
|
renderIndex?(analysis: Analysis, opts?: TOptions): string;
|
|
416
567
|
generate(opts: TOptions): Promise<string[]>;
|
|
@@ -550,11 +701,11 @@ declare function parsesToADate(expr: string): string;
|
|
|
550
701
|
* Refusing a user's emoji is the failure mode this avoids, and it is the same rule applied
|
|
551
702
|
* everywhere else here: never reject what the database accepts.
|
|
552
703
|
*
|
|
553
|
-
* All
|
|
554
|
-
* declarative forms, so
|
|
555
|
-
* kind onto the field
|
|
556
|
-
*
|
|
557
|
-
* different measurement is not a better trade.
|
|
704
|
+
* All five generators count code points. `@sinclair/typebox`, ArkType and Effect cannot say it in
|
|
705
|
+
* their declarative forms, so none of them uses `maxLength` or `string <= n`: TypeBox intersects a
|
|
706
|
+
* registered kind onto the field, ArkType puts a Type carrying a narrow there, and Effect pipes a
|
|
707
|
+
* `Schema.filter`. Each costs the same thing, the cap no longer serialising into a JSON Schema, and
|
|
708
|
+
* emitting a number that means a different measurement is not a better trade.
|
|
558
709
|
*
|
|
559
710
|
* MySQL's TEXT family is a byte budget rather than a character count, carried separately as
|
|
560
711
|
* `maxBytes`. Two measurements on string columns in the same database, verified against a real
|
|
@@ -566,18 +717,24 @@ declare function isIntegerColumn(c: Column): boolean;
|
|
|
566
717
|
* The non-finite doubles the emitted schema must admit beside the column's range, as the analyzer
|
|
567
718
|
* stated them.
|
|
568
719
|
*
|
|
569
|
-
* One reading of two flags, shared so that
|
|
570
|
-
* a shared *rendering*: the
|
|
720
|
+
* One reading of two flags, shared so that five generators cannot drift on what they mean, and not
|
|
721
|
+
* a shared *rendering*: the five libraries do not need the same repair. `z.number()` and
|
|
571
722
|
* `Type.Number()` refuse `NaN` and both infinities outright, `v.number()` and ArkType's `number`
|
|
572
|
-
* refuse only `NaN`, and any bound at all makes all
|
|
573
|
-
* add depends on the library and on whether the column carries a range.
|
|
723
|
+
* refuse only `NaN`, and any bound at all makes all of those refuse the infinities, so what each
|
|
724
|
+
* has to add depends on the library and on whether the column carries a range.
|
|
725
|
+
*
|
|
726
|
+
* Effect is the one that runs the other way, measured on 3.22.1: `Schema.Number` *accepts* `NaN`
|
|
727
|
+
* and both infinities, so the flags being false is what makes that generator emit something. It
|
|
728
|
+
* builds on `Schema.Finite` rather than `Schema.Number` for exactly that reason, and does so
|
|
729
|
+
* unconditionally rather than leaning on the range, since `Infinity >= 0` is true and a lower bound
|
|
730
|
+
* alone therefore excludes nothing.
|
|
574
731
|
*
|
|
575
732
|
* Guarded on `tsType` so an enum, a shape or a string can never pick these up from a stale
|
|
576
733
|
* analysis. `@drzl/generator-json-schema` deliberately does not call this at all: JSON has no `NaN`
|
|
577
734
|
* and no `Infinity`, so there is nothing for a JSON Schema to admit.
|
|
578
735
|
*
|
|
579
736
|
* A numeric CHECK folded into one end of the column's range does not take a branch away, in any of
|
|
580
|
-
* the
|
|
737
|
+
* the five. That is a decision rather than an oversight, and it is deliberately the loose one: what
|
|
581
738
|
* Postgres does with `CHECK (c >= 0)` and a `NaN` was not measured for this change, and dropping
|
|
582
739
|
* the branch on a column that carries a CHECK would put back, for that column, exactly the
|
|
583
740
|
* read-path failure this exists to remove.
|
|
@@ -629,4 +786,4 @@ declare function selectColumns(table: Table): Column[];
|
|
|
629
786
|
*/
|
|
630
787
|
declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
|
|
631
788
|
|
|
632
|
-
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, type CardinalityCheck, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, NAME_MODES, type NameMode, type ParsedCheck, type ResolvedAffix, type RowCheck, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, nonFiniteAccepted, parseCheck, parsesToADate, pascalCase, renderDuplicateFinder, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
|
789
|
+
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, type CardinalityCheck, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, type LengthCheck, MAX_NESTED_DEPTH, NAME_MODES, NESTED_PREFIX, type NameMode, type NestedArm, type NestedMode, type NestedNode, type ParsedCheck, type ResolvedAffix, type RowCheck, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, buildNestedPlan, describeSet, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, parseCheck, parsesToADate, pascalCase, renderDuplicateFinder, resolveAffix, resolveConfiguredImport, resolveNestedDepth, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
package/dist/index.js
CHANGED
|
@@ -384,6 +384,90 @@ function validateAffix(affix, schemaSuffix) {
|
|
|
384
384
|
return issues;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// src/nested.ts
|
|
388
|
+
var NESTED_PREFIX = "Nested";
|
|
389
|
+
function nestedSchemaName(mode, tsName, affix) {
|
|
390
|
+
return NESTED_PREFIX + schemaName(mode, tsName, affix);
|
|
391
|
+
}
|
|
392
|
+
function nestedTypeName(mode, tsName, affix) {
|
|
393
|
+
return NESTED_PREFIX + typeName(mode, tsName, affix);
|
|
394
|
+
}
|
|
395
|
+
var DEFAULT_NESTED_DEPTH = 1;
|
|
396
|
+
var MAX_NESTED_DEPTH = 3;
|
|
397
|
+
function resolveNestedDepth(depth, warn) {
|
|
398
|
+
if (depth === void 0) return DEFAULT_NESTED_DEPTH;
|
|
399
|
+
if (!Number.isFinite(depth)) return DEFAULT_NESTED_DEPTH;
|
|
400
|
+
const whole = Math.trunc(depth);
|
|
401
|
+
const clamped = Math.min(Math.max(whole, 1), MAX_NESTED_DEPTH);
|
|
402
|
+
if (clamped !== depth && warn) {
|
|
403
|
+
warn(
|
|
404
|
+
`[drzl] nestedDepth ${depth} is outside 1..${MAX_NESTED_DEPTH} and was read as ${clamped}.`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return clamped;
|
|
408
|
+
}
|
|
409
|
+
var KINDS_BY_MODE = {
|
|
410
|
+
insert: /* @__PURE__ */ new Set(["many", "manyToMany"]),
|
|
411
|
+
select: /* @__PURE__ */ new Set(["one", "many", "manyToMany"])
|
|
412
|
+
};
|
|
413
|
+
var KIND_ORDER = { many: 0, manyToMany: 1, one: 2 };
|
|
414
|
+
function omittedColumnsFor(parent, child) {
|
|
415
|
+
const back = (child.foreignKeys ?? []).filter((fk) => fk.foreignTable === parent.name);
|
|
416
|
+
if (back.length === 1) return { omitted: [...back[0].columns] };
|
|
417
|
+
if (back.length === 0) return { omitted: [] };
|
|
418
|
+
const named = back.map((fk) => fk.columns.join("+")).join(", ");
|
|
419
|
+
return {
|
|
420
|
+
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.`
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function buildNestedPlan(root, tables, relations, mode, depth) {
|
|
425
|
+
const node = buildNode(root, [], tables, relations, mode, depth);
|
|
426
|
+
return node.arms.length ? node : void 0;
|
|
427
|
+
}
|
|
428
|
+
function buildNode(table, omitted, tables, relations, mode, depth) {
|
|
429
|
+
if (depth <= 0) return { table, omitted, arms: [] };
|
|
430
|
+
const byDbName = new Map(tables.map((t) => [t.name, t]));
|
|
431
|
+
const allowed = KINDS_BY_MODE[mode];
|
|
432
|
+
const columnNames = new Set(table.columns.map((c) => c.name));
|
|
433
|
+
const taken = /* @__PURE__ */ new Set();
|
|
434
|
+
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]);
|
|
436
|
+
for (const rel of candidates) {
|
|
437
|
+
const child = byDbName.get(rel.to);
|
|
438
|
+
if (!child) continue;
|
|
439
|
+
const key = child.tsName;
|
|
440
|
+
if (columnNames.has(key)) continue;
|
|
441
|
+
if (taken.has(key)) continue;
|
|
442
|
+
taken.add(key);
|
|
443
|
+
const { omitted: childOmitted, note } = rel.kind === "many" && mode === "insert" ? omittedColumnsFor(table, child) : { omitted: [], note: void 0 };
|
|
444
|
+
arms.push({
|
|
445
|
+
key,
|
|
446
|
+
kind: rel.kind,
|
|
447
|
+
via: rel.via,
|
|
448
|
+
single: rel.kind === "one",
|
|
449
|
+
note,
|
|
450
|
+
child: buildNode(child, childOmitted, tables, relations, mode, depth - 1)
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return { table, omitted, arms };
|
|
454
|
+
}
|
|
455
|
+
function nestedArmNotes(arm) {
|
|
456
|
+
const out = [];
|
|
457
|
+
if (arm.kind === "manyToMany") {
|
|
458
|
+
out.push(
|
|
459
|
+
`Through ${arm.via ?? "a join table"}. The join row is not described: its columns are the two foreign keys, and the two ends of this payload supply both.`
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (arm.note) out.push(arm.note);
|
|
463
|
+
return out;
|
|
464
|
+
}
|
|
465
|
+
function nestedNodeColumns(columnsForMode, node) {
|
|
466
|
+
if (!node.omitted.length) return [...columnsForMode];
|
|
467
|
+
const drop = new Set(node.omitted);
|
|
468
|
+
return columnsForMode.filter((c) => !drop.has(c.name));
|
|
469
|
+
}
|
|
470
|
+
|
|
387
471
|
// src/duplicates.ts
|
|
388
472
|
function usableKeys(table) {
|
|
389
473
|
return (table.unique ?? []).filter((k) => k.columns.length > 0);
|
|
@@ -567,11 +651,15 @@ export {
|
|
|
567
651
|
COLUMN_FORMATS,
|
|
568
652
|
DEFAULT_IMPORT_EXTENSION,
|
|
569
653
|
DEFAULT_MODE_PREFIX,
|
|
654
|
+
DEFAULT_NESTED_DEPTH,
|
|
570
655
|
DEFAULT_SCHEMA_SUFFIX,
|
|
571
656
|
DEFAULT_TYPE_SUFFIX,
|
|
572
657
|
IMPORT_EXTENSIONS,
|
|
658
|
+
MAX_NESTED_DEPTH,
|
|
573
659
|
NAME_MODES,
|
|
660
|
+
NESTED_PREFIX,
|
|
574
661
|
applyTableCase,
|
|
662
|
+
buildNestedPlan,
|
|
575
663
|
describeSet,
|
|
576
664
|
formatCode,
|
|
577
665
|
importSpecifier,
|
|
@@ -580,6 +668,10 @@ export {
|
|
|
580
668
|
isIntegerColumn,
|
|
581
669
|
moduleFileName,
|
|
582
670
|
moduleSpecifier,
|
|
671
|
+
nestedArmNotes,
|
|
672
|
+
nestedNodeColumns,
|
|
673
|
+
nestedSchemaName,
|
|
674
|
+
nestedTypeName,
|
|
583
675
|
nonFiniteAccepted,
|
|
584
676
|
parseCheck,
|
|
585
677
|
parsesToADate,
|
|
@@ -587,6 +679,7 @@ export {
|
|
|
587
679
|
renderDuplicateFinder,
|
|
588
680
|
resolveAffix,
|
|
589
681
|
resolveConfiguredImport,
|
|
682
|
+
resolveNestedDepth,
|
|
590
683
|
schemaName,
|
|
591
684
|
selectColumns,
|
|
592
685
|
typeName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drzl/validation-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.18.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.
|
|
27
|
+
"@drzl/analyzer": "^1.18.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"prettier": ">=3"
|