@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/README.md +17 -0
- package/dist/index.cjs +159 -84515
- package/dist/index.d.cts +48 -4
- package/dist/index.d.ts +48 -4
- package/dist/index.js +54 -3
- package/package.json +12 -4
- package/dist/acorn-XYPTJ6WH.js +0 -3181
- package/dist/angular-YPOYMZ5Z.js +0 -3629
- package/dist/babel-3Y2WL4BG.js +0 -7335
- package/dist/chunk-ZZGILH5A.js +0 -24
- package/dist/estree-PS47G6OA.js +0 -4955
- package/dist/flow-7NIIFLZC.js +0 -4998
- package/dist/glimmer-HA3GA4CF.js +0 -2908
- package/dist/graphql-3AZLP5TQ.js +0 -1289
- package/dist/html-WHLWODCM.js +0 -3218
- package/dist/markdown-Q4JIPTFK.js +0 -6803
- package/dist/meriyah-PDYHQKRL.js +0 -2737
- package/dist/postcss-66ZEKHWQ.js +0 -5239
- package/dist/prettier-LT6G5PW7.js +0 -20062
- package/dist/typescript-Z6ME4ZZC.js +0 -13435
- package/dist/yaml-QEX7HYMI.js +0 -4833
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
|
|
455
|
-
*
|
|
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
|
|
455
|
-
*
|
|
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("
|
|
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.
|
|
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.
|
|
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
|
}
|