@drzl/validation-core 3.13.0 → 3.15.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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Column, Analysis } from '@drzl/analyzer';
1
+ import { 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,30 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
284
284
  */
285
285
  declare function describeSet(set: ColumnSet): string;
286
286
 
287
+ /**
288
+ * A duplicate finder for a batch of rows, emitted beside the schemas.
289
+ *
290
+ * A validator checks one row at a time, so uniqueness is the one constraint it structurally
291
+ * cannot see: whether a value is unique is a fact about the table, not about the row. That is
292
+ * fine for a single insert, where the database answers immediately. It is not fine for a bulk
293
+ * insert, where a batch of a thousand rows fails whole on one collision and the error names a
294
+ * constraint rather than a row.
295
+ *
296
+ * What *is* checkable without the database is whether a batch collides with **itself**, and that
297
+ * is worth checking, because it is the half a user can fix before sending anything.
298
+ *
299
+ * 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.
301
+ */
302
+
303
+ /**
304
+ * `findDuplicate<Table>s` for a table with unique constraints, or nothing.
305
+ *
306
+ * `rowType` is the name of the insert type, which is what a caller has in hand before an insert.
307
+ * Passed in rather than derived, because each generator names its types differently.
308
+ */
309
+ declare function renderDuplicateFinder(table: Table$1, fnName: string, rowType: string): string | undefined;
310
+
287
311
  interface Table {
288
312
  name: string;
289
313
  tsName: string;
@@ -451,14 +475,34 @@ declare const COLUMN_FORMATS: Record<string, string>;
451
475
  * Refusing a user's emoji is the failure mode this avoids, and it is the same rule applied
452
476
  * everywhere else here: never reject what the database accepts.
453
477
  *
454
- * `@sinclair/typebox` and ArkType cannot express it. Both state a length declaratively with no
455
- * predicate to hook, so they keep the UTF-16 form and are documented as approximate.
478
+ * All four generators count code points. `@sinclair/typebox` and ArkType cannot say it in their
479
+ * declarative forms, so neither uses `maxLength` or `string <= n`: TypeBox intersects a registered
480
+ * kind onto the field and ArkType puts a Type carrying a narrow there. Both cost something,
481
+ * TypeBox's cap no longer serialising into a JSON Schema, and emitting a number that means a
482
+ * different measurement is not a better trade.
483
+ *
484
+ * MySQL's TEXT family is a byte budget rather than a character count, carried separately as
485
+ * `maxBytes`. Two measurements on string columns in the same database, verified against a real
486
+ * MySQL 8 on utf8mb4: `varchar(10)` takes ten emoji, `tinytext` takes 63 of them and refuses 64.
456
487
  */
457
488
  declare const CODEPOINT_LENGTH = "[...v].length";
458
489
  declare function isIntegerColumn(c: Column): boolean;
459
490
  declare function insertColumns(table: Table): Column[];
460
491
  declare function updateColumns(table: Table): Column[];
461
492
  declare function selectColumns(table: Table): Column[];
493
+ /**
494
+ * Pretty-print emitted code with whatever formatter the consumer already has.
495
+ *
496
+ * Both formatters are optional peers, reached at call time and never bundled. Prettier used to be
497
+ * bundled, because tsup resolves the specifier below statically and esbuild then inlined all of
498
+ * it: 11 MB per package across the three that had a copy of this function, roughly 32 MB for
499
+ * anyone installing @drzl/cli. It is `--external` in every build script that can reach it, and
500
+ * no-bundled-formatter.spec.ts builds those scripts and checks.
501
+ *
502
+ * Neither absence is an error. A consumer with no formatter gets the code as rendered, which is
503
+ * valid TypeScript that merely looks worse, and losing generated files at the last step would be
504
+ * a far worse trade than losing their whitespace.
505
+ */
462
506
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
463
507
 
464
- export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, 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, parseCheck, pascalCase, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
508
+ export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, 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, parseCheck, pascalCase, renderDuplicateFinder, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Column, Analysis } from '@drzl/analyzer';
1
+ import { 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,30 @@ declare function parseCheck(expression: string | undefined, name?: string): Pars
284
284
  */
285
285
  declare function describeSet(set: ColumnSet): string;
286
286
 
287
+ /**
288
+ * A duplicate finder for a batch of rows, emitted beside the schemas.
289
+ *
290
+ * A validator checks one row at a time, so uniqueness is the one constraint it structurally
291
+ * cannot see: whether a value is unique is a fact about the table, not about the row. That is
292
+ * fine for a single insert, where the database answers immediately. It is not fine for a bulk
293
+ * insert, where a batch of a thousand rows fails whole on one collision and the error names a
294
+ * constraint rather than a row.
295
+ *
296
+ * What *is* checkable without the database is whether a batch collides with **itself**, and that
297
+ * is worth checking, because it is the half a user can fix before sending anything.
298
+ *
299
+ * 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.
301
+ */
302
+
303
+ /**
304
+ * `findDuplicate<Table>s` for a table with unique constraints, or nothing.
305
+ *
306
+ * `rowType` is the name of the insert type, which is what a caller has in hand before an insert.
307
+ * Passed in rather than derived, because each generator names its types differently.
308
+ */
309
+ declare function renderDuplicateFinder(table: Table$1, fnName: string, rowType: string): string | undefined;
310
+
287
311
  interface Table {
288
312
  name: string;
289
313
  tsName: string;
@@ -451,14 +475,34 @@ declare const COLUMN_FORMATS: Record<string, string>;
451
475
  * Refusing a user's emoji is the failure mode this avoids, and it is the same rule applied
452
476
  * everywhere else here: never reject what the database accepts.
453
477
  *
454
- * `@sinclair/typebox` and ArkType cannot express it. Both state a length declaratively with no
455
- * predicate to hook, so they keep the UTF-16 form and are documented as approximate.
478
+ * All four generators count code points. `@sinclair/typebox` and ArkType cannot say it in their
479
+ * declarative forms, so neither uses `maxLength` or `string <= n`: TypeBox intersects a registered
480
+ * kind onto the field and ArkType puts a Type carrying a narrow there. Both cost something,
481
+ * TypeBox's cap no longer serialising into a JSON Schema, and emitting a number that means a
482
+ * different measurement is not a better trade.
483
+ *
484
+ * MySQL's TEXT family is a byte budget rather than a character count, carried separately as
485
+ * `maxBytes`. Two measurements on string columns in the same database, verified against a real
486
+ * MySQL 8 on utf8mb4: `varchar(10)` takes ten emoji, `tinytext` takes 63 of them and refuses 64.
456
487
  */
457
488
  declare const CODEPOINT_LENGTH = "[...v].length";
458
489
  declare function isIntegerColumn(c: Column): boolean;
459
490
  declare function insertColumns(table: Table): Column[];
460
491
  declare function updateColumns(table: Table): Column[];
461
492
  declare function selectColumns(table: Table): Column[];
493
+ /**
494
+ * Pretty-print emitted code with whatever formatter the consumer already has.
495
+ *
496
+ * Both formatters are optional peers, reached at call time and never bundled. Prettier used to be
497
+ * bundled, because tsup resolves the specifier below statically and esbuild then inlined all of
498
+ * it: 11 MB per package across the three that had a copy of this function, roughly 32 MB for
499
+ * anyone installing @drzl/cli. It is `--external` in every build script that can reach it, and
500
+ * no-bundled-formatter.spec.ts builds those scripts and checks.
501
+ *
502
+ * Neither absence is an error. A consumer with no formatter gets the code as rendered, which is
503
+ * valid TypeScript that merely looks worse, and losing generated files at the last step would be
504
+ * a far worse trade than losing their whitespace.
505
+ */
462
506
  declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
463
507
 
464
- export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, 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, parseCheck, pascalCase, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
508
+ export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, CODEPOINT_LENGTH, 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, parseCheck, pascalCase, renderDuplicateFinder, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
package/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
- import "./chunk-ZZGILH5A.js";
2
-
3
1
  // src/checks.ts
4
2
  var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
5
3
  var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
@@ -379,6 +377,54 @@ function validateAffix(affix, schemaSuffix) {
379
377
  return issues;
380
378
  }
381
379
 
380
+ // src/duplicates.ts
381
+ function usableKeys(table) {
382
+ return (table.unique ?? []).filter((k) => k.columns.length > 0);
383
+ }
384
+ function renderDuplicateFinder(table, fnName, rowType) {
385
+ const keys = usableKeys(table);
386
+ if (!keys.length) return void 0;
387
+ const constraints = keys.map((k, i) => {
388
+ const name = k.name ?? k.columns.join("_");
389
+ return ` { name: ${JSON.stringify(name)}, columns: ${JSON.stringify(k.columns)} }${i === keys.length - 1 ? "" : ","}`;
390
+ }).join("\n");
391
+ return `/**
392
+ * Rows in \`rows\` that collide with an earlier row on a unique constraint.
393
+ *
394
+ * Uniqueness is a fact about the table rather than about a row, so no schema can check it. This
395
+ * checks the half that needs no database: whether the batch collides with itself. A batch that
396
+ * passes here can still collide with rows already stored.
397
+ *
398
+ * A constraint is skipped for any row where one of its columns is null or absent, matching SQL,
399
+ * where NULL is not equal to NULL and a unique index therefore permits repeats.
400
+ */
401
+ export function ${fnName}(
402
+ rows: readonly ${rowType}[]
403
+ ): Array<{ index: number; constraint: string; firstIndex: number }> {
404
+ const constraints = [
405
+ ${constraints}
406
+ ] as const;
407
+ const seen = constraints.map(() => new Map<string, number>());
408
+ const out: Array<{ index: number; constraint: string; firstIndex: number }> = [];
409
+
410
+ for (let i = 0; i < rows.length; i++) {
411
+ const row = rows[i] as Record<string, unknown>;
412
+ for (let c = 0; c < constraints.length; c++) {
413
+ const cols = constraints[c].columns;
414
+ const values = cols.map((col) => row?.[col]);
415
+ if (values.some((v) => v === null || v === undefined)) continue;
416
+ // JSON, so a composite key compares by value and \`[1, "2"]\` never collides with
417
+ // \`["1", 2]\`. A join on a separator would.
418
+ const key = JSON.stringify(values);
419
+ const first = seen[c].get(key);
420
+ if (first === undefined) seen[c].set(key, i);
421
+ else out.push({ index: i, constraint: constraints[c].name, firstIndex: first });
422
+ }
423
+ }
424
+ return out;
425
+ }`;
426
+ }
427
+
382
428
  // src/index.ts
383
429
  function isGeneratedColumn(c, _primaryKeyColumns = []) {
384
430
  return c.isGenerated;
@@ -409,7 +455,7 @@ async function formatCode(code, filePath, fmt) {
409
455
  const engine = fmt?.engine ?? "auto";
410
456
  try {
411
457
  if (engine === "prettier" || engine === "auto") {
412
- const prettier = await import("./prettier-LT6G5PW7.js");
458
+ const prettier = await import("prettier");
413
459
  const cfgRef = fmt?.configPath ?? filePath;
414
460
  const cfg = await prettier.resolveConfig(cfgRef).catch(() => null);
415
461
  return prettier.format(code, { ...cfg ?? {}, parser: "typescript", filepath: filePath });
@@ -424,6 +470,10 @@ async function formatCode(code, filePath, fmt) {
424
470
  const res = await biome.formatContent(code, { filePath });
425
471
  return (res && (res.content || res.formatted)) ?? code;
426
472
  }
473
+ if (biome?.format) {
474
+ const res = await biome.format(code, { filePath });
475
+ return res ?? code;
476
+ }
427
477
  }
428
478
  } catch {
429
479
  }
@@ -450,6 +500,7 @@ export {
450
500
  moduleSpecifier,
451
501
  parseCheck,
452
502
  pascalCase,
503
+ renderDuplicateFinder,
453
504
  resolveAffix,
454
505
  resolveConfiguredImport,
455
506
  schemaName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drzl/validation-core",
3
- "version": "3.13.0",
3
+ "version": "3.15.0",
4
4
  "private": false,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -11,7 +11,15 @@
11
11
  ],
12
12
  "sideEffects": false,
13
13
  "dependencies": {
14
- "@drzl/analyzer": "^1.11.0"
14
+ "@drzl/analyzer": "^1.14.0"
15
+ },
16
+ "peerDependencies": {
17
+ "prettier": ">=3"
18
+ },
19
+ "peerDependenciesMeta": {
20
+ "prettier": {
21
+ "optional": true
22
+ }
15
23
  },
16
24
  "devDependencies": {
17
25
  "tsup": "^8.5.1",
@@ -35,7 +43,7 @@
35
43
  "url": "https://github.com/sponsors/omar-dulaimi"
36
44
  },
37
45
  "scripts": {
38
- "build": "tsup src/index.ts --dts --format esm,cjs",
39
- "test": "vitest run"
46
+ "build": "tsup src/index.ts --dts --format esm,cjs --external prettier --clean",
47
+ "test": "vitest run --testTimeout=20000"
40
48
  }
41
49
  }