@jskit-ai/crud-core 0.1.4
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 +37 -0
- package/package.json +20 -0
- package/src/client/composables/createCrudClientSupport.js +201 -0
- package/src/client/composables/crudClientSupportHelpers.js +111 -0
- package/src/client/composables/useCrudRealtimeInvalidation.js +37 -0
- package/src/client/index.js +24 -0
- package/src/server/repositorySupport.js +29 -0
- package/test/createCrudClientSupport.test.js +90 -0
- package/test/repositorySupport.test.js +24 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
packageVersion: 1,
|
|
3
|
+
packageId: "@jskit-ai/crud-core",
|
|
4
|
+
version: "0.1.4",
|
|
5
|
+
description: "Shared CRUD helpers used by CRUD modules.",
|
|
6
|
+
dependsOn: [
|
|
7
|
+
"@jskit-ai/kernel",
|
|
8
|
+
"@jskit-ai/realtime",
|
|
9
|
+
"@jskit-ai/shell-web",
|
|
10
|
+
"@jskit-ai/users-web"
|
|
11
|
+
],
|
|
12
|
+
capabilities: {
|
|
13
|
+
provides: ["crud.core"],
|
|
14
|
+
requires: []
|
|
15
|
+
},
|
|
16
|
+
runtime: {
|
|
17
|
+
server: {
|
|
18
|
+
providers: []
|
|
19
|
+
},
|
|
20
|
+
client: {
|
|
21
|
+
providers: []
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
mutations: {
|
|
25
|
+
dependencies: {
|
|
26
|
+
runtime: {
|
|
27
|
+
"@jskit-ai/crud-core": "0.1.4"
|
|
28
|
+
},
|
|
29
|
+
dev: {}
|
|
30
|
+
},
|
|
31
|
+
packageJson: {
|
|
32
|
+
scripts: {}
|
|
33
|
+
},
|
|
34
|
+
procfile: {},
|
|
35
|
+
files: []
|
|
36
|
+
}
|
|
37
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/crud-core",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./client": "./src/client/index.js",
|
|
10
|
+
"./client/composables/*": "./src/client/composables/*.js",
|
|
11
|
+
"./server/repositorySupport": "./src/server/repositorySupport.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@tanstack/vue-query": "^5.90.5",
|
|
15
|
+
"@jskit-ai/kernel": "0.1.4",
|
|
16
|
+
"@jskit-ai/realtime": "0.1.4",
|
|
17
|
+
"@jskit-ai/shell-web": "0.1.4",
|
|
18
|
+
"@jskit-ai/users-web": "0.1.4"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useRoute, useRouter } from "vue-router";
|
|
3
|
+
import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
|
|
4
|
+
import {
|
|
5
|
+
resolveCrudClientConfig,
|
|
6
|
+
formatDateTime,
|
|
7
|
+
crudScopeQueryKey,
|
|
8
|
+
invalidateCrudQueries,
|
|
9
|
+
crudListQueryKey,
|
|
10
|
+
crudViewQueryKey,
|
|
11
|
+
toRouteRecordId
|
|
12
|
+
} from "./crudClientSupportHelpers.js";
|
|
13
|
+
import { useCrudRealtimeInvalidation } from "./useCrudRealtimeInvalidation.js";
|
|
14
|
+
|
|
15
|
+
function useCrudClientContext(source = {}) {
|
|
16
|
+
const crudConfig = resolveCrudClientConfig(source);
|
|
17
|
+
const paths = usePaths();
|
|
18
|
+
const route = useRoute();
|
|
19
|
+
const workspaceSlugToken = computed(() => paths.workspaceSlug.value);
|
|
20
|
+
const listPath = computed(() => paths.page(crudConfig.relativePath));
|
|
21
|
+
const createPath = computed(() => paths.page(`${crudConfig.relativePath}/new`));
|
|
22
|
+
|
|
23
|
+
function listQueryKey(surfaceId = "") {
|
|
24
|
+
const normalizedSurfaceId = String(surfaceId || paths.currentSurfaceId.value || "").trim();
|
|
25
|
+
return crudListQueryKey(normalizedSurfaceId, workspaceSlugToken.value, crudConfig.namespace);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function viewQueryKey(surfaceId = "", recordId = 0) {
|
|
29
|
+
const normalizedSurfaceId = String(surfaceId || paths.currentSurfaceId.value || "").trim();
|
|
30
|
+
return crudViewQueryKey(normalizedSurfaceId, workspaceSlugToken.value, recordId, crudConfig.namespace);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveViewPath(recordIdLike) {
|
|
34
|
+
const recordId = toRouteRecordId(recordIdLike);
|
|
35
|
+
if (!recordId) {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return paths.page(`${crudConfig.relativePath}/${recordId}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveEditPath(recordIdLike) {
|
|
43
|
+
const recordId = toRouteRecordId(recordIdLike);
|
|
44
|
+
if (!recordId) {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return paths.page(`${crudConfig.relativePath}/${recordId}/edit`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function scopeQueryKey() {
|
|
52
|
+
return crudScopeQueryKey(crudConfig.namespace);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function invalidateQueries(queryClient) {
|
|
56
|
+
return invalidateCrudQueries(queryClient, crudConfig.namespace);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
route,
|
|
61
|
+
crudConfig,
|
|
62
|
+
listPath,
|
|
63
|
+
createPath,
|
|
64
|
+
listQueryKey,
|
|
65
|
+
viewQueryKey,
|
|
66
|
+
scopeQueryKey,
|
|
67
|
+
invalidateQueries,
|
|
68
|
+
formatDateTime,
|
|
69
|
+
resolveViewPath,
|
|
70
|
+
resolveEditPath
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
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
|
+
function useCrudRecordRuntime(source = {}, { recordIdParam = "recordId" } = {}) {
|
|
84
|
+
const normalizedRecordIdParam = normalizeRecordIdParam(recordIdParam);
|
|
85
|
+
const crudContext = useCrudClientContext(source);
|
|
86
|
+
useCrudRealtimeInvalidation(crudContext.crudConfig.namespace);
|
|
87
|
+
const router = useRouter();
|
|
88
|
+
const recordId = computed(() => toRouteRecordId(crudContext.route.params[normalizedRecordIdParam]));
|
|
89
|
+
const apiSuffix = computed(() => `${crudContext.crudConfig.relativePath}/${recordId.value}`);
|
|
90
|
+
const viewPath = computed(() => crudContext.resolveViewPath(recordId.value));
|
|
91
|
+
const editPath = computed(() => crudContext.resolveEditPath(recordId.value));
|
|
92
|
+
const listPath = crudContext.listPath;
|
|
93
|
+
|
|
94
|
+
function viewQueryKey(surfaceId = "") {
|
|
95
|
+
return crudContext.viewQueryKey(surfaceId, recordId.value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function invalidateAndGoList(queryClient) {
|
|
99
|
+
await crudContext.invalidateQueries(queryClient);
|
|
100
|
+
if (listPath.value) {
|
|
101
|
+
await router.push(listPath.value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function invalidateAndGoView(queryClient, recordIdLike = recordId.value) {
|
|
106
|
+
await crudContext.invalidateQueries(queryClient);
|
|
107
|
+
|
|
108
|
+
const targetRecordId = toRouteRecordId(recordIdLike);
|
|
109
|
+
const targetPath = crudContext.resolveViewPath(targetRecordId || recordId.value);
|
|
110
|
+
if (targetPath) {
|
|
111
|
+
await router.push(targetPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
crudContext,
|
|
117
|
+
listPath,
|
|
118
|
+
recordId,
|
|
119
|
+
apiSuffix,
|
|
120
|
+
viewPath,
|
|
121
|
+
editPath,
|
|
122
|
+
viewQueryKey,
|
|
123
|
+
invalidateAndGoList,
|
|
124
|
+
invalidateAndGoView
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function useCrudCreateRuntime(source = {}) {
|
|
129
|
+
const crudContext = useCrudClientContext(source);
|
|
130
|
+
useCrudRealtimeInvalidation(crudContext.crudConfig.namespace);
|
|
131
|
+
const router = useRouter();
|
|
132
|
+
const listPath = crudContext.listPath;
|
|
133
|
+
const apiSuffix = crudContext.crudConfig.relativePath;
|
|
134
|
+
|
|
135
|
+
function createQueryKey(surfaceId = "") {
|
|
136
|
+
return [...crudContext.listQueryKey(surfaceId), "create"];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function invalidateAndGoView(queryClient, recordIdLike) {
|
|
140
|
+
await crudContext.invalidateQueries(queryClient);
|
|
141
|
+
|
|
142
|
+
const targetPath = crudContext.resolveViewPath(recordIdLike);
|
|
143
|
+
if (targetPath) {
|
|
144
|
+
await router.push(targetPath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return Object.freeze({
|
|
149
|
+
crudContext,
|
|
150
|
+
listPath,
|
|
151
|
+
apiSuffix,
|
|
152
|
+
createQueryKey,
|
|
153
|
+
invalidateAndGoView
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function useCrudListRuntime(source = {}) {
|
|
158
|
+
const crudContext = useCrudClientContext(source);
|
|
159
|
+
useCrudRealtimeInvalidation(crudContext.crudConfig.namespace);
|
|
160
|
+
const createPath = crudContext.createPath;
|
|
161
|
+
const apiSuffix = crudContext.crudConfig.relativePath;
|
|
162
|
+
|
|
163
|
+
function listQueryKey(surfaceId = "") {
|
|
164
|
+
return crudContext.listQueryKey(surfaceId);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
crudContext,
|
|
169
|
+
createPath,
|
|
170
|
+
apiSuffix,
|
|
171
|
+
listQueryKey
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createCrudClientSupport(source = {}) {
|
|
176
|
+
const crudConfig = resolveCrudClientConfig(source);
|
|
177
|
+
|
|
178
|
+
return Object.freeze({
|
|
179
|
+
useCrudClientContext() {
|
|
180
|
+
return useCrudClientContext(crudConfig);
|
|
181
|
+
},
|
|
182
|
+
useCrudListRuntime() {
|
|
183
|
+
return useCrudListRuntime(crudConfig);
|
|
184
|
+
},
|
|
185
|
+
useCrudCreateRuntime() {
|
|
186
|
+
return useCrudCreateRuntime(crudConfig);
|
|
187
|
+
},
|
|
188
|
+
useCrudRecordRuntime(options = {}) {
|
|
189
|
+
return useCrudRecordRuntime(crudConfig, options);
|
|
190
|
+
},
|
|
191
|
+
toRouteRecordId
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export {
|
|
196
|
+
useCrudClientContext,
|
|
197
|
+
useCrudListRuntime,
|
|
198
|
+
useCrudCreateRuntime,
|
|
199
|
+
useCrudRecordRuntime,
|
|
200
|
+
createCrudClientSupport
|
|
201
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { normalizeLowerText, normalizeText, normalizeQueryToken } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { normalizeRouteVisibilityToken } from "@jskit-ai/kernel/shared/support/visibility";
|
|
3
|
+
import { formatDateTime } from "@jskit-ai/kernel/shared/support";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CRUD_OWNERSHIP_FILTER = "workspace";
|
|
6
|
+
|
|
7
|
+
function requireCrudNamespace(namespace, { context = "resolveCrudClientConfig" } = {}) {
|
|
8
|
+
const normalizedNamespace = normalizeLowerText(namespace);
|
|
9
|
+
if (!normalizedNamespace) {
|
|
10
|
+
throw new TypeError(`${context} requires a non-empty namespace.`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return normalizedNamespace;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeRelativePath(value, { context = "resolveCrudClientConfig" } = {}) {
|
|
17
|
+
const raw = normalizeText(value);
|
|
18
|
+
if (!raw) {
|
|
19
|
+
throw new TypeError(`${context} requires a non-empty relative path.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const normalized = `/${raw.replace(/^\/+|\/+$/g, "")}`;
|
|
23
|
+
if (normalized === "/") {
|
|
24
|
+
throw new TypeError(`${context} requires a non-empty relative path.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveCrudClientConfig(source = {}) {
|
|
31
|
+
const payload = source && typeof source === "object" && !Array.isArray(source) ? source : {};
|
|
32
|
+
const namespace = requireCrudNamespace(payload.namespace, {
|
|
33
|
+
context: "resolveCrudClientConfig"
|
|
34
|
+
});
|
|
35
|
+
const ownershipFilter = normalizeRouteVisibilityToken(payload.ownershipFilter, {
|
|
36
|
+
fallback: DEFAULT_CRUD_OWNERSHIP_FILTER
|
|
37
|
+
});
|
|
38
|
+
const inferredRelativePath = `/${namespace}`;
|
|
39
|
+
const relativePath = normalizeRelativePath(
|
|
40
|
+
Object.hasOwn(payload, "relativePath") ? payload.relativePath : inferredRelativePath,
|
|
41
|
+
{ context: "resolveCrudClientConfig" }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return Object.freeze({
|
|
45
|
+
namespace,
|
|
46
|
+
ownershipFilter,
|
|
47
|
+
relativePath
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function crudListQueryKey(surfaceId = "", workspaceSlug = "", namespace = "") {
|
|
52
|
+
return Object.freeze([
|
|
53
|
+
...crudScopeQueryKey(namespace),
|
|
54
|
+
"list",
|
|
55
|
+
normalizeQueryToken(surfaceId),
|
|
56
|
+
normalizeQueryToken(workspaceSlug)
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function crudViewQueryKey(surfaceId = "", workspaceSlug = "", recordId = 0, namespace = "") {
|
|
61
|
+
return Object.freeze([
|
|
62
|
+
...crudScopeQueryKey(namespace),
|
|
63
|
+
"view",
|
|
64
|
+
normalizeQueryToken(surfaceId),
|
|
65
|
+
normalizeQueryToken(workspaceSlug),
|
|
66
|
+
Number(recordId) || 0
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function crudScopeQueryKey(namespace = "") {
|
|
71
|
+
return Object.freeze(["crud", normalizeQueryToken(namespace)]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveCrudRecordChangedEvent(namespace = "") {
|
|
75
|
+
const normalizedNamespace = requireCrudNamespace(namespace, {
|
|
76
|
+
context: "resolveCrudRecordChangedEvent"
|
|
77
|
+
});
|
|
78
|
+
return `${normalizedNamespace.replace(/-/g, "_")}.record.changed`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function invalidateCrudQueries(queryClient, namespace = "") {
|
|
82
|
+
if (!queryClient || typeof queryClient.invalidateQueries !== "function") {
|
|
83
|
+
throw new TypeError("invalidateCrudQueries requires queryClient.invalidateQueries().");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return queryClient.invalidateQueries({
|
|
87
|
+
queryKey: crudScopeQueryKey(namespace)
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function toRouteRecordId(value) {
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return toRouteRecordId(value[0]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const parsed = Number(value);
|
|
97
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
DEFAULT_CRUD_OWNERSHIP_FILTER,
|
|
102
|
+
requireCrudNamespace,
|
|
103
|
+
resolveCrudClientConfig,
|
|
104
|
+
formatDateTime,
|
|
105
|
+
resolveCrudRecordChangedEvent,
|
|
106
|
+
crudScopeQueryKey,
|
|
107
|
+
invalidateCrudQueries,
|
|
108
|
+
crudListQueryKey,
|
|
109
|
+
crudViewQueryKey,
|
|
110
|
+
toRouteRecordId
|
|
111
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useQueryClient } from "@tanstack/vue-query";
|
|
2
|
+
import { useRealtimeEvent } from "@jskit-ai/realtime/client/composables/useRealtimeEvent";
|
|
3
|
+
import {
|
|
4
|
+
crudScopeQueryKey,
|
|
5
|
+
requireCrudNamespace,
|
|
6
|
+
resolveCrudRecordChangedEvent
|
|
7
|
+
} from "./crudClientSupportHelpers.js";
|
|
8
|
+
|
|
9
|
+
function useCrudRealtimeInvalidation(
|
|
10
|
+
namespace = "",
|
|
11
|
+
{
|
|
12
|
+
event = "",
|
|
13
|
+
enabled = true,
|
|
14
|
+
matches = null,
|
|
15
|
+
queryKey = null
|
|
16
|
+
} = {}
|
|
17
|
+
) {
|
|
18
|
+
const normalizedNamespace = requireCrudNamespace(namespace, {
|
|
19
|
+
context: "useCrudRealtimeInvalidation"
|
|
20
|
+
});
|
|
21
|
+
const queryClient = useQueryClient();
|
|
22
|
+
const resolvedEvent = String(event || "").trim() || resolveCrudRecordChangedEvent(normalizedNamespace);
|
|
23
|
+
const resolvedQueryKey = Array.isArray(queryKey) && queryKey.length > 0 ? queryKey : crudScopeQueryKey(normalizedNamespace);
|
|
24
|
+
|
|
25
|
+
return useRealtimeEvent({
|
|
26
|
+
event: resolvedEvent,
|
|
27
|
+
enabled,
|
|
28
|
+
matches,
|
|
29
|
+
onEvent: async () => {
|
|
30
|
+
await queryClient.invalidateQueries({
|
|
31
|
+
queryKey: resolvedQueryKey
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { useCrudRealtimeInvalidation };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_CRUD_OWNERSHIP_FILTER,
|
|
3
|
+
resolveCrudClientConfig,
|
|
4
|
+
crudListQueryKey,
|
|
5
|
+
crudViewQueryKey,
|
|
6
|
+
crudScopeQueryKey,
|
|
7
|
+
invalidateCrudQueries,
|
|
8
|
+
formatDateTime,
|
|
9
|
+
resolveCrudRecordChangedEvent,
|
|
10
|
+
toRouteRecordId,
|
|
11
|
+
requireCrudNamespace
|
|
12
|
+
} from "./composables/crudClientSupportHelpers.js";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
useCrudClientContext,
|
|
16
|
+
useCrudListRuntime,
|
|
17
|
+
useCrudCreateRuntime,
|
|
18
|
+
useCrudRecordRuntime,
|
|
19
|
+
createCrudClientSupport
|
|
20
|
+
} from "./composables/createCrudClientSupport.js";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
useCrudRealtimeInvalidation
|
|
24
|
+
} from "./composables/useCrudRealtimeInvalidation.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_LIST_LIMIT = 20;
|
|
4
|
+
const MAX_LIST_LIMIT = 100;
|
|
5
|
+
|
|
6
|
+
function normalizeCrudListLimit(value, { fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT } = {}) {
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return Math.min(parsed, max);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requireCrudTableName(tableName, { context = "crudRepository" } = {}) {
|
|
16
|
+
const normalizedTableName = normalizeText(tableName);
|
|
17
|
+
if (!normalizedTableName) {
|
|
18
|
+
throw new TypeError(`${context} requires tableName.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return normalizedTableName;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
DEFAULT_LIST_LIMIT,
|
|
26
|
+
MAX_LIST_LIMIT,
|
|
27
|
+
normalizeCrudListLimit,
|
|
28
|
+
requireCrudTableName
|
|
29
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
resolveCrudClientConfig,
|
|
5
|
+
crudScopeQueryKey,
|
|
6
|
+
invalidateCrudQueries,
|
|
7
|
+
crudListQueryKey,
|
|
8
|
+
crudViewQueryKey,
|
|
9
|
+
toRouteRecordId,
|
|
10
|
+
resolveCrudRecordChangedEvent
|
|
11
|
+
} from "../src/client/composables/crudClientSupportHelpers.js";
|
|
12
|
+
|
|
13
|
+
test("resolveCrudClientConfig normalizes namespace, ownership filter, and derives relativePath", () => {
|
|
14
|
+
const config = resolveCrudClientConfig({
|
|
15
|
+
namespace: " Customers ",
|
|
16
|
+
ownershipFilter: "workspace",
|
|
17
|
+
relativePath: "/crm/customers"
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
assert.deepEqual(config, {
|
|
21
|
+
namespace: "customers",
|
|
22
|
+
ownershipFilter: "workspace",
|
|
23
|
+
relativePath: "/crm/customers"
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("resolveCrudClientConfig infers default relativePath from namespace", () => {
|
|
28
|
+
const config = resolveCrudClientConfig({
|
|
29
|
+
namespace: "appointments",
|
|
30
|
+
ownershipFilter: "public"
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.equal(config.relativePath, "/appointments");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("resolveCrudClientConfig throws when namespace is missing", () => {
|
|
37
|
+
assert.throws(
|
|
38
|
+
() => resolveCrudClientConfig({ ownershipFilter: "workspace" }),
|
|
39
|
+
/requires a non-empty namespace/
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("crudListQueryKey and crudViewQueryKey normalize cache keys", () => {
|
|
44
|
+
assert.deepEqual(crudListQueryKey("Admin", " TonymoBily3 ", "Customers"), [
|
|
45
|
+
"crud",
|
|
46
|
+
"customers",
|
|
47
|
+
"list",
|
|
48
|
+
"admin",
|
|
49
|
+
"tonymobily3"
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
assert.deepEqual(crudViewQueryKey("Admin", " TonymoBily3 ", "12", "Customers"), [
|
|
53
|
+
"crud",
|
|
54
|
+
"customers",
|
|
55
|
+
"view",
|
|
56
|
+
"admin",
|
|
57
|
+
"tonymobily3",
|
|
58
|
+
12
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("crudScopeQueryKey normalizes namespace", () => {
|
|
63
|
+
assert.deepEqual(crudScopeQueryKey(" Customers "), ["crud", "customers"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("invalidateCrudQueries invalidates by CRUD namespace scope key", async () => {
|
|
67
|
+
let payload = null;
|
|
68
|
+
const queryClient = {
|
|
69
|
+
async invalidateQueries(input) {
|
|
70
|
+
payload = input;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await invalidateCrudQueries(queryClient, "Customers");
|
|
76
|
+
assert.deepEqual(payload, {
|
|
77
|
+
queryKey: ["crud", "customers"]
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("toRouteRecordId parses scalar and array params safely", () => {
|
|
82
|
+
assert.equal(toRouteRecordId("42"), 42);
|
|
83
|
+
assert.equal(toRouteRecordId(["7"]), 7);
|
|
84
|
+
assert.equal(toRouteRecordId("not-a-number"), 0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("resolveCrudRecordChangedEvent normalizes namespace into event channel", () => {
|
|
88
|
+
assert.equal(resolveCrudRecordChangedEvent("Customers"), "customers.record.changed");
|
|
89
|
+
assert.equal(resolveCrudRecordChangedEvent("customer-orders"), "customer_orders.record.changed");
|
|
90
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_LIST_LIMIT,
|
|
5
|
+
normalizeCrudListLimit,
|
|
6
|
+
requireCrudTableName
|
|
7
|
+
} from "../src/server/repositorySupport.js";
|
|
8
|
+
|
|
9
|
+
test("normalizeCrudListLimit enforces fallback and max", () => {
|
|
10
|
+
assert.equal(normalizeCrudListLimit(null), DEFAULT_LIST_LIMIT);
|
|
11
|
+
assert.equal(normalizeCrudListLimit("abc"), DEFAULT_LIST_LIMIT);
|
|
12
|
+
assert.equal(normalizeCrudListLimit(0), DEFAULT_LIST_LIMIT);
|
|
13
|
+
assert.equal(normalizeCrudListLimit(5), 5);
|
|
14
|
+
assert.equal(normalizeCrudListLimit(200), 100);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("requireCrudTableName trims and rejects empty values", () => {
|
|
18
|
+
assert.equal(requireCrudTableName(" crud_customers "), "crud_customers");
|
|
19
|
+
|
|
20
|
+
assert.throws(
|
|
21
|
+
() => requireCrudTableName(" "),
|
|
22
|
+
/requires tableName/
|
|
23
|
+
);
|
|
24
|
+
});
|