@jskit-ai/crud-core 0.1.25 → 0.1.26
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 +4 -2
- package/package.json +8 -6
- package/src/client/composables/createCrudClientSupport.js +139 -42
- package/src/client/composables/crudClientSupportHelpers.js +54 -2
- package/src/server/crudModuleConfig.js +310 -0
- package/src/server/repositorySupport.js +97 -1
- package/test/createCrudClientSupport.test.js +39 -4
- package/test/repositorySupport.test.js +50 -1
package/package.descriptor.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
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.26",
|
|
5
|
+
kind: "runtime",
|
|
5
6
|
description: "Shared CRUD helpers used by CRUD modules.",
|
|
6
7
|
dependsOn: [
|
|
7
8
|
"@jskit-ai/kernel",
|
|
8
9
|
"@jskit-ai/realtime",
|
|
9
10
|
"@jskit-ai/shell-web",
|
|
11
|
+
"@jskit-ai/users-core",
|
|
10
12
|
"@jskit-ai/users-web"
|
|
11
13
|
],
|
|
12
14
|
capabilities: {
|
|
@@ -24,7 +26,7 @@ export default Object.freeze({
|
|
|
24
26
|
mutations: {
|
|
25
27
|
dependencies: {
|
|
26
28
|
runtime: {
|
|
27
|
-
"@jskit-ai/crud-core": "0.1.
|
|
29
|
+
"@jskit-ai/crud-core": "0.1.26"
|
|
28
30
|
},
|
|
29
31
|
dev: {}
|
|
30
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.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
"./client/composables/createCrudClientSupport": "./src/client/composables/createCrudClientSupport.js",
|
|
11
11
|
"./client/composables/crudClientSupportHelpers": "./src/client/composables/crudClientSupportHelpers.js",
|
|
12
12
|
"./client/composables/useCrudRealtimeInvalidation": "./src/client/composables/useCrudRealtimeInvalidation.js",
|
|
13
|
-
"./server/repositorySupport": "./src/server/repositorySupport.js"
|
|
13
|
+
"./server/repositorySupport": "./src/server/repositorySupport.js",
|
|
14
|
+
"./server/crudModuleConfig": "./src/server/crudModuleConfig.js"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
17
|
"@tanstack/vue-query": "^5.90.5",
|
|
17
|
-
"@jskit-ai/kernel": "0.1.
|
|
18
|
-
"@jskit-ai/realtime": "0.1.
|
|
19
|
-
"@jskit-ai/shell-web": "0.1.
|
|
20
|
-
"@jskit-ai/users-
|
|
18
|
+
"@jskit-ai/kernel": "0.1.18",
|
|
19
|
+
"@jskit-ai/realtime": "0.1.17",
|
|
20
|
+
"@jskit-ai/shell-web": "0.1.17",
|
|
21
|
+
"@jskit-ai/users-core": "0.1.27",
|
|
22
|
+
"@jskit-ai/users-web": "0.1.32"
|
|
21
23
|
}
|
|
22
24
|
}
|
|
@@ -8,44 +8,123 @@ import {
|
|
|
8
8
|
invalidateCrudQueries,
|
|
9
9
|
crudListQueryKey,
|
|
10
10
|
crudViewQueryKey,
|
|
11
|
-
toRouteRecordId
|
|
11
|
+
toRouteRecordId,
|
|
12
|
+
normalizeCrudRouteParamName,
|
|
13
|
+
resolveCrudRecordPathTemplates,
|
|
14
|
+
resolveCrudRecordPathParams
|
|
12
15
|
} from "./crudClientSupportHelpers.js";
|
|
13
16
|
import { useCrudRealtimeInvalidation } from "./useCrudRealtimeInvalidation.js";
|
|
14
17
|
|
|
18
|
+
function normalizeText(value = "") {
|
|
19
|
+
return String(value || "").trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeRouteParams(params = {}) {
|
|
23
|
+
const source = params && typeof params === "object" && !Array.isArray(params) ? params : {};
|
|
24
|
+
const normalized = {};
|
|
25
|
+
|
|
26
|
+
for (const [rawKey, rawValue] of Object.entries(source)) {
|
|
27
|
+
const key = normalizeText(rawKey);
|
|
28
|
+
if (!key) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
|
|
33
|
+
const normalizedValue = normalizeText(value);
|
|
34
|
+
if (!normalizedValue) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
normalized[key] = normalizedValue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizePathTemplate(value = "") {
|
|
45
|
+
const normalized = normalizeText(value)
|
|
46
|
+
.replace(/\\/g, "/")
|
|
47
|
+
.replace(/\/{2,}/g, "/")
|
|
48
|
+
.replace(/^\/+|\/+$/g, "");
|
|
49
|
+
|
|
50
|
+
return normalized ? `/${normalized}` : "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolvePathTemplate(pathTemplate = "", { routeParams = {}, params = {}, context = "resolvePathTemplate" } = {}) {
|
|
54
|
+
const normalizedTemplate = normalizePathTemplate(pathTemplate);
|
|
55
|
+
if (!normalizedTemplate) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resolvedParams = {
|
|
60
|
+
...normalizeRouteParams(routeParams),
|
|
61
|
+
...normalizeRouteParams(params)
|
|
62
|
+
};
|
|
63
|
+
const missingParams = [];
|
|
64
|
+
const resolvedPath = normalizedTemplate.replace(/:([A-Za-z][A-Za-z0-9_]*)/g, (_, key) => {
|
|
65
|
+
const value = normalizeText(resolvedParams[key]);
|
|
66
|
+
if (!value) {
|
|
67
|
+
missingParams.push(key);
|
|
68
|
+
return `:${key}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return encodeURIComponent(value);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (missingParams.length > 0) {
|
|
75
|
+
throw new Error(`${context} missing route parameter(s): ${missingParams.join(", ")}.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return resolvedPath;
|
|
79
|
+
}
|
|
80
|
+
|
|
15
81
|
function useCrudClientContext(source = {}) {
|
|
16
82
|
const crudConfig = resolveCrudClientConfig(source);
|
|
17
83
|
const paths = usePaths();
|
|
18
84
|
const route = useRoute();
|
|
19
85
|
const workspaceSlugToken = computed(() => paths.workspaceSlug.value);
|
|
20
|
-
const
|
|
21
|
-
const
|
|
86
|
+
const defaultRecordIdParam = "recordId";
|
|
87
|
+
const listPathTemplate = crudConfig.relativePath;
|
|
88
|
+
const createPathTemplate = `${crudConfig.relativePath}/new`;
|
|
89
|
+
const defaultRecordPathTemplates = resolveCrudRecordPathTemplates(listPathTemplate, defaultRecordIdParam);
|
|
22
90
|
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
|
|
91
|
+
function resolvePath(pathTemplate = "", params = {}) {
|
|
92
|
+
const resolvedPath = resolvePathTemplate(pathTemplate, {
|
|
93
|
+
routeParams: route.params,
|
|
94
|
+
params,
|
|
95
|
+
context: "useCrudClientContext.resolvePath"
|
|
96
|
+
});
|
|
97
|
+
return resolvedPath ? paths.page(resolvedPath) : "";
|
|
26
98
|
}
|
|
27
99
|
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
|
|
100
|
+
function resolveApiPath(pathTemplate = "", params = {}) {
|
|
101
|
+
const resolvedPath = resolvePathTemplate(pathTemplate, {
|
|
102
|
+
routeParams: route.params,
|
|
103
|
+
params,
|
|
104
|
+
context: "useCrudClientContext.resolveApiPath"
|
|
105
|
+
});
|
|
106
|
+
return resolvedPath ? paths.api(resolvedPath) : "";
|
|
31
107
|
}
|
|
32
108
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!recordId) {
|
|
36
|
-
return "";
|
|
37
|
-
}
|
|
109
|
+
const listPath = computed(() => resolvePath(listPathTemplate));
|
|
110
|
+
const createPath = computed(() => resolvePath(createPathTemplate));
|
|
38
111
|
|
|
39
|
-
|
|
112
|
+
function resolveRecordPathTemplates(recordIdParam = defaultRecordIdParam) {
|
|
113
|
+
return resolveCrudRecordPathTemplates(listPathTemplate, recordIdParam);
|
|
40
114
|
}
|
|
41
115
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
116
|
+
function resolveRecordParams(recordIdLike = 0, { recordIdParam = defaultRecordIdParam } = {}) {
|
|
117
|
+
return resolveCrudRecordPathParams(recordIdLike, recordIdParam);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function listQueryKey(surfaceId = "") {
|
|
121
|
+
const normalizedSurfaceId = String(surfaceId || paths.currentSurfaceId.value || "").trim();
|
|
122
|
+
return crudListQueryKey(normalizedSurfaceId, workspaceSlugToken.value, crudConfig.namespace);
|
|
123
|
+
}
|
|
47
124
|
|
|
48
|
-
|
|
125
|
+
function viewQueryKey(surfaceId = "", recordId = 0) {
|
|
126
|
+
const normalizedSurfaceId = String(surfaceId || paths.currentSurfaceId.value || "").trim();
|
|
127
|
+
return crudViewQueryKey(normalizedSurfaceId, workspaceSlugToken.value, recordId, crudConfig.namespace);
|
|
49
128
|
}
|
|
50
129
|
|
|
51
130
|
function scopeQueryKey() {
|
|
@@ -59,36 +138,47 @@ function useCrudClientContext(source = {}) {
|
|
|
59
138
|
return Object.freeze({
|
|
60
139
|
route,
|
|
61
140
|
crudConfig,
|
|
141
|
+
listPathTemplate,
|
|
142
|
+
createPathTemplate,
|
|
143
|
+
defaultRecordIdParam,
|
|
144
|
+
viewPathTemplate: defaultRecordPathTemplates.viewPathTemplate,
|
|
145
|
+
editPathTemplate: defaultRecordPathTemplates.editPathTemplate,
|
|
146
|
+
resolveRecordPathTemplates,
|
|
147
|
+
resolveRecordParams,
|
|
148
|
+
resolvePath,
|
|
149
|
+
resolveApiPath,
|
|
62
150
|
listPath,
|
|
63
151
|
createPath,
|
|
64
152
|
listQueryKey,
|
|
65
153
|
viewQueryKey,
|
|
66
154
|
scopeQueryKey,
|
|
67
155
|
invalidateQueries,
|
|
68
|
-
formatDateTime
|
|
69
|
-
resolveViewPath,
|
|
70
|
-
resolveEditPath
|
|
156
|
+
formatDateTime
|
|
71
157
|
});
|
|
72
158
|
}
|
|
73
159
|
|
|
74
|
-
function normalizeRecordIdParam(value) {
|
|
75
|
-
const normalized = String(value || "").trim();
|
|
76
|
-
if (!normalized) {
|
|
77
|
-
throw new TypeError("useCrudRecordRuntime requires a non-empty recordIdParam.");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return normalized;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
160
|
function useCrudRecordRuntime(source = {}, { recordIdParam = "recordId" } = {}) {
|
|
84
|
-
const normalizedRecordIdParam =
|
|
161
|
+
const normalizedRecordIdParam = normalizeCrudRouteParamName(recordIdParam, {
|
|
162
|
+
context: "useCrudRecordRuntime"
|
|
163
|
+
});
|
|
85
164
|
const crudContext = useCrudClientContext(source);
|
|
86
165
|
useCrudRealtimeInvalidation(crudContext.crudConfig.namespace);
|
|
87
166
|
const router = useRouter();
|
|
167
|
+
const recordPathTemplates = crudContext.resolveRecordPathTemplates(normalizedRecordIdParam);
|
|
88
168
|
const recordId = computed(() => toRouteRecordId(crudContext.route.params[normalizedRecordIdParam]));
|
|
89
|
-
const apiSuffix = computed(() => `${crudContext.crudConfig.
|
|
90
|
-
const viewPath = computed(() =>
|
|
91
|
-
|
|
169
|
+
const apiSuffix = computed(() => `${crudContext.crudConfig.apiRelativePath}/${recordId.value}`);
|
|
170
|
+
const viewPath = computed(() =>
|
|
171
|
+
crudContext.resolvePath(
|
|
172
|
+
recordPathTemplates.viewPathTemplate,
|
|
173
|
+
crudContext.resolveRecordParams(recordId.value, { recordIdParam: normalizedRecordIdParam })
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
const editPath = computed(() =>
|
|
177
|
+
crudContext.resolvePath(
|
|
178
|
+
recordPathTemplates.editPathTemplate,
|
|
179
|
+
crudContext.resolveRecordParams(recordId.value, { recordIdParam: normalizedRecordIdParam })
|
|
180
|
+
)
|
|
181
|
+
);
|
|
92
182
|
const listPath = crudContext.listPath;
|
|
93
183
|
|
|
94
184
|
function viewQueryKey(surfaceId = "") {
|
|
@@ -105,8 +195,11 @@ function useCrudRecordRuntime(source = {}, { recordIdParam = "recordId" } = {})
|
|
|
105
195
|
async function invalidateAndGoView(queryClient, recordIdLike = recordId.value) {
|
|
106
196
|
await crudContext.invalidateQueries(queryClient);
|
|
107
197
|
|
|
108
|
-
const targetRecordId = toRouteRecordId(recordIdLike);
|
|
109
|
-
const targetPath = crudContext.
|
|
198
|
+
const targetRecordId = toRouteRecordId(recordIdLike) || recordId.value;
|
|
199
|
+
const targetPath = crudContext.resolvePath(
|
|
200
|
+
recordPathTemplates.viewPathTemplate,
|
|
201
|
+
crudContext.resolveRecordParams(targetRecordId, { recordIdParam: normalizedRecordIdParam })
|
|
202
|
+
);
|
|
110
203
|
if (targetPath) {
|
|
111
204
|
await router.push(targetPath);
|
|
112
205
|
}
|
|
@@ -129,8 +222,9 @@ function useCrudCreateRuntime(source = {}) {
|
|
|
129
222
|
const crudContext = useCrudClientContext(source);
|
|
130
223
|
useCrudRealtimeInvalidation(crudContext.crudConfig.namespace);
|
|
131
224
|
const router = useRouter();
|
|
225
|
+
const recordPathTemplates = crudContext.resolveRecordPathTemplates();
|
|
132
226
|
const listPath = crudContext.listPath;
|
|
133
|
-
const apiSuffix = crudContext.crudConfig.
|
|
227
|
+
const apiSuffix = crudContext.crudConfig.apiRelativePath;
|
|
134
228
|
|
|
135
229
|
function createQueryKey(surfaceId = "") {
|
|
136
230
|
return [...crudContext.listQueryKey(surfaceId), "create"];
|
|
@@ -139,7 +233,10 @@ function useCrudCreateRuntime(source = {}) {
|
|
|
139
233
|
async function invalidateAndGoView(queryClient, recordIdLike) {
|
|
140
234
|
await crudContext.invalidateQueries(queryClient);
|
|
141
235
|
|
|
142
|
-
const targetPath = crudContext.
|
|
236
|
+
const targetPath = crudContext.resolvePath(
|
|
237
|
+
recordPathTemplates.viewPathTemplate,
|
|
238
|
+
crudContext.resolveRecordParams(recordIdLike)
|
|
239
|
+
);
|
|
143
240
|
if (targetPath) {
|
|
144
241
|
await router.push(targetPath);
|
|
145
242
|
}
|
|
@@ -158,7 +255,7 @@ function useCrudListRuntime(source = {}) {
|
|
|
158
255
|
const crudContext = useCrudClientContext(source);
|
|
159
256
|
useCrudRealtimeInvalidation(crudContext.crudConfig.namespace);
|
|
160
257
|
const createPath = crudContext.createPath;
|
|
161
|
-
const apiSuffix = crudContext.crudConfig.
|
|
258
|
+
const apiSuffix = crudContext.crudConfig.apiRelativePath;
|
|
162
259
|
|
|
163
260
|
function listQueryKey(surfaceId = "") {
|
|
164
261
|
return crudContext.listQueryKey(surfaceId);
|
|
@@ -3,6 +3,7 @@ import { normalizeRouteVisibilityToken } from "@jskit-ai/kernel/shared/support/v
|
|
|
3
3
|
import { formatDateTime } from "@jskit-ai/kernel/shared/support";
|
|
4
4
|
|
|
5
5
|
const DEFAULT_CRUD_OWNERSHIP_FILTER = "workspace";
|
|
6
|
+
const ROUTE_PARAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
6
7
|
|
|
7
8
|
function requireCrudNamespace(namespace, { context = "resolveCrudClientConfig" } = {}) {
|
|
8
9
|
const normalizedNamespace = normalizeLowerText(namespace);
|
|
@@ -40,11 +41,16 @@ function resolveCrudClientConfig(source = {}) {
|
|
|
40
41
|
Object.hasOwn(payload, "relativePath") ? payload.relativePath : inferredRelativePath,
|
|
41
42
|
{ context: "resolveCrudClientConfig" }
|
|
42
43
|
);
|
|
44
|
+
const apiRelativePath = normalizeRelativePath(
|
|
45
|
+
Object.hasOwn(payload, "apiRelativePath") ? payload.apiRelativePath : relativePath,
|
|
46
|
+
{ context: "resolveCrudClientConfig" }
|
|
47
|
+
);
|
|
43
48
|
|
|
44
49
|
return Object.freeze({
|
|
45
50
|
namespace,
|
|
46
51
|
ownershipFilter,
|
|
47
|
-
relativePath
|
|
52
|
+
relativePath,
|
|
53
|
+
apiRelativePath
|
|
48
54
|
});
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -97,6 +103,49 @@ function toRouteRecordId(value) {
|
|
|
97
103
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
function normalizeCrudRouteParamName(value, { context = "normalizeCrudRouteParamName" } = {}) {
|
|
107
|
+
const normalizedValue = normalizeText(value);
|
|
108
|
+
if (!normalizedValue) {
|
|
109
|
+
throw new TypeError(`${context} requires a non-empty route parameter name.`);
|
|
110
|
+
}
|
|
111
|
+
if (!ROUTE_PARAM_NAME_PATTERN.test(normalizedValue)) {
|
|
112
|
+
throw new TypeError(
|
|
113
|
+
`${context} route parameter "${normalizedValue}" is invalid. Use letters, numbers, and underscores only.`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return normalizedValue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveCrudRecordPathTemplates(relativePath = "", recordIdParam = "recordId") {
|
|
121
|
+
const normalizedRelativePath = normalizeRelativePath(relativePath, {
|
|
122
|
+
context: "resolveCrudRecordPathTemplates"
|
|
123
|
+
});
|
|
124
|
+
const normalizedRecordIdParam = normalizeCrudRouteParamName(recordIdParam, {
|
|
125
|
+
context: "resolveCrudRecordPathTemplates"
|
|
126
|
+
});
|
|
127
|
+
const recordSegment = `:${normalizedRecordIdParam}`;
|
|
128
|
+
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
viewPathTemplate: `${normalizedRelativePath}/${recordSegment}`,
|
|
131
|
+
editPathTemplate: `${normalizedRelativePath}/${recordSegment}/edit`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveCrudRecordPathParams(recordIdLike = 0, recordIdParam = "recordId") {
|
|
136
|
+
const normalizedRecordIdParam = normalizeCrudRouteParamName(recordIdParam, {
|
|
137
|
+
context: "resolveCrudRecordPathParams"
|
|
138
|
+
});
|
|
139
|
+
const normalizedRecordId = toRouteRecordId(recordIdLike);
|
|
140
|
+
if (!normalizedRecordId) {
|
|
141
|
+
return Object.freeze({});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Object.freeze({
|
|
145
|
+
[normalizedRecordIdParam]: String(normalizedRecordId)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
100
149
|
export {
|
|
101
150
|
DEFAULT_CRUD_OWNERSHIP_FILTER,
|
|
102
151
|
requireCrudNamespace,
|
|
@@ -107,5 +156,8 @@ export {
|
|
|
107
156
|
invalidateCrudQueries,
|
|
108
157
|
crudListQueryKey,
|
|
109
158
|
crudViewQueryKey,
|
|
110
|
-
toRouteRecordId
|
|
159
|
+
toRouteRecordId,
|
|
160
|
+
normalizeCrudRouteParamName,
|
|
161
|
+
resolveCrudRecordPathTemplates,
|
|
162
|
+
resolveCrudRecordPathParams
|
|
111
163
|
};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
|
+
import {
|
|
4
|
+
resolveApiBasePath
|
|
5
|
+
} from "@jskit-ai/users-core/shared/support/usersApiPaths";
|
|
6
|
+
import {
|
|
7
|
+
USERS_ROUTE_VISIBILITY_LEVELS,
|
|
8
|
+
normalizeScopedRouteVisibility,
|
|
9
|
+
isWorkspaceVisibility
|
|
10
|
+
} from "@jskit-ai/users-core/shared/support/usersVisibility";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_OWNERSHIP_FILTER = "workspace";
|
|
13
|
+
const CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO = "auto";
|
|
14
|
+
const CRUD_REQUESTED_OWNERSHIP_FILTER_SET = new Set([
|
|
15
|
+
...USERS_ROUTE_VISIBILITY_LEVELS,
|
|
16
|
+
CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO
|
|
17
|
+
]);
|
|
18
|
+
const CRUD_MODULE_ID = "crud";
|
|
19
|
+
|
|
20
|
+
function asRecord(value) {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeCrudNamespace(value) {
|
|
29
|
+
return normalizeText(value)
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
32
|
+
.replace(/-+/g, "-")
|
|
33
|
+
.replace(/^-+|-+$/g, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeCrudOwnershipFilter(value, { fallback = DEFAULT_OWNERSHIP_FILTER } = {}) {
|
|
37
|
+
return normalizeScopedRouteVisibility(value, { fallback });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeCrudRequestedOwnershipFilter(value, { fallback = CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO } = {}) {
|
|
41
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
42
|
+
if (CRUD_REQUESTED_OWNERSHIP_FILTER_SET.has(normalized)) {
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const normalizedFallback = normalizeText(fallback).toLowerCase();
|
|
47
|
+
if (CRUD_REQUESTED_OWNERSHIP_FILTER_SET.has(normalizedFallback)) {
|
|
48
|
+
return normalizedFallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function requireCrudNamespace(namespace, { context = "CRUD config" } = {}) {
|
|
55
|
+
const normalizedNamespace = normalizeCrudNamespace(namespace);
|
|
56
|
+
if (!normalizedNamespace) {
|
|
57
|
+
throw new TypeError(`${context} requires a non-empty namespace.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return normalizedNamespace;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveCrudNamespacePath(namespace = "") {
|
|
64
|
+
const normalizedNamespace = requireCrudNamespace(namespace, {
|
|
65
|
+
context: "resolveCrudNamespacePath"
|
|
66
|
+
});
|
|
67
|
+
return `/${normalizedNamespace}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveCrudRelativePath(namespace = "") {
|
|
71
|
+
return resolveCrudNamespacePath(namespace);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeCrudRelativePath(relativePath = "", { context = "resolveCrudSurfacePolicy" } = {}) {
|
|
75
|
+
const normalizedPath = normalizeText(relativePath);
|
|
76
|
+
if (!normalizedPath) {
|
|
77
|
+
throw new TypeError(`${context} requires a non-empty relativePath.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const withLeadingSlash = normalizedPath.startsWith("/") ? normalizedPath : `/${normalizedPath}`;
|
|
81
|
+
const compacted = withLeadingSlash.replace(/\/{2,}/g, "/");
|
|
82
|
+
return compacted === "/" ? "/" : compacted.replace(/\/+$/, "") || "/";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveCrudApiBasePath({ namespace = "", surfaceRequiresWorkspace = false } = {}) {
|
|
86
|
+
const relativePath = resolveCrudRelativePath(namespace);
|
|
87
|
+
return resolveApiBasePath({
|
|
88
|
+
surfaceRequiresWorkspace: surfaceRequiresWorkspace === true,
|
|
89
|
+
relativePath
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveCrudTableName(namespace = "") {
|
|
94
|
+
const normalizedNamespace = requireCrudNamespace(namespace, {
|
|
95
|
+
context: "resolveCrudTableName"
|
|
96
|
+
});
|
|
97
|
+
return `crud_${normalizedNamespace.replace(/-/g, "_")}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function resolveCrudTokenPart(namespace = "") {
|
|
101
|
+
const normalizedNamespace = requireCrudNamespace(namespace, {
|
|
102
|
+
context: "resolveCrudTokenPart"
|
|
103
|
+
});
|
|
104
|
+
return normalizedNamespace.replace(/-/g, "_");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveCrudActionIdPrefix(namespace = "") {
|
|
108
|
+
const tokenPart = resolveCrudTokenPart(namespace);
|
|
109
|
+
return `crud.${tokenPart}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveCrudContributorId(namespace = "") {
|
|
113
|
+
const tokenPart = resolveCrudTokenPart(namespace);
|
|
114
|
+
return `crud.${tokenPart}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveCrudDomain(namespace = "") {
|
|
118
|
+
return "crud";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveCrudToken(namespace = "", suffix = "") {
|
|
122
|
+
const contributorId = resolveCrudContributorId(namespace);
|
|
123
|
+
return suffix ? `${contributorId}.${suffix}` : contributorId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveCrudConfig(source = {}) {
|
|
127
|
+
const settings = source && typeof source === "object" && !Array.isArray(source) ? source : {};
|
|
128
|
+
const namespace = requireCrudNamespace(settings.namespace, {
|
|
129
|
+
context: "resolveCrudConfig"
|
|
130
|
+
});
|
|
131
|
+
const ownershipFilter = normalizeCrudOwnershipFilter(settings.ownershipFilter);
|
|
132
|
+
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
namespace,
|
|
135
|
+
ownershipFilter,
|
|
136
|
+
workspaceScoped: isWorkspaceVisibility(ownershipFilter),
|
|
137
|
+
namespacePath: resolveCrudNamespacePath(namespace),
|
|
138
|
+
relativePath: resolveCrudRelativePath(namespace),
|
|
139
|
+
apiBasePath: resolveCrudApiBasePath({ namespace }),
|
|
140
|
+
tableName: resolveCrudTableName(namespace),
|
|
141
|
+
actionIdPrefix: resolveCrudActionIdPrefix(namespace),
|
|
142
|
+
contributorId: resolveCrudContributorId(namespace),
|
|
143
|
+
domain: resolveCrudDomain(namespace),
|
|
144
|
+
repositoryToken: resolveCrudToken(namespace, "repository"),
|
|
145
|
+
serviceToken: resolveCrudToken(namespace, "service")
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeSurfaceDefinitions(sourceDefinitions = {}) {
|
|
150
|
+
const definitions = asRecord(sourceDefinitions);
|
|
151
|
+
const normalized = {};
|
|
152
|
+
|
|
153
|
+
for (const [key, value] of Object.entries(definitions)) {
|
|
154
|
+
const definition = asRecord(value);
|
|
155
|
+
const surfaceId = normalizeSurfaceId(definition.id || key);
|
|
156
|
+
if (!surfaceId) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
normalized[surfaceId] = Object.freeze({
|
|
161
|
+
...definition,
|
|
162
|
+
id: surfaceId,
|
|
163
|
+
enabled: definition.enabled !== false,
|
|
164
|
+
requiresAuth: definition.requiresAuth === true,
|
|
165
|
+
requiresWorkspace: definition.requiresWorkspace === true
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return Object.freeze(normalized);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveOwnershipFilterFromSurfaceDefinition(definition = {}) {
|
|
173
|
+
if (definition.requiresWorkspace === true) {
|
|
174
|
+
return "workspace";
|
|
175
|
+
}
|
|
176
|
+
if (definition.requiresAuth === true) {
|
|
177
|
+
return "user";
|
|
178
|
+
}
|
|
179
|
+
return "public";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveCrudSurfacePolicy(
|
|
183
|
+
sourceConfig = {},
|
|
184
|
+
{
|
|
185
|
+
surfaceDefinitions = {},
|
|
186
|
+
defaultSurfaceId = "",
|
|
187
|
+
context = "resolveCrudSurfacePolicy"
|
|
188
|
+
} = {}
|
|
189
|
+
) {
|
|
190
|
+
const config = asRecord(sourceConfig);
|
|
191
|
+
const normalizedDefinitions = normalizeSurfaceDefinitions(surfaceDefinitions);
|
|
192
|
+
const requestedSurfaceId = normalizeSurfaceId(config.surface);
|
|
193
|
+
const fallbackSurfaceId = normalizeSurfaceId(defaultSurfaceId);
|
|
194
|
+
const surfaceId = requestedSurfaceId || fallbackSurfaceId;
|
|
195
|
+
if (!surfaceId) {
|
|
196
|
+
throw new Error(`${context} requires surface or defaultSurfaceId.`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const surfaceDefinition = normalizedDefinitions[surfaceId];
|
|
200
|
+
if (!surfaceDefinition) {
|
|
201
|
+
throw new Error(`${context} cannot resolve surface "${surfaceId}".`);
|
|
202
|
+
}
|
|
203
|
+
if (surfaceDefinition.enabled === false) {
|
|
204
|
+
throw new Error(`${context} surface "${surfaceId}" is disabled.`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const requestedOwnershipFilter = normalizeCrudRequestedOwnershipFilter(config.ownershipFilter);
|
|
208
|
+
const ownershipFilter =
|
|
209
|
+
requestedOwnershipFilter === CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO
|
|
210
|
+
? resolveOwnershipFilterFromSurfaceDefinition(surfaceDefinition)
|
|
211
|
+
: normalizeScopedRouteVisibility(requestedOwnershipFilter, {
|
|
212
|
+
fallback: "public"
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (isWorkspaceVisibility(ownershipFilter) && surfaceDefinition.requiresWorkspace !== true) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`${context} ownershipFilter "${ownershipFilter}" requires a workspace-enabled surface.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const relativePath = normalizeCrudRelativePath(config.relativePath || resolveCrudRelativePath(config.namespace), {
|
|
222
|
+
context
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
surfaceId,
|
|
227
|
+
ownershipFilter,
|
|
228
|
+
requestedOwnershipFilter,
|
|
229
|
+
workspaceScoped: isWorkspaceVisibility(ownershipFilter),
|
|
230
|
+
relativePath,
|
|
231
|
+
surfaceDefinition
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveCrudSurfacePolicyFromAppConfig(sourceConfig = {}, appConfig = {}, options = {}) {
|
|
236
|
+
const config = asRecord(appConfig);
|
|
237
|
+
return resolveCrudSurfacePolicy(sourceConfig, {
|
|
238
|
+
...asRecord(options),
|
|
239
|
+
surfaceDefinitions: config.surfaceDefinitions,
|
|
240
|
+
defaultSurfaceId: config.surfaceDefaultId
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolveCrudConfigsFromModules(modulesSource = {}) {
|
|
245
|
+
const modules = modulesSource && typeof modulesSource === "object" && !Array.isArray(modulesSource)
|
|
246
|
+
? modulesSource
|
|
247
|
+
: {};
|
|
248
|
+
const configs = [];
|
|
249
|
+
const seenContributorIds = new Set();
|
|
250
|
+
|
|
251
|
+
for (const moduleConfig of Object.values(modules)) {
|
|
252
|
+
const source = moduleConfig && typeof moduleConfig === "object" && !Array.isArray(moduleConfig)
|
|
253
|
+
? moduleConfig
|
|
254
|
+
: {};
|
|
255
|
+
|
|
256
|
+
if (normalizeText(source.module).toLowerCase() !== CRUD_MODULE_ID) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const resolved = resolveCrudConfig(source);
|
|
261
|
+
if (seenContributorIds.has(resolved.contributorId)) {
|
|
262
|
+
throw new Error(`Duplicate CRUD namespace in config.modules: "${resolved.namespace}".`);
|
|
263
|
+
}
|
|
264
|
+
seenContributorIds.add(resolved.contributorId);
|
|
265
|
+
configs.push(resolved);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return configs;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function resolveCrudConfigFromModules(modulesSource = {}, options = {}) {
|
|
272
|
+
const configs = resolveCrudConfigsFromModules(modulesSource);
|
|
273
|
+
const hasNamespace = Object.hasOwn(options, "namespace");
|
|
274
|
+
if (hasNamespace) {
|
|
275
|
+
const normalizedNamespace = requireCrudNamespace(options.namespace, {
|
|
276
|
+
context: "resolveCrudConfigFromModules"
|
|
277
|
+
});
|
|
278
|
+
return configs.find((entry) => entry.namespace === normalizedNamespace) || null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (configs.length === 1) {
|
|
282
|
+
return configs[0];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export {
|
|
289
|
+
CRUD_MODULE_ID,
|
|
290
|
+
DEFAULT_OWNERSHIP_FILTER,
|
|
291
|
+
CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO,
|
|
292
|
+
normalizeCrudNamespace,
|
|
293
|
+
normalizeCrudOwnershipFilter,
|
|
294
|
+
normalizeCrudRequestedOwnershipFilter,
|
|
295
|
+
isWorkspaceVisibility,
|
|
296
|
+
requireCrudNamespace,
|
|
297
|
+
resolveCrudNamespacePath,
|
|
298
|
+
resolveCrudRelativePath,
|
|
299
|
+
normalizeCrudRelativePath,
|
|
300
|
+
resolveCrudApiBasePath,
|
|
301
|
+
resolveCrudTableName,
|
|
302
|
+
resolveCrudActionIdPrefix,
|
|
303
|
+
resolveCrudContributorId,
|
|
304
|
+
resolveCrudDomain,
|
|
305
|
+
resolveCrudConfig,
|
|
306
|
+
resolveCrudSurfacePolicy,
|
|
307
|
+
resolveCrudSurfacePolicyFromAppConfig,
|
|
308
|
+
resolveCrudConfigsFromModules,
|
|
309
|
+
resolveCrudConfigFromModules
|
|
310
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
|
|
3
|
+
import { toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
|
|
2
4
|
|
|
3
5
|
const DEFAULT_LIST_LIMIT = 20;
|
|
4
6
|
const MAX_LIST_LIMIT = 100;
|
|
@@ -21,9 +23,103 @@ function requireCrudTableName(tableName, { context = "crudRepository" } = {}) {
|
|
|
21
23
|
return normalizedTableName;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
function resolveColumnName(fieldKey, overrides = {}) {
|
|
27
|
+
const normalizedKey = String(fieldKey || "").trim();
|
|
28
|
+
if (!normalizedKey) {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const overrideValue = String(overrides?.[normalizedKey] || "").trim();
|
|
33
|
+
if (overrideValue) {
|
|
34
|
+
return overrideValue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return toSnakeCase(normalizedKey);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildRepositoryColumnMetadata({
|
|
41
|
+
outputKeys = [],
|
|
42
|
+
writeKeys = [],
|
|
43
|
+
columnOverrides = {}
|
|
44
|
+
} = {}) {
|
|
45
|
+
const normalizedOutputKeys = (Array.isArray(outputKeys) ? outputKeys : [])
|
|
46
|
+
.map((key) => String(key || "").trim())
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
const normalizedWriteKeys = (Array.isArray(writeKeys) ? writeKeys : [])
|
|
49
|
+
.map((key) => String(key || "").trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
|
|
52
|
+
const deriveMapping = (key) => {
|
|
53
|
+
const column = resolveColumnName(key, columnOverrides);
|
|
54
|
+
if (!column) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return { key, column };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const outputMappings = normalizedOutputKeys.map(deriveMapping).filter(Boolean);
|
|
61
|
+
const writeMappings = normalizedWriteKeys.map(deriveMapping).filter(Boolean);
|
|
62
|
+
const selectColumns = Object.freeze(
|
|
63
|
+
[...new Set(outputMappings.map((mapping) => mapping.column))]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
selectColumns,
|
|
68
|
+
outputMappings: Object.freeze(outputMappings),
|
|
69
|
+
writeMappings: Object.freeze(writeMappings)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mapRecordRow(row, fieldKeys = [], overrides = {}) {
|
|
74
|
+
if (!row) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const mapped = {};
|
|
79
|
+
for (const key of fieldKeys) {
|
|
80
|
+
const normalizedKey = String(key || "").trim();
|
|
81
|
+
const columnName = resolveColumnName(normalizedKey, overrides);
|
|
82
|
+
if (!normalizedKey || !columnName) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
mapped[normalizedKey] = row[columnName];
|
|
86
|
+
}
|
|
87
|
+
return mapped;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildWritePayload(sourcePayload = {}, fieldKeys = [], overrides = {}) {
|
|
91
|
+
const source = normalizeObjectInput(sourcePayload);
|
|
92
|
+
const payload = {};
|
|
93
|
+
for (const key of fieldKeys) {
|
|
94
|
+
const normalizedKey = String(key || "").trim();
|
|
95
|
+
const columnName = resolveColumnName(normalizedKey, overrides);
|
|
96
|
+
if (!normalizedKey || !columnName) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!Object.hasOwn(source, normalizedKey)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
payload[columnName] = source[normalizedKey];
|
|
103
|
+
}
|
|
104
|
+
return payload;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveCrudIdColumn(idColumn, { fallback = "id" } = {}) {
|
|
108
|
+
const normalized = String(idColumn ?? fallback ?? "").trim();
|
|
109
|
+
if (!normalized) {
|
|
110
|
+
throw new TypeError("crudRepository requires idColumn.");
|
|
111
|
+
}
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
|
|
24
115
|
export {
|
|
25
116
|
DEFAULT_LIST_LIMIT,
|
|
26
117
|
MAX_LIST_LIMIT,
|
|
27
118
|
normalizeCrudListLimit,
|
|
28
|
-
requireCrudTableName
|
|
119
|
+
requireCrudTableName,
|
|
120
|
+
mapRecordRow,
|
|
121
|
+
buildWritePayload,
|
|
122
|
+
resolveColumnName,
|
|
123
|
+
resolveCrudIdColumn,
|
|
124
|
+
buildRepositoryColumnMetadata
|
|
29
125
|
};
|
|
@@ -7,20 +7,25 @@ import {
|
|
|
7
7
|
crudListQueryKey,
|
|
8
8
|
crudViewQueryKey,
|
|
9
9
|
toRouteRecordId,
|
|
10
|
-
resolveCrudRecordChangedEvent
|
|
10
|
+
resolveCrudRecordChangedEvent,
|
|
11
|
+
normalizeCrudRouteParamName,
|
|
12
|
+
resolveCrudRecordPathTemplates,
|
|
13
|
+
resolveCrudRecordPathParams
|
|
11
14
|
} from "../src/client/composables/crudClientSupportHelpers.js";
|
|
12
15
|
|
|
13
|
-
test("resolveCrudClientConfig normalizes namespace, ownership filter, and
|
|
16
|
+
test("resolveCrudClientConfig normalizes namespace, ownership filter, and resolves route/api paths", () => {
|
|
14
17
|
const config = resolveCrudClientConfig({
|
|
15
18
|
namespace: " Customers ",
|
|
16
19
|
ownershipFilter: "workspace",
|
|
17
|
-
relativePath: "/
|
|
20
|
+
relativePath: "/ops/customers-ui",
|
|
21
|
+
apiRelativePath: "/crud/customers"
|
|
18
22
|
});
|
|
19
23
|
|
|
20
24
|
assert.deepEqual(config, {
|
|
21
25
|
namespace: "customers",
|
|
22
26
|
ownershipFilter: "workspace",
|
|
23
|
-
relativePath: "/
|
|
27
|
+
relativePath: "/ops/customers-ui",
|
|
28
|
+
apiRelativePath: "/crud/customers"
|
|
24
29
|
});
|
|
25
30
|
});
|
|
26
31
|
|
|
@@ -31,6 +36,7 @@ test("resolveCrudClientConfig infers default relativePath from namespace", () =>
|
|
|
31
36
|
});
|
|
32
37
|
|
|
33
38
|
assert.equal(config.relativePath, "/appointments");
|
|
39
|
+
assert.equal(config.apiRelativePath, "/appointments");
|
|
34
40
|
});
|
|
35
41
|
|
|
36
42
|
test("resolveCrudClientConfig throws when namespace is missing", () => {
|
|
@@ -84,6 +90,35 @@ test("toRouteRecordId parses scalar and array params safely", () => {
|
|
|
84
90
|
assert.equal(toRouteRecordId("not-a-number"), 0);
|
|
85
91
|
});
|
|
86
92
|
|
|
93
|
+
test("normalizeCrudRouteParamName validates route parameter names", () => {
|
|
94
|
+
assert.equal(normalizeCrudRouteParamName("recordId"), "recordId");
|
|
95
|
+
assert.equal(normalizeCrudRouteParamName("addressId"), "addressId");
|
|
96
|
+
assert.throws(
|
|
97
|
+
() => normalizeCrudRouteParamName(""),
|
|
98
|
+
/requires a non-empty route parameter name/
|
|
99
|
+
);
|
|
100
|
+
assert.throws(
|
|
101
|
+
() => normalizeCrudRouteParamName("address-id"),
|
|
102
|
+
/route parameter "address-id" is invalid/
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("resolveCrudRecordPathTemplates supports custom route parameter names", () => {
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
resolveCrudRecordPathTemplates("/users/:userId/addresses", "addressId"),
|
|
109
|
+
{
|
|
110
|
+
viewPathTemplate: "/users/:userId/addresses/:addressId",
|
|
111
|
+
editPathTemplate: "/users/:userId/addresses/:addressId/edit"
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("resolveCrudRecordPathParams maps record ids to selected route parameter names", () => {
|
|
117
|
+
assert.deepEqual(resolveCrudRecordPathParams(42, "addressId"), { addressId: "42" });
|
|
118
|
+
assert.deepEqual(resolveCrudRecordPathParams("7", "recordId"), { recordId: "7" });
|
|
119
|
+
assert.deepEqual(resolveCrudRecordPathParams("invalid", "addressId"), {});
|
|
120
|
+
});
|
|
121
|
+
|
|
87
122
|
test("resolveCrudRecordChangedEvent normalizes namespace into event channel", () => {
|
|
88
123
|
assert.equal(resolveCrudRecordChangedEvent("Customers"), "customers.record.changed");
|
|
89
124
|
assert.equal(resolveCrudRecordChangedEvent("customer-orders"), "customer_orders.record.changed");
|
|
@@ -3,7 +3,11 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_LIST_LIMIT,
|
|
5
5
|
normalizeCrudListLimit,
|
|
6
|
-
requireCrudTableName
|
|
6
|
+
requireCrudTableName,
|
|
7
|
+
buildWritePayload,
|
|
8
|
+
mapRecordRow,
|
|
9
|
+
resolveCrudIdColumn,
|
|
10
|
+
buildRepositoryColumnMetadata
|
|
7
11
|
} from "../src/server/repositorySupport.js";
|
|
8
12
|
|
|
9
13
|
test("normalizeCrudListLimit enforces fallback and max", () => {
|
|
@@ -22,3 +26,48 @@ test("requireCrudTableName trims and rejects empty values", () => {
|
|
|
22
26
|
/requires tableName/
|
|
23
27
|
);
|
|
24
28
|
});
|
|
29
|
+
|
|
30
|
+
test("mapRecordRow remaps rows by key/column pairs", () => {
|
|
31
|
+
const row = { some_column: 1, other_column: 2 };
|
|
32
|
+
const mapped = mapRecordRow(row, ["someKey", "otherKey"], {
|
|
33
|
+
someKey: "some_column",
|
|
34
|
+
otherKey: "other_column"
|
|
35
|
+
});
|
|
36
|
+
assert.deepEqual(mapped, {
|
|
37
|
+
someKey: 1,
|
|
38
|
+
otherKey: 2
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("buildWritePayload respects defined keys", () => {
|
|
43
|
+
const payload = buildWritePayload(
|
|
44
|
+
{ foo: "bar", missing: true },
|
|
45
|
+
["foo", "notPresent"],
|
|
46
|
+
{
|
|
47
|
+
foo: "foo_column",
|
|
48
|
+
notPresent: "not_present_column"
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
assert.deepEqual(payload, {
|
|
52
|
+
foo_column: "bar"
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("resolveCrudIdColumn falls back and rejects empties", () => {
|
|
57
|
+
assert.equal(resolveCrudIdColumn(" custom_id "), "custom_id");
|
|
58
|
+
assert.equal(resolveCrudIdColumn(undefined, { fallback: "fallback_id" }), "fallback_id");
|
|
59
|
+
assert.throws(() => resolveCrudIdColumn("", { fallback: "" }), /requires idColumn/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("buildRepositoryColumnMetadata normalizes columns and applies overrides", () => {
|
|
63
|
+
const metadata = buildRepositoryColumnMetadata({
|
|
64
|
+
outputKeys: ["firstName", "lastName"],
|
|
65
|
+
writeKeys: ["firstName"],
|
|
66
|
+
columnOverrides: { lastName: "surname" }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(metadata.selectColumns, Object.freeze(["first_name", "surname"]));
|
|
70
|
+
assert.equal(metadata.outputMappings.length, 2);
|
|
71
|
+
assert.equal(metadata.writeMappings.length, 1);
|
|
72
|
+
assert.equal(metadata.outputMappings[1].column, "surname");
|
|
73
|
+
});
|