@jskit-ai/crud-core 0.1.166 → 0.1.167

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.json CHANGED
@@ -1,18 +1,11 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-core",
3
- "version": "0.1.166",
3
+ "version": "0.1.167",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "exports": {
9
- "./client": "./src/client/index.js",
10
- "./client/composables/createCrudClientSupport": "./src/client/composables/createCrudClientSupport.js",
11
- "./client/composables/crudClientSupportHelpers": "./src/client/composables/crudClientSupportHelpers.js",
12
- "./client/composables/useCrudRealtimeInvalidation": "./src/client/composables/useCrudRealtimeInvalidation.js",
13
- "./shared/crudFieldSupport": "./src/shared/crudFieldSupport.js",
14
- "./shared/crudNamespaceSupport": "./src/shared/crudNamespaceSupport.js",
15
- "./shared/crudResource": "./src/shared/crudResource.js",
16
9
  "./server/repositorySupport": "./src/server/repositorySupport.js",
17
10
  "./server/resourceRuntime": "./src/server/resourceRuntime/index.js",
18
11
  "./server/createHooksToCollectChildren": "./src/server/createHooksToCollectChildren.js",
@@ -27,20 +20,13 @@
27
20
  "./server/routeContracts": "./src/server/routeContracts.js"
28
21
  },
29
22
  "dependencies": {
30
- "@jskit-ai/database-runtime": "0.1.155",
31
- "@jskit-ai/http-runtime": "0.1.153",
32
- "@jskit-ai/kernel": "0.1.155",
33
- "@jskit-ai/realtime": "0.1.152",
34
- "@jskit-ai/resource-crud-core": "0.1.97",
35
- "@jskit-ai/shell-web": "0.1.159",
23
+ "@jskit-ai/database-runtime": "0.1.156",
24
+ "@jskit-ai/http-runtime": "0.1.154",
25
+ "@jskit-ai/kernel": "0.1.156",
26
+ "@jskit-ai/resource-crud-core": "0.1.98",
36
27
  "json-rest-schema": "^1.0.17"
37
28
  },
38
- "peerDependencies": {
39
- "@tanstack/vue-query": "^5.90.5",
40
- "vue": "^3.5.13",
41
- "vue-router": "^5.0.4"
42
- },
43
- "description": "Shared CRUD helpers used by CRUD modules.",
29
+ "description": "Shared server-side CRUD service, repository, route, and query helpers.",
44
30
  "jskit": {
45
31
  "kind": "runtime",
46
32
  "capabilities": {
@@ -60,7 +46,7 @@
60
46
  "mutations": {
61
47
  "dependencies": {
62
48
  "runtime": {
63
- "@jskit-ai/crud-core": "0.1.166"
49
+ "@jskit-ai/crud-core": "0.1.167"
64
50
  },
65
51
  "dev": {}
66
52
  },
@@ -3,7 +3,7 @@ import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
3
3
  import {
4
4
  normalizeCrudNamespace,
5
5
  requireCrudNamespace
6
- } from "../shared/crudNamespaceSupport.js";
6
+ } from "@jskit-ai/resource-crud-core/shared/crudNamespaceSupport";
7
7
  import {
8
8
  resolveScopedApiBasePath
9
9
  } from "@jskit-ai/kernel/shared/surface";
@@ -1,5 +1,5 @@
1
1
  import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
- import { resolveCrudFieldSchemaProperties } from "@jskit-ai/kernel/shared/support/crudFieldContract";
2
+ import { resolveCrudFieldSchemaProperties } from "@jskit-ai/resource-crud-core/shared/crudFieldContract";
3
3
  import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
4
  import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
5
5
 
@@ -26,7 +26,7 @@ import {
26
26
  INVALID_CRUD_LIST_FILTER_QUERY_VALUE,
27
27
  normalizeCrudListFilterInvalidValues,
28
28
  parseCrudListFilterQueryValue
29
- } from "@jskit-ai/kernel/shared/support/crudListFilters";
29
+ } from "@jskit-ai/resource-crud-core/shared/crudListFilters";
30
30
 
31
31
  const DATE_FILTER_VALUE_PATTERN_SOURCE = "\\d{4}-\\d{2}-\\d{2}";
32
32
  const DATE_FILTER_PATTERN_SOURCE = `^${DATE_FILTER_VALUE_PATTERN_SOURCE}$`;
@@ -3,7 +3,7 @@ import {
3
3
  cursorPaginationQueryValidator
4
4
  } from "@jskit-ai/kernel/shared/validators";
5
5
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
6
- import { resolveCrudParentFilterKeys as resolveSharedCrudParentFilterKeys } from "@jskit-ai/kernel/shared/support/crudLookup";
6
+ import { resolveCrudParentFilterKeys as resolveSharedCrudParentFilterKeys } from "@jskit-ai/resource-crud-core/shared/crudLookup";
7
7
 
8
8
  const listSearchQueryValidator = Object.freeze({
9
9
  schema: createSchema({
@@ -5,8 +5,8 @@ import {
5
5
  resolveCrudLookupApiPathFromNamespace,
6
6
  normalizeCrudLookupContainerKey,
7
7
  resolveCrudLookupContainerKey
8
- } from "@jskit-ai/kernel/shared/support/crudLookup";
9
- import { buildCrudFieldContractMap } from "@jskit-ai/kernel/shared/support/crudFieldContract";
8
+ } from "@jskit-ai/resource-crud-core/shared/crudLookup";
9
+ import { buildCrudFieldContractMap } from "@jskit-ai/resource-crud-core/shared/crudFieldContract";
10
10
  import { normalizeCrudLookupApiPath } from "./lookupPathSupport.js";
11
11
 
12
12
  const DEFAULT_LOOKUP_INCLUDE = "*";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  normalizeCrudLookupApiPath,
3
3
  normalizeCrudLookupNamespace
4
- } from "@jskit-ai/kernel/shared/support/crudLookup";
4
+ } from "@jskit-ai/resource-crud-core/shared/crudLookup";
5
5
  import { toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
6
6
 
7
7
  function requireCrudLookupNamespace(value = "", { context = "crudLookup" } = {}) {
@@ -16,13 +16,13 @@ import {
16
16
  CRUD_FIELD_STORAGE_COLUMN,
17
17
  CRUD_FIELD_STORAGE_VIRTUAL,
18
18
  CRUD_FIELD_WRITE_SERIALIZER_DATETIME_UTC
19
- } from "@jskit-ai/kernel/shared/support/crudFieldContract";
19
+ } from "@jskit-ai/resource-crud-core/shared/crudFieldContract";
20
20
  import { toSnakeCase } from "@jskit-ai/kernel/shared/support/stringCase";
21
21
  import {
22
+ isCrudRuntimeOutputOnlyFieldKey,
22
23
  resolveCrudLookupContainerKey,
23
24
  resolveCrudLookupFieldKeys
24
- } from "@jskit-ai/kernel/shared/support/crudLookup";
25
- import { isCrudRuntimeOutputOnlyFieldKey } from "../shared/crudFieldSupport.js";
25
+ } from "@jskit-ai/resource-crud-core/shared/crudLookup";
26
26
 
27
27
  const DEFAULT_LIST_LIMIT = 20;
28
28
  const MAX_LIST_LIMIT = 100;
@@ -27,7 +27,7 @@ import {
27
27
  createCrudLookupRuntime,
28
28
  hydrateCrudLookupRecords
29
29
  } from "./lookupHydration.js";
30
- import { CRUD_FIELD_STORAGE_COLUMN } from "@jskit-ai/kernel/shared/support/crudFieldContract";
30
+ import { CRUD_FIELD_STORAGE_COLUMN } from "@jskit-ai/resource-crud-core/shared/crudFieldContract";
31
31
 
32
32
  const LIST_ORDER_DIRECTION_ASC = "asc";
33
33
  const LIST_ORDER_DIRECTION_DESC = "desc";
@@ -5,8 +5,8 @@ import {
5
5
  resolveCrudLookupApiPathFromNamespace,
6
6
  normalizeCrudLookupContainerKey,
7
7
  resolveCrudLookupContainerKey
8
- } from "@jskit-ai/kernel/shared/support/crudLookup";
9
- import { buildCrudFieldContractMap } from "@jskit-ai/kernel/shared/support/crudFieldContract";
8
+ } from "@jskit-ai/resource-crud-core/shared/crudLookup";
9
+ import { buildCrudFieldContractMap } from "@jskit-ai/resource-crud-core/shared/crudFieldContract";
10
10
  import { normalizeCrudLookupApiPath } from "../lookupPathSupport.js";
11
11
 
12
12
  const DEFAULT_LOOKUP_INCLUDE = "*";
@@ -6,7 +6,7 @@ import {
6
6
  composeSchemaDefinitions,
7
7
  recordIdParamsValidator
8
8
  } from "@jskit-ai/kernel/shared/validators";
9
- import { resolveCrudResourceScopeName } from "@jskit-ai/kernel/shared/support/crudLookup";
9
+ import { resolveCrudResourceScopeName } from "@jskit-ai/resource-crud-core/shared/crudLookup";
10
10
  import {
11
11
  createStandardCrudListQueryValidators,
12
12
  createStandardCrudViewQueryValidators,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  requireCrudNamespace,
3
3
  resolveCrudRecordChangedEvent
4
- } from "../shared/crudNamespaceSupport.js";
4
+ } from "@jskit-ai/resource-crud-core/shared/crudNamespaceSupport";
5
5
 
6
6
  function normalizeCrudEventEntityId(value = null) {
7
7
  return value == null ? "" : String(value).trim();
@@ -1,7 +1,7 @@
1
1
  import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
2
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
3
  import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators";
4
- import { requireCrudNamespace } from "../shared/crudNamespaceSupport.js";
4
+ import { requireCrudNamespace } from "@jskit-ai/resource-crud-core/shared/crudNamespaceSupport";
5
5
  import { createCrudFieldAccessRuntime } from "./fieldAccess.js";
6
6
 
7
7
  function createCrudServiceRuntime(resource = {}, { context = "crudService" } = {}) {
@@ -2,7 +2,7 @@ import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { createSchema } from "json-rest-schema";
4
4
  import { compileRouteValidator } from "@jskit-ai/kernel/_testable";
5
- import { defineCrudListFilters } from "@jskit-ai/kernel/shared/support/crudListFilters";
5
+ import { defineCrudListFilters } from "@jskit-ai/resource-crud-core/shared/crudListFilters";
6
6
  import {
7
7
  composeSchemaDefinitions,
8
8
  cursorPaginationQueryValidator
@@ -1,298 +0,0 @@
1
- import { computed } from "vue";
2
- import { useRoute, useRouter } from "vue-router";
3
- import { usePaths } from "@jskit-ai/shell-web/client/navigation/usePaths";
4
- import {
5
- resolveCrudClientConfig,
6
- formatDateTime,
7
- crudScopeQueryKey,
8
- invalidateCrudQueries,
9
- crudListQueryKey,
10
- crudViewQueryKey,
11
- toRouteRecordId,
12
- normalizeCrudRouteParamName,
13
- resolveCrudRecordPathTemplates,
14
- resolveCrudRecordPathParams
15
- } from "./crudClientSupportHelpers.js";
16
- import { useCrudRealtimeInvalidation } from "./useCrudRealtimeInvalidation.js";
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
-
81
- function useCrudClientContext(source = {}) {
82
- const crudConfig = resolveCrudClientConfig(source);
83
- const paths = usePaths();
84
- const route = useRoute();
85
- const workspaceSlugToken = computed(() => paths.workspaceSlug.value);
86
- const defaultRecordIdParam = "recordId";
87
- const listPathTemplate = crudConfig.relativePath;
88
- const createPathTemplate = `${crudConfig.relativePath}/new`;
89
- const defaultRecordPathTemplates = resolveCrudRecordPathTemplates(listPathTemplate, defaultRecordIdParam);
90
-
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) : "";
98
- }
99
-
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) : "";
107
- }
108
-
109
- const listPath = computed(() => resolvePath(listPathTemplate));
110
- const createPath = computed(() => resolvePath(createPathTemplate));
111
-
112
- function resolveRecordPathTemplates(recordIdParam = defaultRecordIdParam) {
113
- return resolveCrudRecordPathTemplates(listPathTemplate, recordIdParam);
114
- }
115
-
116
- function resolveRecordParams(recordIdLike = "", { 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
- }
124
-
125
- function viewQueryKey(surfaceId = "", recordId = "") {
126
- const normalizedSurfaceId = String(surfaceId || paths.currentSurfaceId.value || "").trim();
127
- return crudViewQueryKey(normalizedSurfaceId, workspaceSlugToken.value, recordId, crudConfig.namespace);
128
- }
129
-
130
- function scopeQueryKey() {
131
- return crudScopeQueryKey(crudConfig.namespace);
132
- }
133
-
134
- async function invalidateQueries(queryClient) {
135
- return invalidateCrudQueries(queryClient, crudConfig.namespace);
136
- }
137
-
138
- return Object.freeze({
139
- route,
140
- crudConfig,
141
- listPathTemplate,
142
- createPathTemplate,
143
- defaultRecordIdParam,
144
- viewPathTemplate: defaultRecordPathTemplates.viewPathTemplate,
145
- editPathTemplate: defaultRecordPathTemplates.editPathTemplate,
146
- resolveRecordPathTemplates,
147
- resolveRecordParams,
148
- resolvePath,
149
- resolveApiPath,
150
- listPath,
151
- createPath,
152
- listQueryKey,
153
- viewQueryKey,
154
- scopeQueryKey,
155
- invalidateQueries,
156
- formatDateTime
157
- });
158
- }
159
-
160
- function useCrudRecordRuntime(source = {}, { recordIdParam = "recordId" } = {}) {
161
- const normalizedRecordIdParam = normalizeCrudRouteParamName(recordIdParam, {
162
- context: "useCrudRecordRuntime"
163
- });
164
- const crudContext = useCrudClientContext(source);
165
- useCrudRealtimeInvalidation(crudContext.crudConfig.namespace);
166
- const router = useRouter();
167
- const recordPathTemplates = crudContext.resolveRecordPathTemplates(normalizedRecordIdParam);
168
- const recordId = computed(() => toRouteRecordId(crudContext.route.params[normalizedRecordIdParam]));
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
- );
182
- const listPath = crudContext.listPath;
183
-
184
- function viewQueryKey(surfaceId = "") {
185
- return crudContext.viewQueryKey(surfaceId, recordId.value);
186
- }
187
-
188
- async function invalidateAndGoList(queryClient) {
189
- await crudContext.invalidateQueries(queryClient);
190
- if (listPath.value) {
191
- await router.push(listPath.value);
192
- }
193
- }
194
-
195
- async function invalidateAndGoView(queryClient, recordIdLike = recordId.value) {
196
- await crudContext.invalidateQueries(queryClient);
197
-
198
- const targetRecordId = toRouteRecordId(recordIdLike) || recordId.value;
199
- const targetPath = crudContext.resolvePath(
200
- recordPathTemplates.viewPathTemplate,
201
- crudContext.resolveRecordParams(targetRecordId, { recordIdParam: normalizedRecordIdParam })
202
- );
203
- if (targetPath) {
204
- await router.push(targetPath);
205
- }
206
- }
207
-
208
- return Object.freeze({
209
- crudContext,
210
- listPath,
211
- recordId,
212
- apiSuffix,
213
- viewPath,
214
- editPath,
215
- viewQueryKey,
216
- invalidateAndGoList,
217
- invalidateAndGoView
218
- });
219
- }
220
-
221
- function useCrudCreateRuntime(source = {}) {
222
- const crudContext = useCrudClientContext(source);
223
- useCrudRealtimeInvalidation(crudContext.crudConfig.namespace);
224
- const router = useRouter();
225
- const recordPathTemplates = crudContext.resolveRecordPathTemplates();
226
- const listPath = crudContext.listPath;
227
- const apiSuffix = crudContext.crudConfig.apiRelativePath;
228
-
229
- function createQueryKey(surfaceId = "") {
230
- return [...crudContext.listQueryKey(surfaceId), "create"];
231
- }
232
-
233
- async function invalidateAndGoView(queryClient, recordIdLike) {
234
- await crudContext.invalidateQueries(queryClient);
235
-
236
- const targetPath = crudContext.resolvePath(
237
- recordPathTemplates.viewPathTemplate,
238
- crudContext.resolveRecordParams(recordIdLike)
239
- );
240
- if (targetPath) {
241
- await router.push(targetPath);
242
- }
243
- }
244
-
245
- return Object.freeze({
246
- crudContext,
247
- listPath,
248
- apiSuffix,
249
- createQueryKey,
250
- invalidateAndGoView
251
- });
252
- }
253
-
254
- function useCrudListRuntime(source = {}) {
255
- const crudContext = useCrudClientContext(source);
256
- useCrudRealtimeInvalidation(crudContext.crudConfig.namespace);
257
- const createPath = crudContext.createPath;
258
- const apiSuffix = crudContext.crudConfig.apiRelativePath;
259
-
260
- function listQueryKey(surfaceId = "") {
261
- return crudContext.listQueryKey(surfaceId);
262
- }
263
-
264
- return Object.freeze({
265
- crudContext,
266
- createPath,
267
- apiSuffix,
268
- listQueryKey
269
- });
270
- }
271
-
272
- function createCrudClientSupport(source = {}) {
273
- const crudConfig = resolveCrudClientConfig(source);
274
-
275
- return Object.freeze({
276
- useCrudClientContext() {
277
- return useCrudClientContext(crudConfig);
278
- },
279
- useCrudListRuntime() {
280
- return useCrudListRuntime(crudConfig);
281
- },
282
- useCrudCreateRuntime() {
283
- return useCrudCreateRuntime(crudConfig);
284
- },
285
- useCrudRecordRuntime(options = {}) {
286
- return useCrudRecordRuntime(crudConfig, options);
287
- },
288
- toRouteRecordId
289
- });
290
- }
291
-
292
- export {
293
- useCrudClientContext,
294
- useCrudListRuntime,
295
- useCrudCreateRuntime,
296
- useCrudRecordRuntime,
297
- createCrudClientSupport
298
- };
@@ -1,152 +0,0 @@
1
- import { normalizeQueryToken, normalizeRecordId, normalizeText } 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
- import {
5
- requireCrudNamespace,
6
- resolveCrudRecordChangedEvent
7
- } from "../../shared/crudNamespaceSupport.js";
8
-
9
- const DEFAULT_CRUD_OWNERSHIP_FILTER = "workspace";
10
- const ROUTE_PARAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
11
-
12
- function normalizeRelativePath(value, { context = "resolveCrudClientConfig" } = {}) {
13
- const raw = normalizeText(value);
14
- if (!raw) {
15
- throw new TypeError(`${context} requires a non-empty relative path.`);
16
- }
17
-
18
- const normalized = `/${raw.replace(/^\/+|\/+$/g, "")}`;
19
- if (normalized === "/") {
20
- throw new TypeError(`${context} requires a non-empty relative path.`);
21
- }
22
-
23
- return normalized;
24
- }
25
-
26
- function resolveCrudClientConfig(source = {}) {
27
- const payload = source && typeof source === "object" && !Array.isArray(source) ? source : {};
28
- const namespace = requireCrudNamespace(payload.namespace, {
29
- context: "resolveCrudClientConfig"
30
- });
31
- const ownershipFilter = normalizeRouteVisibilityToken(payload.ownershipFilter, {
32
- fallback: DEFAULT_CRUD_OWNERSHIP_FILTER
33
- });
34
- const inferredRelativePath = `/${namespace}`;
35
- const relativePath = normalizeRelativePath(
36
- Object.hasOwn(payload, "relativePath") ? payload.relativePath : inferredRelativePath,
37
- { context: "resolveCrudClientConfig" }
38
- );
39
- const apiRelativePath = normalizeRelativePath(
40
- Object.hasOwn(payload, "apiRelativePath") ? payload.apiRelativePath : relativePath,
41
- { context: "resolveCrudClientConfig" }
42
- );
43
-
44
- return Object.freeze({
45
- namespace,
46
- ownershipFilter,
47
- relativePath,
48
- apiRelativePath
49
- });
50
- }
51
-
52
- function crudListQueryKey(surfaceId = "", workspaceSlug = "", namespace = "") {
53
- return Object.freeze([
54
- ...crudScopeQueryKey(namespace),
55
- "list",
56
- normalizeQueryToken(surfaceId),
57
- normalizeQueryToken(workspaceSlug)
58
- ]);
59
- }
60
-
61
- function crudViewQueryKey(surfaceId = "", workspaceSlug = "", recordId = "", namespace = "") {
62
- return Object.freeze([
63
- ...crudScopeQueryKey(namespace),
64
- "view",
65
- normalizeQueryToken(surfaceId),
66
- normalizeQueryToken(workspaceSlug),
67
- normalizeRecordId(recordId, { fallback: "0" })
68
- ]);
69
- }
70
-
71
- function crudScopeQueryKey(namespace = "") {
72
- return Object.freeze(["crud", normalizeQueryToken(namespace)]);
73
- }
74
-
75
- async function invalidateCrudQueries(queryClient, namespace = "") {
76
- if (!queryClient || typeof queryClient.invalidateQueries !== "function") {
77
- throw new TypeError("invalidateCrudQueries requires queryClient.invalidateQueries().");
78
- }
79
-
80
- return queryClient.invalidateQueries({
81
- queryKey: crudScopeQueryKey(namespace)
82
- });
83
- }
84
-
85
- function toRouteRecordId(value) {
86
- if (Array.isArray(value)) {
87
- return toRouteRecordId(value[0]);
88
- }
89
-
90
- return normalizeRecordId(value, {
91
- fallback: ""
92
- });
93
- }
94
-
95
- function normalizeCrudRouteParamName(value, { context = "normalizeCrudRouteParamName" } = {}) {
96
- const normalizedValue = normalizeText(value);
97
- if (!normalizedValue) {
98
- throw new TypeError(`${context} requires a non-empty route parameter name.`);
99
- }
100
- if (!ROUTE_PARAM_NAME_PATTERN.test(normalizedValue)) {
101
- throw new TypeError(
102
- `${context} route parameter "${normalizedValue}" is invalid. Use letters, numbers, and underscores only.`
103
- );
104
- }
105
-
106
- return normalizedValue;
107
- }
108
-
109
- function resolveCrudRecordPathTemplates(relativePath = "", recordIdParam = "recordId") {
110
- const normalizedRelativePath = normalizeRelativePath(relativePath, {
111
- context: "resolveCrudRecordPathTemplates"
112
- });
113
- const normalizedRecordIdParam = normalizeCrudRouteParamName(recordIdParam, {
114
- context: "resolveCrudRecordPathTemplates"
115
- });
116
- const recordSegment = `:${normalizedRecordIdParam}`;
117
-
118
- return Object.freeze({
119
- viewPathTemplate: `${normalizedRelativePath}/${recordSegment}`,
120
- editPathTemplate: `${normalizedRelativePath}/${recordSegment}/edit`
121
- });
122
- }
123
-
124
- function resolveCrudRecordPathParams(recordIdLike = "", recordIdParam = "recordId") {
125
- const normalizedRecordIdParam = normalizeCrudRouteParamName(recordIdParam, {
126
- context: "resolveCrudRecordPathParams"
127
- });
128
- const normalizedRecordId = toRouteRecordId(recordIdLike);
129
- if (!normalizedRecordId) {
130
- return Object.freeze({});
131
- }
132
-
133
- return Object.freeze({
134
- [normalizedRecordIdParam]: normalizedRecordId
135
- });
136
- }
137
-
138
- export {
139
- DEFAULT_CRUD_OWNERSHIP_FILTER,
140
- requireCrudNamespace,
141
- resolveCrudClientConfig,
142
- formatDateTime,
143
- resolveCrudRecordChangedEvent,
144
- crudScopeQueryKey,
145
- invalidateCrudQueries,
146
- crudListQueryKey,
147
- crudViewQueryKey,
148
- toRouteRecordId,
149
- normalizeCrudRouteParamName,
150
- resolveCrudRecordPathTemplates,
151
- resolveCrudRecordPathParams
152
- };
@@ -1,37 +0,0 @@
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 };
@@ -1,24 +0,0 @@
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";
@@ -1,54 +0,0 @@
1
- import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
- import {
3
- DEFAULT_CRUD_LOOKUP_CONTAINER_KEY,
4
- normalizeCrudLookupContainerKey
5
- } from "@jskit-ai/kernel/shared/support/crudLookup";
6
-
7
- const CRUD_RUNTIME_LOOKUPS_FIELD_KEY = DEFAULT_CRUD_LOOKUP_CONTAINER_KEY;
8
- const CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE = "autocomplete";
9
- const CRUD_LOOKUP_FORM_CONTROL_SELECT = "select";
10
-
11
- function checkCrudLookupFormControl(
12
- value,
13
- {
14
- context = "crud field ui.formControl",
15
- defaultValue = CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE
16
- } = {}
17
- ) {
18
- const resolvedValue = value === undefined || value === null || value === "" ? defaultValue : value;
19
- if (resolvedValue === "") {
20
- return "";
21
- }
22
-
23
- if (
24
- resolvedValue === CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE ||
25
- resolvedValue === CRUD_LOOKUP_FORM_CONTROL_SELECT
26
- ) {
27
- return resolvedValue;
28
- }
29
-
30
- throw new Error(
31
- `${context} must be "${CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE}" or "${CRUD_LOOKUP_FORM_CONTROL_SELECT}". ` +
32
- `Received: ${JSON.stringify(resolvedValue)}.`
33
- );
34
- }
35
-
36
- function isCrudRuntimeOutputOnlyFieldKey(
37
- value = "",
38
- {
39
- lookupContainerKey = CRUD_RUNTIME_LOOKUPS_FIELD_KEY
40
- } = {}
41
- ) {
42
- const resolvedLookupContainerKey = normalizeCrudLookupContainerKey(lookupContainerKey, {
43
- context: "crud runtime lookup container key"
44
- });
45
- return normalizeText(value) === resolvedLookupContainerKey;
46
- }
47
-
48
- export {
49
- CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE,
50
- CRUD_LOOKUP_FORM_CONTROL_SELECT,
51
- CRUD_RUNTIME_LOOKUPS_FIELD_KEY,
52
- checkCrudLookupFormControl,
53
- isCrudRuntimeOutputOnlyFieldKey
54
- };
@@ -1,5 +0,0 @@
1
- export {
2
- normalizeCrudNamespace,
3
- requireCrudNamespace,
4
- resolveCrudRecordChangedEvent
5
- } from "@jskit-ai/resource-crud-core/shared/crudNamespaceSupport";
@@ -1 +0,0 @@
1
- export { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
@@ -1,125 +0,0 @@
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
- normalizeCrudRouteParamName,
12
- resolveCrudRecordPathTemplates,
13
- resolveCrudRecordPathParams
14
- } from "../src/client/composables/crudClientSupportHelpers.js";
15
-
16
- test("resolveCrudClientConfig normalizes namespace, ownership filter, and resolves route/api paths", () => {
17
- const config = resolveCrudClientConfig({
18
- namespace: " Customers ",
19
- ownershipFilter: "workspace",
20
- relativePath: "/ops/customers-ui",
21
- apiRelativePath: "/crud/customers"
22
- });
23
-
24
- assert.deepEqual(config, {
25
- namespace: "customers",
26
- ownershipFilter: "workspace",
27
- relativePath: "/ops/customers-ui",
28
- apiRelativePath: "/crud/customers"
29
- });
30
- });
31
-
32
- test("resolveCrudClientConfig infers default relativePath from namespace", () => {
33
- const config = resolveCrudClientConfig({
34
- namespace: "appointments",
35
- ownershipFilter: "public"
36
- });
37
-
38
- assert.equal(config.relativePath, "/appointments");
39
- assert.equal(config.apiRelativePath, "/appointments");
40
- });
41
-
42
- test("resolveCrudClientConfig throws when namespace is missing", () => {
43
- assert.throws(
44
- () => resolveCrudClientConfig({ ownershipFilter: "workspace" }),
45
- /requires a non-empty namespace/
46
- );
47
- });
48
-
49
- test("crudListQueryKey and crudViewQueryKey normalize cache keys", () => {
50
- assert.deepEqual(crudListQueryKey("Admin", " TonymoBily3 ", "Customers"), [
51
- "crud",
52
- "customers",
53
- "list",
54
- "admin",
55
- "tonymobily3"
56
- ]);
57
-
58
- assert.deepEqual(crudViewQueryKey("Admin", " TonymoBily3 ", "12", "Customers"), [
59
- "crud",
60
- "customers",
61
- "view",
62
- "admin",
63
- "tonymobily3",
64
- "12"
65
- ]);
66
- });
67
-
68
- test("crudScopeQueryKey normalizes namespace", () => {
69
- assert.deepEqual(crudScopeQueryKey(" Customers "), ["crud", "customers"]);
70
- });
71
-
72
- test("invalidateCrudQueries invalidates by CRUD namespace scope key", async () => {
73
- let payload = null;
74
- const queryClient = {
75
- async invalidateQueries(input) {
76
- payload = input;
77
- return true;
78
- }
79
- };
80
-
81
- await invalidateCrudQueries(queryClient, "Customers");
82
- assert.deepEqual(payload, {
83
- queryKey: ["crud", "customers"]
84
- });
85
- });
86
-
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"), "");
91
- });
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
-
122
- test("resolveCrudRecordChangedEvent normalizes namespace into event channel", () => {
123
- assert.equal(resolveCrudRecordChangedEvent("Customers"), "customers.record.changed");
124
- assert.equal(resolveCrudRecordChangedEvent("customer-orders"), "customer_orders.record.changed");
125
- });
@@ -1,47 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import {
4
- checkCrudLookupFormControl,
5
- isCrudRuntimeOutputOnlyFieldKey
6
- } from "../src/shared/crudFieldSupport.js";
7
-
8
- test("checkCrudLookupFormControl defaults to autocomplete", () => {
9
- assert.equal(checkCrudLookupFormControl(undefined), "autocomplete");
10
- assert.equal(checkCrudLookupFormControl(""), "autocomplete");
11
- });
12
-
13
- test("checkCrudLookupFormControl accepts select", () => {
14
- assert.equal(checkCrudLookupFormControl("select"), "select");
15
- });
16
-
17
- test("checkCrudLookupFormControl accepts explicit empty default", () => {
18
- assert.equal(checkCrudLookupFormControl(undefined, { defaultValue: "" }), "");
19
- });
20
-
21
- test("checkCrudLookupFormControl throws for invalid value", () => {
22
- assert.throws(
23
- () => checkCrudLookupFormControl("Select", { context: "testContext" }),
24
- /testContext must be "autocomplete" or "select"\./
25
- );
26
- });
27
-
28
- test("isCrudRuntimeOutputOnlyFieldKey matches lookups", () => {
29
- assert.equal(isCrudRuntimeOutputOnlyFieldKey("lookups"), true);
30
- assert.equal(isCrudRuntimeOutputOnlyFieldKey(" lookups "), true);
31
- assert.equal(isCrudRuntimeOutputOnlyFieldKey("id"), false);
32
- });
33
-
34
- test("isCrudRuntimeOutputOnlyFieldKey supports custom lookup container key", () => {
35
- assert.equal(
36
- isCrudRuntimeOutputOnlyFieldKey("lookupData", {
37
- lookupContainerKey: "lookupData"
38
- }),
39
- true
40
- );
41
- assert.equal(
42
- isCrudRuntimeOutputOnlyFieldKey("lookups", {
43
- lookupContainerKey: "lookupData"
44
- }),
45
- false
46
- );
47
- });
@@ -1,97 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { createSchema, validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
4
- import { defineCrudResource } from "../src/shared/crudResource.js";
5
-
6
- function createContactsResource(overrides = {}) {
7
- return defineCrudResource({
8
- namespace: "contacts",
9
- schema: {
10
- name: {
11
- type: "string",
12
- maxLength: 190,
13
- operations: {
14
- output: { required: true },
15
- create: { required: true },
16
- patch: { required: false }
17
- }
18
- },
19
- createdAt: {
20
- type: "dateTime",
21
- operations: {
22
- output: { required: true }
23
- }
24
- }
25
- },
26
- contract: {
27
- lookup: {
28
- containerKey: "lookups"
29
- }
30
- },
31
- ...overrides
32
- });
33
- }
34
-
35
- test("defineCrudResource derives full CRUD operations by default", async () => {
36
- const resource = createContactsResource();
37
-
38
- assert.deepEqual(
39
- Object.keys(resource.operations),
40
- ["list", "view", "create", "patch", "delete"]
41
- );
42
- assert.deepEqual(resource.operations.list.realtime?.events, ["contacts.record.changed"]);
43
-
44
- const normalizedCreateBody = await validateSchemaPayload(resource.operations.create.body, {
45
- name: " Example "
46
- }, { phase: "input" });
47
- assert.equal(normalizedCreateBody.name, "Example");
48
-
49
- const normalizedViewOutput = await validateSchemaPayload(resource.operations.view.output, {
50
- id: 7,
51
- name: " Example ",
52
- createdAt: "2026-05-01T12:30:00.000Z",
53
- lookups: {}
54
- }, { phase: "output" });
55
- assert.equal(normalizedViewOutput.id, "7");
56
- assert.equal(normalizedViewOutput.name, "Example");
57
- assert.equal(normalizedViewOutput.createdAt, "2026-05-01T12:30:00.000Z");
58
- });
59
-
60
- test("defineCrudResource supports an explicit standard CRUD operation subset", () => {
61
- const resource = createContactsResource({
62
- crudOperations: ["list", "view", "create"]
63
- });
64
-
65
- assert.deepEqual(
66
- Object.keys(resource.operations),
67
- ["list", "view", "create"]
68
- );
69
- assert.equal(Object.hasOwn(resource, "crudOperations"), false);
70
- });
71
-
72
- test("defineCrudResource merges authored operation overrides into derived defaults", () => {
73
- const resource = createContactsResource({
74
- operations: {
75
- list: {
76
- realtime: {
77
- events: ["contacts.custom.changed"]
78
- }
79
- },
80
- archive: {
81
- method: "POST",
82
- output: Object.freeze({
83
- mode: "replace",
84
- schema: createSchema({
85
- archived: {
86
- type: "boolean",
87
- required: true
88
- }
89
- })
90
- })
91
- }
92
- }
93
- });
94
-
95
- assert.deepEqual(resource.operations.list.realtime?.events, ["contacts.custom.changed"]);
96
- assert.equal(resource.operations.archive.method, "POST");
97
- });