@jskit-ai/crud-core 0.1.40 → 0.1.42
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 +2 -2
- package/package.json +7 -6
- package/src/client/composables/createCrudClientSupport.js +2 -2
- package/src/client/composables/crudClientSupportHelpers.js +8 -7
- package/src/server/createCrudRepositoryFromResource.js +3 -0
- package/src/server/lookupHydration.js +1 -1
- package/src/server/repositoryMethods.js +39 -9
- package/src/server/repositorySupport.js +57 -11
- package/test/createCrudClientSupport.test.js +5 -5
- package/test/createCrudRepositoryFromResource.test.js +103 -74
- package/test/repositorySupport.test.js +3 -3
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.42",
|
|
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.42"
|
|
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.42",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -26,11 +26,12 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@tanstack/vue-query": "^5.90.5",
|
|
29
|
-
"@jskit-ai/
|
|
30
|
-
"@jskit-ai/
|
|
31
|
-
"@jskit-ai/
|
|
32
|
-
"@jskit-ai/
|
|
33
|
-
"@jskit-ai/users-
|
|
29
|
+
"@jskit-ai/database-runtime": "0.1.34",
|
|
30
|
+
"@jskit-ai/kernel": "0.1.34",
|
|
31
|
+
"@jskit-ai/realtime": "0.1.33",
|
|
32
|
+
"@jskit-ai/shell-web": "0.1.33",
|
|
33
|
+
"@jskit-ai/users-core": "0.1.44",
|
|
34
|
+
"@jskit-ai/users-web": "0.1.49",
|
|
34
35
|
"typebox": "^1.0.81"
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -113,7 +113,7 @@ function useCrudClientContext(source = {}) {
|
|
|
113
113
|
return resolveCrudRecordPathTemplates(listPathTemplate, recordIdParam);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
function resolveRecordParams(recordIdLike =
|
|
116
|
+
function resolveRecordParams(recordIdLike = "", { recordIdParam = defaultRecordIdParam } = {}) {
|
|
117
117
|
return resolveCrudRecordPathParams(recordIdLike, recordIdParam);
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -122,7 +122,7 @@ function useCrudClientContext(source = {}) {
|
|
|
122
122
|
return crudListQueryKey(normalizedSurfaceId, workspaceSlugToken.value, crudConfig.namespace);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function viewQueryKey(surfaceId = "", recordId =
|
|
125
|
+
function viewQueryKey(surfaceId = "", recordId = "") {
|
|
126
126
|
const normalizedSurfaceId = String(surfaceId || paths.currentSurfaceId.value || "").trim();
|
|
127
127
|
return crudViewQueryKey(normalizedSurfaceId, workspaceSlugToken.value, recordId, crudConfig.namespace);
|
|
128
128
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { normalizeQueryToken, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
2
|
import { normalizeRouteVisibilityToken } from "@jskit-ai/kernel/shared/support/visibility";
|
|
3
3
|
import { formatDateTime } from "@jskit-ai/kernel/shared/support";
|
|
4
4
|
import {
|
|
@@ -58,13 +58,13 @@ function crudListQueryKey(surfaceId = "", workspaceSlug = "", namespace = "") {
|
|
|
58
58
|
]);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
function crudViewQueryKey(surfaceId = "", workspaceSlug = "", recordId =
|
|
61
|
+
function crudViewQueryKey(surfaceId = "", workspaceSlug = "", recordId = "", namespace = "") {
|
|
62
62
|
return Object.freeze([
|
|
63
63
|
...crudScopeQueryKey(namespace),
|
|
64
64
|
"view",
|
|
65
65
|
normalizeQueryToken(surfaceId),
|
|
66
66
|
normalizeQueryToken(workspaceSlug),
|
|
67
|
-
|
|
67
|
+
normalizeRecordId(recordId, { fallback: "0" })
|
|
68
68
|
]);
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -87,8 +87,9 @@ function toRouteRecordId(value) {
|
|
|
87
87
|
return toRouteRecordId(value[0]);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
return normalizeRecordId(value, {
|
|
91
|
+
fallback: ""
|
|
92
|
+
});
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
function normalizeCrudRouteParamName(value, { context = "normalizeCrudRouteParamName" } = {}) {
|
|
@@ -120,7 +121,7 @@ function resolveCrudRecordPathTemplates(relativePath = "", recordIdParam = "reco
|
|
|
120
121
|
});
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
function resolveCrudRecordPathParams(recordIdLike =
|
|
124
|
+
function resolveCrudRecordPathParams(recordIdLike = "", recordIdParam = "recordId") {
|
|
124
125
|
const normalizedRecordIdParam = normalizeCrudRouteParamName(recordIdParam, {
|
|
125
126
|
context: "resolveCrudRecordPathParams"
|
|
126
127
|
});
|
|
@@ -130,7 +131,7 @@ function resolveCrudRecordPathParams(recordIdLike = 0, recordIdParam = "recordId
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
return Object.freeze({
|
|
133
|
-
[normalizedRecordIdParam]:
|
|
134
|
+
[normalizedRecordIdParam]: normalizedRecordId
|
|
134
135
|
});
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
crudRepositoryUpdateById,
|
|
9
9
|
crudRepositoryDeleteById
|
|
10
10
|
} from "./repositoryMethods.js";
|
|
11
|
+
import { createWithTransaction } from "@jskit-ai/database-runtime/shared";
|
|
11
12
|
|
|
12
13
|
function createCrudRepositoryFromResource(resource = {}, { context = "crudRepository", list = {} } = {}) {
|
|
13
14
|
const runtime = createCrudRepositoryRuntime(resource, {
|
|
@@ -19,6 +20,7 @@ function createCrudRepositoryFromResource(resource = {}, { context = "crudReposi
|
|
|
19
20
|
if (typeof knex !== "function") {
|
|
20
21
|
throw new TypeError("crudRepository requires knex.");
|
|
21
22
|
}
|
|
23
|
+
const withTransaction = createWithTransaction(knex);
|
|
22
24
|
|
|
23
25
|
async function listRecords(query = {}, callOptions = {}, hooks = null) {
|
|
24
26
|
return crudRepositoryList(runtime, knex, query, options, callOptions, hooks);
|
|
@@ -49,6 +51,7 @@ function createCrudRepositoryFromResource(resource = {}, { context = "crudReposi
|
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
return Object.freeze({
|
|
54
|
+
withTransaction,
|
|
52
55
|
list: listRecords,
|
|
53
56
|
findById,
|
|
54
57
|
listByIds,
|
|
@@ -529,7 +529,7 @@ function resolveLookupVisibilityContext(
|
|
|
529
529
|
nextVisibilityContext.scopeOwnerId = parentVisibilityContext.scopeOwnerId;
|
|
530
530
|
}
|
|
531
531
|
if (providerOwnershipFilter === "user" || providerOwnershipFilter === "workspace_user") {
|
|
532
|
-
nextVisibilityContext.
|
|
532
|
+
nextVisibilityContext.userId = parentVisibilityContext.userId;
|
|
533
533
|
}
|
|
534
534
|
|
|
535
535
|
return nextVisibilityContext;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { toInsertDateTime } from "@jskit-ai/database-runtime/shared";
|
|
1
|
+
import { resolveInsertedRecordId, toInsertDateTime } from "@jskit-ai/database-runtime/shared";
|
|
2
2
|
import { applyVisibility, applyVisibilityOwners } from "@jskit-ai/database-runtime/shared/visibility";
|
|
3
3
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
4
|
-
import { normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import { normalizeRecordId, normalizeText, normalizeUniqueTextList } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
5
|
import { Check, Errors } from "typebox/value";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_LIST_LIMIT,
|
|
@@ -29,6 +29,14 @@ const ORDERED_LIST_CURSOR_VALUE_TYPE_KEY = "__jskitCursorValueType";
|
|
|
29
29
|
const ORDERED_LIST_CURSOR_VALUE_KEY = "value";
|
|
30
30
|
const ORDERED_LIST_CURSOR_VALUE_TYPE_DATE = "date";
|
|
31
31
|
|
|
32
|
+
function requireCrudRecordId(value, { context = "crudRepository" } = {}) {
|
|
33
|
+
const recordId = normalizeRecordId(value, { fallback: null });
|
|
34
|
+
if (!recordId) {
|
|
35
|
+
throw new TypeError(`${context} requires recordId.`);
|
|
36
|
+
}
|
|
37
|
+
return recordId;
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
function resolveRepositoryDefaults(resource = {}, repositoryMapping = {}) {
|
|
33
41
|
const resourceName = normalizeText(resource.resource);
|
|
34
42
|
const tableName = normalizeText(resource.tableName) || resourceName;
|
|
@@ -757,7 +765,12 @@ async function crudRepositoryList(runtime, knex, query = {}, repositoryOptions =
|
|
|
757
765
|
const pageRows = hasMore ? rows.slice(0, normalizedLimit) : rows;
|
|
758
766
|
const items = [];
|
|
759
767
|
for (const row of pageRows) {
|
|
760
|
-
const mappedRecord = mapRecordRow(
|
|
768
|
+
const mappedRecord = mapRecordRow(
|
|
769
|
+
row,
|
|
770
|
+
runtime.mapping.outputKeys,
|
|
771
|
+
runtime.mapping.columnOverrides,
|
|
772
|
+
{ recordIdKeys: runtime.mapping.outputRecordIdKeys }
|
|
773
|
+
);
|
|
761
774
|
if (!mappedRecord) {
|
|
762
775
|
continue;
|
|
763
776
|
}
|
|
@@ -855,6 +868,7 @@ async function crudRepositoryList(runtime, knex, query = {}, repositoryOptions =
|
|
|
855
868
|
|
|
856
869
|
async function crudRepositoryFindById(runtime, knex, recordId, repositoryOptions = {}, callOptions = {}, hooks = null) {
|
|
857
870
|
const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, repositoryOptions, callOptions);
|
|
871
|
+
const normalizedRecordId = requireCrudRecordId(recordId, { context: "crudRepositoryFindById" });
|
|
858
872
|
const methodHooks = normalizeCrudRepositoryHooks(
|
|
859
873
|
hooks,
|
|
860
874
|
["modifyQuery", "afterQuery", "transformReturnedRecord", "finalizeOutput"],
|
|
@@ -895,12 +909,17 @@ async function crudRepositoryFindById(runtime, knex, recordId, repositoryOptions
|
|
|
895
909
|
dbQuery = dbQuery
|
|
896
910
|
.where(visible)
|
|
897
911
|
.where({
|
|
898
|
-
[idColumn]:
|
|
912
|
+
[idColumn]: normalizedRecordId
|
|
899
913
|
});
|
|
900
914
|
|
|
901
915
|
const row = await dbQuery.first();
|
|
902
916
|
|
|
903
|
-
const mappedRecord = mapRecordRow(
|
|
917
|
+
const mappedRecord = mapRecordRow(
|
|
918
|
+
row,
|
|
919
|
+
runtime.mapping.outputKeys,
|
|
920
|
+
runtime.mapping.columnOverrides,
|
|
921
|
+
{ recordIdKeys: runtime.mapping.outputRecordIdKeys }
|
|
922
|
+
);
|
|
904
923
|
let records = [];
|
|
905
924
|
if (mappedRecord) {
|
|
906
925
|
const normalizedRecord = await normalizeRepositoryOutputRecord(runtime, mappedRecord, {
|
|
@@ -1032,7 +1051,12 @@ async function crudRepositoryListByIds(runtime, knex, ids = [], repositoryOption
|
|
|
1032
1051
|
|
|
1033
1052
|
const records = [];
|
|
1034
1053
|
for (const row of rows) {
|
|
1035
|
-
const mappedRecord = mapRecordRow(
|
|
1054
|
+
const mappedRecord = mapRecordRow(
|
|
1055
|
+
row,
|
|
1056
|
+
runtime.mapping.outputKeys,
|
|
1057
|
+
runtime.mapping.columnOverrides,
|
|
1058
|
+
{ recordIdKeys: runtime.mapping.outputRecordIdKeys }
|
|
1059
|
+
);
|
|
1036
1060
|
if (!mappedRecord) {
|
|
1037
1061
|
continue;
|
|
1038
1062
|
}
|
|
@@ -1206,7 +1230,11 @@ async function crudRepositoryCreate(runtime, knex, payload = {}, repositoryOptio
|
|
|
1206
1230
|
);
|
|
1207
1231
|
createQuery = createHookResult.queryBuilder;
|
|
1208
1232
|
|
|
1209
|
-
const
|
|
1233
|
+
const insertResult = await createQuery.insert(withOwners);
|
|
1234
|
+
const recordId = resolveInsertedRecordId(insertResult, { fallback: null });
|
|
1235
|
+
if (!recordId) {
|
|
1236
|
+
throw new Error("crudRepositoryCreate could not resolve inserted id.");
|
|
1237
|
+
}
|
|
1210
1238
|
|
|
1211
1239
|
const createdRecord = await crudRepositoryFindById(runtime, knex, recordId, repositoryOptions, {
|
|
1212
1240
|
...callOptions,
|
|
@@ -1237,6 +1265,7 @@ async function crudRepositoryCreate(runtime, knex, payload = {}, repositoryOptio
|
|
|
1237
1265
|
|
|
1238
1266
|
async function crudRepositoryUpdateById(runtime, knex, recordId, patch = {}, repositoryOptions = {}, callOptions = {}, hooks = null) {
|
|
1239
1267
|
const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, repositoryOptions, callOptions);
|
|
1268
|
+
const normalizedRecordId = requireCrudRecordId(recordId, { context: "crudRepositoryUpdateById" });
|
|
1240
1269
|
const methodHooks = normalizeCrudRepositoryHooks(hooks, ["modifyPatch", "modifyQuery", "afterWrite"], {
|
|
1241
1270
|
context: "crudRepositoryUpdateById"
|
|
1242
1271
|
});
|
|
@@ -1299,7 +1328,7 @@ async function crudRepositoryUpdateById(runtime, knex, recordId, patch = {}, rep
|
|
|
1299
1328
|
updateQuery = updateQuery
|
|
1300
1329
|
.where(visible)
|
|
1301
1330
|
.where({
|
|
1302
|
-
[idColumn]:
|
|
1331
|
+
[idColumn]: normalizedRecordId
|
|
1303
1332
|
});
|
|
1304
1333
|
|
|
1305
1334
|
await updateQuery.update(dbPatch);
|
|
@@ -1333,6 +1362,7 @@ async function crudRepositoryUpdateById(runtime, knex, recordId, patch = {}, rep
|
|
|
1333
1362
|
|
|
1334
1363
|
async function crudRepositoryDeleteById(runtime, knex, recordId, repositoryOptions = {}, callOptions = {}, hooks = null) {
|
|
1335
1364
|
const { client, tableName, idColumn, visible } = resolveCrudRepositoryCall(runtime, knex, repositoryOptions, callOptions);
|
|
1365
|
+
const normalizedRecordId = requireCrudRecordId(recordId, { context: "crudRepositoryDeleteById" });
|
|
1336
1366
|
const methodHooks = normalizeCrudRepositoryHooks(hooks, ["modifyQuery", "finalizeOutput", "afterWrite"], {
|
|
1337
1367
|
context: "crudRepositoryDeleteById"
|
|
1338
1368
|
});
|
|
@@ -1394,7 +1424,7 @@ async function crudRepositoryDeleteById(runtime, knex, recordId, repositoryOptio
|
|
|
1394
1424
|
deleteQuery = deleteQuery
|
|
1395
1425
|
.where(visible)
|
|
1396
1426
|
.where({
|
|
1397
|
-
[idColumn]:
|
|
1427
|
+
[idColumn]: normalizedRecordId
|
|
1398
1428
|
});
|
|
1399
1429
|
|
|
1400
1430
|
await deleteQuery.delete();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { normalizeDbRecordId } from "@jskit-ai/database-runtime/shared";
|
|
1
2
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
-
import {
|
|
3
|
+
import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
|
|
4
|
+
import { normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
5
|
import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
|
|
4
6
|
import { toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
|
|
5
7
|
import {
|
|
@@ -13,24 +15,24 @@ const MAX_LIST_LIMIT = 100;
|
|
|
13
15
|
|
|
14
16
|
function normalizeCrudListCursor(cursor = null, { allowEmpty = true } = {}) {
|
|
15
17
|
if (cursor === undefined || cursor === null) {
|
|
16
|
-
return allowEmpty === true ?
|
|
18
|
+
return allowEmpty === true ? "" : null;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
const normalizedCursor = typeof cursor === "string"
|
|
20
22
|
? cursor.trim()
|
|
21
23
|
: cursor;
|
|
22
24
|
if (normalizedCursor === "" || normalizedCursor === 0 || normalizedCursor === "0") {
|
|
23
|
-
return allowEmpty === true ?
|
|
25
|
+
return allowEmpty === true ? "" : null;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const
|
|
27
|
-
if (!
|
|
28
|
+
const recordId = normalizeRecordId(normalizedCursor, { fallback: null });
|
|
29
|
+
if (!recordId) {
|
|
28
30
|
throw new AppError(400, "Invalid cursor.", {
|
|
29
31
|
code: "INVALID_CURSOR"
|
|
30
32
|
});
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
return
|
|
35
|
+
return recordId;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
function normalizeCrudListLimit(value, { fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT } = {}) {
|
|
@@ -152,6 +154,29 @@ function schemaIncludesStringType(schema = {}) {
|
|
|
152
154
|
return variants.some((entry) => schemaIncludesStringType(entry));
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
function schemaIncludesRecordIdType(schema = {}) {
|
|
158
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const type = Array.isArray(schema.type)
|
|
163
|
+
? schema.type.map((entry) => normalizeText(entry).toLowerCase()).filter(Boolean)
|
|
164
|
+
: normalizeText(schema.type).toLowerCase();
|
|
165
|
+
const hasStringType = Array.isArray(type)
|
|
166
|
+
? type.includes("string")
|
|
167
|
+
: type === "string";
|
|
168
|
+
if (hasStringType && normalizeText(schema.pattern) === RECORD_ID_PATTERN) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const variants = Array.isArray(schema.anyOf)
|
|
173
|
+
? schema.anyOf
|
|
174
|
+
: Array.isArray(schema.oneOf)
|
|
175
|
+
? schema.oneOf
|
|
176
|
+
: [];
|
|
177
|
+
return variants.some((entry) => schemaIncludesRecordIdType(entry));
|
|
178
|
+
}
|
|
179
|
+
|
|
155
180
|
function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRepository" } = {}) {
|
|
156
181
|
if (!resource || typeof resource !== "object" || Array.isArray(resource)) {
|
|
157
182
|
throw new TypeError(`${context} requires resource object.`);
|
|
@@ -214,20 +239,33 @@ function deriveRepositoryMappingFromResource(resource = {}, { context = "crudRep
|
|
|
214
239
|
parentFilterColumns[key] = columnName;
|
|
215
240
|
}
|
|
216
241
|
|
|
242
|
+
const outputRecordIdKeys = [];
|
|
243
|
+
for (const [key, schema] of Object.entries(outputProperties)) {
|
|
244
|
+
if (schemaIncludesRecordIdType(schema)) {
|
|
245
|
+
outputRecordIdKeys.push(key);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
217
249
|
return Object.freeze({
|
|
218
250
|
outputKeys,
|
|
219
251
|
writeKeys,
|
|
220
252
|
columnOverrides: Object.freeze(columnOverrides),
|
|
221
253
|
listSearchColumns: Object.freeze(listSearchColumns),
|
|
222
|
-
parentFilterColumns: Object.freeze(parentFilterColumns)
|
|
254
|
+
parentFilterColumns: Object.freeze(parentFilterColumns),
|
|
255
|
+
outputRecordIdKeys: Object.freeze(outputRecordIdKeys)
|
|
223
256
|
});
|
|
224
257
|
}
|
|
225
258
|
|
|
226
|
-
function mapRecordRow(row, fieldKeys = [], overrides = {}) {
|
|
259
|
+
function mapRecordRow(row, fieldKeys = [], overrides = {}, { recordIdKeys = [] } = {}) {
|
|
227
260
|
if (!row) {
|
|
228
261
|
return null;
|
|
229
262
|
}
|
|
230
263
|
|
|
264
|
+
const recordIdKeySet = new Set(
|
|
265
|
+
(Array.isArray(recordIdKeys) ? recordIdKeys : [])
|
|
266
|
+
.map((key) => String(key || "").trim())
|
|
267
|
+
.filter(Boolean)
|
|
268
|
+
);
|
|
231
269
|
const mapped = {};
|
|
232
270
|
for (const key of fieldKeys) {
|
|
233
271
|
const normalizedKey = String(key || "").trim();
|
|
@@ -235,7 +273,15 @@ function mapRecordRow(row, fieldKeys = [], overrides = {}) {
|
|
|
235
273
|
if (!normalizedKey || !columnName) {
|
|
236
274
|
continue;
|
|
237
275
|
}
|
|
238
|
-
|
|
276
|
+
|
|
277
|
+
const rawValue = row[columnName];
|
|
278
|
+
if (recordIdKeySet.has(normalizedKey)) {
|
|
279
|
+
const normalizedIdValue = normalizeDbRecordId(rawValue, { fallback: null });
|
|
280
|
+
mapped[normalizedKey] = normalizedIdValue || rawValue;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
mapped[normalizedKey] = rawValue;
|
|
239
285
|
}
|
|
240
286
|
return mapped;
|
|
241
287
|
}
|
|
@@ -244,7 +290,7 @@ function applyCrudListQueryFilters(
|
|
|
244
290
|
query,
|
|
245
291
|
{
|
|
246
292
|
idColumn = "id",
|
|
247
|
-
cursor =
|
|
293
|
+
cursor = "",
|
|
248
294
|
applyCursor = true,
|
|
249
295
|
q = "",
|
|
250
296
|
searchColumns = [],
|
|
@@ -307,7 +353,7 @@ function applyCrudListQueryFilters(
|
|
|
307
353
|
const normalizedIdColumn = String(idColumn || "").trim() || "id";
|
|
308
354
|
if (applyCursor !== false) {
|
|
309
355
|
const normalizedCursor = normalizeCrudListCursor(cursor);
|
|
310
|
-
if (normalizedCursor
|
|
356
|
+
if (normalizedCursor) {
|
|
311
357
|
nextQuery = nextQuery.where(normalizedIdColumn, ">", normalizedCursor);
|
|
312
358
|
}
|
|
313
359
|
}
|
|
@@ -61,7 +61,7 @@ test("crudListQueryKey and crudViewQueryKey normalize cache keys", () => {
|
|
|
61
61
|
"view",
|
|
62
62
|
"admin",
|
|
63
63
|
"tonymobily3",
|
|
64
|
-
12
|
|
64
|
+
"12"
|
|
65
65
|
]);
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -85,9 +85,9 @@ test("invalidateCrudQueries invalidates by CRUD namespace scope key", async () =
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
test("toRouteRecordId parses scalar and array params safely", () => {
|
|
88
|
-
assert.equal(toRouteRecordId("42"), 42);
|
|
89
|
-
assert.equal(toRouteRecordId(["7"]), 7);
|
|
90
|
-
assert.equal(toRouteRecordId("not-a-number"),
|
|
88
|
+
assert.equal(toRouteRecordId("42"), "42");
|
|
89
|
+
assert.equal(toRouteRecordId(["7"]), "7");
|
|
90
|
+
assert.equal(toRouteRecordId("not-a-number"), "");
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
test("normalizeCrudRouteParamName validates route parameter names", () => {
|
|
@@ -114,7 +114,7 @@ test("resolveCrudRecordPathTemplates supports custom route parameter names", ()
|
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
test("resolveCrudRecordPathParams maps record ids to selected route parameter names", () => {
|
|
117
|
-
assert.deepEqual(resolveCrudRecordPathParams(42, "addressId"), { addressId: "42" });
|
|
117
|
+
assert.deepEqual(resolveCrudRecordPathParams("42", "addressId"), { addressId: "42" });
|
|
118
118
|
assert.deepEqual(resolveCrudRecordPathParams("7", "recordId"), { recordId: "7" });
|
|
119
119
|
assert.deepEqual(resolveCrudRecordPathParams("invalid", "addressId"), {});
|
|
120
120
|
});
|
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { RECORD_ID_PATTERN } from "@jskit-ai/kernel/shared/validators";
|
|
3
4
|
import { createCrudRepositoryFromResource } from "../src/server/createCrudRepositoryFromResource.js";
|
|
4
5
|
|
|
6
|
+
const recordIdSchema = Object.freeze({
|
|
7
|
+
type: "string",
|
|
8
|
+
pattern: RECORD_ID_PATTERN
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const nullableRecordIdSchema = Object.freeze({
|
|
12
|
+
anyOf: [
|
|
13
|
+
recordIdSchema,
|
|
14
|
+
{ type: "null" }
|
|
15
|
+
]
|
|
16
|
+
});
|
|
17
|
+
|
|
5
18
|
function createListKnexDouble(
|
|
6
19
|
rows = [],
|
|
7
20
|
{
|
|
@@ -185,7 +198,7 @@ function createResourceFixture() {
|
|
|
185
198
|
schema: {
|
|
186
199
|
type: "object",
|
|
187
200
|
properties: {
|
|
188
|
-
id:
|
|
201
|
+
id: recordIdSchema,
|
|
189
202
|
firstName: { type: "string" }
|
|
190
203
|
}
|
|
191
204
|
}
|
|
@@ -222,10 +235,10 @@ function createLookupResourceFixture() {
|
|
|
222
235
|
schema: {
|
|
223
236
|
type: "object",
|
|
224
237
|
properties: {
|
|
225
|
-
id:
|
|
238
|
+
id: recordIdSchema,
|
|
226
239
|
firstName: { type: "string" },
|
|
227
|
-
primaryVetId:
|
|
228
|
-
secondaryVetId:
|
|
240
|
+
primaryVetId: recordIdSchema,
|
|
241
|
+
secondaryVetId: nullableRecordIdSchema,
|
|
229
242
|
lookups: {
|
|
230
243
|
type: "object"
|
|
231
244
|
}
|
|
@@ -239,8 +252,8 @@ function createLookupResourceFixture() {
|
|
|
239
252
|
type: "object",
|
|
240
253
|
properties: {
|
|
241
254
|
firstName: { type: "string" },
|
|
242
|
-
primaryVetId:
|
|
243
|
-
secondaryVetId:
|
|
255
|
+
primaryVetId: recordIdSchema,
|
|
256
|
+
secondaryVetId: recordIdSchema
|
|
244
257
|
}
|
|
245
258
|
}
|
|
246
259
|
}
|
|
@@ -289,10 +302,10 @@ function createLookupResourceWithCustomContainerKeyFixture() {
|
|
|
289
302
|
schema: {
|
|
290
303
|
type: "object",
|
|
291
304
|
properties: {
|
|
292
|
-
id:
|
|
305
|
+
id: recordIdSchema,
|
|
293
306
|
firstName: { type: "string" },
|
|
294
|
-
primaryVetId:
|
|
295
|
-
secondaryVetId:
|
|
307
|
+
primaryVetId: recordIdSchema,
|
|
308
|
+
secondaryVetId: recordIdSchema,
|
|
296
309
|
lookupData: {
|
|
297
310
|
type: "object"
|
|
298
311
|
}
|
|
@@ -306,8 +319,8 @@ function createLookupResourceWithCustomContainerKeyFixture() {
|
|
|
306
319
|
type: "object",
|
|
307
320
|
properties: {
|
|
308
321
|
firstName: { type: "string" },
|
|
309
|
-
primaryVetId:
|
|
310
|
-
secondaryVetId:
|
|
322
|
+
primaryVetId: recordIdSchema,
|
|
323
|
+
secondaryVetId: recordIdSchema
|
|
311
324
|
}
|
|
312
325
|
}
|
|
313
326
|
}
|
|
@@ -351,7 +364,7 @@ function createCollectionLookupResourceFixture() {
|
|
|
351
364
|
schema: {
|
|
352
365
|
type: "object",
|
|
353
366
|
properties: {
|
|
354
|
-
id:
|
|
367
|
+
id: recordIdSchema,
|
|
355
368
|
firstName: { type: "string" },
|
|
356
369
|
lookups: {
|
|
357
370
|
type: "object"
|
|
@@ -400,9 +413,9 @@ function createPetsLookupBackToContactsResourceFixture() {
|
|
|
400
413
|
schema: {
|
|
401
414
|
type: "object",
|
|
402
415
|
properties: {
|
|
403
|
-
id:
|
|
416
|
+
id: recordIdSchema,
|
|
404
417
|
name: { type: "string" },
|
|
405
|
-
customerId:
|
|
418
|
+
customerId: recordIdSchema,
|
|
406
419
|
lookups: {
|
|
407
420
|
type: "object"
|
|
408
421
|
}
|
|
@@ -450,14 +463,14 @@ function createNormalizedResourceFixture() {
|
|
|
450
463
|
schema: {
|
|
451
464
|
type: "object",
|
|
452
465
|
properties: {
|
|
453
|
-
id:
|
|
466
|
+
id: recordIdSchema,
|
|
454
467
|
firstName: { type: "string" }
|
|
455
468
|
},
|
|
456
469
|
required: ["id", "firstName"]
|
|
457
470
|
},
|
|
458
471
|
normalize(payload = {}) {
|
|
459
472
|
return {
|
|
460
|
-
id:
|
|
473
|
+
id: String(payload.id || ""),
|
|
461
474
|
firstName: String(payload.firstName || "").trim()
|
|
462
475
|
};
|
|
463
476
|
}
|
|
@@ -494,7 +507,7 @@ function createWritableHookResourceFixture() {
|
|
|
494
507
|
schema: {
|
|
495
508
|
type: "object",
|
|
496
509
|
properties: {
|
|
497
|
-
id:
|
|
510
|
+
id: recordIdSchema,
|
|
498
511
|
firstName: { type: "string" },
|
|
499
512
|
createdAt: { type: "string" },
|
|
500
513
|
updatedAt: { type: "string" }
|
|
@@ -545,7 +558,7 @@ test("createCrudRepositoryFromResource requires table metadata from resource", (
|
|
|
545
558
|
schema: {
|
|
546
559
|
type: "object",
|
|
547
560
|
properties: {
|
|
548
|
-
id:
|
|
561
|
+
id: recordIdSchema
|
|
549
562
|
}
|
|
550
563
|
}
|
|
551
564
|
}
|
|
@@ -576,21 +589,21 @@ test("createCrudRepositoryFromResource defaults table and id columns from resour
|
|
|
576
589
|
const repository = createRepository(knex);
|
|
577
590
|
|
|
578
591
|
const result = await repository.list({
|
|
579
|
-
cursor: 2,
|
|
592
|
+
cursor: "2",
|
|
580
593
|
q: "to"
|
|
581
594
|
});
|
|
582
595
|
|
|
583
596
|
assert.deepEqual(result, {
|
|
584
597
|
items: [
|
|
585
598
|
{
|
|
586
|
-
id: 3,
|
|
599
|
+
id: "3",
|
|
587
600
|
firstName: "Tony"
|
|
588
601
|
}
|
|
589
602
|
],
|
|
590
603
|
nextCursor: null
|
|
591
604
|
});
|
|
592
605
|
assert.equal(calls[0][1], "contacts_table");
|
|
593
|
-
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "contact_id" && call[2] === ">" && call[3] === 2));
|
|
606
|
+
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "contact_id" && call[2] === ">" && call[3] === "2"));
|
|
594
607
|
});
|
|
595
608
|
|
|
596
609
|
test("createCrudRepositoryFromResource createRepository requires knex", () => {
|
|
@@ -601,6 +614,22 @@ test("createCrudRepositoryFromResource createRepository requires knex", () => {
|
|
|
601
614
|
);
|
|
602
615
|
});
|
|
603
616
|
|
|
617
|
+
test("createCrudRepositoryFromResource adds withTransaction to repository instances", async () => {
|
|
618
|
+
const createRepository = createCrudRepositoryFromResource(createResourceFixture());
|
|
619
|
+
const knex = Object.assign(() => {
|
|
620
|
+
throw new Error("query execution not expected");
|
|
621
|
+
}, {
|
|
622
|
+
async transaction(work) {
|
|
623
|
+
return work({ trxId: "trx-1" });
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const repository = createRepository(knex);
|
|
628
|
+
assert.equal(typeof repository.withTransaction, "function");
|
|
629
|
+
const result = await repository.withTransaction(async (trx) => ({ id: trx.trxId }));
|
|
630
|
+
assert.deepEqual(result, { id: "trx-1" });
|
|
631
|
+
});
|
|
632
|
+
|
|
604
633
|
test("createCrudRepositoryFromResource allows list tuning through list config", async () => {
|
|
605
634
|
const createRepository = createCrudRepositoryFromResource(createResourceFixture(), {
|
|
606
635
|
list: {
|
|
@@ -646,8 +675,8 @@ test("createCrudRepositoryFromResource supports declarative ordered list paginat
|
|
|
646
675
|
|
|
647
676
|
assert.deepEqual(result, {
|
|
648
677
|
items: [
|
|
649
|
-
{ id: 9, firstName: "Tina" },
|
|
650
|
-
{ id: 7, firstName: "Tony" }
|
|
678
|
+
{ id: "9", firstName: "Tina" },
|
|
679
|
+
{ id: "7", firstName: "Tony" }
|
|
651
680
|
],
|
|
652
681
|
nextCursor: Buffer.from(
|
|
653
682
|
JSON.stringify({ values: ["2026-04-04T09:00:00.000Z", 7] }),
|
|
@@ -855,11 +884,11 @@ test("createCrudRepositoryFromResource exposes listByIds for lookup providers",
|
|
|
855
884
|
]);
|
|
856
885
|
const repository = createRepository(knex);
|
|
857
886
|
|
|
858
|
-
const records = await repository.listByIds([3, 3, 4]);
|
|
887
|
+
const records = await repository.listByIds(["3", "3", "4"]);
|
|
859
888
|
|
|
860
889
|
assert.equal(records.length, 1);
|
|
861
890
|
assert.deepEqual(records[0], {
|
|
862
|
-
id: 3,
|
|
891
|
+
id: "3",
|
|
863
892
|
firstName: "Tony"
|
|
864
893
|
});
|
|
865
894
|
assert.ok(calls.some((call) => call[0] === "whereIn" && call[1] === "contact_id"));
|
|
@@ -879,7 +908,7 @@ test("createCrudRepositoryFromResource exposes listByForeignIds for arbitrary ou
|
|
|
879
908
|
|
|
880
909
|
assert.equal(records.length, 1);
|
|
881
910
|
assert.deepEqual(records[0], {
|
|
882
|
-
id: 3,
|
|
911
|
+
id: "3",
|
|
883
912
|
firstName: "Tony"
|
|
884
913
|
});
|
|
885
914
|
assert.ok(calls.some((call) => call[0] === "whereIn" && call[1] === "first_name"));
|
|
@@ -913,7 +942,7 @@ test("createCrudRepositoryFromResource listByIds fails fast when valueKey is not
|
|
|
913
942
|
|
|
914
943
|
await assert.rejects(
|
|
915
944
|
() =>
|
|
916
|
-
repository.listByIds([3], {
|
|
945
|
+
repository.listByIds(["3"], {
|
|
917
946
|
valueKey: "externalCustomerId"
|
|
918
947
|
}),
|
|
919
948
|
/valueKey "externalCustomerId" to exist in output schema/
|
|
@@ -930,10 +959,10 @@ test("createCrudRepositoryFromResource normalizes listByIds output using resourc
|
|
|
930
959
|
]);
|
|
931
960
|
const repository = createRepository(knex);
|
|
932
961
|
|
|
933
|
-
const result = await repository.listByIds([3]);
|
|
962
|
+
const result = await repository.listByIds(["3"]);
|
|
934
963
|
assert.deepEqual(result, [
|
|
935
964
|
{
|
|
936
|
-
id: 3,
|
|
965
|
+
id: "3",
|
|
937
966
|
firstName: "Tony"
|
|
938
967
|
}
|
|
939
968
|
]);
|
|
@@ -943,14 +972,14 @@ test("createCrudRepositoryFromResource fails when mapped output violates resourc
|
|
|
943
972
|
const createRepository = createCrudRepositoryFromResource(createResourceFixture());
|
|
944
973
|
const { knex } = createListKnexDouble([
|
|
945
974
|
{
|
|
946
|
-
contact_id: "
|
|
975
|
+
contact_id: "invalid-id",
|
|
947
976
|
first_name: "Tony"
|
|
948
977
|
}
|
|
949
978
|
]);
|
|
950
979
|
const repository = createRepository(knex);
|
|
951
980
|
|
|
952
981
|
await assert.rejects(
|
|
953
|
-
() => repository.listByIds([3]),
|
|
982
|
+
() => repository.listByIds(["3"]),
|
|
954
983
|
/output validation failed/
|
|
955
984
|
);
|
|
956
985
|
});
|
|
@@ -983,8 +1012,8 @@ test("createCrudRepositoryFromResource hydrates lookup relations by default and
|
|
|
983
1012
|
options
|
|
984
1013
|
});
|
|
985
1014
|
return [
|
|
986
|
-
{ id: 10, name: "Vet A" },
|
|
987
|
-
{ id: 12, name: "Vet B" }
|
|
1015
|
+
{ id: "10", name: "Vet A" },
|
|
1016
|
+
{ id: "12", name: "Vet B" }
|
|
988
1017
|
];
|
|
989
1018
|
}
|
|
990
1019
|
};
|
|
@@ -994,16 +1023,16 @@ test("createCrudRepositoryFromResource hydrates lookup relations by default and
|
|
|
994
1023
|
const result = await repository.list({});
|
|
995
1024
|
|
|
996
1025
|
assert.equal(lookupCalls.length, 1);
|
|
997
|
-
assert.deepEqual(lookupCalls[0].ids, [10, 12]);
|
|
1026
|
+
assert.deepEqual(lookupCalls[0].ids, ["10", "12"]);
|
|
998
1027
|
assert.equal(lookupCalls[0].options.include, "*");
|
|
999
1028
|
assert.equal(lookupCalls[0].options.lookupDepth, 1);
|
|
1000
1029
|
assert.equal(lookupCalls[0].options.lookupMaxDepth, 3);
|
|
1001
1030
|
assert.deepEqual(result.items[0].lookups, {
|
|
1002
|
-
primaryVetId: { id: 10, name: "Vet A" },
|
|
1003
|
-
secondaryVetId: { id: 12, name: "Vet B" }
|
|
1031
|
+
primaryVetId: { id: "10", name: "Vet A" },
|
|
1032
|
+
secondaryVetId: { id: "12", name: "Vet B" }
|
|
1004
1033
|
});
|
|
1005
1034
|
assert.deepEqual(result.items[1].lookups, {
|
|
1006
|
-
primaryVetId: { id: 10, name: "Vet A" },
|
|
1035
|
+
primaryVetId: { id: "10", name: "Vet A" },
|
|
1007
1036
|
secondaryVetId: null
|
|
1008
1037
|
});
|
|
1009
1038
|
});
|
|
@@ -1023,8 +1052,8 @@ test("createCrudRepositoryFromResource writes hydrated lookups into custom outpu
|
|
|
1023
1052
|
return {
|
|
1024
1053
|
async listByIds() {
|
|
1025
1054
|
return [
|
|
1026
|
-
{ id: 10, name: "Vet A" },
|
|
1027
|
-
{ id: 12, name: "Vet B" }
|
|
1055
|
+
{ id: "10", name: "Vet A" },
|
|
1056
|
+
{ id: "12", name: "Vet B" }
|
|
1028
1057
|
];
|
|
1029
1058
|
}
|
|
1030
1059
|
};
|
|
@@ -1034,8 +1063,8 @@ test("createCrudRepositoryFromResource writes hydrated lookups into custom outpu
|
|
|
1034
1063
|
const result = await repository.list({});
|
|
1035
1064
|
assert.equal(Object.hasOwn(result.items[0], "lookups"), false);
|
|
1036
1065
|
assert.deepEqual(result.items[0].lookupData, {
|
|
1037
|
-
primaryVetId: { id: 10, name: "Vet A" },
|
|
1038
|
-
secondaryVetId: { id: 12, name: "Vet B" }
|
|
1066
|
+
primaryVetId: { id: "10", name: "Vet A" },
|
|
1067
|
+
secondaryVetId: { id: "12", name: "Vet B" }
|
|
1039
1068
|
});
|
|
1040
1069
|
});
|
|
1041
1070
|
|
|
@@ -1113,7 +1142,7 @@ test("createCrudRepositoryFromResource forwards nested include paths to child lo
|
|
|
1113
1142
|
ids,
|
|
1114
1143
|
options
|
|
1115
1144
|
});
|
|
1116
|
-
return [{ id: 10, name: "Vet A" }, { id: 12, name: "Vet B" }];
|
|
1145
|
+
return [{ id: "10", name: "Vet A" }, { id: "12", name: "Vet B" }];
|
|
1117
1146
|
}
|
|
1118
1147
|
};
|
|
1119
1148
|
}
|
|
@@ -1147,7 +1176,7 @@ test("createCrudRepositoryFromResource forwards wildcard nested include paths to
|
|
|
1147
1176
|
ids,
|
|
1148
1177
|
options
|
|
1149
1178
|
});
|
|
1150
|
-
return [{ id: 10, name: "Vet A" }, { id: 12, name: "Vet B" }];
|
|
1179
|
+
return [{ id: "10", name: "Vet A" }, { id: "12", name: "Vet B" }];
|
|
1151
1180
|
}
|
|
1152
1181
|
};
|
|
1153
1182
|
}
|
|
@@ -1182,7 +1211,7 @@ test("createCrudRepositoryFromResource remaps child lookup visibility for public
|
|
|
1182
1211
|
ids,
|
|
1183
1212
|
options
|
|
1184
1213
|
});
|
|
1185
|
-
return [{ id: 10, name: "Vet A" }, { id: 12, name: "Vet B" }];
|
|
1214
|
+
return [{ id: "10", name: "Vet A" }, { id: "12", name: "Vet B" }];
|
|
1186
1215
|
}
|
|
1187
1216
|
};
|
|
1188
1217
|
}
|
|
@@ -1222,7 +1251,7 @@ test("createCrudRepositoryFromResource remaps child lookup visibility for worksp
|
|
|
1222
1251
|
ids,
|
|
1223
1252
|
options
|
|
1224
1253
|
});
|
|
1225
|
-
return [{ id: 10, name: "Vet A" }, { id: 12, name: "Vet B" }];
|
|
1254
|
+
return [{ id: "10", name: "Vet A" }, { id: "12", name: "Vet B" }];
|
|
1226
1255
|
}
|
|
1227
1256
|
};
|
|
1228
1257
|
}
|
|
@@ -1232,7 +1261,7 @@ test("createCrudRepositoryFromResource remaps child lookup visibility for worksp
|
|
|
1232
1261
|
visibilityContext: {
|
|
1233
1262
|
visibility: "workspace_user",
|
|
1234
1263
|
scopeOwnerId: "workspace-1",
|
|
1235
|
-
|
|
1264
|
+
userId: "user-1"
|
|
1236
1265
|
}
|
|
1237
1266
|
});
|
|
1238
1267
|
|
|
@@ -1299,9 +1328,9 @@ test("createCrudRepositoryFromResource hydrates collection relations through lis
|
|
|
1299
1328
|
options
|
|
1300
1329
|
});
|
|
1301
1330
|
return [
|
|
1302
|
-
{ id: 11, name: "Milo", customerId: 3 },
|
|
1303
|
-
{ id: 12, name: "Luna", customerId: 3 },
|
|
1304
|
-
{ id: 20, name: "Ruby", customerId: 4 }
|
|
1331
|
+
{ id: "11", name: "Milo", customerId: "3" },
|
|
1332
|
+
{ id: "12", name: "Luna", customerId: "3" },
|
|
1333
|
+
{ id: "20", name: "Ruby", customerId: "4" }
|
|
1305
1334
|
];
|
|
1306
1335
|
}
|
|
1307
1336
|
};
|
|
@@ -1313,7 +1342,7 @@ test("createCrudRepositoryFromResource hydrates collection relations through lis
|
|
|
1313
1342
|
});
|
|
1314
1343
|
|
|
1315
1344
|
assert.equal(lookupCalls.length, 1);
|
|
1316
|
-
assert.deepEqual(lookupCalls[0].ids, [3, 4]);
|
|
1345
|
+
assert.deepEqual(lookupCalls[0].ids, ["3", "4"]);
|
|
1317
1346
|
assert.equal(lookupCalls[0].options.include, "none");
|
|
1318
1347
|
assert.equal(lookupCalls[0].options.valueKey, "customerId");
|
|
1319
1348
|
assert.deepEqual(result.items[0].lookups?.pets?.map((item) => item.name), ["Milo", "Luna"]);
|
|
@@ -1402,7 +1431,7 @@ test("createCrudRepositoryFromResource forwards configured lookup maxDepth to ch
|
|
|
1402
1431
|
ids,
|
|
1403
1432
|
options
|
|
1404
1433
|
});
|
|
1405
|
-
return [{ id: 10, name: "Vet A" }, { id: 12, name: "Vet B" }];
|
|
1434
|
+
return [{ id: "10", name: "Vet A" }, { id: "12", name: "Vet B" }];
|
|
1406
1435
|
}
|
|
1407
1436
|
};
|
|
1408
1437
|
}
|
|
@@ -1598,7 +1627,7 @@ test("createCrudRepositoryFromResource list hooks keep visibility and canonical
|
|
|
1598
1627
|
const repository = createRepository(knex);
|
|
1599
1628
|
|
|
1600
1629
|
await repository.list({
|
|
1601
|
-
cursor: 2,
|
|
1630
|
+
cursor: "2",
|
|
1602
1631
|
limit: 5
|
|
1603
1632
|
}, {
|
|
1604
1633
|
visibilityContext: {
|
|
@@ -1612,8 +1641,8 @@ test("createCrudRepositoryFromResource list hooks keep visibility and canonical
|
|
|
1612
1641
|
}
|
|
1613
1642
|
});
|
|
1614
1643
|
|
|
1615
|
-
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "contact_id" && call[2] === ">" && call[3] === 2));
|
|
1616
|
-
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "
|
|
1644
|
+
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "contact_id" && call[2] === ">" && call[3] === "2"));
|
|
1645
|
+
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "workspace_id" && call[2] === "workspace-1"));
|
|
1617
1646
|
assert.ok(calls.some((call) => call[0] === "clearOrder"));
|
|
1618
1647
|
assert.ok(calls.some((call) => call[0] === "clear" && call[1] === "limit"));
|
|
1619
1648
|
assert.ok(calls.some((call) => call[0] === "orderBy" && call[1] === "contact_id" && call[2] === "asc"));
|
|
@@ -1630,7 +1659,7 @@ test("createCrudRepositoryFromResource findById hooks keep visibility and id pre
|
|
|
1630
1659
|
]);
|
|
1631
1660
|
const repository = createRepository(knex);
|
|
1632
1661
|
|
|
1633
|
-
await repository.findById(7, {
|
|
1662
|
+
await repository.findById("7", {
|
|
1634
1663
|
visibilityContext: {
|
|
1635
1664
|
visibility: "workspace",
|
|
1636
1665
|
scopeOwnerId: "workspace-1"
|
|
@@ -1643,8 +1672,8 @@ test("createCrudRepositoryFromResource findById hooks keep visibility and id pre
|
|
|
1643
1672
|
});
|
|
1644
1673
|
|
|
1645
1674
|
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "contact_id" && call[2] === 999));
|
|
1646
|
-
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "
|
|
1647
|
-
assert.ok(calls.some((call) => call[0] === "where" && call[1]?.contact_id === 7));
|
|
1675
|
+
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "workspace_id" && call[2] === "workspace-1"));
|
|
1676
|
+
assert.ok(calls.some((call) => call[0] === "where" && call[1]?.contact_id === "7"));
|
|
1648
1677
|
});
|
|
1649
1678
|
|
|
1650
1679
|
test("createCrudRepositoryFromResource findById hooks share state between query and transformReturnedRecord", async () => {
|
|
@@ -1657,7 +1686,7 @@ test("createCrudRepositoryFromResource findById hooks share state between query
|
|
|
1657
1686
|
]);
|
|
1658
1687
|
const repository = createRepository(knex);
|
|
1659
1688
|
|
|
1660
|
-
const record = await repository.findById(7, {}, {
|
|
1689
|
+
const record = await repository.findById("7", {}, {
|
|
1661
1690
|
modifyQuery(_dbQuery, context = {}) {
|
|
1662
1691
|
context.state.recordTag = "from-state";
|
|
1663
1692
|
},
|
|
@@ -1712,7 +1741,7 @@ test("createCrudRepositoryFromResource listByIds hooks support afterQuery/transf
|
|
|
1712
1741
|
}
|
|
1713
1742
|
});
|
|
1714
1743
|
|
|
1715
|
-
assert.deepEqual(items.map((item) => item.id), [4, 3]);
|
|
1744
|
+
assert.deepEqual(items.map((item) => item.id), ["4", "3"]);
|
|
1716
1745
|
assert.deepEqual(items.map((item) => item.nameTag), ["SAM", "TONY"]);
|
|
1717
1746
|
});
|
|
1718
1747
|
|
|
@@ -1742,7 +1771,7 @@ test("createCrudRepositoryFromResource create hooks keep write-key filtering and
|
|
|
1742
1771
|
return {
|
|
1743
1772
|
...payload,
|
|
1744
1773
|
unexpectedField: "blocked",
|
|
1745
|
-
|
|
1774
|
+
workspaceId: "blocked"
|
|
1746
1775
|
};
|
|
1747
1776
|
},
|
|
1748
1777
|
modifyQuery(_dbQuery, context = {}) {
|
|
@@ -1756,8 +1785,8 @@ test("createCrudRepositoryFromResource create hooks keep write-key filtering and
|
|
|
1756
1785
|
assert.deepEqual(state.insertPayloads[0].first_name, "Tony");
|
|
1757
1786
|
assert.equal(Object.hasOwn(state.insertPayloads[0], "unexpectedField"), false);
|
|
1758
1787
|
assert.equal(Object.hasOwn(state.insertPayloads[0], "unexpectedFieldFromQuery"), false);
|
|
1759
|
-
assert.equal(Object.hasOwn(state.insertPayloads[0], "
|
|
1760
|
-
assert.equal(state.insertPayloads[0].
|
|
1788
|
+
assert.equal(Object.hasOwn(state.insertPayloads[0], "workspaceId"), false);
|
|
1789
|
+
assert.equal(state.insertPayloads[0].workspace_id, "workspace-1");
|
|
1761
1790
|
assert.ok(state.insertPayloads[0].created_at);
|
|
1762
1791
|
assert.ok(state.insertPayloads[0].updated_at);
|
|
1763
1792
|
});
|
|
@@ -1855,7 +1884,7 @@ test("createCrudRepositoryFromResource create hooks support afterWrite and canon
|
|
|
1855
1884
|
assert.equal(afterWriteCalls.length, 1);
|
|
1856
1885
|
assert.equal(afterWriteCalls[0].operation, "create");
|
|
1857
1886
|
assert.equal(afterWriteCalls[0].createdName, "Tony");
|
|
1858
|
-
assert.equal(afterWriteCalls[0].recordId, 11);
|
|
1887
|
+
assert.equal(afterWriteCalls[0].recordId, "11");
|
|
1859
1888
|
});
|
|
1860
1889
|
|
|
1861
1890
|
test("createCrudRepositoryFromResource update hooks keep write-key filtering and by-id visibility constraints", async () => {
|
|
@@ -1870,7 +1899,7 @@ test("createCrudRepositoryFromResource update hooks keep write-key filtering and
|
|
|
1870
1899
|
]);
|
|
1871
1900
|
const repository = createRepository(knex);
|
|
1872
1901
|
|
|
1873
|
-
await repository.updateById(11, {
|
|
1902
|
+
await repository.updateById("11", {
|
|
1874
1903
|
firstName: "Tony"
|
|
1875
1904
|
}, {
|
|
1876
1905
|
visibilityContext: {
|
|
@@ -1899,8 +1928,8 @@ test("createCrudRepositoryFromResource update hooks keep write-key filtering and
|
|
|
1899
1928
|
assert.equal(Object.hasOwn(state.updatePayloads[0], "unexpectedFieldFromQuery"), false);
|
|
1900
1929
|
assert.ok(state.updatePayloads[0].updated_at);
|
|
1901
1930
|
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "vip" && call[2] === 1));
|
|
1902
|
-
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "
|
|
1903
|
-
assert.ok(calls.some((call) => call[0] === "where" && call[1]?.contact_id === 11));
|
|
1931
|
+
assert.ok(calls.some((call) => call[0] === "where" && call[1] === "workspace_id" && call[2] === "workspace-1"));
|
|
1932
|
+
assert.ok(calls.some((call) => call[0] === "where" && call[1]?.contact_id === "11"));
|
|
1904
1933
|
});
|
|
1905
1934
|
|
|
1906
1935
|
test("createCrudRepositoryFromResource update hooks reject read-phase hook keys", async () => {
|
|
@@ -1916,7 +1945,7 @@ test("createCrudRepositoryFromResource update hooks reject read-phase hook keys"
|
|
|
1916
1945
|
const repository = createRepository(knex);
|
|
1917
1946
|
|
|
1918
1947
|
await assert.rejects(
|
|
1919
|
-
() => repository.updateById(11, {
|
|
1948
|
+
() => repository.updateById("11", {
|
|
1920
1949
|
firstName: "Tony"
|
|
1921
1950
|
}, {}, {
|
|
1922
1951
|
transformReturnedRecord(record = {}) {
|
|
@@ -1940,7 +1969,7 @@ test("createCrudRepositoryFromResource update hooks support afterWrite and canon
|
|
|
1940
1969
|
const repository = createRepository(knex);
|
|
1941
1970
|
const afterWriteCalls = [];
|
|
1942
1971
|
|
|
1943
|
-
const record = await repository.updateById(11, {
|
|
1972
|
+
const record = await repository.updateById("11", {
|
|
1944
1973
|
firstName: "Tony"
|
|
1945
1974
|
}, {}, {
|
|
1946
1975
|
modifyPatch(patch = {}, context = {}) {
|
|
@@ -1961,7 +1990,7 @@ test("createCrudRepositoryFromResource update hooks support afterWrite and canon
|
|
|
1961
1990
|
assert.equal(afterWriteCalls.length, 1);
|
|
1962
1991
|
assert.equal(afterWriteCalls[0].operation, "update");
|
|
1963
1992
|
assert.deepEqual(afterWriteCalls[0].patchKeys, ["firstName"]);
|
|
1964
|
-
assert.equal(afterWriteCalls[0].recordId, 11);
|
|
1993
|
+
assert.equal(afterWriteCalls[0].recordId, "11");
|
|
1965
1994
|
});
|
|
1966
1995
|
|
|
1967
1996
|
test("createCrudRepositoryFromResource delete hooks run through callOptions.trx client", async () => {
|
|
@@ -1980,7 +2009,7 @@ test("createCrudRepositoryFromResource delete hooks run through callOptions.trx
|
|
|
1980
2009
|
]);
|
|
1981
2010
|
const repository = createRepository(baseKnex.knex);
|
|
1982
2011
|
|
|
1983
|
-
await repository.deleteById(3, {
|
|
2012
|
+
await repository.deleteById("3", {
|
|
1984
2013
|
trx: trxKnex.knex,
|
|
1985
2014
|
visibilityContext: {
|
|
1986
2015
|
visibility: "workspace",
|
|
@@ -2010,7 +2039,7 @@ test("createCrudRepositoryFromResource delete hooks support afterWrite", async (
|
|
|
2010
2039
|
const repository = createRepository(knex);
|
|
2011
2040
|
const afterWriteCalls = [];
|
|
2012
2041
|
|
|
2013
|
-
const result = await repository.deleteById(3, {}, {
|
|
2042
|
+
const result = await repository.deleteById("3", {}, {
|
|
2014
2043
|
afterWrite(meta = {}, context = {}) {
|
|
2015
2044
|
context.state.deletedId = meta?.output?.id || null;
|
|
2016
2045
|
afterWriteCalls.push({
|
|
@@ -2023,7 +2052,7 @@ test("createCrudRepositoryFromResource delete hooks support afterWrite", async (
|
|
|
2023
2052
|
assert.equal(result.deleted, true);
|
|
2024
2053
|
assert.equal(afterWriteCalls.length, 1);
|
|
2025
2054
|
assert.equal(afterWriteCalls[0].operation, "delete");
|
|
2026
|
-
assert.equal(afterWriteCalls[0].deletedId, 3);
|
|
2055
|
+
assert.equal(afterWriteCalls[0].deletedId, "3");
|
|
2027
2056
|
});
|
|
2028
2057
|
|
|
2029
2058
|
test("createCrudRepositoryFromResource delete hooks support finalizeOutput for record and null flows", async () => {
|
|
@@ -2038,7 +2067,7 @@ test("createCrudRepositoryFromResource delete hooks support finalizeOutput for r
|
|
|
2038
2067
|
const presentRepository = createRepository(presentKnex.knex);
|
|
2039
2068
|
const missingRepository = createRepository(missingKnex.knex);
|
|
2040
2069
|
|
|
2041
|
-
const deletedOutput = await presentRepository.deleteById(3, {}, {
|
|
2070
|
+
const deletedOutput = await presentRepository.deleteById("3", {}, {
|
|
2042
2071
|
finalizeOutput(output) {
|
|
2043
2072
|
return output
|
|
2044
2073
|
? { ...output, status: "deleted" }
|
|
@@ -2046,7 +2075,7 @@ test("createCrudRepositoryFromResource delete hooks support finalizeOutput for r
|
|
|
2046
2075
|
}
|
|
2047
2076
|
});
|
|
2048
2077
|
|
|
2049
|
-
const missingOutput = await missingRepository.deleteById(3, {}, {
|
|
2078
|
+
const missingOutput = await missingRepository.deleteById("3", {}, {
|
|
2050
2079
|
finalizeOutput(output) {
|
|
2051
2080
|
return output === null ? { deleted: false } : output;
|
|
2052
2081
|
}
|
|
@@ -89,8 +89,8 @@ test("normalizeCrudListLimit enforces fallback and max", () => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
test("normalizeCrudListCursor rejects malformed id cursors", () => {
|
|
92
|
-
assert.equal(normalizeCrudListCursor("7"), 7);
|
|
93
|
-
assert.equal(normalizeCrudListCursor(""),
|
|
92
|
+
assert.equal(normalizeCrudListCursor("7"), "7");
|
|
93
|
+
assert.equal(normalizeCrudListCursor(""), "");
|
|
94
94
|
assert.throws(
|
|
95
95
|
() => normalizeCrudListCursor("abc"),
|
|
96
96
|
/Invalid cursor/
|
|
@@ -147,7 +147,7 @@ test("applyCrudListQueryFilters applies search and cursor filters", () => {
|
|
|
147
147
|
["whereGroup"],
|
|
148
148
|
["innerWhere", "first_name", "like", "%ani%"],
|
|
149
149
|
["innerOrWhere", "last_name", "like", "%ani%"],
|
|
150
|
-
["where", "id", ">", 3]
|
|
150
|
+
["where", "id", ">", "3"]
|
|
151
151
|
]);
|
|
152
152
|
});
|
|
153
153
|
|