@jskit-ai/crud-server-generator 0.1.55 → 0.1.57
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/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/crud-server-generator",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.57",
|
|
5
5
|
kind: "generator",
|
|
6
6
|
description: "CRUD server generator with routes, actions, and persistence scaffolding.",
|
|
7
7
|
options: {
|
|
@@ -151,13 +151,13 @@ export default Object.freeze({
|
|
|
151
151
|
mutations: {
|
|
152
152
|
dependencies: {
|
|
153
153
|
runtime: {
|
|
154
|
-
"@jskit-ai/auth-core": "0.1.
|
|
155
|
-
"@jskit-ai/crud-core": "0.1.
|
|
156
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
157
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
158
|
-
"@jskit-ai/kernel": "0.1.
|
|
159
|
-
"@jskit-ai/realtime": "0.1.
|
|
160
|
-
"@jskit-ai/users-core": "0.1.
|
|
154
|
+
"@jskit-ai/auth-core": "0.1.48",
|
|
155
|
+
"@jskit-ai/crud-core": "0.1.57",
|
|
156
|
+
"@jskit-ai/database-runtime": "0.1.49",
|
|
157
|
+
"@jskit-ai/http-runtime": "0.1.48",
|
|
158
|
+
"@jskit-ai/kernel": "0.1.49",
|
|
159
|
+
"@jskit-ai/realtime": "0.1.48",
|
|
160
|
+
"@jskit-ai/users-core": "0.1.59",
|
|
161
161
|
"@local/${option:namespace|kebab}": "file:packages/${option:namespace|kebab}",
|
|
162
162
|
"typebox": "^1.0.81"
|
|
163
163
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/crud-server-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@babel/parser": "^7.29.2",
|
|
16
|
-
"@jskit-ai/crud-core": "0.1.
|
|
17
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
18
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
19
|
-
"@jskit-ai/kernel": "0.1.
|
|
20
|
-
"@jskit-ai/users-core": "0.1.
|
|
16
|
+
"@jskit-ai/crud-core": "0.1.57",
|
|
17
|
+
"@jskit-ai/database-runtime": "0.1.49",
|
|
18
|
+
"@jskit-ai/http-runtime": "0.1.48",
|
|
19
|
+
"@jskit-ai/kernel": "0.1.49",
|
|
20
|
+
"@jskit-ai/users-core": "0.1.59",
|
|
21
21
|
"recast": "^0.23.11",
|
|
22
22
|
"typebox": "^1.0.81"
|
|
23
23
|
}
|
|
@@ -391,9 +391,194 @@ function resolveColumnKey(column, idColumn) {
|
|
|
391
391
|
return String(column.key || "");
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
const NUMERIC_CHECK_CONSTRAINT_PATTERN = /(?:`([^`]+)`|([A-Za-z_][A-Za-z0-9_]*))\s*(>=|>|<=|<)\s*(-?\d+(?:\.\d+)?)/g;
|
|
395
|
+
|
|
396
|
+
function normalizeNumericBoundValue(value, scale = null) {
|
|
397
|
+
const parsed = Number(value);
|
|
398
|
+
if (!Number.isFinite(parsed)) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
if (!Number.isInteger(scale) || scale < 0) {
|
|
402
|
+
return parsed;
|
|
403
|
+
}
|
|
404
|
+
return Number(parsed.toFixed(scale));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function resolveNumericExclusiveStep(column) {
|
|
408
|
+
if (column?.typeKind === "integer") {
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
if (column?.typeKind === "number" && Number.isInteger(column?.numericScale) && column.numericScale > 0) {
|
|
412
|
+
return 1 / (10 ** column.numericScale);
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function applyLowerBound(current = null, candidate = null) {
|
|
418
|
+
if (!candidate) {
|
|
419
|
+
return current;
|
|
420
|
+
}
|
|
421
|
+
if (!current) {
|
|
422
|
+
return candidate;
|
|
423
|
+
}
|
|
424
|
+
if (candidate.value > current.value) {
|
|
425
|
+
return candidate;
|
|
426
|
+
}
|
|
427
|
+
if (candidate.value < current.value) {
|
|
428
|
+
return current;
|
|
429
|
+
}
|
|
430
|
+
if (candidate.exclusive === true && current.exclusive !== true) {
|
|
431
|
+
return candidate;
|
|
432
|
+
}
|
|
433
|
+
return current;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function applyUpperBound(current = null, candidate = null) {
|
|
437
|
+
if (!candidate) {
|
|
438
|
+
return current;
|
|
439
|
+
}
|
|
440
|
+
if (!current) {
|
|
441
|
+
return candidate;
|
|
442
|
+
}
|
|
443
|
+
if (candidate.value < current.value) {
|
|
444
|
+
return candidate;
|
|
445
|
+
}
|
|
446
|
+
if (candidate.value > current.value) {
|
|
447
|
+
return current;
|
|
448
|
+
}
|
|
449
|
+
if (candidate.exclusive === true && current.exclusive !== true) {
|
|
450
|
+
return candidate;
|
|
451
|
+
}
|
|
452
|
+
return current;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function resolveColumnNumericBounds(snapshot = {}) {
|
|
456
|
+
const byColumnName = new Map();
|
|
457
|
+
const columns = Array.isArray(snapshot.columns) ? snapshot.columns : [];
|
|
458
|
+
const checkConstraints = Array.isArray(snapshot.checkConstraints) ? snapshot.checkConstraints : [];
|
|
459
|
+
const numericColumnsByName = new Map(
|
|
460
|
+
columns
|
|
461
|
+
.filter((column) => column?.typeKind === "integer" || column?.typeKind === "number")
|
|
462
|
+
.map((column) => [String(column.name || ""), column])
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
function getColumnBounds(columnName) {
|
|
466
|
+
if (!byColumnName.has(columnName)) {
|
|
467
|
+
byColumnName.set(columnName, {
|
|
468
|
+
minimum: null,
|
|
469
|
+
exclusiveMinimum: null,
|
|
470
|
+
maximum: null,
|
|
471
|
+
exclusiveMaximum: null
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return byColumnName.get(columnName);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
for (const column of numericColumnsByName.values()) {
|
|
478
|
+
if (column.unsigned === true) {
|
|
479
|
+
const target = getColumnBounds(column.name);
|
|
480
|
+
target.minimum = 0;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
for (const constraint of checkConstraints) {
|
|
485
|
+
const clause = String(constraint?.clause || "");
|
|
486
|
+
if (!clause) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let match = null;
|
|
491
|
+
while ((match = NUMERIC_CHECK_CONSTRAINT_PATTERN.exec(clause)) != null) {
|
|
492
|
+
const columnName = String(match[1] || match[2] || "");
|
|
493
|
+
const operator = String(match[3] || "");
|
|
494
|
+
const rawValue = Number(match[4]);
|
|
495
|
+
const column = numericColumnsByName.get(columnName) || null;
|
|
496
|
+
if (!column || !Number.isFinite(rawValue)) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const target = getColumnBounds(columnName);
|
|
501
|
+
if (operator === ">=" || operator === ">") {
|
|
502
|
+
let candidate = null;
|
|
503
|
+
if (operator === ">=") {
|
|
504
|
+
candidate = {
|
|
505
|
+
value: normalizeNumericBoundValue(rawValue, column.numericScale),
|
|
506
|
+
exclusive: false
|
|
507
|
+
};
|
|
508
|
+
} else {
|
|
509
|
+
const exclusiveStep = resolveNumericExclusiveStep(column);
|
|
510
|
+
if (exclusiveStep != null) {
|
|
511
|
+
candidate = {
|
|
512
|
+
value: normalizeNumericBoundValue(rawValue + exclusiveStep, column.numericScale),
|
|
513
|
+
exclusive: false
|
|
514
|
+
};
|
|
515
|
+
} else {
|
|
516
|
+
candidate = {
|
|
517
|
+
value: normalizeNumericBoundValue(rawValue, column.numericScale),
|
|
518
|
+
exclusive: true
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const nextBound = applyLowerBound(
|
|
524
|
+
target.minimum != null || target.exclusiveMinimum != null
|
|
525
|
+
? {
|
|
526
|
+
value: target.minimum ?? target.exclusiveMinimum,
|
|
527
|
+
exclusive: target.exclusiveMinimum != null
|
|
528
|
+
}
|
|
529
|
+
: null,
|
|
530
|
+
candidate
|
|
531
|
+
);
|
|
532
|
+
target.minimum = nextBound?.exclusive === true ? null : nextBound?.value ?? null;
|
|
533
|
+
target.exclusiveMinimum = nextBound?.exclusive === true ? nextBound?.value ?? null : null;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (operator === "<=" || operator === "<") {
|
|
538
|
+
let candidate = null;
|
|
539
|
+
if (operator === "<=") {
|
|
540
|
+
candidate = {
|
|
541
|
+
value: normalizeNumericBoundValue(rawValue, column.numericScale),
|
|
542
|
+
exclusive: false
|
|
543
|
+
};
|
|
544
|
+
} else {
|
|
545
|
+
const exclusiveStep = resolveNumericExclusiveStep(column);
|
|
546
|
+
if (exclusiveStep != null) {
|
|
547
|
+
candidate = {
|
|
548
|
+
value: normalizeNumericBoundValue(rawValue - exclusiveStep, column.numericScale),
|
|
549
|
+
exclusive: false
|
|
550
|
+
};
|
|
551
|
+
} else {
|
|
552
|
+
candidate = {
|
|
553
|
+
value: normalizeNumericBoundValue(rawValue, column.numericScale),
|
|
554
|
+
exclusive: true
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const nextBound = applyUpperBound(
|
|
560
|
+
target.maximum != null || target.exclusiveMaximum != null
|
|
561
|
+
? {
|
|
562
|
+
value: target.maximum ?? target.exclusiveMaximum,
|
|
563
|
+
exclusive: target.exclusiveMaximum != null
|
|
564
|
+
}
|
|
565
|
+
: null,
|
|
566
|
+
candidate
|
|
567
|
+
);
|
|
568
|
+
target.maximum = nextBound?.exclusive === true ? null : nextBound?.value ?? null;
|
|
569
|
+
target.exclusiveMaximum = nextBound?.exclusive === true ? nextBound?.value ?? null : null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
NUMERIC_CHECK_CONSTRAINT_PATTERN.lastIndex = 0;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return byColumnName;
|
|
576
|
+
}
|
|
577
|
+
|
|
394
578
|
function resolveScaffoldColumns(snapshot) {
|
|
395
579
|
const idColumn = String(snapshot.idColumn || DEFAULT_ID_COLUMN);
|
|
396
580
|
const sourceColumns = Array.isArray(snapshot.columns) ? snapshot.columns : [];
|
|
581
|
+
const numericBoundsByColumnName = resolveColumnNumericBounds(snapshot);
|
|
397
582
|
const foreignKeyColumnNames = new Set(
|
|
398
583
|
(Array.isArray(snapshot.foreignKeys) ? snapshot.foreignKeys : [])
|
|
399
584
|
.flatMap((foreignKey) => Array.isArray(foreignKey?.columns) ? foreignKey.columns : [])
|
|
@@ -427,6 +612,7 @@ function resolveScaffoldColumns(snapshot) {
|
|
|
427
612
|
|
|
428
613
|
return Object.freeze({
|
|
429
614
|
...column,
|
|
615
|
+
...(numericBoundsByColumnName.get(column.name) || {}),
|
|
430
616
|
key,
|
|
431
617
|
isOwnerColumn,
|
|
432
618
|
isIdColumn,
|
|
@@ -462,9 +648,19 @@ function renderPropertyAccess(sourceName, key) {
|
|
|
462
648
|
|
|
463
649
|
function renderIntegerSchema(column) {
|
|
464
650
|
const options = [];
|
|
465
|
-
if (column
|
|
651
|
+
if (Number.isFinite(column?.minimum)) {
|
|
652
|
+
options.push(`minimum: ${column.minimum}`);
|
|
653
|
+
} else if (Number.isFinite(column?.exclusiveMinimum)) {
|
|
654
|
+
options.push(`exclusiveMinimum: ${column.exclusiveMinimum}`);
|
|
655
|
+
} else if (column.unsigned === true) {
|
|
466
656
|
options.push("minimum: 0");
|
|
467
657
|
}
|
|
658
|
+
if (Number.isFinite(column?.maximum)) {
|
|
659
|
+
options.push(`maximum: ${column.maximum}`);
|
|
660
|
+
}
|
|
661
|
+
if (Number.isFinite(column?.exclusiveMaximum)) {
|
|
662
|
+
options.push(`exclusiveMaximum: ${column.exclusiveMaximum}`);
|
|
663
|
+
}
|
|
468
664
|
if (options.length > 0) {
|
|
469
665
|
return `Type.Integer({ ${options.join(", ")} })`;
|
|
470
666
|
}
|
|
@@ -499,7 +695,22 @@ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
|
|
|
499
695
|
}
|
|
500
696
|
schemaExpression = renderIntegerSchema(column);
|
|
501
697
|
} else if (typeKind === "number") {
|
|
502
|
-
|
|
698
|
+
const options = [];
|
|
699
|
+
if (Number.isFinite(column?.minimum)) {
|
|
700
|
+
options.push(`minimum: ${column.minimum}`);
|
|
701
|
+
}
|
|
702
|
+
if (Number.isFinite(column?.exclusiveMinimum)) {
|
|
703
|
+
options.push(`exclusiveMinimum: ${column.exclusiveMinimum}`);
|
|
704
|
+
}
|
|
705
|
+
if (Number.isFinite(column?.maximum)) {
|
|
706
|
+
options.push(`maximum: ${column.maximum}`);
|
|
707
|
+
}
|
|
708
|
+
if (Number.isFinite(column?.exclusiveMaximum)) {
|
|
709
|
+
options.push(`exclusiveMaximum: ${column.exclusiveMaximum}`);
|
|
710
|
+
}
|
|
711
|
+
schemaExpression = options.length > 0
|
|
712
|
+
? `Type.Number({ ${options.join(", ")} })`
|
|
713
|
+
: "Type.Number()";
|
|
503
714
|
} else if (typeKind === "boolean") {
|
|
504
715
|
schemaExpression = "Type.Boolean()";
|
|
505
716
|
} else if (typeKind === "datetime") {
|
|
@@ -520,13 +731,15 @@ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
|
|
|
520
731
|
return schemaExpression;
|
|
521
732
|
}
|
|
522
733
|
|
|
523
|
-
function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false,
|
|
734
|
+
function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false, recordIdValidatorImports = [] } = {}) {
|
|
524
735
|
const imports = [
|
|
525
736
|
"normalizeObjectInput",
|
|
526
737
|
"createCursorListValidator"
|
|
527
738
|
];
|
|
528
|
-
|
|
529
|
-
imports.
|
|
739
|
+
for (const importName of Array.isArray(recordIdValidatorImports) ? recordIdValidatorImports : []) {
|
|
740
|
+
if (!imports.includes(importName)) {
|
|
741
|
+
imports.push(importName);
|
|
742
|
+
}
|
|
530
743
|
}
|
|
531
744
|
if (needsHtmlTimeSchemas) {
|
|
532
745
|
imports.push("HTML_TIME_STRING_SCHEMA", "NULLABLE_HTML_TIME_STRING_SCHEMA");
|
|
@@ -534,6 +747,19 @@ function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false, needsRec
|
|
|
534
747
|
return `import {\n ${imports.join(",\n ")}\n} from "@jskit-ai/kernel/shared/validators";`;
|
|
535
748
|
}
|
|
536
749
|
|
|
750
|
+
function resolveRecordIdValidatorImports(...sources) {
|
|
751
|
+
const imports = ["recordIdSchema"];
|
|
752
|
+
const joinedSource = sources
|
|
753
|
+
.map((source) => String(source || ""))
|
|
754
|
+
.join("\n");
|
|
755
|
+
for (const importName of ["recordIdInputSchema", "nullableRecordIdSchema", "nullableRecordIdInputSchema"]) {
|
|
756
|
+
if (joinedSource.includes(importName)) {
|
|
757
|
+
imports.push(importName);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return imports;
|
|
761
|
+
}
|
|
762
|
+
|
|
537
763
|
function renderInputNormalizer(column) {
|
|
538
764
|
const typeKind = String(column.typeKind || "");
|
|
539
765
|
const nullable = column?.nullable === true;
|
|
@@ -1621,6 +1847,7 @@ function buildReplacementsFromSnapshot({
|
|
|
1621
1847
|
requiresNamedPermissions
|
|
1622
1848
|
}),
|
|
1623
1849
|
__JSKIT_CRUD_ROUTE_SURFACE_REQUIRES_WORKSPACE__: String(surfaceRequiresWorkspace === true),
|
|
1850
|
+
__JSKIT_CRUD_ROUTE_BASE__: JSON.stringify(surfaceRequiresWorkspace === true ? "/w/:workspaceSlug" : "/"),
|
|
1624
1851
|
__JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__: renderRouteWorkspaceSupportImports({
|
|
1625
1852
|
surfaceRequiresWorkspace
|
|
1626
1853
|
}),
|
|
@@ -1656,7 +1883,14 @@ function buildReplacementsFromSnapshot({
|
|
|
1656
1883
|
}),
|
|
1657
1884
|
__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__: renderResourceValidatorsImport({
|
|
1658
1885
|
needsHtmlTimeSchemas,
|
|
1659
|
-
|
|
1886
|
+
recordIdValidatorImports: resolveRecordIdValidatorImports(
|
|
1887
|
+
renderResourceSchemaPropertyLines(outputColumns, {
|
|
1888
|
+
forOutput: true
|
|
1889
|
+
}),
|
|
1890
|
+
renderResourceSchemaPropertyLines(writableColumns, {
|
|
1891
|
+
forOutput: false
|
|
1892
|
+
})
|
|
1893
|
+
)
|
|
1660
1894
|
}),
|
|
1661
1895
|
__JSKIT_CRUD_RESOURCE_DATABASE_RUNTIME_IMPORT__: renderResourceDatabaseRuntimeImport({
|
|
1662
1896
|
needsToIsoString: needsDateTimeOutput || needsDate,
|
|
@@ -30,7 +30,7 @@ function registerRoutes(
|
|
|
30
30
|
const router = app.make("jskit.http.router");
|
|
31
31
|
const normalizedRouteSurface = normalizeSurfaceId(routeSurface);
|
|
32
32
|
const routeBase = resolveScopedApiBasePath({
|
|
33
|
-
routeBase:
|
|
33
|
+
routeBase: __JSKIT_CRUD_ROUTE_BASE__,
|
|
34
34
|
relativePath: routeRelativePath,
|
|
35
35
|
strictParams: false
|
|
36
36
|
});
|
|
@@ -738,6 +738,118 @@ test("buildReplacementsFromSnapshot preserves custom collations, hash unique ind
|
|
|
738
738
|
);
|
|
739
739
|
});
|
|
740
740
|
|
|
741
|
+
test("resolveScaffoldColumns derives resource numeric bounds from check constraints", () => {
|
|
742
|
+
const snapshot = createSnapshot({
|
|
743
|
+
tableName: "batch_receivals",
|
|
744
|
+
hasWorkspaceIdColumn: false,
|
|
745
|
+
hasUserIdColumn: false
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const inputWeightColumn = Object.freeze({
|
|
749
|
+
name: "input_weight",
|
|
750
|
+
key: "inputWeight",
|
|
751
|
+
dataType: "decimal",
|
|
752
|
+
columnType: "decimal(10,3)",
|
|
753
|
+
typeKind: "number",
|
|
754
|
+
nullable: false,
|
|
755
|
+
hasDefault: false,
|
|
756
|
+
defaultValue: null,
|
|
757
|
+
autoIncrement: false,
|
|
758
|
+
unsigned: false,
|
|
759
|
+
extra: "",
|
|
760
|
+
maxLength: null,
|
|
761
|
+
numericPrecision: 10,
|
|
762
|
+
numericScale: 3,
|
|
763
|
+
datetimePrecision: null,
|
|
764
|
+
characterSetName: "",
|
|
765
|
+
collationName: "",
|
|
766
|
+
enumValues: Object.freeze([])
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const batchedDailySequenceColumn = Object.freeze({
|
|
770
|
+
name: "batched_daily_sequence",
|
|
771
|
+
key: "batchedDailySequence",
|
|
772
|
+
dataType: "int",
|
|
773
|
+
columnType: "int unsigned",
|
|
774
|
+
typeKind: "integer",
|
|
775
|
+
nullable: false,
|
|
776
|
+
hasDefault: false,
|
|
777
|
+
defaultValue: null,
|
|
778
|
+
autoIncrement: false,
|
|
779
|
+
unsigned: true,
|
|
780
|
+
extra: "",
|
|
781
|
+
maxLength: null,
|
|
782
|
+
numericPrecision: 10,
|
|
783
|
+
numericScale: 0,
|
|
784
|
+
datetimePrecision: null,
|
|
785
|
+
characterSetName: "",
|
|
786
|
+
collationName: "",
|
|
787
|
+
enumValues: Object.freeze([])
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const moistureLevelColumn = Object.freeze({
|
|
791
|
+
name: "moisture_level",
|
|
792
|
+
key: "moistureLevel",
|
|
793
|
+
dataType: "decimal",
|
|
794
|
+
columnType: "decimal(5,2)",
|
|
795
|
+
typeKind: "number",
|
|
796
|
+
nullable: true,
|
|
797
|
+
hasDefault: false,
|
|
798
|
+
defaultValue: null,
|
|
799
|
+
autoIncrement: false,
|
|
800
|
+
unsigned: false,
|
|
801
|
+
extra: "",
|
|
802
|
+
maxLength: null,
|
|
803
|
+
numericPrecision: 5,
|
|
804
|
+
numericScale: 2,
|
|
805
|
+
datetimePrecision: null,
|
|
806
|
+
characterSetName: "",
|
|
807
|
+
collationName: "",
|
|
808
|
+
enumValues: Object.freeze([])
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const scaffoldColumns = __testables.resolveScaffoldColumns({
|
|
812
|
+
...snapshot,
|
|
813
|
+
columns: Object.freeze([
|
|
814
|
+
snapshot.columns[0],
|
|
815
|
+
inputWeightColumn,
|
|
816
|
+
batchedDailySequenceColumn,
|
|
817
|
+
moistureLevelColumn
|
|
818
|
+
]),
|
|
819
|
+
checkConstraints: Object.freeze([
|
|
820
|
+
Object.freeze({
|
|
821
|
+
name: "chk_batch_receivals_input_weight",
|
|
822
|
+
clause: "`input_weight` > 0"
|
|
823
|
+
}),
|
|
824
|
+
Object.freeze({
|
|
825
|
+
name: "chk_batches_batched_daily_sequence",
|
|
826
|
+
clause: "`batched_daily_sequence` >= 1"
|
|
827
|
+
}),
|
|
828
|
+
Object.freeze({
|
|
829
|
+
name: "chk_batches_moisture_level",
|
|
830
|
+
clause: "`moisture_level` is null or `moisture_level` >= 0 and `moisture_level` <= 100"
|
|
831
|
+
})
|
|
832
|
+
])
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
const inputWeight = scaffoldColumns.find((column) => column.name === "input_weight");
|
|
836
|
+
const batchedDailySequence = scaffoldColumns.find((column) => column.name === "batched_daily_sequence");
|
|
837
|
+
const moistureLevel = scaffoldColumns.find((column) => column.name === "moisture_level");
|
|
838
|
+
|
|
839
|
+
assert.equal(
|
|
840
|
+
__testables.renderResourceFieldSchema(inputWeight),
|
|
841
|
+
"Type.Number({ minimum: 0.001 })"
|
|
842
|
+
);
|
|
843
|
+
assert.equal(
|
|
844
|
+
__testables.renderResourceFieldSchema(batchedDailySequence),
|
|
845
|
+
"Type.Integer({ minimum: 1 })"
|
|
846
|
+
);
|
|
847
|
+
assert.equal(
|
|
848
|
+
__testables.renderResourceFieldSchema(moistureLevel),
|
|
849
|
+
"Type.Union([Type.Number({ minimum: 0, maximum: 100 }), Type.Null()])"
|
|
850
|
+
);
|
|
851
|
+
});
|
|
852
|
+
|
|
741
853
|
test("buildReplacementsFromSnapshot normalizes nullable temporal inputs without invalid date errors", () => {
|
|
742
854
|
const snapshot = createSnapshot({
|
|
743
855
|
hasWorkspaceIdColumn: false,
|
|
@@ -960,6 +1072,40 @@ test("buildReplacementsFromSnapshot uses shared framework time schemas in genera
|
|
|
960
1072
|
);
|
|
961
1073
|
});
|
|
962
1074
|
|
|
1075
|
+
test("buildReplacementsFromSnapshot only imports record-id validator helpers that the resource actually uses", () => {
|
|
1076
|
+
const snapshot = createSnapshot({
|
|
1077
|
+
tableName: "pollen_types",
|
|
1078
|
+
columns: [
|
|
1079
|
+
{
|
|
1080
|
+
name: "id",
|
|
1081
|
+
dataType: "bigint",
|
|
1082
|
+
columnType: "bigint unsigned",
|
|
1083
|
+
nullable: false,
|
|
1084
|
+
key: "id"
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
name: "name",
|
|
1088
|
+
dataType: "varchar",
|
|
1089
|
+
columnType: "varchar(32)",
|
|
1090
|
+
nullable: false,
|
|
1091
|
+
maxLength: 32,
|
|
1092
|
+
key: "name"
|
|
1093
|
+
}
|
|
1094
|
+
]
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
1098
|
+
namespace: "pollen-types",
|
|
1099
|
+
snapshot,
|
|
1100
|
+
resolvedOwnershipFilter: "public"
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
assert.match(replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__, /recordIdSchema/);
|
|
1104
|
+
assert.doesNotMatch(replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__, /recordIdInputSchema/);
|
|
1105
|
+
assert.doesNotMatch(replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__, /nullableRecordIdSchema/);
|
|
1106
|
+
assert.doesNotMatch(replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__, /nullableRecordIdInputSchema/);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
963
1109
|
test("crud provider template uses shared lookup provider helpers instead of inline wiring", async () => {
|
|
964
1110
|
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
965
1111
|
const templatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "CrudProvider.js");
|
|
@@ -89,6 +89,7 @@ function buildTemplateReplacements({
|
|
|
89
89
|
? "[workspaceSlugParamsValidator, recordIdParamsValidator]"
|
|
90
90
|
: "recordIdParamsValidator"],
|
|
91
91
|
["__JSKIT_CRUD_ROUTE_SURFACE_REQUIRES_WORKSPACE__", String(surfaceRequiresWorkspace === true)],
|
|
92
|
+
["__JSKIT_CRUD_ROUTE_BASE__", JSON.stringify(surfaceRequiresWorkspace ? "/w/:workspaceSlug" : "/")],
|
|
92
93
|
["__JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__", routeWorkspaceSupportImports],
|
|
93
94
|
["__JSKIT_CRUD_LIST_ROUTE_PARAMS_VALIDATOR_LINE__", surfaceRequiresWorkspace ? " paramsValidator: routeParamsValidator," : ""],
|
|
94
95
|
["__JSKIT_CRUD_VIEW_ROUTE_PARAMS_VALIDATOR_LINE__", surfaceRequiresWorkspace ? " paramsValidator: [routeParamsValidator, recordIdParamsValidator]," : " paramsValidator: recordIdParamsValidator,"],
|