@jskit-ai/crud-core 0.1.158 → 0.1.159
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-core",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.159",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Shared CRUD helpers used by CRUD modules.",
|
|
7
7
|
dependsOn: [
|
|
@@ -26,7 +26,7 @@ export default Object.freeze({
|
|
|
26
26
|
mutations: {
|
|
27
27
|
dependencies: {
|
|
28
28
|
runtime: {
|
|
29
|
-
"@jskit-ai/crud-core": "0.1.
|
|
29
|
+
"@jskit-ai/crud-core": "0.1.159"
|
|
30
30
|
},
|
|
31
31
|
dev: {}
|
|
32
32
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/crud-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.159",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"./server/routeContracts": "./src/server/routeContracts.js"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
30
|
+
"@jskit-ai/database-runtime": "0.1.148",
|
|
31
31
|
"@jskit-ai/http-runtime": "0.1.146",
|
|
32
32
|
"@jskit-ai/kernel": "0.1.147",
|
|
33
33
|
"json-rest-schema": "^1.0.17",
|
|
34
|
-
"@jskit-ai/realtime": "0.1.
|
|
34
|
+
"@jskit-ai/realtime": "0.1.145",
|
|
35
35
|
"@jskit-ai/resource-crud-core": "0.1.90",
|
|
36
|
-
"@jskit-ai/shell-web": "0.1.
|
|
36
|
+
"@jskit-ai/shell-web": "0.1.149",
|
|
37
37
|
"@jskit-ai/users-core": "0.1.161",
|
|
38
|
-
"@jskit-ai/users-web": "0.1.
|
|
38
|
+
"@jskit-ai/users-web": "0.1.166"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@tanstack/vue-query": "^5.90.5",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeDbRecordId,
|
|
3
|
-
toDatabaseDateTimeUtc
|
|
3
|
+
toDatabaseDateTimeUtc,
|
|
4
|
+
toJsonDate,
|
|
5
|
+
toJsonTime,
|
|
6
|
+
toJsonDateTime
|
|
4
7
|
} from "@jskit-ai/database-runtime/shared";
|
|
5
8
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
6
9
|
import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
|
|
@@ -26,6 +29,11 @@ const MAX_LIST_LIMIT = 100;
|
|
|
26
29
|
const CRUD_WRITE_SERIALIZERS = Object.freeze({
|
|
27
30
|
[CRUD_FIELD_WRITE_SERIALIZER_DATETIME_UTC]: (value) => toDatabaseDateTimeUtc(value)
|
|
28
31
|
});
|
|
32
|
+
const CRUD_OUTPUT_SERIALIZERS = Object.freeze({
|
|
33
|
+
date: toJsonDate,
|
|
34
|
+
time: toJsonTime,
|
|
35
|
+
datetime: toJsonDateTime
|
|
36
|
+
});
|
|
29
37
|
|
|
30
38
|
function normalizeCrudListCursor(cursor = null, { allowEmpty = true } = {}) {
|
|
31
39
|
if (cursor === undefined || cursor === null) {
|
|
@@ -285,10 +293,20 @@ function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRep
|
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
const outputRecordIdKeys = [];
|
|
296
|
+
const outputSerializerByKey = {};
|
|
288
297
|
for (const [key, definition] of Object.entries(outputProperties)) {
|
|
289
298
|
if (definitionIncludesRecordIdType(definition)) {
|
|
290
299
|
outputRecordIdKeys.push(key);
|
|
291
300
|
}
|
|
301
|
+
const fieldType = resolveFieldDefinitionType(definition);
|
|
302
|
+
if (Object.hasOwn(CRUD_OUTPUT_SERIALIZERS, fieldType)) {
|
|
303
|
+
outputSerializerByKey[key] = Object.freeze({
|
|
304
|
+
type: fieldType,
|
|
305
|
+
...(Number.isInteger(definition?.temporalPrecision)
|
|
306
|
+
? { temporalPrecision: definition.temporalPrecision }
|
|
307
|
+
: {})
|
|
308
|
+
});
|
|
309
|
+
}
|
|
292
310
|
}
|
|
293
311
|
|
|
294
312
|
for (const key of writeKeys) {
|
|
@@ -318,11 +336,17 @@ function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRep
|
|
|
318
336
|
virtualOutputKeys: Object.freeze(virtualOutputKeys),
|
|
319
337
|
listSearchColumns: Object.freeze(listSearchColumns),
|
|
320
338
|
parentFilterColumns: Object.freeze(parentFilterColumns),
|
|
321
|
-
outputRecordIdKeys: Object.freeze(outputRecordIdKeys)
|
|
339
|
+
outputRecordIdKeys: Object.freeze(outputRecordIdKeys),
|
|
340
|
+
outputSerializerByKey: Object.freeze(outputSerializerByKey)
|
|
322
341
|
});
|
|
323
342
|
}
|
|
324
343
|
|
|
325
|
-
function mapRecordRow(
|
|
344
|
+
function mapRecordRow(
|
|
345
|
+
row,
|
|
346
|
+
fieldKeys = [],
|
|
347
|
+
overrides = {},
|
|
348
|
+
{ recordIdKeys = [], serializerByKey = {} } = {}
|
|
349
|
+
) {
|
|
326
350
|
if (!row) {
|
|
327
351
|
return null;
|
|
328
352
|
}
|
|
@@ -333,6 +357,7 @@ function mapRecordRow(row, fieldKeys = [], overrides = {}, { recordIdKeys = [] }
|
|
|
333
357
|
.filter(Boolean)
|
|
334
358
|
);
|
|
335
359
|
const mapped = {};
|
|
360
|
+
const normalizedSerializerByKey = normalizeObjectInput(serializerByKey);
|
|
336
361
|
for (const key of fieldKeys) {
|
|
337
362
|
const normalizedKey = String(key || "").trim();
|
|
338
363
|
const columnName = resolveColumnName(normalizedKey, overrides);
|
|
@@ -351,6 +376,21 @@ function mapRecordRow(row, fieldKeys = [], overrides = {}, { recordIdKeys = [] }
|
|
|
351
376
|
continue;
|
|
352
377
|
}
|
|
353
378
|
|
|
379
|
+
const serializerConfig = normalizedSerializerByKey[normalizedKey];
|
|
380
|
+
const serializerId = normalizeText(
|
|
381
|
+
typeof serializerConfig === "string" ? serializerConfig : serializerConfig?.type
|
|
382
|
+
).toLowerCase();
|
|
383
|
+
if (serializerId && rawValue != null) {
|
|
384
|
+
const serializer = CRUD_OUTPUT_SERIALIZERS[serializerId];
|
|
385
|
+
if (typeof serializer !== "function") {
|
|
386
|
+
throw new Error(`crudRepository output serializer "${serializerId}" is not supported.`);
|
|
387
|
+
}
|
|
388
|
+
mapped[normalizedKey] = serializer(rawValue, {
|
|
389
|
+
temporalPrecision: serializerConfig?.temporalPrecision
|
|
390
|
+
});
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
354
394
|
mapped[normalizedKey] = rawValue;
|
|
355
395
|
}
|
|
356
396
|
return mapped;
|
|
@@ -925,7 +925,10 @@ async function listRecords(runtime, knex, query = {}, callOptions = {}) {
|
|
|
925
925
|
row,
|
|
926
926
|
runtime.mapping.outputKeys,
|
|
927
927
|
runtime.mapping.columnOverrides,
|
|
928
|
-
{
|
|
928
|
+
{
|
|
929
|
+
recordIdKeys: runtime.mapping.outputRecordIdKeys,
|
|
930
|
+
serializerByKey: runtime.mapping.outputSerializerByKey
|
|
931
|
+
}
|
|
929
932
|
);
|
|
930
933
|
if (!mappedRecord) {
|
|
931
934
|
continue;
|
|
@@ -1014,7 +1017,10 @@ async function findRecordById(runtime, knex, recordId, callOptions = {}) {
|
|
|
1014
1017
|
row,
|
|
1015
1018
|
runtime.mapping.outputKeys,
|
|
1016
1019
|
runtime.mapping.columnOverrides,
|
|
1017
|
-
{
|
|
1020
|
+
{
|
|
1021
|
+
recordIdKeys: runtime.mapping.outputRecordIdKeys,
|
|
1022
|
+
serializerByKey: runtime.mapping.outputSerializerByKey
|
|
1023
|
+
}
|
|
1018
1024
|
);
|
|
1019
1025
|
if (!mappedRecord) {
|
|
1020
1026
|
return null;
|
|
@@ -1115,7 +1121,10 @@ async function listRecordsByIds(runtime, knex, ids = [], callOptions = {}) {
|
|
|
1115
1121
|
row,
|
|
1116
1122
|
runtime.mapping.outputKeys,
|
|
1117
1123
|
runtime.mapping.columnOverrides,
|
|
1118
|
-
{
|
|
1124
|
+
{
|
|
1125
|
+
recordIdKeys: runtime.mapping.outputRecordIdKeys,
|
|
1126
|
+
serializerByKey: runtime.mapping.outputSerializerByKey
|
|
1127
|
+
}
|
|
1119
1128
|
);
|
|
1120
1129
|
if (!mappedRecord) {
|
|
1121
1130
|
continue;
|
|
@@ -138,6 +138,36 @@ test("mapRecordRow omits keys whose source column is absent", () => {
|
|
|
138
138
|
});
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
test("mapRecordRow serializes database temporal values for strict JSON resource output", () => {
|
|
142
|
+
const row = {
|
|
143
|
+
service_date: new Date("2026-08-13T23:59:58.123Z"),
|
|
144
|
+
opening_time: "07:08:09.123456",
|
|
145
|
+
scheduled_at: "2026-08-13 07:08:09.123456"
|
|
146
|
+
};
|
|
147
|
+
const mapped = mapRecordRow(
|
|
148
|
+
row,
|
|
149
|
+
["serviceDate", "openingTime", "scheduledAt"],
|
|
150
|
+
{
|
|
151
|
+
serviceDate: "service_date",
|
|
152
|
+
openingTime: "opening_time",
|
|
153
|
+
scheduledAt: "scheduled_at"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
serializerByKey: {
|
|
157
|
+
serviceDate: "date",
|
|
158
|
+
openingTime: "time",
|
|
159
|
+
scheduledAt: "datetime"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
assert.deepEqual(mapped, {
|
|
165
|
+
serviceDate: "2026-08-13",
|
|
166
|
+
openingTime: "07:08:09.123456",
|
|
167
|
+
scheduledAt: "2026-08-13T07:08:09.123456Z"
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
141
171
|
test("buildWritePayload respects defined keys", () => {
|
|
142
172
|
const payload = buildWritePayload(
|
|
143
173
|
{ foo: "bar", missing: true },
|
|
@@ -574,6 +604,36 @@ test("deriveRepositoryMappingFromResource tracks writable column-backed write se
|
|
|
574
604
|
scheduledAt: "datetime-utc",
|
|
575
605
|
archivedAt: "datetime-utc"
|
|
576
606
|
});
|
|
607
|
+
assert.deepEqual(mapping.outputSerializerByKey, {
|
|
608
|
+
scheduledAt: { type: "datetime" },
|
|
609
|
+
archivedAt: { type: "datetime" }
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("deriveRepositoryMappingFromResource tracks JSON temporal output serializers", () => {
|
|
614
|
+
const resource = {
|
|
615
|
+
operations: {
|
|
616
|
+
view: {
|
|
617
|
+
output: createOperationSchemaDefinition({
|
|
618
|
+
id: { type: "integer", required: true },
|
|
619
|
+
serviceDate: { type: "date", required: true },
|
|
620
|
+
openingTime: { type: "time", required: true },
|
|
621
|
+
scheduledAt: { type: "dateTime", temporalPrecision: 3, required: true }
|
|
622
|
+
})
|
|
623
|
+
},
|
|
624
|
+
create: {
|
|
625
|
+
body: createOperationSchemaDefinition({}, "create")
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const mapping = deriveRepositoryMappingFromResource(resource);
|
|
631
|
+
|
|
632
|
+
assert.deepEqual(mapping.outputSerializerByKey, {
|
|
633
|
+
serviceDate: { type: "date" },
|
|
634
|
+
openingTime: { type: "time" },
|
|
635
|
+
scheduledAt: { type: "datetime", temporalPrecision: 3 }
|
|
636
|
+
});
|
|
577
637
|
});
|
|
578
638
|
|
|
579
639
|
test("deriveRepositoryMappingFromResource keeps explicit storage.writeSerializer metadata", () => {
|
|
@@ -499,6 +499,64 @@ test("list uses resource table and id defaults", async () => {
|
|
|
499
499
|
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "contact_id" && call[2] === ">" && call[3] === "2"));
|
|
500
500
|
});
|
|
501
501
|
|
|
502
|
+
test("list converts database temporal values before strict resource validation", async () => {
|
|
503
|
+
const resource = {
|
|
504
|
+
namespace: "appointments",
|
|
505
|
+
tableName: "appointments",
|
|
506
|
+
operations: {
|
|
507
|
+
view: {
|
|
508
|
+
output: {
|
|
509
|
+
schema: createSchema({
|
|
510
|
+
id: {
|
|
511
|
+
...recordIdSchema,
|
|
512
|
+
required: true
|
|
513
|
+
},
|
|
514
|
+
serviceDate: {
|
|
515
|
+
type: "date",
|
|
516
|
+
required: true
|
|
517
|
+
},
|
|
518
|
+
openingTime: {
|
|
519
|
+
type: "time",
|
|
520
|
+
required: true
|
|
521
|
+
},
|
|
522
|
+
scheduledAt: {
|
|
523
|
+
type: "dateTime",
|
|
524
|
+
required: true
|
|
525
|
+
}
|
|
526
|
+
}),
|
|
527
|
+
mode: "replace"
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
create: {
|
|
531
|
+
body: {
|
|
532
|
+
schema: createSchema({}),
|
|
533
|
+
mode: "create"
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const { knex } = createKnexDouble([
|
|
539
|
+
{
|
|
540
|
+
id: 3,
|
|
541
|
+
service_date: new Date("2026-08-13T23:59:58.123Z"),
|
|
542
|
+
opening_time: "07:08:09.123456",
|
|
543
|
+
scheduled_at: "2026-08-13 07:08:09.123456"
|
|
544
|
+
}
|
|
545
|
+
]);
|
|
546
|
+
const repository = createCrudResourceRuntime(resource, knex);
|
|
547
|
+
|
|
548
|
+
const result = await repository.list();
|
|
549
|
+
|
|
550
|
+
assert.deepEqual(result.items, [
|
|
551
|
+
{
|
|
552
|
+
id: "3",
|
|
553
|
+
serviceDate: "2026-08-13",
|
|
554
|
+
openingTime: "07:08:09.123456",
|
|
555
|
+
scheduledAt: "2026-08-13T07:08:09.123456Z"
|
|
556
|
+
}
|
|
557
|
+
]);
|
|
558
|
+
});
|
|
559
|
+
|
|
502
560
|
test("list respects bound list config", async () => {
|
|
503
561
|
const { knex, calls } = createKnexDouble([
|
|
504
562
|
{ contact_id: 3, first_name: "Tony" },
|