@jskit-ai/crud-server-generator 0.1.63 → 0.1.64
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 +13 -30
- package/package.json +8 -8
- package/src/server/buildTemplateContext.js +449 -496
- package/src/server/subcommands/addField.js +8 -80
- package/src/server/subcommands/resourceAst.js +40 -393
- package/src/shared/crud/crudResource.js +85 -185
- package/templates/src/local-package/package.descriptor.mjs +3 -6
- package/templates/src/local-package/package.json +0 -1
- package/templates/src/local-package/server/CrudProvider.js +28 -21
- package/templates/src/local-package/server/actions.js +42 -54
- package/templates/src/local-package/server/registerRoutes.js +22 -50
- package/templates/src/local-package/server/repository.js +82 -38
- package/templates/src/local-package/server/service.js +45 -73
- package/templates/src/local-package/shared/crudResource.js +15 -140
- package/test/addFieldSubcommand.test.js +100 -77
- package/test/buildTemplateContext.test.js +139 -203
- package/test/crudResource.test.js +26 -31
- package/test/crudServerGuards.test.js +157 -42
- package/test/crudService.test.js +91 -173
- package/test/packageDescriptor.test.js +3 -11
- package/test/routeInputContracts.test.js +77 -8
- package/test/templateSymbolConsistency.test.js +19 -3
- package/test-support/templateServerFixture.js +155 -112
- package/templates/src/local-package/server/actionIds.js +0 -9
- package/templates/src/local-package/server/listConfig.js +0 -5
|
@@ -285,13 +285,15 @@ test("buildReplacementsFromSnapshot builds deterministic template replacement pa
|
|
|
285
285
|
/buildWorkspaceInputFromRouteParams/
|
|
286
286
|
);
|
|
287
287
|
assert.equal(replacements.__JSKIT_CRUD_ROUTE_SURFACE_REQUIRES_WORKSPACE__, "true");
|
|
288
|
-
assert.
|
|
289
|
-
|
|
290
|
-
"[workspaceSlugParamsValidator, listCursorPaginationQueryValidator, listSearchQueryValidator, listParentFilterQueryValidator, lookupIncludeQueryValidator]"
|
|
291
|
-
);
|
|
288
|
+
assert.match(replacements.__JSKIT_CRUD_LIST_ACTION_INPUT__, /composeSchemaDefinitions\(\[/);
|
|
289
|
+
assert.match(replacements.__JSKIT_CRUD_LIST_ACTION_INPUT__, /workspaceSlugParamsValidator,/);
|
|
292
290
|
assert.equal(
|
|
293
291
|
replacements.__JSKIT_CRUD_VIEW_ROUTE_PARAMS_VALIDATOR_LINE__,
|
|
294
|
-
"
|
|
292
|
+
" params: recordRouteParamsValidator,"
|
|
293
|
+
);
|
|
294
|
+
assert.match(
|
|
295
|
+
replacements.__JSKIT_CRUD_ROUTE_VALIDATOR_CONSTANTS__,
|
|
296
|
+
/const recordRouteParamsValidator = composeSchemaDefinitions\(/
|
|
295
297
|
);
|
|
296
298
|
assert.match(
|
|
297
299
|
replacements.__JSKIT_CRUD_ROLE_CATALOG_PERMISSION_GRANTS__,
|
|
@@ -303,42 +305,36 @@ test("buildReplacementsFromSnapshot builds deterministic template replacement pa
|
|
|
303
305
|
);
|
|
304
306
|
assert.match(replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__, /table\.bigIncrements\("id"\)/);
|
|
305
307
|
assert.match(replacements.__JSKIT_CRUD_MIGRATION_COLUMN_LINES__, /table\.string\("first_name", 160\)/);
|
|
306
|
-
assert.equal(replacements
|
|
307
|
-
assert.
|
|
308
|
+
assert.equal(Object.hasOwn(replacements, "__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__"), false);
|
|
309
|
+
assert.equal(Object.hasOwn(replacements, "__JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__"), false);
|
|
310
|
+
assert.equal(Object.hasOwn(replacements, "__JSKIT_CRUD_RESOURCE_PATCH_SCHEMA_PROPERTIES__"), false);
|
|
311
|
+
assert.match(replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__, /updatedAt: \{/);
|
|
308
312
|
assert.match(
|
|
309
|
-
replacements.
|
|
310
|
-
/
|
|
313
|
+
replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__,
|
|
314
|
+
/firstName: \{[\s\S]*type: "string",[\s\S]*maxLength: 160,[\s\S]*required: true,[\s\S]*search: true,[\s\S]*output: \{ required: true \},[\s\S]*create: \{ required: true \},[\s\S]*patch: \{ required: false \}[\s\S]*\},/s
|
|
311
315
|
);
|
|
312
|
-
assert.match(replacements.__JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__, /firstName: Type\.String/);
|
|
313
316
|
assert.match(
|
|
314
|
-
replacements.
|
|
315
|
-
/
|
|
316
|
-
);
|
|
317
|
-
assert.doesNotMatch(
|
|
318
|
-
replacements.__JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__,
|
|
319
|
-
/\(value\) =>/
|
|
320
|
-
);
|
|
321
|
-
assert.doesNotMatch(
|
|
322
|
-
replacements.__JSKIT_CRUD_RESOURCE_INPUT_NORMALIZATION_LINES__,
|
|
323
|
-
/value == null/
|
|
317
|
+
replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__,
|
|
318
|
+
/createdAt: \{[\s\S]*type: "dateTime",[\s\S]*default: "now\(\)",[\s\S]*storage: \{ writeSerializer: "datetime-utc" \}[\s\S]*output: \{ required: true \}[\s\S]*\},/s
|
|
324
319
|
);
|
|
320
|
+
assert.doesNotMatch(replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__, /^\s*id:\s*\{/m);
|
|
321
|
+
assert.doesNotMatch(replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__, /pattern: RECORD_ID_PATTERN/);
|
|
325
322
|
assert.match(
|
|
326
|
-
replacements.
|
|
327
|
-
/
|
|
323
|
+
replacements.__JSKIT_CRUD_JSONREST_SCHEMA_PROPERTIES__,
|
|
324
|
+
/createdAt: \{[\s\S]*storage: \{ serialize: serializeNullableDateTime \}[\s\S]*\},/s
|
|
328
325
|
);
|
|
329
|
-
assert.
|
|
330
|
-
replacements.
|
|
331
|
-
/
|
|
326
|
+
assert.doesNotMatch(
|
|
327
|
+
replacements.__JSKIT_CRUD_JSONREST_SCHEMA_PROPERTIES__,
|
|
328
|
+
/workspaceId: \{[^}]*storage: \{/s
|
|
332
329
|
);
|
|
333
|
-
assert.
|
|
334
|
-
replacements.
|
|
335
|
-
|
|
330
|
+
assert.doesNotMatch(
|
|
331
|
+
replacements.__JSKIT_CRUD_JSONREST_SCHEMA_PROPERTIES__,
|
|
332
|
+
/howManyCars: \{[^}]*storage: \{/s
|
|
336
333
|
);
|
|
337
334
|
assert.doesNotMatch(
|
|
338
|
-
replacements.
|
|
339
|
-
|
|
335
|
+
replacements.__JSKIT_CRUD_JSONREST_SCHEMA_PROPERTIES__,
|
|
336
|
+
/^\s*id:\s*\{/m
|
|
340
337
|
);
|
|
341
|
-
assert.equal(replacements.__JSKIT_CRUD_RESOURCE_CREATE_REQUIRED_FIELDS__, "[\"firstName\"]");
|
|
342
338
|
assert.equal(replacements.__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__, "");
|
|
343
339
|
});
|
|
344
340
|
|
|
@@ -364,14 +360,12 @@ test("buildReplacementsFromSnapshot omits named permissions and role grants when
|
|
|
364
360
|
assert.equal(replacements.__JSKIT_CRUD_ACTION_WORKSPACE_VALIDATOR_IMPORT__, "");
|
|
365
361
|
assert.equal(replacements.__JSKIT_CRUD_ROUTE_WORKSPACE_SUPPORT_IMPORTS__, "");
|
|
366
362
|
assert.equal(replacements.__JSKIT_CRUD_ROUTE_SURFACE_REQUIRES_WORKSPACE__, "false");
|
|
367
|
-
assert.equal(
|
|
368
|
-
|
|
369
|
-
"{ payload: resource.operations.create.bodyValidator }"
|
|
370
|
-
);
|
|
363
|
+
assert.equal(replacements.__JSKIT_CRUD_CREATE_ACTION_INPUT__, "resource.operations.create.body");
|
|
364
|
+
assert.equal(replacements.__JSKIT_CRUD_DELETE_ACTION_INPUT__, "recordIdParamsValidator");
|
|
371
365
|
assert.equal(replacements.__JSKIT_CRUD_LIST_ROUTE_PARAMS_VALIDATOR_LINE__, "");
|
|
372
366
|
assert.equal(
|
|
373
367
|
replacements.__JSKIT_CRUD_VIEW_ROUTE_PARAMS_VALIDATOR_LINE__,
|
|
374
|
-
"
|
|
368
|
+
" params: recordRouteParamsValidator,"
|
|
375
369
|
);
|
|
376
370
|
assert.equal(
|
|
377
371
|
replacements.__JSKIT_CRUD_VIEW_ROUTE_INPUT_LINES__,
|
|
@@ -469,17 +463,14 @@ test("buildReplacementsFromSnapshot omits default list ordering when created_at
|
|
|
469
463
|
const snapshot = createSnapshot({
|
|
470
464
|
hasCreatedAtColumn: false
|
|
471
465
|
});
|
|
472
|
-
|
|
466
|
+
__testables.buildReplacementsFromSnapshot({
|
|
473
467
|
namespace: "contacts",
|
|
474
468
|
snapshot,
|
|
475
469
|
resolvedOwnershipFilter: "workspace_user"
|
|
476
470
|
});
|
|
477
|
-
|
|
478
|
-
assert.doesNotMatch(replacements.__JSKIT_CRUD_LIST_CONFIG_LINES__, /orderBy/);
|
|
479
|
-
assert.match(replacements.__JSKIT_CRUD_LIST_CONFIG_LINES__, /searchColumns/);
|
|
480
471
|
});
|
|
481
472
|
|
|
482
|
-
test("buildReplacementsFromSnapshot renders
|
|
473
|
+
test("buildReplacementsFromSnapshot renders inline field relation metadata from foreign keys", () => {
|
|
483
474
|
const snapshot = {
|
|
484
475
|
...createSnapshot(),
|
|
485
476
|
columns: Object.freeze([
|
|
@@ -524,17 +515,15 @@ test("buildReplacementsFromSnapshot renders append-only field meta entries from
|
|
|
524
515
|
resolvedOwnershipFilter: "workspace_user"
|
|
525
516
|
});
|
|
526
517
|
|
|
527
|
-
assert.match(replacements.
|
|
528
|
-
assert.match(replacements.__JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__, /key: "vetId"/);
|
|
529
|
-
assert.match(replacements.__JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__, /namespace: "customer-categories"/);
|
|
518
|
+
assert.match(replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__, /vetId: \{/);
|
|
530
519
|
assert.match(
|
|
531
|
-
replacements.
|
|
532
|
-
/
|
|
520
|
+
replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__,
|
|
521
|
+
/relation: \{ kind: "lookup", namespace: "customer-categories", valueKey: "id" \}.*belongsTo: "customerCategories".*as: "vet".*ui: \{ formControl: "autocomplete" \}/s
|
|
533
522
|
);
|
|
534
523
|
assert.match(replacements.__JSKIT_CRUD_MIGRATION_FOREIGN_KEY_LINES__, /table\.foreign\(\["vet_id"\]/);
|
|
535
524
|
});
|
|
536
525
|
|
|
537
|
-
test("buildReplacementsFromSnapshot renders enum field
|
|
526
|
+
test("buildReplacementsFromSnapshot renders inline enum field ui options as select controls", () => {
|
|
538
527
|
const baseSnapshot = createSnapshot({
|
|
539
528
|
hasWorkspaceIdColumn: false,
|
|
540
529
|
hasUserIdColumn: false
|
|
@@ -569,11 +558,11 @@ test("buildReplacementsFromSnapshot renders enum field meta options as select co
|
|
|
569
558
|
resolvedOwnershipFilter: "public"
|
|
570
559
|
});
|
|
571
560
|
|
|
572
|
-
assert.match(replacements.
|
|
573
|
-
assert.match(replacements.
|
|
574
|
-
assert.match(replacements.
|
|
575
|
-
assert.match(replacements.
|
|
576
|
-
assert.match(replacements.
|
|
561
|
+
assert.match(replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__, /temperament: \{/);
|
|
562
|
+
assert.match(replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__, /ui: \{ formControl: "select"/);
|
|
563
|
+
assert.match(replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__, /options: \[/);
|
|
564
|
+
assert.match(replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__, /"value":"friendly_excitable"/);
|
|
565
|
+
assert.match(replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__, /"label":"Friendly Excitable"/);
|
|
577
566
|
});
|
|
578
567
|
|
|
579
568
|
test("renderMigrationColumnLine ignores SQL NULL string defaults", () => {
|
|
@@ -738,7 +727,7 @@ test("buildReplacementsFromSnapshot preserves custom collations, hash unique ind
|
|
|
738
727
|
);
|
|
739
728
|
});
|
|
740
729
|
|
|
741
|
-
test("resolveScaffoldColumns derives resource numeric bounds from check constraints", () => {
|
|
730
|
+
test("resolveScaffoldColumns derives canonical resource numeric bounds from check constraints", () => {
|
|
742
731
|
const snapshot = createSnapshot({
|
|
743
732
|
tableName: "batch_receivals",
|
|
744
733
|
hasWorkspaceIdColumn: false,
|
|
@@ -863,25 +852,25 @@ test("resolveScaffoldColumns derives resource numeric bounds from check constrai
|
|
|
863
852
|
const moistureLevel = scaffoldColumns.find((column) => column.name === "moisture_level");
|
|
864
853
|
const severity = scaffoldColumns.find((column) => column.name === "severity");
|
|
865
854
|
|
|
866
|
-
assert.
|
|
867
|
-
__testables.
|
|
868
|
-
"
|
|
855
|
+
assert.match(
|
|
856
|
+
__testables.renderCanonicalResourceFieldSchema(inputWeight),
|
|
857
|
+
/type: "number".*min: 0.001.*required: true.*search: true.*create: \{ required: true \}/s
|
|
869
858
|
);
|
|
870
|
-
assert.
|
|
871
|
-
__testables.
|
|
872
|
-
"
|
|
859
|
+
assert.match(
|
|
860
|
+
__testables.renderCanonicalResourceFieldSchema(batchedDailySequence),
|
|
861
|
+
/type: "integer".*min: 1.*required: true.*search: true.*create: \{ required: true \}/s
|
|
873
862
|
);
|
|
874
|
-
assert.
|
|
875
|
-
__testables.
|
|
876
|
-
"
|
|
863
|
+
assert.match(
|
|
864
|
+
__testables.renderCanonicalResourceFieldSchema(moistureLevel),
|
|
865
|
+
/type: "number".*min: 0.*max: 100.*nullable: true.*search: true.*create: \{ required: false \}/s
|
|
877
866
|
);
|
|
878
|
-
assert.
|
|
879
|
-
__testables.
|
|
880
|
-
"
|
|
867
|
+
assert.match(
|
|
868
|
+
__testables.renderCanonicalResourceFieldSchema(severity),
|
|
869
|
+
/type: "integer".*min: 1.*max: 10.*nullable: true.*search: true.*create: \{ required: false \}/s
|
|
881
870
|
);
|
|
882
871
|
});
|
|
883
872
|
|
|
884
|
-
test("buildReplacementsFromSnapshot
|
|
873
|
+
test("buildReplacementsFromSnapshot renders canonical nullable temporal fields without invalid date errors", () => {
|
|
885
874
|
const snapshot = createSnapshot({
|
|
886
875
|
hasWorkspaceIdColumn: false,
|
|
887
876
|
hasUserIdColumn: false
|
|
@@ -951,96 +940,107 @@ test("buildReplacementsFromSnapshot normalizes nullable temporal inputs without
|
|
|
951
940
|
});
|
|
952
941
|
|
|
953
942
|
assert.match(
|
|
954
|
-
replacements.
|
|
955
|
-
/
|
|
943
|
+
replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__,
|
|
944
|
+
/scheduledAt: \{[\s\S]*type: "dateTime",[\s\S]*nullable: true,[\s\S]*storage: \{ writeSerializer: "datetime-utc" \},[\s\S]*create: \{ required: false \}[\s\S]*\},/s
|
|
956
945
|
);
|
|
957
946
|
assert.match(
|
|
958
|
-
replacements.
|
|
959
|
-
/
|
|
947
|
+
replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__,
|
|
948
|
+
/birthDate: \{[\s\S]*type: "date",[\s\S]*nullable: true,[\s\S]*create: \{ required: false \}[\s\S]*\},/s
|
|
960
949
|
);
|
|
961
950
|
assert.match(
|
|
962
|
-
replacements.
|
|
963
|
-
/
|
|
964
|
-
);
|
|
965
|
-
assert.doesNotMatch(
|
|
966
|
-
replacements.__JSKIT_CRUD_RESOURCE_FIELD_META_PUSH_LINES__,
|
|
967
|
-
/writeSerializer: "datetime-utc"/
|
|
951
|
+
replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__,
|
|
952
|
+
/preferredTime: \{[\s\S]*type: "time",[\s\S]*nullable: true,[\s\S]*create: \{ required: false \}[\s\S]*\},/s
|
|
968
953
|
);
|
|
969
954
|
});
|
|
970
955
|
|
|
971
|
-
test("crud repository template defines
|
|
956
|
+
test("crud repository template defines a json-rest-api adapter over the injected internal host", async () => {
|
|
972
957
|
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
973
958
|
const templatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "repository.js");
|
|
974
959
|
const templateSource = await readFile(templatePath, "utf8");
|
|
975
|
-
assert.
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
);
|
|
979
|
-
assert.match(templateSource, /
|
|
980
|
-
assert.match(templateSource, /
|
|
981
|
-
assert.match(templateSource, /
|
|
982
|
-
assert.match(templateSource,
|
|
983
|
-
assert.match(templateSource,
|
|
984
|
-
assert.match(templateSource, /
|
|
985
|
-
assert.
|
|
986
|
-
assert.
|
|
987
|
-
assert.
|
|
988
|
-
assert.
|
|
989
|
-
assert.
|
|
990
|
-
assert.match(templateSource, /
|
|
991
|
-
assert.match(templateSource, /async function listByForeignIds\(ids = \[\], foreignKey = "", callOptions = \{\}\) \{/);
|
|
992
|
-
assert.match(templateSource, /withTransaction: resourceRuntime\.withTransaction/);
|
|
960
|
+
assert.doesNotMatch(templateSource, /from "@jskit-ai\/http-runtime\/shared";/);
|
|
961
|
+
assert.match(templateSource, /returnNullWhenJsonRestResourceMissing/);
|
|
962
|
+
assert.match(templateSource, /return api\.resources\.\$\{option:namespace\|camel\}\.query\(/);
|
|
963
|
+
assert.match(templateSource, /return returnNullWhenJsonRestResourceMissing\(\(\) =>\s+api\.resources\.\$\{option:namespace\|camel\}\.get\(/s);
|
|
964
|
+
assert.match(templateSource, /return api\.resources\.\$\{option:namespace\|camel\}\.post\(/);
|
|
965
|
+
assert.match(templateSource, /return returnNullWhenJsonRestResourceMissing\(\(\) =>\s+api\.resources\.\$\{option:namespace\|camel\}\.patch\(/s);
|
|
966
|
+
assert.match(templateSource, /return returnNullWhenJsonRestResourceMissing\(async \(\) => \{\s+await api\.resources\.\$\{option:namespace\|camel\}\.delete\(/s);
|
|
967
|
+
assert.match(templateSource, /createJsonRestContext\(options\?\.context \|\| null\)/);
|
|
968
|
+
assert.match(templateSource, /buildJsonRestQueryParams\(RESOURCE_TYPE, query\)/);
|
|
969
|
+
assert.match(templateSource, /createJsonApiInputRecord\(RESOURCE_TYPE, payload\)/);
|
|
970
|
+
assert.doesNotMatch(templateSource, /function toJsonRestContext\(context = null\)/);
|
|
971
|
+
assert.doesNotMatch(templateSource, /function normalizeArrayInput\(value\)/);
|
|
972
|
+
assert.doesNotMatch(templateSource, /function buildJsonRestQueryParams\(query = \{\}/);
|
|
973
|
+
assert.doesNotMatch(templateSource, /function createJsonApiInputRecord\(attributes = \{\}/);
|
|
974
|
+
assert.doesNotMatch(templateSource, /normalizeVisibilityContext/);
|
|
975
|
+
assert.match(templateSource, /withTransaction,/);
|
|
993
976
|
assert.match(templateSource, /return Object\.freeze\(\{/);
|
|
994
|
-
assert.doesNotMatch(templateSource, /
|
|
977
|
+
assert.doesNotMatch(templateSource, /createCrudResourceRuntime|resourceRuntime\./);
|
|
995
978
|
});
|
|
996
979
|
|
|
997
|
-
test("crud
|
|
980
|
+
test("crud provider template derives json-rest host options directly from the shared resource", async () => {
|
|
981
|
+
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
982
|
+
const templatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "CrudProvider.js");
|
|
983
|
+
const templateSource = await readFile(templatePath, "utf8");
|
|
984
|
+
|
|
985
|
+
assert.match(templateSource, /import \{ createCrudJsonApiServiceEvents \} from "@jskit-ai\/crud-core\/server\/serviceEvents";/);
|
|
986
|
+
assert.match(templateSource, /createJsonRestResourceScopeOptions/);
|
|
987
|
+
assert.match(templateSource, /import \{ createService \} from "\.\/service\.js";/);
|
|
988
|
+
assert.doesNotMatch(templateSource, /serviceEvents\s*\}\s*from "\.\/service\.js";/);
|
|
989
|
+
assert.match(templateSource, /import \{ resource \} from "\.\.\/shared\/\$\{option:namespace\|singular\|camel\}Resource\.js";/);
|
|
990
|
+
assert.doesNotMatch(templateSource, /normalizeRecordId/);
|
|
991
|
+
assert.match(templateSource, /toDatabaseDateTimeUtc/);
|
|
992
|
+
assert.doesNotMatch(templateSource, /register\$\{option:namespace\|pascal\}JsonRestResources/);
|
|
993
|
+
assert.match(templateSource, /const baseServiceEvents = createCrudJsonApiServiceEvents\(CRUD_MODULE_CONFIG\.namespace\);/);
|
|
994
|
+
assert.match(templateSource, /const serviceEvents = \{\s+\.\.\.baseServiceEvents\s+\};/s);
|
|
995
|
+
assert.match(templateSource, /const api = app\.make\(INTERNAL_JSON_REST_API\);/);
|
|
996
|
+
assert.match(templateSource, /await addResourceIfMissing\(\s*api,\s*__JSKIT_CRUD_JSONREST_SCOPE_NAME__,\s*createJsonRestResourceScopeOptions\(resource,/s);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
test("crud actions and routes templates derive cursor validation and route contracts from the shared resource", async () => {
|
|
998
1000
|
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
999
1001
|
const actionsTemplatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "actions.js");
|
|
1000
1002
|
const registerRoutesTemplatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "registerRoutes.js");
|
|
1001
|
-
const listConfigTemplatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "listConfig.js");
|
|
1002
1003
|
|
|
1003
1004
|
const actionsTemplateSource = await readFile(actionsTemplatePath, "utf8");
|
|
1004
1005
|
const registerRoutesTemplateSource = await readFile(registerRoutesTemplatePath, "utf8");
|
|
1005
|
-
const listConfigTemplateSource = await readFile(listConfigTemplatePath, "utf8");
|
|
1006
1006
|
|
|
1007
1007
|
assert.match(actionsTemplateSource, /createCrudCursorPaginationQueryValidator/);
|
|
1008
|
-
assert.match(actionsTemplateSource, /import \{
|
|
1009
|
-
assert.match(actionsTemplateSource, /const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator\(
|
|
1008
|
+
assert.match(actionsTemplateSource, /import \{ resource \} from "\.\.\/shared\/\$\{option:namespace\|singular\|camel\}Resource\.js";/);
|
|
1009
|
+
assert.match(actionsTemplateSource, /const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator\(\{\s+orderBy: resource\.defaultSort\s+\}\);/s);
|
|
1010
1010
|
assert.match(actionsTemplateSource, /__JSKIT_CRUD_ACTION_PERMISSION_SUPPORT__/);
|
|
1011
1011
|
assert.match(actionsTemplateSource, /__JSKIT_CRUD_LIST_ACTION_PERMISSION__/);
|
|
1012
|
+
assert.match(actionsTemplateSource, /output: null,/);
|
|
1012
1013
|
assert.doesNotMatch(actionsTemplateSource, /ACTIONS_REQUIRE_NAMED_PERMISSIONS/);
|
|
1013
1014
|
assert.doesNotMatch(actionsTemplateSource, /createActionPermission/);
|
|
1014
|
-
assert.match(registerRoutesTemplateSource, /
|
|
1015
|
-
assert.match(registerRoutesTemplateSource, /
|
|
1016
|
-
assert.match(registerRoutesTemplateSource, /
|
|
1017
|
-
assert.
|
|
1018
|
-
assert.match(
|
|
1015
|
+
assert.match(registerRoutesTemplateSource, /createCrudJsonApiRouteContracts/);
|
|
1016
|
+
assert.match(registerRoutesTemplateSource, /const \{\s+listRouteContract,\s+viewRouteContract,\s+createRouteContract,\s+updateRouteContract,\s+deleteRouteContract,\s+recordRouteParamsValidator\s+\} = createCrudJsonApiRouteContracts\(\{/s);
|
|
1017
|
+
assert.match(registerRoutesTemplateSource, /resource__JSKIT_CRUD_ROUTE_CONTRACTS_RESOURCE_ARGS__/);
|
|
1018
|
+
assert.doesNotMatch(registerRoutesTemplateSource, /wrapResponse/);
|
|
1019
|
+
assert.match(registerRoutesTemplateSource, /reply\.code\(204\)\.send\(response\);/);
|
|
1020
|
+
assert.doesNotMatch(registerRoutesTemplateSource, /withStandardErrorResponses/);
|
|
1019
1021
|
});
|
|
1020
1022
|
|
|
1021
|
-
test("crud service template
|
|
1023
|
+
test("crud service template preserves JSON:API output and emits entity ids from resource documents", async () => {
|
|
1022
1024
|
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
1023
1025
|
const templatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "service.js");
|
|
1024
1026
|
const templateSource = await readFile(templatePath, "utf8");
|
|
1025
1027
|
|
|
1026
|
-
assert.
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
);
|
|
1030
|
-
assert.
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
);
|
|
1034
|
-
assert.match(templateSource, /
|
|
1035
|
-
assert.match(templateSource, /
|
|
1036
|
-
assert.match(templateSource, /
|
|
1037
|
-
assert.match(templateSource, /
|
|
1038
|
-
assert.match(templateSource, /function
|
|
1039
|
-
assert.match(templateSource, /
|
|
1040
|
-
assert.match(templateSource, /return crudServiceListRecords\(serviceRuntime, \$\{option:namespace\|camel\}Repository, fieldAccess, query, options\);/);
|
|
1041
|
-
assert.match(templateSource, /async function updateRecord\(recordId, payload = \{\}, options = \{\}\)/);
|
|
1042
|
-
assert.match(templateSource, /return crudServiceUpdateRecord\(serviceRuntime, \$\{option:namespace\|camel\}Repository, fieldAccess, recordId, payload, options\);/);
|
|
1028
|
+
assert.doesNotMatch(templateSource, /crudNamespaceSupport/);
|
|
1029
|
+
assert.doesNotMatch(templateSource, /serviceEvents/);
|
|
1030
|
+
assert.doesNotMatch(templateSource, /recordChangedEventName/);
|
|
1031
|
+
assert.doesNotMatch(templateSource, /resolveJsonApiResultRecordId/);
|
|
1032
|
+
assert.doesNotMatch(templateSource, /entityId:/);
|
|
1033
|
+
assert.match(templateSource, /function return404IfNotFound\(document = null\) \{/);
|
|
1034
|
+
assert.match(templateSource, /throw new AppError\(404, "Document not found\."\);/);
|
|
1035
|
+
assert.match(templateSource, /function createService\(\{ \$\{option:namespace\|camel\}Repository \} = \{\}\)/);
|
|
1036
|
+
assert.match(templateSource, /throw new TypeError\("createService requires \$\{option:namespace\|camel\}Repository\."\);/);
|
|
1037
|
+
assert.match(templateSource, /returnJsonApiDocument/);
|
|
1038
|
+
assert.match(templateSource, /async function queryDocuments\(query = \{\}, options = \{\}\)/);
|
|
1039
|
+
assert.match(templateSource, /returnJsonApiDocument\(await \$\{option:namespace\|camel\}Repository\.queryDocuments\(query, \{/);
|
|
1040
|
+
assert.match(templateSource, /async function patchDocumentById\(recordId, payload = \{\}, options = \{\}\)/);
|
|
1041
|
+
assert.match(templateSource, /returnJsonApiDocument\(return404IfNotFound\(await \$\{option:namespace\|camel\}Repository\.patchDocumentById\(recordId, payload, \{/);
|
|
1043
1042
|
assert.match(templateSource, /return Object\.freeze\(\{/);
|
|
1043
|
+
assert.match(templateSource, /export \{ createService \};/);
|
|
1044
1044
|
});
|
|
1045
1045
|
|
|
1046
1046
|
test("crud generator renders time columns with html-time-compatible schemas", async () => {
|
|
@@ -1050,16 +1050,12 @@ test("crud generator renders time columns with html-time-compatible schemas", as
|
|
|
1050
1050
|
|
|
1051
1051
|
assert.match(
|
|
1052
1052
|
templateSource,
|
|
1053
|
-
/
|
|
1053
|
+
/type: "time"/
|
|
1054
1054
|
);
|
|
1055
|
-
assert.
|
|
1056
|
-
templateSource,
|
|
1057
|
-
/HTML_TIME_STRING_SCHEMA/
|
|
1058
|
-
);
|
|
1059
|
-
assert.doesNotMatch(templateSource, /format: "time"/);
|
|
1055
|
+
assert.doesNotMatch(templateSource, /HTML_TIME_STRING_SCHEMA|NULLABLE_HTML_TIME_STRING_SCHEMA/);
|
|
1060
1056
|
});
|
|
1061
1057
|
|
|
1062
|
-
test("buildReplacementsFromSnapshot
|
|
1058
|
+
test("buildReplacementsFromSnapshot renders canonical nullable time fields", () => {
|
|
1063
1059
|
const snapshot = createSnapshot({
|
|
1064
1060
|
tableName: "opening_hours"
|
|
1065
1061
|
});
|
|
@@ -1090,28 +1086,12 @@ test("buildReplacementsFromSnapshot uses shared framework time schemas in genera
|
|
|
1090
1086
|
});
|
|
1091
1087
|
|
|
1092
1088
|
assert.match(
|
|
1093
|
-
replacements.
|
|
1094
|
-
/
|
|
1095
|
-
);
|
|
1096
|
-
assert.doesNotMatch(
|
|
1097
|
-
replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__,
|
|
1098
|
-
/(^|\n)\s*HTML_TIME_STRING_SCHEMA(,|\n)/m
|
|
1099
|
-
);
|
|
1100
|
-
assert.match(
|
|
1101
|
-
replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__,
|
|
1102
|
-
/fromTime: NULLABLE_HTML_TIME_STRING_SCHEMA/
|
|
1103
|
-
);
|
|
1104
|
-
assert.match(
|
|
1105
|
-
replacements.__JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__,
|
|
1106
|
-
/fromTime: NULLABLE_HTML_TIME_STRING_SCHEMA/
|
|
1107
|
-
);
|
|
1108
|
-
assert.doesNotMatch(
|
|
1109
|
-
replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__,
|
|
1110
|
-
/Type\.String\(\{ pattern:/
|
|
1089
|
+
replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__,
|
|
1090
|
+
/fromTime: \{[\s\S]*type: "time",[\s\S]*nullable: true,[\s\S]*output: \{ required: true \},[\s\S]*create: \{ required: false \},[\s\S]*patch: \{ required: false \}[\s\S]*\},/s
|
|
1111
1091
|
);
|
|
1112
1092
|
});
|
|
1113
1093
|
|
|
1114
|
-
test("buildReplacementsFromSnapshot
|
|
1094
|
+
test("buildReplacementsFromSnapshot renders canonical non-nullable time fields", () => {
|
|
1115
1095
|
const snapshot = createSnapshot({
|
|
1116
1096
|
tableName: "opening_hours"
|
|
1117
1097
|
});
|
|
@@ -1142,74 +1122,30 @@ test("buildReplacementsFromSnapshot imports only the non-nullable time schema wh
|
|
|
1142
1122
|
});
|
|
1143
1123
|
|
|
1144
1124
|
assert.match(
|
|
1145
|
-
replacements.
|
|
1146
|
-
/
|
|
1147
|
-
);
|
|
1148
|
-
assert.doesNotMatch(
|
|
1149
|
-
replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__,
|
|
1150
|
-
/(^|\n)\s*NULLABLE_HTML_TIME_STRING_SCHEMA(,|\n)/m
|
|
1151
|
-
);
|
|
1152
|
-
assert.match(
|
|
1153
|
-
replacements.__JSKIT_CRUD_RESOURCE_OUTPUT_SCHEMA_PROPERTIES__,
|
|
1154
|
-
/fromTime: HTML_TIME_STRING_SCHEMA/
|
|
1125
|
+
replacements.__JSKIT_CRUD_RESOURCE_SCHEMA_PROPERTIES__,
|
|
1126
|
+
/fromTime: \{[\s\S]*type: "time",[\s\S]*required: true,[\s\S]*output: \{ required: true \},[\s\S]*create: \{ required: true \},[\s\S]*patch: \{ required: false \}[\s\S]*\},/s
|
|
1155
1127
|
);
|
|
1156
|
-
assert.match(
|
|
1157
|
-
replacements.__JSKIT_CRUD_RESOURCE_CREATE_SCHEMA_PROPERTIES__,
|
|
1158
|
-
/fromTime: HTML_TIME_STRING_SCHEMA/
|
|
1159
|
-
);
|
|
1160
|
-
});
|
|
1161
|
-
|
|
1162
|
-
test("buildReplacementsFromSnapshot only imports record-id validator helpers that the resource actually uses", () => {
|
|
1163
|
-
const snapshot = createSnapshot({
|
|
1164
|
-
tableName: "pollen_types",
|
|
1165
|
-
columns: [
|
|
1166
|
-
{
|
|
1167
|
-
name: "id",
|
|
1168
|
-
dataType: "bigint",
|
|
1169
|
-
columnType: "bigint unsigned",
|
|
1170
|
-
nullable: false,
|
|
1171
|
-
key: "id"
|
|
1172
|
-
},
|
|
1173
|
-
{
|
|
1174
|
-
name: "name",
|
|
1175
|
-
dataType: "varchar",
|
|
1176
|
-
columnType: "varchar(32)",
|
|
1177
|
-
nullable: false,
|
|
1178
|
-
maxLength: 32,
|
|
1179
|
-
key: "name"
|
|
1180
|
-
}
|
|
1181
|
-
]
|
|
1182
|
-
});
|
|
1183
|
-
|
|
1184
|
-
const replacements = __testables.buildReplacementsFromSnapshot({
|
|
1185
|
-
namespace: "pollen-types",
|
|
1186
|
-
snapshot,
|
|
1187
|
-
resolvedOwnershipFilter: "public"
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
assert.match(replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__, /recordIdSchema/);
|
|
1191
|
-
assert.doesNotMatch(replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__, /recordIdInputSchema/);
|
|
1192
|
-
assert.doesNotMatch(replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__, /nullableRecordIdSchema/);
|
|
1193
|
-
assert.doesNotMatch(replacements.__JSKIT_CRUD_RESOURCE_VALIDATORS_IMPORT__, /nullableRecordIdInputSchema/);
|
|
1194
1128
|
});
|
|
1195
1129
|
|
|
1196
|
-
test("crud provider template
|
|
1130
|
+
test("crud provider template injects the shared internal json-rest-api host and registers local resources at boot", async () => {
|
|
1197
1131
|
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
1198
1132
|
const templatePath = path.resolve(testDirectory, "..", "templates", "src", "local-package", "server", "CrudProvider.js");
|
|
1199
1133
|
const templateSource = await readFile(templatePath, "utf8");
|
|
1200
1134
|
|
|
1201
1135
|
assert.match(
|
|
1202
1136
|
templateSource,
|
|
1203
|
-
/from "@jskit-ai\/
|
|
1137
|
+
/from "@jskit-ai\/json-rest-api-core\/server\/jsonRestApiHost";/
|
|
1204
1138
|
);
|
|
1205
|
-
assert.match(templateSource, /resolveLookup: createCrudLookupResolver\(scope\)/);
|
|
1206
1139
|
assert.match(
|
|
1207
1140
|
templateSource,
|
|
1208
|
-
/
|
|
1141
|
+
/const api = scope\.make\(INTERNAL_JSON_REST_API\);/
|
|
1209
1142
|
);
|
|
1143
|
+
assert.match(templateSource, /return createRepository\(\{\s*api,\s*knex\s*\}\);/s);
|
|
1144
|
+
assert.match(templateSource, /await addResourceIfMissing\(\s*api,\s*__JSKIT_CRUD_JSONREST_SCOPE_NAME__,\s*createJsonRestResourceScopeOptions\(resource,/s);
|
|
1145
|
+
assert.doesNotMatch(templateSource, /register\$\{option:namespace\|pascal\}JsonRestResources/);
|
|
1210
1146
|
assert.match(
|
|
1211
1147
|
templateSource,
|
|
1212
1148
|
/routeSurfaceRequiresWorkspace: crudPolicy\.surfaceDefinition\.requiresWorkspace === true,/
|
|
1213
1149
|
);
|
|
1214
|
-
assert.doesNotMatch(templateSource, /
|
|
1150
|
+
assert.doesNotMatch(templateSource, /createCrudLookup|lookup\.\$\{option:namespace\|snake\}/);
|
|
1215
1151
|
});
|
|
@@ -1,47 +1,42 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
3
4
|
import { crudResource } from "../src/shared/crud/crudResource.js";
|
|
4
5
|
|
|
5
|
-
test("crudResource normalizes create payload", () => {
|
|
6
|
-
const normalized = crudResource.operations.create.
|
|
6
|
+
test("crudResource normalizes create payload through schema validation", async () => {
|
|
7
|
+
const normalized = await validateSchemaPayload(crudResource.operations.create.body, {
|
|
7
8
|
textField: " Example text ",
|
|
8
9
|
dateField: "2026-03-11",
|
|
9
10
|
numberField: "42.5"
|
|
10
|
-
});
|
|
11
|
+
}, { phase: "input" });
|
|
11
12
|
|
|
12
|
-
assert.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
});
|
|
13
|
+
assert.equal(normalized.textField, "Example text");
|
|
14
|
+
assert.equal(normalized.numberField, 42.5);
|
|
15
|
+
assert.ok(normalized.dateField instanceof Date);
|
|
16
|
+
assert.equal(normalized.dateField.toISOString(), "2026-03-11T00:00:00.000Z");
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test("crudResource normalizes
|
|
20
|
-
const normalized = crudResource.operations.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
updatedAt: "2026-03-11 00:00:00.000"
|
|
29
|
-
}
|
|
30
|
-
],
|
|
31
|
-
nextCursor: " 8 "
|
|
32
|
-
});
|
|
19
|
+
test("crudResource normalizes record output through schema validation", async () => {
|
|
20
|
+
const normalized = await validateSchemaPayload(crudResource.operations.view.output, {
|
|
21
|
+
id: 7,
|
|
22
|
+
textField: " Example text ",
|
|
23
|
+
dateField: "2026-03-10",
|
|
24
|
+
numberField: "99",
|
|
25
|
+
createdAt: "2026-03-11 00:00:00.000",
|
|
26
|
+
updatedAt: "2026-03-11 00:00:00.000"
|
|
27
|
+
}, { phase: "output" });
|
|
33
28
|
|
|
34
|
-
assert.equal(normalized.
|
|
35
|
-
assert.equal(normalized.
|
|
36
|
-
assert.
|
|
37
|
-
assert.equal(normalized.
|
|
38
|
-
assert.
|
|
39
|
-
assert.
|
|
40
|
-
assert.
|
|
29
|
+
assert.equal(normalized.id, "7");
|
|
30
|
+
assert.equal(normalized.textField, "Example text");
|
|
31
|
+
assert.ok(normalized.dateField instanceof Date);
|
|
32
|
+
assert.equal(normalized.dateField.toISOString(), "2026-03-10T00:00:00.000Z");
|
|
33
|
+
assert.equal(normalized.numberField, 99);
|
|
34
|
+
assert.ok(normalized.createdAt instanceof Date);
|
|
35
|
+
assert.ok(normalized.updatedAt instanceof Date);
|
|
41
36
|
});
|
|
42
37
|
|
|
43
38
|
test("crudResource list operation exposes output validator only", () => {
|
|
44
|
-
assert.equal(
|
|
45
|
-
assert.equal(crudResource.operations.list.
|
|
39
|
+
assert.equal(crudResource.operations.list.output?.normalize, undefined);
|
|
40
|
+
assert.equal(crudResource.operations.list.input, undefined);
|
|
46
41
|
assert.deepEqual(crudResource.operations.list.realtime?.events, ["crud.record.changed"]);
|
|
47
42
|
});
|