@drzl/validation-core 3.13.0 → 3.14.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 +50 -0
- package/dist/index.d.cts +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +49 -0
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -84460,6 +84460,7 @@ __export(index_exports2, {
|
|
|
84460
84460
|
moduleSpecifier: () => moduleSpecifier,
|
|
84461
84461
|
parseCheck: () => parseCheck,
|
|
84462
84462
|
pascalCase: () => pascalCase,
|
|
84463
|
+
renderDuplicateFinder: () => renderDuplicateFinder,
|
|
84463
84464
|
resolveAffix: () => resolveAffix,
|
|
84464
84465
|
resolveConfiguredImport: () => resolveConfiguredImport,
|
|
84465
84466
|
schemaName: () => schemaName,
|
|
@@ -84849,6 +84850,54 @@ function validateAffix(affix, schemaSuffix) {
|
|
|
84849
84850
|
return issues;
|
|
84850
84851
|
}
|
|
84851
84852
|
|
|
84853
|
+
// src/duplicates.ts
|
|
84854
|
+
function usableKeys(table) {
|
|
84855
|
+
return (table.unique ?? []).filter((k5) => k5.columns.length > 0);
|
|
84856
|
+
}
|
|
84857
|
+
function renderDuplicateFinder(table, fnName, rowType) {
|
|
84858
|
+
const keys = usableKeys(table);
|
|
84859
|
+
if (!keys.length) return void 0;
|
|
84860
|
+
const constraints = keys.map((k5, i) => {
|
|
84861
|
+
const name = k5.name ?? k5.columns.join("_");
|
|
84862
|
+
return ` { name: ${JSON.stringify(name)}, columns: ${JSON.stringify(k5.columns)} }${i === keys.length - 1 ? "" : ","}`;
|
|
84863
|
+
}).join("\n");
|
|
84864
|
+
return `/**
|
|
84865
|
+
* Rows in \`rows\` that collide with an earlier row on a unique constraint.
|
|
84866
|
+
*
|
|
84867
|
+
* Uniqueness is a fact about the table rather than about a row, so no schema can check it. This
|
|
84868
|
+
* checks the half that needs no database: whether the batch collides with itself. A batch that
|
|
84869
|
+
* passes here can still collide with rows already stored.
|
|
84870
|
+
*
|
|
84871
|
+
* A constraint is skipped for any row where one of its columns is null or absent, matching SQL,
|
|
84872
|
+
* where NULL is not equal to NULL and a unique index therefore permits repeats.
|
|
84873
|
+
*/
|
|
84874
|
+
export function ${fnName}(
|
|
84875
|
+
rows: readonly ${rowType}[]
|
|
84876
|
+
): Array<{ index: number; constraint: string; firstIndex: number }> {
|
|
84877
|
+
const constraints = [
|
|
84878
|
+
${constraints}
|
|
84879
|
+
] as const;
|
|
84880
|
+
const seen = constraints.map(() => new Map<string, number>());
|
|
84881
|
+
const out: Array<{ index: number; constraint: string; firstIndex: number }> = [];
|
|
84882
|
+
|
|
84883
|
+
for (let i = 0; i < rows.length; i++) {
|
|
84884
|
+
const row = rows[i] as Record<string, unknown>;
|
|
84885
|
+
for (let c = 0; c < constraints.length; c++) {
|
|
84886
|
+
const cols = constraints[c].columns;
|
|
84887
|
+
const values = cols.map((col) => row?.[col]);
|
|
84888
|
+
if (values.some((v) => v === null || v === undefined)) continue;
|
|
84889
|
+
// JSON, so a composite key compares by value and \`[1, "2"]\` never collides with
|
|
84890
|
+
// \`["1", 2]\`. A join on a separator would.
|
|
84891
|
+
const key = JSON.stringify(values);
|
|
84892
|
+
const first = seen[c].get(key);
|
|
84893
|
+
if (first === undefined) seen[c].set(key, i);
|
|
84894
|
+
else out.push({ index: i, constraint: constraints[c].name, firstIndex: first });
|
|
84895
|
+
}
|
|
84896
|
+
}
|
|
84897
|
+
return out;
|
|
84898
|
+
}`;
|
|
84899
|
+
}
|
|
84900
|
+
|
|
84852
84901
|
// src/index.ts
|
|
84853
84902
|
function isGeneratedColumn(c5, _primaryKeyColumns = []) {
|
|
84854
84903
|
return c5.isGenerated;
|
|
@@ -84921,6 +84970,7 @@ async function formatCode(code, filePath, fmt) {
|
|
|
84921
84970
|
moduleSpecifier,
|
|
84922
84971
|
parseCheck,
|
|
84923
84972
|
pascalCase,
|
|
84973
|
+
renderDuplicateFinder,
|
|
84924
84974
|
resolveAffix,
|
|
84925
84975
|
resolveConfiguredImport,
|
|
84926
84976
|
schemaName,
|
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;
|
|
@@ -461,4 +485,4 @@ declare function updateColumns(table: Table): Column[];
|
|
|
461
485
|
declare function selectColumns(table: Table): Column[];
|
|
462
486
|
declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
|
|
463
487
|
|
|
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 };
|
|
488
|
+
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;
|
|
@@ -461,4 +485,4 @@ declare function updateColumns(table: Table): Column[];
|
|
|
461
485
|
declare function selectColumns(table: Table): Column[];
|
|
462
486
|
declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
|
|
463
487
|
|
|
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 };
|
|
488
|
+
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
|
@@ -379,6 +379,54 @@ function validateAffix(affix, schemaSuffix) {
|
|
|
379
379
|
return issues;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
// src/duplicates.ts
|
|
383
|
+
function usableKeys(table) {
|
|
384
|
+
return (table.unique ?? []).filter((k) => k.columns.length > 0);
|
|
385
|
+
}
|
|
386
|
+
function renderDuplicateFinder(table, fnName, rowType) {
|
|
387
|
+
const keys = usableKeys(table);
|
|
388
|
+
if (!keys.length) return void 0;
|
|
389
|
+
const constraints = keys.map((k, i) => {
|
|
390
|
+
const name = k.name ?? k.columns.join("_");
|
|
391
|
+
return ` { name: ${JSON.stringify(name)}, columns: ${JSON.stringify(k.columns)} }${i === keys.length - 1 ? "" : ","}`;
|
|
392
|
+
}).join("\n");
|
|
393
|
+
return `/**
|
|
394
|
+
* Rows in \`rows\` that collide with an earlier row on a unique constraint.
|
|
395
|
+
*
|
|
396
|
+
* Uniqueness is a fact about the table rather than about a row, so no schema can check it. This
|
|
397
|
+
* checks the half that needs no database: whether the batch collides with itself. A batch that
|
|
398
|
+
* passes here can still collide with rows already stored.
|
|
399
|
+
*
|
|
400
|
+
* A constraint is skipped for any row where one of its columns is null or absent, matching SQL,
|
|
401
|
+
* where NULL is not equal to NULL and a unique index therefore permits repeats.
|
|
402
|
+
*/
|
|
403
|
+
export function ${fnName}(
|
|
404
|
+
rows: readonly ${rowType}[]
|
|
405
|
+
): Array<{ index: number; constraint: string; firstIndex: number }> {
|
|
406
|
+
const constraints = [
|
|
407
|
+
${constraints}
|
|
408
|
+
] as const;
|
|
409
|
+
const seen = constraints.map(() => new Map<string, number>());
|
|
410
|
+
const out: Array<{ index: number; constraint: string; firstIndex: number }> = [];
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < rows.length; i++) {
|
|
413
|
+
const row = rows[i] as Record<string, unknown>;
|
|
414
|
+
for (let c = 0; c < constraints.length; c++) {
|
|
415
|
+
const cols = constraints[c].columns;
|
|
416
|
+
const values = cols.map((col) => row?.[col]);
|
|
417
|
+
if (values.some((v) => v === null || v === undefined)) continue;
|
|
418
|
+
// JSON, so a composite key compares by value and \`[1, "2"]\` never collides with
|
|
419
|
+
// \`["1", 2]\`. A join on a separator would.
|
|
420
|
+
const key = JSON.stringify(values);
|
|
421
|
+
const first = seen[c].get(key);
|
|
422
|
+
if (first === undefined) seen[c].set(key, i);
|
|
423
|
+
else out.push({ index: i, constraint: constraints[c].name, firstIndex: first });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
382
430
|
// src/index.ts
|
|
383
431
|
function isGeneratedColumn(c, _primaryKeyColumns = []) {
|
|
384
432
|
return c.isGenerated;
|
|
@@ -450,6 +498,7 @@ export {
|
|
|
450
498
|
moduleSpecifier,
|
|
451
499
|
parseCheck,
|
|
452
500
|
pascalCase,
|
|
501
|
+
renderDuplicateFinder,
|
|
453
502
|
resolveAffix,
|
|
454
503
|
resolveConfiguredImport,
|
|
455
504
|
schemaName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drzl/validation-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"sideEffects": false,
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@drzl/analyzer": "^1.
|
|
14
|
+
"@drzl/analyzer": "^1.14.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"tsup": "^8.5.1",
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsup src/index.ts --dts --format esm,cjs",
|
|
39
|
-
"test": "vitest run"
|
|
39
|
+
"test": "vitest run --testTimeout=20000"
|
|
40
40
|
}
|
|
41
41
|
}
|