@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 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
- * four generators emit the same thing. It is rendered from one place for that reason.
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 four generators count code points. `@sinclair/typebox` and ArkType cannot say it in their
554
- * declarative forms, so neither uses `maxLength` or `string <= n`: TypeBox intersects a registered
555
- * kind onto the field and ArkType puts a Type carrying a narrow there. Both cost something,
556
- * TypeBox's cap no longer serialising into a JSON Schema, and emitting a number that means a
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 four generators cannot drift on what they mean, and not
570
- * a shared *rendering*: the four libraries do not need the same repair. `z.number()` and
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 four refuse the infinities, so what each has to
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 four. That is a decision rather than an oversight, and it is deliberately the loose one: what
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
- * four generators emit the same thing. It is rendered from one place for that reason.
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 four generators count code points. `@sinclair/typebox` and ArkType cannot say it in their
554
- * declarative forms, so neither uses `maxLength` or `string <= n`: TypeBox intersects a registered
555
- * kind onto the field and ArkType puts a Type carrying a narrow there. Both cost something,
556
- * TypeBox's cap no longer serialising into a JSON Schema, and emitting a number that means a
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 four generators cannot drift on what they mean, and not
570
- * a shared *rendering*: the four libraries do not need the same repair. `z.number()` and
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 four refuse the infinities, so what each has to
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 four. That is a decision rather than an oversight, and it is deliberately the loose one: what
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.16.4",
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.17.4"
27
+ "@drzl/analyzer": "^1.18.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "prettier": ">=3"