@jskit-ai/crud-server-generator 0.1.58 → 0.1.60

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.
@@ -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.58",
4
+ version: "0.1.60",
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.49",
155
- "@jskit-ai/crud-core": "0.1.58",
156
- "@jskit-ai/database-runtime": "0.1.50",
157
- "@jskit-ai/http-runtime": "0.1.49",
158
- "@jskit-ai/kernel": "0.1.50",
159
- "@jskit-ai/realtime": "0.1.49",
160
- "@jskit-ai/users-core": "0.1.60",
154
+ "@jskit-ai/auth-core": "0.1.51",
155
+ "@jskit-ai/crud-core": "0.1.60",
156
+ "@jskit-ai/database-runtime": "0.1.52",
157
+ "@jskit-ai/http-runtime": "0.1.51",
158
+ "@jskit-ai/kernel": "0.1.52",
159
+ "@jskit-ai/realtime": "0.1.51",
160
+ "@jskit-ai/users-core": "0.1.62",
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.58",
3
+ "version": "0.1.60",
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.58",
17
- "@jskit-ai/database-runtime": "0.1.50",
18
- "@jskit-ai/http-runtime": "0.1.49",
19
- "@jskit-ai/kernel": "0.1.50",
20
- "@jskit-ai/users-core": "0.1.60",
16
+ "@jskit-ai/crud-core": "0.1.60",
17
+ "@jskit-ai/database-runtime": "0.1.52",
18
+ "@jskit-ai/http-runtime": "0.1.51",
19
+ "@jskit-ai/kernel": "0.1.52",
20
+ "@jskit-ai/users-core": "0.1.62",
21
21
  "recast": "^0.23.11",
22
22
  "typebox": "^1.0.81"
23
23
  }
@@ -392,6 +392,7 @@ function resolveColumnKey(column, idColumn) {
392
392
  }
393
393
 
394
394
  const NUMERIC_CHECK_CONSTRAINT_PATTERN = /(?:`([^`]+)`|([A-Za-z_][A-Za-z0-9_]*))\s*(>=|>|<=|<)\s*(-?\d+(?:\.\d+)?)/g;
395
+ const NUMERIC_CHECK_CONSTRAINT_BETWEEN_PATTERN = /(?:`([^`]+)`|([A-Za-z_][A-Za-z0-9_]*))\s+between\s+(-?\d+(?:\.\d+)?)\s+and\s+(-?\d+(?:\.\d+)?)/gi;
395
396
 
396
397
  function normalizeNumericBoundValue(value, scale = null) {
397
398
  const parsed = Number(value);
@@ -452,6 +453,83 @@ function applyUpperBound(current = null, candidate = null) {
452
453
  return current;
453
454
  }
454
455
 
456
+ function applyNumericConstraintBound(target = {}, column = null, operator = "", rawValue = null) {
457
+ if (!column || !Number.isFinite(rawValue)) {
458
+ return;
459
+ }
460
+
461
+ if (operator === ">=" || operator === ">") {
462
+ let candidate = null;
463
+ if (operator === ">=") {
464
+ candidate = {
465
+ value: normalizeNumericBoundValue(rawValue, column.numericScale),
466
+ exclusive: false
467
+ };
468
+ } else {
469
+ const exclusiveStep = resolveNumericExclusiveStep(column);
470
+ if (exclusiveStep != null) {
471
+ candidate = {
472
+ value: normalizeNumericBoundValue(rawValue + exclusiveStep, column.numericScale),
473
+ exclusive: false
474
+ };
475
+ } else {
476
+ candidate = {
477
+ value: normalizeNumericBoundValue(rawValue, column.numericScale),
478
+ exclusive: true
479
+ };
480
+ }
481
+ }
482
+
483
+ const nextBound = applyLowerBound(
484
+ target.minimum != null || target.exclusiveMinimum != null
485
+ ? {
486
+ value: target.minimum ?? target.exclusiveMinimum,
487
+ exclusive: target.exclusiveMinimum != null
488
+ }
489
+ : null,
490
+ candidate
491
+ );
492
+ target.minimum = nextBound?.exclusive === true ? null : nextBound?.value ?? null;
493
+ target.exclusiveMinimum = nextBound?.exclusive === true ? nextBound?.value ?? null : null;
494
+ return;
495
+ }
496
+
497
+ if (operator === "<=" || operator === "<") {
498
+ let candidate = null;
499
+ if (operator === "<=") {
500
+ candidate = {
501
+ value: normalizeNumericBoundValue(rawValue, column.numericScale),
502
+ exclusive: false
503
+ };
504
+ } else {
505
+ const exclusiveStep = resolveNumericExclusiveStep(column);
506
+ if (exclusiveStep != null) {
507
+ candidate = {
508
+ value: normalizeNumericBoundValue(rawValue - exclusiveStep, column.numericScale),
509
+ exclusive: false
510
+ };
511
+ } else {
512
+ candidate = {
513
+ value: normalizeNumericBoundValue(rawValue, column.numericScale),
514
+ exclusive: true
515
+ };
516
+ }
517
+ }
518
+
519
+ const nextBound = applyUpperBound(
520
+ target.maximum != null || target.exclusiveMaximum != null
521
+ ? {
522
+ value: target.maximum ?? target.exclusiveMaximum,
523
+ exclusive: target.exclusiveMaximum != null
524
+ }
525
+ : null,
526
+ candidate
527
+ );
528
+ target.maximum = nextBound?.exclusive === true ? null : nextBound?.value ?? null;
529
+ target.exclusiveMaximum = nextBound?.exclusive === true ? nextBound?.value ?? null : null;
530
+ }
531
+ }
532
+
455
533
  function resolveColumnNumericBounds(snapshot = {}) {
456
534
  const byColumnName = new Map();
457
535
  const columns = Array.isArray(snapshot.columns) ? snapshot.columns : [];
@@ -487,6 +565,22 @@ function resolveColumnNumericBounds(snapshot = {}) {
487
565
  continue;
488
566
  }
489
567
 
568
+ let betweenMatch = null;
569
+ while ((betweenMatch = NUMERIC_CHECK_CONSTRAINT_BETWEEN_PATTERN.exec(clause)) != null) {
570
+ const columnName = String(betweenMatch[1] || betweenMatch[2] || "");
571
+ const lowerValue = Number(betweenMatch[3]);
572
+ const upperValue = Number(betweenMatch[4]);
573
+ const column = numericColumnsByName.get(columnName) || null;
574
+ if (!column || !Number.isFinite(lowerValue) || !Number.isFinite(upperValue)) {
575
+ continue;
576
+ }
577
+
578
+ const target = getColumnBounds(columnName);
579
+ applyNumericConstraintBound(target, column, ">=", lowerValue);
580
+ applyNumericConstraintBound(target, column, "<=", upperValue);
581
+ }
582
+ NUMERIC_CHECK_CONSTRAINT_BETWEEN_PATTERN.lastIndex = 0;
583
+
490
584
  let match = null;
491
585
  while ((match = NUMERIC_CHECK_CONSTRAINT_PATTERN.exec(clause)) != null) {
492
586
  const columnName = String(match[1] || match[2] || "");
@@ -498,76 +592,7 @@ function resolveColumnNumericBounds(snapshot = {}) {
498
592
  }
499
593
 
500
594
  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
- }
595
+ applyNumericConstraintBound(target, column, operator, rawValue);
571
596
  }
572
597
  NUMERIC_CHECK_CONSTRAINT_PATTERN.lastIndex = 0;
573
598
  }
@@ -731,7 +756,7 @@ function renderResourceFieldSchema(column, { forOutput = false } = {}) {
731
756
  return schemaExpression;
732
757
  }
733
758
 
734
- function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false, recordIdValidatorImports = [] } = {}) {
759
+ function renderResourceValidatorsImport({ htmlTimeSchemaImports = [], recordIdValidatorImports = [] } = {}) {
735
760
  const imports = [
736
761
  "normalizeObjectInput",
737
762
  "createCursorListValidator"
@@ -741,12 +766,30 @@ function renderResourceValidatorsImport({ needsHtmlTimeSchemas = false, recordId
741
766
  imports.push(importName);
742
767
  }
743
768
  }
744
- if (needsHtmlTimeSchemas) {
745
- imports.push("HTML_TIME_STRING_SCHEMA", "NULLABLE_HTML_TIME_STRING_SCHEMA");
769
+ for (const importName of Array.isArray(htmlTimeSchemaImports) ? htmlTimeSchemaImports : []) {
770
+ if (!imports.includes(importName)) {
771
+ imports.push(importName);
772
+ }
746
773
  }
747
774
  return `import {\n ${imports.join(",\n ")}\n} from "@jskit-ai/kernel/shared/validators";`;
748
775
  }
749
776
 
777
+ function resolveHtmlTimeSchemaImports(columns = []) {
778
+ const imports = [];
779
+ for (const column of Array.isArray(columns) ? columns : []) {
780
+ if (column?.typeKind !== "time") {
781
+ continue;
782
+ }
783
+ const importName = column.nullable === true
784
+ ? "NULLABLE_HTML_TIME_STRING_SCHEMA"
785
+ : "HTML_TIME_STRING_SCHEMA";
786
+ if (!imports.includes(importName)) {
787
+ imports.push(importName);
788
+ }
789
+ }
790
+ return imports;
791
+ }
792
+
750
793
  function resolveRecordIdValidatorImports(...sources) {
751
794
  const imports = ["recordIdSchema"];
752
795
  const joinedSource = sources
@@ -1788,7 +1831,7 @@ function buildReplacementsFromSnapshot({
1788
1831
  const needsNullableDateInput = writableColumns.some(
1789
1832
  (column) => column.typeKind === "date" && column.nullable === true
1790
1833
  );
1791
- const needsHtmlTimeSchemas = resourceColumns.some((column) => column.typeKind === "time");
1834
+ const htmlTimeSchemaImports = resolveHtmlTimeSchemaImports(resourceColumns);
1792
1835
  const needsDate = resourceColumns.some((column) => column.typeKind === "date");
1793
1836
  const needsJson = resourceColumns.some((column) => column.typeKind === "json");
1794
1837
  const needsNormalizeText = resourceColumns.some((column) =>
@@ -1882,7 +1925,7 @@ function buildReplacementsFromSnapshot({
1882
1925
  surfaceRequiresWorkspace
1883
1926
  }),
1884
1927
  __JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__: renderResourceValidatorsImport({
1885
- needsHtmlTimeSchemas,
1928
+ htmlTimeSchemaImports,
1886
1929
  recordIdValidatorImports: resolveRecordIdValidatorImports(
1887
1930
  renderResourceSchemaPropertyLines(outputColumns, {
1888
1931
  forOutput: true
@@ -808,13 +808,35 @@ test("resolveScaffoldColumns derives resource numeric bounds from check constrai
808
808
  enumValues: Object.freeze([])
809
809
  });
810
810
 
811
+ const severityColumn = Object.freeze({
812
+ name: "severity",
813
+ key: "severity",
814
+ dataType: "tinyint",
815
+ columnType: "tinyint unsigned",
816
+ typeKind: "integer",
817
+ nullable: true,
818
+ hasDefault: false,
819
+ defaultValue: null,
820
+ autoIncrement: false,
821
+ unsigned: true,
822
+ extra: "",
823
+ maxLength: null,
824
+ numericPrecision: 3,
825
+ numericScale: 0,
826
+ datetimePrecision: null,
827
+ characterSetName: "",
828
+ collationName: "",
829
+ enumValues: Object.freeze([])
830
+ });
831
+
811
832
  const scaffoldColumns = __testables.resolveScaffoldColumns({
812
833
  ...snapshot,
813
834
  columns: Object.freeze([
814
835
  snapshot.columns[0],
815
836
  inputWeightColumn,
816
837
  batchedDailySequenceColumn,
817
- moistureLevelColumn
838
+ moistureLevelColumn,
839
+ severityColumn
818
840
  ]),
819
841
  checkConstraints: Object.freeze([
820
842
  Object.freeze({
@@ -828,6 +850,10 @@ test("resolveScaffoldColumns derives resource numeric bounds from check constrai
828
850
  Object.freeze({
829
851
  name: "chk_batches_moisture_level",
830
852
  clause: "`moisture_level` is null or `moisture_level` >= 0 and `moisture_level` <= 100"
853
+ }),
854
+ Object.freeze({
855
+ name: "chk_pet_notes_severity",
856
+ clause: "`severity` is null or `severity` between 1 and 10"
831
857
  })
832
858
  ])
833
859
  });
@@ -835,6 +861,7 @@ test("resolveScaffoldColumns derives resource numeric bounds from check constrai
835
861
  const inputWeight = scaffoldColumns.find((column) => column.name === "input_weight");
836
862
  const batchedDailySequence = scaffoldColumns.find((column) => column.name === "batched_daily_sequence");
837
863
  const moistureLevel = scaffoldColumns.find((column) => column.name === "moisture_level");
864
+ const severity = scaffoldColumns.find((column) => column.name === "severity");
838
865
 
839
866
  assert.equal(
840
867
  __testables.renderResourceFieldSchema(inputWeight),
@@ -848,6 +875,10 @@ test("resolveScaffoldColumns derives resource numeric bounds from check constrai
848
875
  __testables.renderResourceFieldSchema(moistureLevel),
849
876
  "Type.Union([Type.Number({ minimum: 0, maximum: 100 }), Type.Null()])"
850
877
  );
878
+ assert.equal(
879
+ __testables.renderResourceFieldSchema(severity),
880
+ "Type.Union([Type.Integer({ minimum: 1, maximum: 10 }), Type.Null()])"
881
+ );
851
882
  });
852
883
 
853
884
  test("buildReplacementsFromSnapshot normalizes nullable temporal inputs without invalid date errors", () => {
@@ -1056,7 +1087,11 @@ test("buildReplacementsFromSnapshot uses shared framework time schemas in genera
1056
1087
 
1057
1088
  assert.match(
1058
1089
  replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__,
1059
- /NULLABLE_HTML_TIME_STRING_SCHEMA/
1090
+ /(^|\n)\s*NULLABLE_HTML_TIME_STRING_SCHEMA(,|\n)/m
1091
+ );
1092
+ assert.doesNotMatch(
1093
+ replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__,
1094
+ /(^|\n)\s*HTML_TIME_STRING_SCHEMA(,|\n)/m
1060
1095
  );
1061
1096
  assert.match(
1062
1097
  replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__,
@@ -1072,6 +1107,54 @@ test("buildReplacementsFromSnapshot uses shared framework time schemas in genera
1072
1107
  );
1073
1108
  });
1074
1109
 
1110
+ test("buildReplacementsFromSnapshot imports only the non-nullable time schema when nullable time fields are absent", () => {
1111
+ const snapshot = createSnapshot({
1112
+ tableName: "opening_hours"
1113
+ });
1114
+ const timeColumn = Object.freeze({
1115
+ name: "from_time",
1116
+ key: "fromTime",
1117
+ dataType: "time",
1118
+ columnType: "time",
1119
+ typeKind: "time",
1120
+ nullable: false,
1121
+ hasDefault: false,
1122
+ defaultValue: null,
1123
+ autoIncrement: false,
1124
+ unsigned: false,
1125
+ extra: "",
1126
+ maxLength: null,
1127
+ numericPrecision: null,
1128
+ numericScale: null,
1129
+ enumValues: Object.freeze([])
1130
+ });
1131
+ const replacements = __testables.buildReplacementsFromSnapshot({
1132
+ namespace: "opening-hours",
1133
+ snapshot: {
1134
+ ...snapshot,
1135
+ columns: Object.freeze([...snapshot.columns, timeColumn])
1136
+ },
1137
+ resolvedOwnershipFilter: "workspace_user"
1138
+ });
1139
+
1140
+ assert.match(
1141
+ replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__,
1142
+ /(^|\n)\s*HTML_TIME_STRING_SCHEMA(,|\n)/m
1143
+ );
1144
+ assert.doesNotMatch(
1145
+ replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__,
1146
+ /(^|\n)\s*NULLABLE_HTML_TIME_STRING_SCHEMA(,|\n)/m
1147
+ );
1148
+ assert.match(
1149
+ replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__,
1150
+ /fromTime: HTML_TIME_STRING_SCHEMA/
1151
+ );
1152
+ assert.match(
1153
+ replacements.__JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__,
1154
+ /fromTime: HTML_TIME_STRING_SCHEMA/
1155
+ );
1156
+ });
1157
+
1075
1158
  test("buildReplacementsFromSnapshot only imports record-id validator helpers that the resource actually uses", () => {
1076
1159
  const snapshot = createSnapshot({
1077
1160
  tableName: "pollen_types",