@jskit-ai/crud-core 0.1.63 → 0.1.64
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 +18 -10
- package/src/server/crudModuleConfig.js +25 -5
- package/src/server/fieldAccess.js +9 -39
- package/src/server/listFilters.js +384 -389
- package/src/server/listQueryValidators.js +39 -77
- package/src/server/lookupHydration.js +4 -1
- package/src/server/repositorySupport.js +71 -121
- package/src/server/resourceRuntime/index.js +49 -74
- package/src/server/resourceRuntime/lookupHydration.js +4 -1
- package/src/server/routeContracts.js +74 -0
- package/src/server/serviceEvents.js +75 -4
- package/src/shared/crudFieldSupport.js +54 -0
- package/src/shared/crudNamespaceSupport.js +1 -27
- package/src/shared/crudResource.js +1 -0
- package/test/createCrudServiceFromResource.test.js +30 -28
- package/test/{crudFieldMetaSupport.test.js → crudFieldSupport.test.js} +1 -1
- package/test/crudModuleConfig.test.js +33 -0
- package/test/crudResource.test.js +97 -0
- package/test/listFilters.test.js +221 -59
- package/test/listQueryValidators.test.js +131 -97
- package/test/repositorySupport.test.js +241 -241
- package/test/resourceRuntime.test.js +204 -248
- package/test/routeContracts.test.js +146 -0
- package/test/serviceEvents.test.js +41 -1
- package/test/serviceMethods.test.js +12 -10
- package/src/shared/crudFieldMetaSupport.js +0 -153
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.64",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Shared CRUD helpers used by CRUD modules.",
|
|
7
7
|
dependsOn: [
|
|
8
|
+
"@jskit-ai/http-runtime",
|
|
8
9
|
"@jskit-ai/kernel",
|
|
9
10
|
"@jskit-ai/realtime",
|
|
11
|
+
"@jskit-ai/resource-crud-core",
|
|
10
12
|
"@jskit-ai/shell-web",
|
|
11
13
|
"@jskit-ai/users-core",
|
|
12
14
|
"@jskit-ai/users-web"
|
|
@@ -26,7 +28,7 @@ export default Object.freeze({
|
|
|
26
28
|
mutations: {
|
|
27
29
|
dependencies: {
|
|
28
30
|
runtime: {
|
|
29
|
-
"@jskit-ai/crud-core": "0.1.
|
|
31
|
+
"@jskit-ai/crud-core": "0.1.64"
|
|
30
32
|
},
|
|
31
33
|
dev: {}
|
|
32
34
|
},
|
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.64",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -10,8 +10,9 @@
|
|
|
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
|
-
"./shared/
|
|
13
|
+
"./shared/crudFieldSupport": "./src/shared/crudFieldSupport.js",
|
|
14
14
|
"./shared/crudNamespaceSupport": "./src/shared/crudNamespaceSupport.js",
|
|
15
|
+
"./shared/crudResource": "./src/shared/crudResource.js",
|
|
15
16
|
"./server/repositorySupport": "./src/server/repositorySupport.js",
|
|
16
17
|
"./server/resourceRuntime": "./src/server/resourceRuntime/index.js",
|
|
17
18
|
"./server/createHooksToCollectChildren": "./src/server/createHooksToCollectChildren.js",
|
|
@@ -22,16 +23,23 @@
|
|
|
22
23
|
"./server/createCrudServiceFromResource": "./src/server/createCrudServiceFromResource.js",
|
|
23
24
|
"./server/crudModuleConfig": "./src/server/crudModuleConfig.js",
|
|
24
25
|
"./server/listQueryValidators": "./src/server/listQueryValidators.js",
|
|
25
|
-
"./server/listFilters": "./src/server/listFilters.js"
|
|
26
|
+
"./server/listFilters": "./src/server/listFilters.js",
|
|
27
|
+
"./server/routeContracts": "./src/server/routeContracts.js"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"@tanstack/vue-query": "^5.90.5",
|
|
29
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
30
|
-
"@jskit-ai/
|
|
31
|
-
"@jskit-ai/
|
|
32
|
-
"
|
|
33
|
-
"@jskit-ai/
|
|
34
|
-
"@jskit-ai/
|
|
35
|
-
"
|
|
31
|
+
"@jskit-ai/database-runtime": "0.1.56",
|
|
32
|
+
"@jskit-ai/http-runtime": "0.1.55",
|
|
33
|
+
"@jskit-ai/kernel": "0.1.56",
|
|
34
|
+
"json-rest-schema": "1.x.x",
|
|
35
|
+
"@jskit-ai/realtime": "0.1.55",
|
|
36
|
+
"@jskit-ai/resource-crud-core": "0.1.1",
|
|
37
|
+
"@jskit-ai/shell-web": "0.1.55",
|
|
38
|
+
"@jskit-ai/users-core": "0.1.66",
|
|
39
|
+
"@jskit-ai/users-web": "0.1.71"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"vue": "^3.5.13",
|
|
43
|
+
"vue-router": "^5.0.4"
|
|
36
44
|
}
|
|
37
45
|
}
|
|
@@ -20,6 +20,7 @@ const CRUD_REQUESTED_OWNERSHIP_FILTER_SET = new Set([
|
|
|
20
20
|
CRUD_REQUESTED_OWNERSHIP_FILTER_AUTO
|
|
21
21
|
]);
|
|
22
22
|
const CRUD_MODULE_ID = "crud";
|
|
23
|
+
const WORKSPACE_CAPABLE_TENANCY_MODES = new Set(["personal", "workspaces"]);
|
|
23
24
|
|
|
24
25
|
function asRecord(value) {
|
|
25
26
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -227,11 +228,30 @@ function resolveCrudSurfacePolicy(
|
|
|
227
228
|
|
|
228
229
|
function resolveCrudSurfacePolicyFromAppConfig(sourceConfig = {}, appConfig = {}, options = {}) {
|
|
229
230
|
const config = asRecord(appConfig);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
const requestedSurfaceId = normalizeSurfaceId(asRecord(sourceConfig).surface);
|
|
232
|
+
const fallbackSurfaceId = normalizeSurfaceId(config.surfaceDefaultId);
|
|
233
|
+
const resolvedSurfaceId = requestedSurfaceId || fallbackSurfaceId;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
return resolveCrudSurfacePolicy(sourceConfig, {
|
|
237
|
+
...asRecord(options),
|
|
238
|
+
surfaceDefinitions: config.surfaceDefinitions,
|
|
239
|
+
defaultSurfaceId: config.surfaceDefaultId
|
|
240
|
+
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const normalizedTenancyMode = normalizeText(config.tenancyMode).toLowerCase();
|
|
243
|
+
const message = String(error?.message || "");
|
|
244
|
+
if (
|
|
245
|
+
message.includes("cannot resolve surface") &&
|
|
246
|
+
(resolvedSurfaceId === "admin" || resolvedSurfaceId === "app") &&
|
|
247
|
+
WORKSPACE_CAPABLE_TENANCY_MODES.has(normalizedTenancyMode)
|
|
248
|
+
) {
|
|
249
|
+
error.message = `${message} Workspace-capable tenancy mode "${normalizedTenancyMode}" usually requires ` +
|
|
250
|
+
'@jskit-ai/workspaces-core, which defines the "app" and "admin" surfaces. ' +
|
|
251
|
+
"Install that package or add matching surface definitions in config/public.js.";
|
|
252
|
+
}
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
235
255
|
}
|
|
236
256
|
|
|
237
257
|
function resolveCrudConfigsFromModules(modulesSource = {}) {
|
|
@@ -1,34 +1,8 @@
|
|
|
1
1
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { resolveCrudFieldSchemaProperties } from "@jskit-ai/kernel/shared/support/crudFieldContract";
|
|
2
3
|
import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
4
|
import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
|
|
4
5
|
|
|
5
|
-
function isSchemaNullable(schema = {}) {
|
|
6
|
-
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
7
|
-
return false;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const schemaType = schema.type;
|
|
11
|
-
if (typeof schemaType === "string" && normalizeText(schemaType).toLowerCase() === "null") {
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
if (Array.isArray(schemaType)) {
|
|
15
|
-
const hasNullType = schemaType.some((entry) => normalizeText(entry).toLowerCase() === "null");
|
|
16
|
-
if (hasNullType) {
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const variants = [];
|
|
22
|
-
if (Array.isArray(schema.anyOf)) {
|
|
23
|
-
variants.push(...schema.anyOf);
|
|
24
|
-
}
|
|
25
|
-
if (Array.isArray(schema.oneOf)) {
|
|
26
|
-
variants.push(...schema.oneOf);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return variants.some((entry) => isSchemaNullable(entry));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
6
|
function normalizeFieldSet(value, { context = "crudFieldAccess", label = "field list" } = {}) {
|
|
33
7
|
if (value == null || value === "*") {
|
|
34
8
|
return null;
|
|
@@ -73,17 +47,13 @@ function resolveWriteMode(fieldAccess = {}, { context = "crudFieldAccess" } = {}
|
|
|
73
47
|
}
|
|
74
48
|
|
|
75
49
|
function buildOutputFieldRules(resource = {}) {
|
|
76
|
-
const
|
|
77
|
-
|
|
50
|
+
const outputProperties = resolveCrudFieldSchemaProperties(resource?.operations?.view?.output, {
|
|
51
|
+
context: "crudFieldAccess resource.operations.view.output"
|
|
52
|
+
});
|
|
53
|
+
if (Object.keys(outputProperties).length < 1) {
|
|
78
54
|
return null;
|
|
79
55
|
}
|
|
80
56
|
|
|
81
|
-
const outputProperties = normalizeObject(viewOutputSchema.properties);
|
|
82
|
-
const requiredFields = new Set(
|
|
83
|
-
(Array.isArray(viewOutputSchema.required) ? viewOutputSchema.required : [])
|
|
84
|
-
.map((entry) => normalizeText(entry))
|
|
85
|
-
.filter(Boolean)
|
|
86
|
-
);
|
|
87
57
|
const fieldRules = new Map();
|
|
88
58
|
|
|
89
59
|
for (const [fieldKey, fieldSchemaRaw] of Object.entries(outputProperties)) {
|
|
@@ -96,8 +66,8 @@ function buildOutputFieldRules(resource = {}) {
|
|
|
96
66
|
fieldRules.set(
|
|
97
67
|
normalizedFieldKey,
|
|
98
68
|
Object.freeze({
|
|
99
|
-
required:
|
|
100
|
-
nullable:
|
|
69
|
+
required: fieldSchema.required === true,
|
|
70
|
+
nullable: fieldSchema.nullable === true,
|
|
101
71
|
hasDefault: Object.hasOwn(fieldSchema, "default"),
|
|
102
72
|
defaultValue: fieldSchema.default
|
|
103
73
|
})
|
|
@@ -210,7 +180,7 @@ function applyReadableFieldPolicyToRecord(record, allowedFields, outputRules = n
|
|
|
210
180
|
}
|
|
211
181
|
|
|
212
182
|
throw new Error(
|
|
213
|
-
`${context} cannot redact required non-nullable field "${fieldKey}" without
|
|
183
|
+
`${context} cannot redact required non-nullable field "${fieldKey}" without a default value.`
|
|
214
184
|
);
|
|
215
185
|
}
|
|
216
186
|
|
|
@@ -233,7 +203,7 @@ function createCrudFieldAccessRuntime(resource = {}, { context = "crudFieldAcces
|
|
|
233
203
|
return null;
|
|
234
204
|
}
|
|
235
205
|
if (!outputRules) {
|
|
236
|
-
throw new TypeError(`${context} requires resource.operations.view.
|
|
206
|
+
throw new TypeError(`${context} requires resource.operations.view.output for fieldAccess.readable.`);
|
|
237
207
|
}
|
|
238
208
|
|
|
239
209
|
return allowedFields;
|