@drzl/validation-core 3.7.0 → 3.9.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 +147 -2
- package/dist/index.d.cts +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +146 -2
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -84449,6 +84449,7 @@ __export(index_exports2, {
|
|
|
84449
84449
|
IMPORT_EXTENSIONS: () => IMPORT_EXTENSIONS,
|
|
84450
84450
|
NAME_MODES: () => NAME_MODES,
|
|
84451
84451
|
applyTableCase: () => applyTableCase,
|
|
84452
|
+
describeSet: () => describeSet,
|
|
84452
84453
|
formatCode: () => formatCode,
|
|
84453
84454
|
importSpecifier: () => importSpecifier,
|
|
84454
84455
|
insertColumns: () => insertColumns,
|
|
@@ -84470,6 +84471,94 @@ module.exports = __toCommonJS(index_exports2);
|
|
|
84470
84471
|
|
|
84471
84472
|
// src/checks.ts
|
|
84472
84473
|
var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
|
|
84474
|
+
var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
|
|
84475
|
+
function splitTopLevelAnd(expr) {
|
|
84476
|
+
const parts = [];
|
|
84477
|
+
let depth = 0;
|
|
84478
|
+
let inString = false;
|
|
84479
|
+
let start = 0;
|
|
84480
|
+
for (let i = 0; i < expr.length; i++) {
|
|
84481
|
+
const c5 = expr[i];
|
|
84482
|
+
if (inString) {
|
|
84483
|
+
if (c5 === "'") {
|
|
84484
|
+
if (expr[i + 1] === "'") i++;
|
|
84485
|
+
else inString = false;
|
|
84486
|
+
}
|
|
84487
|
+
continue;
|
|
84488
|
+
}
|
|
84489
|
+
if (c5 === "'") {
|
|
84490
|
+
inString = true;
|
|
84491
|
+
continue;
|
|
84492
|
+
}
|
|
84493
|
+
if (c5 === "(") depth++;
|
|
84494
|
+
else if (c5 === ")") depth--;
|
|
84495
|
+
else if (depth === 0 && /\s/.test(c5)) {
|
|
84496
|
+
const m7 = /^\s+AND\s+/i.exec(expr.slice(i));
|
|
84497
|
+
if (m7) {
|
|
84498
|
+
parts.push(expr.slice(start, i));
|
|
84499
|
+
i += m7[0].length - 1;
|
|
84500
|
+
start = i + 1;
|
|
84501
|
+
}
|
|
84502
|
+
}
|
|
84503
|
+
}
|
|
84504
|
+
parts.push(expr.slice(start));
|
|
84505
|
+
return parts;
|
|
84506
|
+
}
|
|
84507
|
+
function unwrap(expr) {
|
|
84508
|
+
let e7 = expr.trim();
|
|
84509
|
+
while (e7.startsWith("(") && e7.endsWith(")")) {
|
|
84510
|
+
let depth = 0;
|
|
84511
|
+
let inString = false;
|
|
84512
|
+
let wrapsWhole = true;
|
|
84513
|
+
for (let i = 0; i < e7.length; i++) {
|
|
84514
|
+
const c5 = e7[i];
|
|
84515
|
+
if (inString) {
|
|
84516
|
+
if (c5 === "'") {
|
|
84517
|
+
if (e7[i + 1] === "'") i++;
|
|
84518
|
+
else inString = false;
|
|
84519
|
+
}
|
|
84520
|
+
continue;
|
|
84521
|
+
}
|
|
84522
|
+
if (c5 === "'") inString = true;
|
|
84523
|
+
else if (c5 === "(") depth++;
|
|
84524
|
+
else if (c5 === ")") {
|
|
84525
|
+
depth--;
|
|
84526
|
+
if (depth === 0 && i < e7.length - 1) {
|
|
84527
|
+
wrapsWhole = false;
|
|
84528
|
+
break;
|
|
84529
|
+
}
|
|
84530
|
+
}
|
|
84531
|
+
}
|
|
84532
|
+
if (!wrapsWhole) break;
|
|
84533
|
+
e7 = e7.slice(1, -1).trim();
|
|
84534
|
+
}
|
|
84535
|
+
return e7;
|
|
84536
|
+
}
|
|
84537
|
+
function splitTopLevelCommas(list) {
|
|
84538
|
+
const parts = [];
|
|
84539
|
+
let depth = 0;
|
|
84540
|
+
let inString = false;
|
|
84541
|
+
let start = 0;
|
|
84542
|
+
for (let i = 0; i < list.length; i++) {
|
|
84543
|
+
const c5 = list[i];
|
|
84544
|
+
if (inString) {
|
|
84545
|
+
if (c5 === "'") {
|
|
84546
|
+
if (list[i + 1] === "'") i++;
|
|
84547
|
+
else inString = false;
|
|
84548
|
+
}
|
|
84549
|
+
continue;
|
|
84550
|
+
}
|
|
84551
|
+
if (c5 === "'") inString = true;
|
|
84552
|
+
else if (c5 === "(") depth++;
|
|
84553
|
+
else if (c5 === ")") depth--;
|
|
84554
|
+
else if (c5 === "," && depth === 0) {
|
|
84555
|
+
parts.push(list.slice(start, i));
|
|
84556
|
+
start = i + 1;
|
|
84557
|
+
}
|
|
84558
|
+
}
|
|
84559
|
+
parts.push(list.slice(start));
|
|
84560
|
+
return parts;
|
|
84561
|
+
}
|
|
84473
84562
|
var BETWEEN = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+BETWEEN\s+(.+?)\s+AND\s+(.+?)\s*$/i;
|
|
84474
84563
|
function literal(raw) {
|
|
84475
84564
|
const t36 = raw.trim();
|
|
@@ -84479,9 +84568,11 @@ function literal(raw) {
|
|
|
84479
84568
|
return void 0;
|
|
84480
84569
|
}
|
|
84481
84570
|
function parseCheck(expression, name) {
|
|
84482
|
-
const expr = (expression ?? "").trim();
|
|
84571
|
+
const expr = unwrap((expression ?? "").trim());
|
|
84483
84572
|
if (!expr) return { ok: false, reason: "empty expression" };
|
|
84484
84573
|
if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
|
|
84574
|
+
if (/(^|[\s)])OR($|[\s(])/i.test(expr)) return { ok: false, reason: "contains OR" };
|
|
84575
|
+
if (/(^|[\s(])NOT($|[\s(])/i.test(expr)) return { ok: false, reason: "contains NOT" };
|
|
84485
84576
|
const between = expr.match(BETWEEN);
|
|
84486
84577
|
if (between) {
|
|
84487
84578
|
const lo6 = literal(between[2]);
|
|
@@ -84496,14 +84587,67 @@ function parseCheck(expression, name) {
|
|
|
84496
84587
|
]
|
|
84497
84588
|
};
|
|
84498
84589
|
}
|
|
84590
|
+
const parts = splitTopLevelAnd(expr);
|
|
84591
|
+
if (parts.length > 1) {
|
|
84592
|
+
const checks = [];
|
|
84593
|
+
const sets = [];
|
|
84594
|
+
const rows = [];
|
|
84595
|
+
for (const part of parts) {
|
|
84596
|
+
const parsed = parseCheck(part, name);
|
|
84597
|
+
if (!parsed.ok)
|
|
84598
|
+
return { ok: false, reason: `part of an AND was not understood: ${parsed.reason}` };
|
|
84599
|
+
checks.push(...parsed.checks);
|
|
84600
|
+
if (parsed.sets) sets.push(...parsed.sets);
|
|
84601
|
+
if (parsed.rows) rows.push(...parsed.rows);
|
|
84602
|
+
}
|
|
84603
|
+
return {
|
|
84604
|
+
ok: true,
|
|
84605
|
+
checks,
|
|
84606
|
+
...sets.length ? { sets } : {},
|
|
84607
|
+
...rows.length ? { rows } : {}
|
|
84608
|
+
};
|
|
84609
|
+
}
|
|
84610
|
+
const inList = expr.match(IN_LIST);
|
|
84611
|
+
if (inList) {
|
|
84612
|
+
const raw = splitTopLevelCommas(inList[2]);
|
|
84613
|
+
const parsedValues = raw.map((r5) => literal(r5));
|
|
84614
|
+
if (parsedValues.some((v5) => !v5)) return { ok: false, reason: "IN list holds a non-literal" };
|
|
84615
|
+
const kinds = new Set(parsedValues.map((v5) => v5.kind));
|
|
84616
|
+
if (kinds.size > 1) return { ok: false, reason: "IN list mixes types" };
|
|
84617
|
+
if (!parsedValues.length) return { ok: false, reason: "IN list is empty" };
|
|
84618
|
+
return {
|
|
84619
|
+
ok: true,
|
|
84620
|
+
checks: [],
|
|
84621
|
+
sets: [
|
|
84622
|
+
{
|
|
84623
|
+
column: inList[1],
|
|
84624
|
+
values: parsedValues.map((v5) => v5.value),
|
|
84625
|
+
kind: parsedValues[0].kind,
|
|
84626
|
+
name
|
|
84627
|
+
}
|
|
84628
|
+
]
|
|
84629
|
+
};
|
|
84630
|
+
}
|
|
84499
84631
|
const cmp = expr.match(COMPARISON);
|
|
84500
84632
|
if (!cmp) return { ok: false, reason: "not a single comparison this version understands" };
|
|
84501
84633
|
const value = literal(cmp[3]);
|
|
84502
84634
|
if (!value) {
|
|
84635
|
+
const right = cmp[3].trim();
|
|
84636
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(right)) {
|
|
84637
|
+
const op4 = cmp[2] === "!=" ? "<>" : cmp[2];
|
|
84638
|
+
return { ok: true, checks: [], rows: [{ left: cmp[1], right, operator: op4, name }] };
|
|
84639
|
+
}
|
|
84503
84640
|
return { ok: false, reason: "right side is not a literal" };
|
|
84504
84641
|
}
|
|
84505
84642
|
const op3 = cmp[2] === "!=" ? "<>" : cmp[2];
|
|
84506
|
-
return {
|
|
84643
|
+
return {
|
|
84644
|
+
ok: true,
|
|
84645
|
+
checks: [{ column: cmp[1], operator: op3, value: value.value, kind: value.kind, name }]
|
|
84646
|
+
};
|
|
84647
|
+
}
|
|
84648
|
+
function describeSet(set) {
|
|
84649
|
+
const shown = set.values.map((v5) => set.kind === "string" ? `'${v5}'` : v5).join(", ");
|
|
84650
|
+
return `${set.name ? `${set.name}: ` : ""}${set.column} IN (${shown})`;
|
|
84507
84651
|
}
|
|
84508
84652
|
|
|
84509
84653
|
// src/files.ts
|
|
@@ -84731,6 +84875,7 @@ async function formatCode(code, filePath, fmt) {
|
|
|
84731
84875
|
IMPORT_EXTENSIONS,
|
|
84732
84876
|
NAME_MODES,
|
|
84733
84877
|
applyTableCase,
|
|
84878
|
+
describeSet,
|
|
84734
84879
|
formatCode,
|
|
84735
84880
|
importSpecifier,
|
|
84736
84881
|
insertColumns,
|
package/dist/index.d.cts
CHANGED
|
@@ -206,10 +206,36 @@ interface ColumnCheck {
|
|
|
206
206
|
/** Constraint name, used to say which one failed. */
|
|
207
207
|
name?: string;
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* A column constrained to a set of literals, from `col IN ('a', 'b')`.
|
|
211
|
+
*
|
|
212
|
+
* Kept separate from `ColumnCheck` rather than folded into it as another operator, so the
|
|
213
|
+
* existing shape is unchanged for anything already consuming it.
|
|
214
|
+
*/
|
|
215
|
+
interface ColumnSet {
|
|
216
|
+
column: string;
|
|
217
|
+
values: string[];
|
|
218
|
+
kind: 'number' | 'string';
|
|
219
|
+
name?: string;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* A comparison between two columns of the same row, from `CHECK (start_date < end_date)`.
|
|
223
|
+
*
|
|
224
|
+
* It cannot live on a field, because it is a statement about the row: neither column alone can
|
|
225
|
+
* say whether it holds. It can live on the *object* schema, which is where this ends up.
|
|
226
|
+
*/
|
|
227
|
+
interface RowCheck {
|
|
228
|
+
left: string;
|
|
229
|
+
right: string;
|
|
230
|
+
operator: ColumnCheck['operator'];
|
|
231
|
+
name?: string;
|
|
232
|
+
}
|
|
209
233
|
/** A check that was understood, or the reason it was not. */
|
|
210
234
|
type ParsedCheck = {
|
|
211
235
|
ok: true;
|
|
212
236
|
checks: ColumnCheck[];
|
|
237
|
+
sets?: ColumnSet[];
|
|
238
|
+
rows?: RowCheck[];
|
|
213
239
|
} | {
|
|
214
240
|
ok: false;
|
|
215
241
|
reason: string;
|
|
@@ -222,6 +248,13 @@ type ParsedCheck = {
|
|
|
222
248
|
* silently change what is enforced.
|
|
223
249
|
*/
|
|
224
250
|
declare function parseCheck(expression: string | undefined, name?: string): ParsedCheck;
|
|
251
|
+
/**
|
|
252
|
+
* A human-readable rendering of a set constraint, for an error message.
|
|
253
|
+
*
|
|
254
|
+
* Values are re-quoted the way SQL wrote them, so the message reads like the constraint in the
|
|
255
|
+
* schema rather than like its JavaScript translation.
|
|
256
|
+
*/
|
|
257
|
+
declare function describeSet(set: ColumnSet): string;
|
|
225
258
|
|
|
226
259
|
interface Table {
|
|
227
260
|
name: string;
|
|
@@ -363,4 +396,4 @@ declare function updateColumns(table: Table): Column[];
|
|
|
363
396
|
declare function selectColumns(table: Table): Column[];
|
|
364
397
|
declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
|
|
365
398
|
|
|
366
|
-
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, COLUMN_FORMATS, type ColumnCheck, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, NAME_MODES, type NameMode, type ParsedCheck, type ResolvedAffix, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, parseCheck, pascalCase, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
|
399
|
+
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, COLUMN_FORMATS, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, 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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -206,10 +206,36 @@ interface ColumnCheck {
|
|
|
206
206
|
/** Constraint name, used to say which one failed. */
|
|
207
207
|
name?: string;
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* A column constrained to a set of literals, from `col IN ('a', 'b')`.
|
|
211
|
+
*
|
|
212
|
+
* Kept separate from `ColumnCheck` rather than folded into it as another operator, so the
|
|
213
|
+
* existing shape is unchanged for anything already consuming it.
|
|
214
|
+
*/
|
|
215
|
+
interface ColumnSet {
|
|
216
|
+
column: string;
|
|
217
|
+
values: string[];
|
|
218
|
+
kind: 'number' | 'string';
|
|
219
|
+
name?: string;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* A comparison between two columns of the same row, from `CHECK (start_date < end_date)`.
|
|
223
|
+
*
|
|
224
|
+
* It cannot live on a field, because it is a statement about the row: neither column alone can
|
|
225
|
+
* say whether it holds. It can live on the *object* schema, which is where this ends up.
|
|
226
|
+
*/
|
|
227
|
+
interface RowCheck {
|
|
228
|
+
left: string;
|
|
229
|
+
right: string;
|
|
230
|
+
operator: ColumnCheck['operator'];
|
|
231
|
+
name?: string;
|
|
232
|
+
}
|
|
209
233
|
/** A check that was understood, or the reason it was not. */
|
|
210
234
|
type ParsedCheck = {
|
|
211
235
|
ok: true;
|
|
212
236
|
checks: ColumnCheck[];
|
|
237
|
+
sets?: ColumnSet[];
|
|
238
|
+
rows?: RowCheck[];
|
|
213
239
|
} | {
|
|
214
240
|
ok: false;
|
|
215
241
|
reason: string;
|
|
@@ -222,6 +248,13 @@ type ParsedCheck = {
|
|
|
222
248
|
* silently change what is enforced.
|
|
223
249
|
*/
|
|
224
250
|
declare function parseCheck(expression: string | undefined, name?: string): ParsedCheck;
|
|
251
|
+
/**
|
|
252
|
+
* A human-readable rendering of a set constraint, for an error message.
|
|
253
|
+
*
|
|
254
|
+
* Values are re-quoted the way SQL wrote them, so the message reads like the constraint in the
|
|
255
|
+
* schema rather than like its JavaScript translation.
|
|
256
|
+
*/
|
|
257
|
+
declare function describeSet(set: ColumnSet): string;
|
|
225
258
|
|
|
226
259
|
interface Table {
|
|
227
260
|
name: string;
|
|
@@ -363,4 +396,4 @@ declare function updateColumns(table: Table): Column[];
|
|
|
363
396
|
declare function selectColumns(table: Table): Column[];
|
|
364
397
|
declare function formatCode(code: string, filePath: string, fmt?: FormatOptions): Promise<any>;
|
|
365
398
|
|
|
366
|
-
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, COLUMN_FORMATS, type ColumnCheck, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, NAME_MODES, type NameMode, type ParsedCheck, type ResolvedAffix, type Table, type TableCase, type ValidationGenerateOptions, type ValidationLibrary, type ValidationRenderer, applyTableCase, formatCode, importSpecifier, insertColumns, isGeneratedColumn, isIntegerColumn, moduleFileName, moduleSpecifier, parseCheck, pascalCase, resolveAffix, resolveConfiguredImport, schemaName, selectColumns, typeName, updateColumns, validateAffix };
|
|
399
|
+
export { AFFIX_PROBE_TABLE, type AffixIssue, type AffixOptions, type AffixValue, COLUMN_FORMATS, type ColumnCheck, type ColumnSet, DEFAULT_IMPORT_EXTENSION, DEFAULT_MODE_PREFIX, DEFAULT_SCHEMA_SUFFIX, DEFAULT_TYPE_SUFFIX, type FormatOptions, IMPORT_EXTENSIONS, type ImportExtension, 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 };
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,94 @@ import "./chunk-ZZGILH5A.js";
|
|
|
2
2
|
|
|
3
3
|
// src/checks.ts
|
|
4
4
|
var COMPARISON = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|>|<|=)\s*(.+?)\s*$/;
|
|
5
|
+
var IN_LIST = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.+)\)\s*$/i;
|
|
6
|
+
function splitTopLevelAnd(expr) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
let depth = 0;
|
|
9
|
+
let inString = false;
|
|
10
|
+
let start = 0;
|
|
11
|
+
for (let i = 0; i < expr.length; i++) {
|
|
12
|
+
const c = expr[i];
|
|
13
|
+
if (inString) {
|
|
14
|
+
if (c === "'") {
|
|
15
|
+
if (expr[i + 1] === "'") i++;
|
|
16
|
+
else inString = false;
|
|
17
|
+
}
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (c === "'") {
|
|
21
|
+
inString = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (c === "(") depth++;
|
|
25
|
+
else if (c === ")") depth--;
|
|
26
|
+
else if (depth === 0 && /\s/.test(c)) {
|
|
27
|
+
const m = /^\s+AND\s+/i.exec(expr.slice(i));
|
|
28
|
+
if (m) {
|
|
29
|
+
parts.push(expr.slice(start, i));
|
|
30
|
+
i += m[0].length - 1;
|
|
31
|
+
start = i + 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
parts.push(expr.slice(start));
|
|
36
|
+
return parts;
|
|
37
|
+
}
|
|
38
|
+
function unwrap(expr) {
|
|
39
|
+
let e = expr.trim();
|
|
40
|
+
while (e.startsWith("(") && e.endsWith(")")) {
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let inString = false;
|
|
43
|
+
let wrapsWhole = true;
|
|
44
|
+
for (let i = 0; i < e.length; i++) {
|
|
45
|
+
const c = e[i];
|
|
46
|
+
if (inString) {
|
|
47
|
+
if (c === "'") {
|
|
48
|
+
if (e[i + 1] === "'") i++;
|
|
49
|
+
else inString = false;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (c === "'") inString = true;
|
|
54
|
+
else if (c === "(") depth++;
|
|
55
|
+
else if (c === ")") {
|
|
56
|
+
depth--;
|
|
57
|
+
if (depth === 0 && i < e.length - 1) {
|
|
58
|
+
wrapsWhole = false;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!wrapsWhole) break;
|
|
64
|
+
e = e.slice(1, -1).trim();
|
|
65
|
+
}
|
|
66
|
+
return e;
|
|
67
|
+
}
|
|
68
|
+
function splitTopLevelCommas(list) {
|
|
69
|
+
const parts = [];
|
|
70
|
+
let depth = 0;
|
|
71
|
+
let inString = false;
|
|
72
|
+
let start = 0;
|
|
73
|
+
for (let i = 0; i < list.length; i++) {
|
|
74
|
+
const c = list[i];
|
|
75
|
+
if (inString) {
|
|
76
|
+
if (c === "'") {
|
|
77
|
+
if (list[i + 1] === "'") i++;
|
|
78
|
+
else inString = false;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (c === "'") inString = true;
|
|
83
|
+
else if (c === "(") depth++;
|
|
84
|
+
else if (c === ")") depth--;
|
|
85
|
+
else if (c === "," && depth === 0) {
|
|
86
|
+
parts.push(list.slice(start, i));
|
|
87
|
+
start = i + 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
parts.push(list.slice(start));
|
|
91
|
+
return parts;
|
|
92
|
+
}
|
|
5
93
|
var BETWEEN = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s+BETWEEN\s+(.+?)\s+AND\s+(.+?)\s*$/i;
|
|
6
94
|
function literal(raw) {
|
|
7
95
|
const t = raw.trim();
|
|
@@ -11,9 +99,11 @@ function literal(raw) {
|
|
|
11
99
|
return void 0;
|
|
12
100
|
}
|
|
13
101
|
function parseCheck(expression, name) {
|
|
14
|
-
const expr = (expression ?? "").trim();
|
|
102
|
+
const expr = unwrap((expression ?? "").trim());
|
|
15
103
|
if (!expr) return { ok: false, reason: "empty expression" };
|
|
16
104
|
if (expr.includes("?")) return { ok: false, reason: "expression contains an unresolved value" };
|
|
105
|
+
if (/(^|[\s)])OR($|[\s(])/i.test(expr)) return { ok: false, reason: "contains OR" };
|
|
106
|
+
if (/(^|[\s(])NOT($|[\s(])/i.test(expr)) return { ok: false, reason: "contains NOT" };
|
|
17
107
|
const between = expr.match(BETWEEN);
|
|
18
108
|
if (between) {
|
|
19
109
|
const lo = literal(between[2]);
|
|
@@ -28,14 +118,67 @@ function parseCheck(expression, name) {
|
|
|
28
118
|
]
|
|
29
119
|
};
|
|
30
120
|
}
|
|
121
|
+
const parts = splitTopLevelAnd(expr);
|
|
122
|
+
if (parts.length > 1) {
|
|
123
|
+
const checks = [];
|
|
124
|
+
const sets = [];
|
|
125
|
+
const rows = [];
|
|
126
|
+
for (const part of parts) {
|
|
127
|
+
const parsed = parseCheck(part, name);
|
|
128
|
+
if (!parsed.ok)
|
|
129
|
+
return { ok: false, reason: `part of an AND was not understood: ${parsed.reason}` };
|
|
130
|
+
checks.push(...parsed.checks);
|
|
131
|
+
if (parsed.sets) sets.push(...parsed.sets);
|
|
132
|
+
if (parsed.rows) rows.push(...parsed.rows);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
ok: true,
|
|
136
|
+
checks,
|
|
137
|
+
...sets.length ? { sets } : {},
|
|
138
|
+
...rows.length ? { rows } : {}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const inList = expr.match(IN_LIST);
|
|
142
|
+
if (inList) {
|
|
143
|
+
const raw = splitTopLevelCommas(inList[2]);
|
|
144
|
+
const parsedValues = raw.map((r) => literal(r));
|
|
145
|
+
if (parsedValues.some((v) => !v)) return { ok: false, reason: "IN list holds a non-literal" };
|
|
146
|
+
const kinds = new Set(parsedValues.map((v) => v.kind));
|
|
147
|
+
if (kinds.size > 1) return { ok: false, reason: "IN list mixes types" };
|
|
148
|
+
if (!parsedValues.length) return { ok: false, reason: "IN list is empty" };
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
checks: [],
|
|
152
|
+
sets: [
|
|
153
|
+
{
|
|
154
|
+
column: inList[1],
|
|
155
|
+
values: parsedValues.map((v) => v.value),
|
|
156
|
+
kind: parsedValues[0].kind,
|
|
157
|
+
name
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
};
|
|
161
|
+
}
|
|
31
162
|
const cmp = expr.match(COMPARISON);
|
|
32
163
|
if (!cmp) return { ok: false, reason: "not a single comparison this version understands" };
|
|
33
164
|
const value = literal(cmp[3]);
|
|
34
165
|
if (!value) {
|
|
166
|
+
const right = cmp[3].trim();
|
|
167
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(right)) {
|
|
168
|
+
const op2 = cmp[2] === "!=" ? "<>" : cmp[2];
|
|
169
|
+
return { ok: true, checks: [], rows: [{ left: cmp[1], right, operator: op2, name }] };
|
|
170
|
+
}
|
|
35
171
|
return { ok: false, reason: "right side is not a literal" };
|
|
36
172
|
}
|
|
37
173
|
const op = cmp[2] === "!=" ? "<>" : cmp[2];
|
|
38
|
-
return {
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
checks: [{ column: cmp[1], operator: op, value: value.value, kind: value.kind, name }]
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function describeSet(set) {
|
|
180
|
+
const shown = set.values.map((v) => set.kind === "string" ? `'${v}'` : v).join(", ");
|
|
181
|
+
return `${set.name ? `${set.name}: ` : ""}${set.column} IN (${shown})`;
|
|
39
182
|
}
|
|
40
183
|
|
|
41
184
|
// src/files.ts
|
|
@@ -262,6 +405,7 @@ export {
|
|
|
262
405
|
IMPORT_EXTENSIONS,
|
|
263
406
|
NAME_MODES,
|
|
264
407
|
applyTableCase,
|
|
408
|
+
describeSet,
|
|
265
409
|
formatCode,
|
|
266
410
|
importSpecifier,
|
|
267
411
|
insertColumns,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drzl/validation-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.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.10.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"tsup": "^8.5.1",
|