@drzl/validation-core 3.22.6 → 3.23.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
@@ -64,6 +64,7 @@ __export(index_exports, {
64
64
  columnMetaFacts: () => columnMetaFacts,
65
65
  comparisonWire: () => comparisonWire,
66
66
  describeSet: () => describeSet,
67
+ fieldFacts: () => fieldFacts,
67
68
  fileWriter: () => fileWriter,
68
69
  formatCode: () => formatCode,
69
70
  importSpecifier: () => importSpecifier,
@@ -75,6 +76,7 @@ __export(index_exports, {
75
76
  lengthMeasure: () => lengthMeasure,
76
77
  measureCompare: () => measureCompare,
77
78
  measureExpression: () => measureExpression,
79
+ metaSchemaId: () => metaSchemaId,
78
80
  moduleFileName: () => moduleFileName,
79
81
  moduleSpecifier: () => moduleSpecifier,
80
82
  needsNumericCanon: () => needsNumericCanon,
@@ -1461,6 +1463,60 @@ function fileWriter(sink) {
1461
1463
  };
1462
1464
  }
1463
1465
 
1466
+ // src/fields.ts
1467
+ var UUID_PATTERN = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
1468
+ function controlFor(c) {
1469
+ if (c.enumValues?.length) return "select";
1470
+ if (c.tsType === "boolean") return "checkbox";
1471
+ if (c.tsType === "number" || c.tsType === "bigint") return "number";
1472
+ if (c.tsType === "Date") return "datetime-local";
1473
+ return "text";
1474
+ }
1475
+ function boundsFor(c, checks) {
1476
+ let lo = c.min !== void 0 ? { value: c.min, exclusive: false } : void 0;
1477
+ let hi = c.max !== void 0 ? { value: c.max, exclusive: false } : void 0;
1478
+ for (const k of checks) {
1479
+ if (k.column !== c.name || k.kind !== "number") continue;
1480
+ if (k.operator === ">=") lo = { value: k.value, exclusive: false };
1481
+ else if (k.operator === ">") lo = { value: k.value, exclusive: true };
1482
+ else if (k.operator === "<=") hi = { value: k.value, exclusive: false };
1483
+ else if (k.operator === "<") hi = { value: k.value, exclusive: true };
1484
+ }
1485
+ return {
1486
+ ...lo ? { min: lo.value, ...lo.exclusive ? { exclusiveMin: true } : {} } : {},
1487
+ ...hi ? { max: hi.value, ...hi.exclusive ? { exclusiveMax: true } : {} } : {}
1488
+ };
1489
+ }
1490
+ function fieldFacts(table) {
1491
+ const parsed = (table.checks ?? []).map(
1492
+ (k) => parseCheck(k.expression, k.name, table.dialect)
1493
+ );
1494
+ const ok = parsed.filter((p) => !!p && p.ok);
1495
+ const columnChecks = ok.flatMap((p) => p.checks ?? []);
1496
+ const lengthChecks = ok.flatMap((p) => p.lengths ?? []);
1497
+ return table.columns.filter((c) => !c.isGenerated).map((c) => ({
1498
+ name: c.name,
1499
+ control: controlFor(c),
1500
+ // A column the database fills is not one a form has to supply, which is the insert-side
1501
+ // question a form asks. `nullable` is reported beside it for a reader asking the other one.
1502
+ required: !c.nullable && !c.hasDefault,
1503
+ nullable: c.nullable,
1504
+ ...(() => {
1505
+ const declared = c.maxLength;
1506
+ const fromCheck = lengthChecks.filter(
1507
+ (l) => l.column === c.name && (l.unit ?? "characters") === "characters" && (l.operator === "<=" || l.operator === "<")
1508
+ ).map((l) => l.operator === "<" ? Number(l.value) - 1 : Number(l.value)).filter((n) => Number.isFinite(n));
1509
+ const all = [...declared !== void 0 ? [declared] : [], ...fromCheck];
1510
+ return all.length ? { maxLength: Math.min(...all) } : {};
1511
+ })(),
1512
+ ...boundsFor(c, columnChecks),
1513
+ ...c.integer ? { integer: true } : {},
1514
+ ...c.enumValues?.length ? { options: [...c.enumValues] } : {},
1515
+ ...c.format === "uuid" ? { pattern: UUID_PATTERN } : {},
1516
+ ...c.defaultValue !== void 0 ? { defaultValue: c.defaultValue } : {}
1517
+ }));
1518
+ }
1519
+
1464
1520
  // src/files.ts
1465
1521
  var import_node_fs = __toESM(require("fs"), 1);
1466
1522
  var import_node_path = __toESM(require("path"), 1);
@@ -1521,6 +1577,11 @@ function withTsExtension(p) {
1521
1577
  }
1522
1578
 
1523
1579
  // src/meta.ts
1580
+ function metaSchemaId(table, mode) {
1581
+ const qualified = table.schema ? `${table.schema}_${table.name}` : table.name;
1582
+ const safe = qualified.replace(/[^A-Za-z0-9_]/g, "_");
1583
+ return `${safe}${mode.charAt(0).toUpperCase()}${mode.slice(1)}`;
1584
+ }
1524
1585
  function classifyChecks(table) {
1525
1586
  const perColumn = /* @__PURE__ */ new Map();
1526
1587
  const rows = [];
@@ -1579,6 +1640,7 @@ function tableMetaFacts(table, opts) {
1579
1640
  const pk = table.primaryKey?.columns ?? [];
1580
1641
  const unique = (table.unique ?? []).map((k) => k.columns).filter((c) => c.length > 0);
1581
1642
  const facts = {
1643
+ ...opts.id ? { id: opts.id } : {},
1582
1644
  table: table.name,
1583
1645
  ...table.schema ? { schema: table.schema } : {},
1584
1646
  ...opts.dialect ? { dialect: opts.dialect } : {},
@@ -2036,6 +2098,7 @@ async function formatCode(code, filePath, fmt) {
2036
2098
  columnMetaFacts,
2037
2099
  comparisonWire,
2038
2100
  describeSet,
2101
+ fieldFacts,
2039
2102
  fileWriter,
2040
2103
  formatCode,
2041
2104
  importSpecifier,
@@ -2047,6 +2110,7 @@ async function formatCode(code, filePath, fmt) {
2047
2110
  lengthMeasure,
2048
2111
  measureCompare,
2049
2112
  measureExpression,
2113
+ metaSchemaId,
2050
2114
  moduleFileName,
2051
2115
  moduleSpecifier,
2052
2116
  needsNumericCanon,
package/dist/index.d.cts CHANGED
@@ -966,6 +966,65 @@ declare const CONSTRAINTS_MODULE = "constraints.ts";
966
966
  */
967
967
  declare function renderConstraintsModule(tables: Table$1[], opts?: ConstraintsModuleOptions): string;
968
968
 
969
+ /**
970
+ * What one column is, in the terms a form control needs.
971
+ *
972
+ * Every validation generator already folds a table's `CHECK` constraints into the bounds it emits,
973
+ * and every one of them does it with a private copy of the same loop. A form generator reading
974
+ * `Column.min` and `Column.max` directly would be a fourth derivation and a wrong one: measured
975
+ * 2026-08-12, a column carrying `check('adult', age >= 18)` still reports
976
+ * `min: '-2147483648'`, the plain int32 range, because the analyzer leaves checks on the table and
977
+ * the generators fold them at emit time. An input rendered from that would carry
978
+ * `min="-2147483648"`, which looks like a bound and is not one.
979
+ *
980
+ * So the fold lives here, beside `classifyTableChecks` and `tableConstraints`, which are already
981
+ * the shared home for the same question. `fieldFacts` is what a form generator reads, and what the
982
+ * emitted schema's own bounds agree with by construction rather than by coincidence.
983
+ */
984
+ interface FieldFacts {
985
+ /** The column, under the TypeScript name a generated schema spells. */
986
+ name: string;
987
+ /** The input type a control should use, derived from the column's TypeScript type. */
988
+ control: 'text' | 'number' | 'checkbox' | 'datetime-local' | 'select';
989
+ /**
990
+ * Whether a value has to be supplied on insert.
991
+ *
992
+ * False for a nullable column and for one carrying a default, since the database fills it. This
993
+ * is the insert-side answer; a select-side reader wants `nullable` instead.
994
+ */
995
+ required: boolean;
996
+ nullable: boolean;
997
+ /** `maxlength`, from a declared width. Absent on an unbounded text column. */
998
+ maxLength?: number;
999
+ /**
1000
+ * `min` and `max`, as text.
1001
+ *
1002
+ * Text because a 64 bit bound is not representable as a JS number, which is the same reason the
1003
+ * analyzer reports them that way. Narrowed by any `CHECK` on the column: a check replaces the end
1004
+ * it constrains rather than sitting beside it, and it can never widen, because the declared range
1005
+ * is the column's type and no check makes an int32 hold more.
1006
+ */
1007
+ min?: string;
1008
+ max?: string;
1009
+ /** Whether either bound is exclusive, which HTML cannot express and a validator can. */
1010
+ exclusiveMin?: boolean;
1011
+ exclusiveMax?: boolean;
1012
+ /** `step="1"` for an integer column. */
1013
+ integer?: boolean;
1014
+ /** The options a `<select>` should carry, from a native enum or a declared set. */
1015
+ options?: string[];
1016
+ /** A `pattern` where the column's format implies one and HTML can state it. */
1017
+ pattern?: string;
1018
+ /** The default the database applies, for a form's initial value. */
1019
+ defaultValue?: unknown;
1020
+ }
1021
+ /**
1022
+ * Every column of a table, as the facts a form control needs.
1023
+ *
1024
+ * Generated columns are left out: the database computes them and no form supplies one.
1025
+ */
1026
+ declare function fieldFacts(table: Table$1): FieldFacts[];
1027
+
969
1028
  /**
970
1029
  * The facts a generated schema can carry beside itself.
971
1030
  *
@@ -1037,6 +1096,15 @@ interface ColumnMetaFacts {
1037
1096
  }
1038
1097
  /** What a table's schema adds beside itself. */
1039
1098
  interface TableMetaFacts {
1099
+ /**
1100
+ * The registry id, present only when asked for.
1101
+ *
1102
+ * zod reads this key out of the same object and registers the schema under it, which is what
1103
+ * makes `z.toJSONSchema` emit a `$ref` instead of inlining a copy, and what gives
1104
+ * `z.toJSONSchema(z.globalRegistry)` a name to file each schema under. See `metaSchemaId` for why
1105
+ * it is built from the qualified table name.
1106
+ */
1107
+ id?: string;
1040
1108
  /** The SQL table name, which is not the Drizzle export name the schema is named after. */
1041
1109
  table: string;
1042
1110
  /**
@@ -1085,7 +1153,33 @@ interface MetaFactOptions {
1085
1153
  interface TableMetaOptions extends MetaFactOptions {
1086
1154
  mode: string;
1087
1155
  dialect?: string;
1156
+ /**
1157
+ * Also give the schema an `id`, which is what puts it in zod's registry under a name.
1158
+ *
1159
+ * Off by default, because it is the one metadata key with a failure mode. Measured against zod
1160
+ * 4.4.3: an `id` makes `z.toJSONSchema` emit `$ref: '#/$defs/<id>'` wherever one schema references
1161
+ * another, and `z.toJSONSchema(z.globalRegistry)` return a `{ schemas: { <id>: ... } }` bundle,
1162
+ * which is what makes a generated document self-describing. But two schemas sharing an id do not
1163
+ * report the collision: measured, `z.toJSONSchema(registry)` keeps the last one and **silently
1164
+ * drops the other**. Two tables, one entry, no warning, and nothing a consumer can check.
1165
+ *
1166
+ * So the id has to be unique across the whole analysis rather than within a file, which is why
1167
+ * `metaSchemaId` builds it from the qualified table name.
1168
+ */
1169
+ id?: string;
1088
1170
  }
1171
+ /**
1172
+ * The registry id for one table's schema, unique across an analysis.
1173
+ *
1174
+ * Built from the qualified name, because two tables in two SQL schemas share a bare one and zod
1175
+ * does not report the collision: a registry dump silently keeps one of them. `reporting.users` becomes
1176
+ * `reporting_usersSelect`; a table in the default schema keeps the short `usersSelect`, so the
1177
+ * common case reads the way anybody would have written it by hand.
1178
+ *
1179
+ * Anything outside `[A-Za-z0-9_]` becomes `_`, because the id lands in a `$ref` as a JSON Pointer
1180
+ * segment and a `/` or a `~` there means something else entirely.
1181
+ */
1182
+ declare function metaSchemaId(table: Table$1, mode: string): string;
1089
1183
  /**
1090
1184
  * The metadata for one column.
1091
1185
  *
@@ -1736,4 +1830,4 @@ declare function isProjectInstallPath(manifestPath: string): boolean;
1736
1830
  */
1737
1831
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
1738
1832
 
1739
- export { AFFIX_PREFIX_PATTERN, AFFIX_PROBE_TABLE, AFFIX_SUFFIX_PATTERN, type AffixIssue, type AffixOptions, type AffixValue, BIGINT_DIGITS_PATTERN, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, CONSTRAINTS_MODULE, type CardinalityCheck, type CheckPart, type CheckPartPlace, type ClassifiedCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, type ComparisonWire, type ConstraintFacts, type ConstraintKind, type ConstraintsModuleOptions, type ConstraintsOption, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FileSink, type FormatOptions, type GeneratorFs, IMPORT_EXTENSIONS, type ImportExtension, JSON_VALUE_TS_CONST, JSON_VALUE_TS_SOURCE, type LengthCheck, type LengthMeasure, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, NUMERIC_CANON_NAME, NUMERIC_CANON_SOURCE, type NameMode, type NestedArm, type NestedMode, type NestedNode, type NullCheck, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableConstraints, type TableMetaFacts, type TableMetaOptions, type UnenforcedLiteral, type UnenforcedPart, VALIBOT_JSON_CONST, VALIBOT_JSON_SOURCE, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, type WireLiteralFit, type WireLiteralQuestion, applyTableCase, applyWirePolicy, buildBrandPlan, buildNestedPlan, canonicalMembers, canonicalNumericText, classifyTableChecks, codePointCompare, columnMetaFacts, comparisonWire, describeSet, fileWriter, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, isProjectInstallPath, lengthCheckLabel, lengthMeasure, measureCompare, measureExpression, moduleFileName, moduleSpecifier, needsNumericCanon, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, nonFiniteRefused, parseCheck, parsesToADate, pascalCase, renderConstraintsModule, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveConstraints, resolveNestedDepth, schemaName, selectColumns, tableConstraints, tableMetaFacts, typeName, updateColumns, validateAffix, wireLiteralFit, wireNumberLiteral };
1833
+ export { AFFIX_PREFIX_PATTERN, AFFIX_PROBE_TABLE, AFFIX_SUFFIX_PATTERN, type AffixIssue, type AffixOptions, type AffixValue, BIGINT_DIGITS_PATTERN, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, CONSTRAINTS_MODULE, type CardinalityCheck, type CheckPart, type CheckPartPlace, type ClassifiedCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, type ComparisonWire, type ConstraintFacts, type ConstraintKind, type ConstraintsModuleOptions, type ConstraintsOption, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FieldFacts, type FileSink, type FormatOptions, type GeneratorFs, IMPORT_EXTENSIONS, type ImportExtension, JSON_VALUE_TS_CONST, JSON_VALUE_TS_SOURCE, type LengthCheck, type LengthMeasure, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, NUMERIC_CANON_NAME, NUMERIC_CANON_SOURCE, type NameMode, type NestedArm, type NestedMode, type NestedNode, type NullCheck, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableConstraints, type TableMetaFacts, type TableMetaOptions, type UnenforcedLiteral, type UnenforcedPart, VALIBOT_JSON_CONST, VALIBOT_JSON_SOURCE, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, type WireLiteralFit, type WireLiteralQuestion, applyTableCase, applyWirePolicy, buildBrandPlan, buildNestedPlan, canonicalMembers, canonicalNumericText, classifyTableChecks, codePointCompare, columnMetaFacts, comparisonWire, describeSet, fieldFacts, fileWriter, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, isProjectInstallPath, lengthCheckLabel, lengthMeasure, measureCompare, measureExpression, metaSchemaId, moduleFileName, moduleSpecifier, needsNumericCanon, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, nonFiniteRefused, parseCheck, parsesToADate, pascalCase, renderConstraintsModule, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveConstraints, resolveNestedDepth, schemaName, selectColumns, tableConstraints, tableMetaFacts, typeName, updateColumns, validateAffix, wireLiteralFit, wireNumberLiteral };
package/dist/index.d.ts CHANGED
@@ -966,6 +966,65 @@ declare const CONSTRAINTS_MODULE = "constraints.ts";
966
966
  */
967
967
  declare function renderConstraintsModule(tables: Table$1[], opts?: ConstraintsModuleOptions): string;
968
968
 
969
+ /**
970
+ * What one column is, in the terms a form control needs.
971
+ *
972
+ * Every validation generator already folds a table's `CHECK` constraints into the bounds it emits,
973
+ * and every one of them does it with a private copy of the same loop. A form generator reading
974
+ * `Column.min` and `Column.max` directly would be a fourth derivation and a wrong one: measured
975
+ * 2026-08-12, a column carrying `check('adult', age >= 18)` still reports
976
+ * `min: '-2147483648'`, the plain int32 range, because the analyzer leaves checks on the table and
977
+ * the generators fold them at emit time. An input rendered from that would carry
978
+ * `min="-2147483648"`, which looks like a bound and is not one.
979
+ *
980
+ * So the fold lives here, beside `classifyTableChecks` and `tableConstraints`, which are already
981
+ * the shared home for the same question. `fieldFacts` is what a form generator reads, and what the
982
+ * emitted schema's own bounds agree with by construction rather than by coincidence.
983
+ */
984
+ interface FieldFacts {
985
+ /** The column, under the TypeScript name a generated schema spells. */
986
+ name: string;
987
+ /** The input type a control should use, derived from the column's TypeScript type. */
988
+ control: 'text' | 'number' | 'checkbox' | 'datetime-local' | 'select';
989
+ /**
990
+ * Whether a value has to be supplied on insert.
991
+ *
992
+ * False for a nullable column and for one carrying a default, since the database fills it. This
993
+ * is the insert-side answer; a select-side reader wants `nullable` instead.
994
+ */
995
+ required: boolean;
996
+ nullable: boolean;
997
+ /** `maxlength`, from a declared width. Absent on an unbounded text column. */
998
+ maxLength?: number;
999
+ /**
1000
+ * `min` and `max`, as text.
1001
+ *
1002
+ * Text because a 64 bit bound is not representable as a JS number, which is the same reason the
1003
+ * analyzer reports them that way. Narrowed by any `CHECK` on the column: a check replaces the end
1004
+ * it constrains rather than sitting beside it, and it can never widen, because the declared range
1005
+ * is the column's type and no check makes an int32 hold more.
1006
+ */
1007
+ min?: string;
1008
+ max?: string;
1009
+ /** Whether either bound is exclusive, which HTML cannot express and a validator can. */
1010
+ exclusiveMin?: boolean;
1011
+ exclusiveMax?: boolean;
1012
+ /** `step="1"` for an integer column. */
1013
+ integer?: boolean;
1014
+ /** The options a `<select>` should carry, from a native enum or a declared set. */
1015
+ options?: string[];
1016
+ /** A `pattern` where the column's format implies one and HTML can state it. */
1017
+ pattern?: string;
1018
+ /** The default the database applies, for a form's initial value. */
1019
+ defaultValue?: unknown;
1020
+ }
1021
+ /**
1022
+ * Every column of a table, as the facts a form control needs.
1023
+ *
1024
+ * Generated columns are left out: the database computes them and no form supplies one.
1025
+ */
1026
+ declare function fieldFacts(table: Table$1): FieldFacts[];
1027
+
969
1028
  /**
970
1029
  * The facts a generated schema can carry beside itself.
971
1030
  *
@@ -1037,6 +1096,15 @@ interface ColumnMetaFacts {
1037
1096
  }
1038
1097
  /** What a table's schema adds beside itself. */
1039
1098
  interface TableMetaFacts {
1099
+ /**
1100
+ * The registry id, present only when asked for.
1101
+ *
1102
+ * zod reads this key out of the same object and registers the schema under it, which is what
1103
+ * makes `z.toJSONSchema` emit a `$ref` instead of inlining a copy, and what gives
1104
+ * `z.toJSONSchema(z.globalRegistry)` a name to file each schema under. See `metaSchemaId` for why
1105
+ * it is built from the qualified table name.
1106
+ */
1107
+ id?: string;
1040
1108
  /** The SQL table name, which is not the Drizzle export name the schema is named after. */
1041
1109
  table: string;
1042
1110
  /**
@@ -1085,7 +1153,33 @@ interface MetaFactOptions {
1085
1153
  interface TableMetaOptions extends MetaFactOptions {
1086
1154
  mode: string;
1087
1155
  dialect?: string;
1156
+ /**
1157
+ * Also give the schema an `id`, which is what puts it in zod's registry under a name.
1158
+ *
1159
+ * Off by default, because it is the one metadata key with a failure mode. Measured against zod
1160
+ * 4.4.3: an `id` makes `z.toJSONSchema` emit `$ref: '#/$defs/<id>'` wherever one schema references
1161
+ * another, and `z.toJSONSchema(z.globalRegistry)` return a `{ schemas: { <id>: ... } }` bundle,
1162
+ * which is what makes a generated document self-describing. But two schemas sharing an id do not
1163
+ * report the collision: measured, `z.toJSONSchema(registry)` keeps the last one and **silently
1164
+ * drops the other**. Two tables, one entry, no warning, and nothing a consumer can check.
1165
+ *
1166
+ * So the id has to be unique across the whole analysis rather than within a file, which is why
1167
+ * `metaSchemaId` builds it from the qualified table name.
1168
+ */
1169
+ id?: string;
1088
1170
  }
1171
+ /**
1172
+ * The registry id for one table's schema, unique across an analysis.
1173
+ *
1174
+ * Built from the qualified name, because two tables in two SQL schemas share a bare one and zod
1175
+ * does not report the collision: a registry dump silently keeps one of them. `reporting.users` becomes
1176
+ * `reporting_usersSelect`; a table in the default schema keeps the short `usersSelect`, so the
1177
+ * common case reads the way anybody would have written it by hand.
1178
+ *
1179
+ * Anything outside `[A-Za-z0-9_]` becomes `_`, because the id lands in a `$ref` as a JSON Pointer
1180
+ * segment and a `/` or a `~` there means something else entirely.
1181
+ */
1182
+ declare function metaSchemaId(table: Table$1, mode: string): string;
1089
1183
  /**
1090
1184
  * The metadata for one column.
1091
1185
  *
@@ -1736,4 +1830,4 @@ declare function isProjectInstallPath(manifestPath: string): boolean;
1736
1830
  */
1737
1831
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
1738
1832
 
1739
- export { AFFIX_PREFIX_PATTERN, AFFIX_PROBE_TABLE, AFFIX_SUFFIX_PATTERN, type AffixIssue, type AffixOptions, type AffixValue, BIGINT_DIGITS_PATTERN, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, CONSTRAINTS_MODULE, type CardinalityCheck, type CheckPart, type CheckPartPlace, type ClassifiedCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, type ComparisonWire, type ConstraintFacts, type ConstraintKind, type ConstraintsModuleOptions, type ConstraintsOption, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FileSink, type FormatOptions, type GeneratorFs, IMPORT_EXTENSIONS, type ImportExtension, JSON_VALUE_TS_CONST, JSON_VALUE_TS_SOURCE, type LengthCheck, type LengthMeasure, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, NUMERIC_CANON_NAME, NUMERIC_CANON_SOURCE, type NameMode, type NestedArm, type NestedMode, type NestedNode, type NullCheck, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableConstraints, type TableMetaFacts, type TableMetaOptions, type UnenforcedLiteral, type UnenforcedPart, VALIBOT_JSON_CONST, VALIBOT_JSON_SOURCE, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, type WireLiteralFit, type WireLiteralQuestion, applyTableCase, applyWirePolicy, buildBrandPlan, buildNestedPlan, canonicalMembers, canonicalNumericText, classifyTableChecks, codePointCompare, columnMetaFacts, comparisonWire, describeSet, fileWriter, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, isProjectInstallPath, lengthCheckLabel, lengthMeasure, measureCompare, measureExpression, moduleFileName, moduleSpecifier, needsNumericCanon, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, nonFiniteRefused, parseCheck, parsesToADate, pascalCase, renderConstraintsModule, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveConstraints, resolveNestedDepth, schemaName, selectColumns, tableConstraints, tableMetaFacts, typeName, updateColumns, validateAffix, wireLiteralFit, wireNumberLiteral };
1833
+ export { AFFIX_PREFIX_PATTERN, AFFIX_PROBE_TABLE, AFFIX_SUFFIX_PATTERN, type AffixIssue, type AffixOptions, type AffixValue, BIGINT_DIGITS_PATTERN, type BrandAlias, type BrandPlan, type BrandingOption, type BrandingOptions, CODEPOINT_LENGTH, COERCIBLE_DATE_STRING, COLUMN_FORMATS, CONSTRAINTS_MODULE, type CardinalityCheck, type CheckPart, type CheckPartPlace, type ClassifiedCheck, type ColumnCheck, type ColumnMetaFacts, type ColumnSet, type ComparisonWire, type ConstraintFacts, type ConstraintKind, type ConstraintsModuleOptions, type ConstraintsOption, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_NESTED_DEPTH, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FieldFacts, type FileSink, type FormatOptions, type GeneratorFs, IMPORT_EXTENSIONS, type ImportExtension, JSON_VALUE_TS_CONST, JSON_VALUE_TS_SOURCE, type LengthCheck, type LengthMeasure, MAX_NESTED_DEPTH, type MetaFactOptions, NAME_MODES, NESTED_PREFIX, NUMERIC_CANON_NAME, NUMERIC_CANON_SOURCE, type NameMode, type NestedArm, type NestedMode, type NestedNode, type NullCheck, type ParsedCheck, type ResolvedAffix, type ResolvedBranding, type RowCheck, type Table, type TableCase, type TableConstraints, type TableMetaFacts, type TableMetaOptions, type UnenforcedLiteral, type UnenforcedPart, VALIBOT_JSON_CONST, VALIBOT_JSON_SOURCE, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, type WireLiteralFit, type WireLiteralQuestion, applyTableCase, applyWirePolicy, buildBrandPlan, buildNestedPlan, canonicalMembers, canonicalNumericText, classifyTableChecks, codePointCompare, columnMetaFacts, comparisonWire, describeSet, fieldFacts, fileWriter, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, isProjectInstallPath, lengthCheckLabel, lengthMeasure, measureCompare, measureExpression, metaSchemaId, moduleFileName, moduleSpecifier, needsNumericCanon, nestedArmNotes, nestedNodeColumns, nestedSchemaName, nestedTypeName, nonFiniteAccepted, nonFiniteRefused, parseCheck, parsesToADate, pascalCase, renderConstraintsModule, renderDuplicateFinder, resolveAffix, resolveBranding, resolveConfiguredImport, resolveConstraints, resolveNestedDepth, schemaName, selectColumns, tableConstraints, tableMetaFacts, typeName, updateColumns, validateAffix, wireLiteralFit, wireNumberLiteral };
package/dist/index.js CHANGED
@@ -1355,6 +1355,60 @@ function fileWriter(sink) {
1355
1355
  };
1356
1356
  }
1357
1357
 
1358
+ // src/fields.ts
1359
+ var UUID_PATTERN = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
1360
+ function controlFor(c) {
1361
+ if (c.enumValues?.length) return "select";
1362
+ if (c.tsType === "boolean") return "checkbox";
1363
+ if (c.tsType === "number" || c.tsType === "bigint") return "number";
1364
+ if (c.tsType === "Date") return "datetime-local";
1365
+ return "text";
1366
+ }
1367
+ function boundsFor(c, checks) {
1368
+ let lo = c.min !== void 0 ? { value: c.min, exclusive: false } : void 0;
1369
+ let hi = c.max !== void 0 ? { value: c.max, exclusive: false } : void 0;
1370
+ for (const k of checks) {
1371
+ if (k.column !== c.name || k.kind !== "number") continue;
1372
+ if (k.operator === ">=") lo = { value: k.value, exclusive: false };
1373
+ else if (k.operator === ">") lo = { value: k.value, exclusive: true };
1374
+ else if (k.operator === "<=") hi = { value: k.value, exclusive: false };
1375
+ else if (k.operator === "<") hi = { value: k.value, exclusive: true };
1376
+ }
1377
+ return {
1378
+ ...lo ? { min: lo.value, ...lo.exclusive ? { exclusiveMin: true } : {} } : {},
1379
+ ...hi ? { max: hi.value, ...hi.exclusive ? { exclusiveMax: true } : {} } : {}
1380
+ };
1381
+ }
1382
+ function fieldFacts(table) {
1383
+ const parsed = (table.checks ?? []).map(
1384
+ (k) => parseCheck(k.expression, k.name, table.dialect)
1385
+ );
1386
+ const ok = parsed.filter((p) => !!p && p.ok);
1387
+ const columnChecks = ok.flatMap((p) => p.checks ?? []);
1388
+ const lengthChecks = ok.flatMap((p) => p.lengths ?? []);
1389
+ return table.columns.filter((c) => !c.isGenerated).map((c) => ({
1390
+ name: c.name,
1391
+ control: controlFor(c),
1392
+ // A column the database fills is not one a form has to supply, which is the insert-side
1393
+ // question a form asks. `nullable` is reported beside it for a reader asking the other one.
1394
+ required: !c.nullable && !c.hasDefault,
1395
+ nullable: c.nullable,
1396
+ ...(() => {
1397
+ const declared = c.maxLength;
1398
+ const fromCheck = lengthChecks.filter(
1399
+ (l) => l.column === c.name && (l.unit ?? "characters") === "characters" && (l.operator === "<=" || l.operator === "<")
1400
+ ).map((l) => l.operator === "<" ? Number(l.value) - 1 : Number(l.value)).filter((n) => Number.isFinite(n));
1401
+ const all = [...declared !== void 0 ? [declared] : [], ...fromCheck];
1402
+ return all.length ? { maxLength: Math.min(...all) } : {};
1403
+ })(),
1404
+ ...boundsFor(c, columnChecks),
1405
+ ...c.integer ? { integer: true } : {},
1406
+ ...c.enumValues?.length ? { options: [...c.enumValues] } : {},
1407
+ ...c.format === "uuid" ? { pattern: UUID_PATTERN } : {},
1408
+ ...c.defaultValue !== void 0 ? { defaultValue: c.defaultValue } : {}
1409
+ }));
1410
+ }
1411
+
1358
1412
  // src/files.ts
1359
1413
  import nodeFs2 from "fs";
1360
1414
  import nodePath from "path";
@@ -1415,6 +1469,11 @@ function withTsExtension(p) {
1415
1469
  }
1416
1470
 
1417
1471
  // src/meta.ts
1472
+ function metaSchemaId(table, mode) {
1473
+ const qualified = table.schema ? `${table.schema}_${table.name}` : table.name;
1474
+ const safe = qualified.replace(/[^A-Za-z0-9_]/g, "_");
1475
+ return `${safe}${mode.charAt(0).toUpperCase()}${mode.slice(1)}`;
1476
+ }
1418
1477
  function classifyChecks(table) {
1419
1478
  const perColumn = /* @__PURE__ */ new Map();
1420
1479
  const rows = [];
@@ -1473,6 +1532,7 @@ function tableMetaFacts(table, opts) {
1473
1532
  const pk = table.primaryKey?.columns ?? [];
1474
1533
  const unique = (table.unique ?? []).map((k) => k.columns).filter((c) => c.length > 0);
1475
1534
  const facts = {
1535
+ ...opts.id ? { id: opts.id } : {},
1476
1536
  table: table.name,
1477
1537
  ...table.schema ? { schema: table.schema } : {},
1478
1538
  ...opts.dialect ? { dialect: opts.dialect } : {},
@@ -1929,6 +1989,7 @@ export {
1929
1989
  columnMetaFacts,
1930
1990
  comparisonWire,
1931
1991
  describeSet,
1992
+ fieldFacts,
1932
1993
  fileWriter,
1933
1994
  formatCode,
1934
1995
  importSpecifier,
@@ -1940,6 +2001,7 @@ export {
1940
2001
  lengthMeasure,
1941
2002
  measureCompare,
1942
2003
  measureExpression,
2004
+ metaSchemaId,
1943
2005
  moduleFileName,
1944
2006
  moduleSpecifier,
1945
2007
  needsNumericCanon,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/validation-core",
3
- "version": "3.22.6",
3
+ "version": "3.23.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.21.5"
27
+ "@drzl/analyzer": "^1.22.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "prettier": ">=3"