@jskit-ai/crud-core 0.1.176 → 0.1.178
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/crud-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.178",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"./server/routeContracts": "./src/server/routeContracts.js"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
27
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
28
|
-
"@jskit-ai/json-rest-api-core": "0.1.
|
|
29
|
-
"@jskit-ai/kernel": "0.1.
|
|
30
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
26
|
+
"@jskit-ai/database-runtime": "0.1.167",
|
|
27
|
+
"@jskit-ai/http-runtime": "0.1.165",
|
|
28
|
+
"@jskit-ai/json-rest-api-core": "0.1.111",
|
|
29
|
+
"@jskit-ai/kernel": "0.1.167",
|
|
30
|
+
"@jskit-ai/resource-crud-core": "0.1.109",
|
|
31
31
|
"json-rest-schema": "^1.0.17"
|
|
32
32
|
},
|
|
33
33
|
"description": "Shared server-side CRUD service, repository, route, and query helpers.",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
"./shared": "./src/shared/index.js"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@jskit-ai/crud-core": "0.1.
|
|
12
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
11
|
+
"@jskit-ai/crud-core": "0.1.178",
|
|
12
|
+
"@jskit-ai/resource-crud-core": "0.1.109"
|
|
13
13
|
},
|
|
14
14
|
"jskit": {
|
|
15
15
|
"kind": "runtime",
|
|
@@ -3,6 +3,11 @@ import {
|
|
|
3
3
|
recordIdParamsValidator
|
|
4
4
|
} from "@jskit-ai/kernel/shared/validators";
|
|
5
5
|
import { createEntityChangedActionEvent } from "@jskit-ai/kernel/server/actions";
|
|
6
|
+
import {
|
|
7
|
+
normalizeJsonApiDocument,
|
|
8
|
+
simplifyJsonApiDocument,
|
|
9
|
+
unwrapJsonApiResult
|
|
10
|
+
} from "@jskit-ai/http-runtime/shared";
|
|
6
11
|
import { resolveCrudRecordChangedEvent } from "@jskit-ai/resource-crud-core/shared/crudNamespaceSupport";
|
|
7
12
|
import {
|
|
8
13
|
createStandardCrudListQueryValidators,
|
|
@@ -12,6 +17,104 @@ import {
|
|
|
12
17
|
const CRUD_OPERATION_NAMES = Object.freeze(["list", "view", "create", "update", "delete"]);
|
|
13
18
|
const CRUD_MUTATION_NAMES = new Set(["create", "update", "delete"]);
|
|
14
19
|
const CRUD_LIFECYCLE_PHASES = Object.freeze(["before", "execute", "after", "afterCommit"]);
|
|
20
|
+
const CRUD_ASSISTANT_RESOURCE_OPERATION = Object.freeze({
|
|
21
|
+
list: "list",
|
|
22
|
+
view: "view",
|
|
23
|
+
create: "create",
|
|
24
|
+
update: "patch",
|
|
25
|
+
delete: "delete"
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeOptionalCursor(value) {
|
|
33
|
+
if (value == null) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const normalized = String(value).trim();
|
|
37
|
+
return normalized || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveDocumentNextCursor(document = {}) {
|
|
41
|
+
return normalizeOptionalCursor(
|
|
42
|
+
document?.meta?.page?.nextCursor ?? document?.meta?.pagination?.cursor?.next
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveAssistantResultValue(result) {
|
|
47
|
+
const taggedResult = unwrapJsonApiResult(result);
|
|
48
|
+
const value = taggedResult ? taggedResult.value : result;
|
|
49
|
+
const document = normalizeJsonApiDocument(value);
|
|
50
|
+
return {
|
|
51
|
+
value,
|
|
52
|
+
document
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function transformCrudAssistantResult(operation, result, { input = {} } = {}) {
|
|
57
|
+
if (operation === "delete") {
|
|
58
|
+
const resolved = resolveAssistantResultValue(result);
|
|
59
|
+
if (isRecord(resolved.value) && resolved.value.deleted === true && resolved.value.id != null) {
|
|
60
|
+
return resolved.value;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
id: String(input.recordId || ""),
|
|
64
|
+
deleted: true
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolved = resolveAssistantResultValue(result);
|
|
69
|
+
if (operation === "list") {
|
|
70
|
+
if (resolved.document.kind === "collection") {
|
|
71
|
+
return {
|
|
72
|
+
items: simplifyJsonApiDocument(resolved.value),
|
|
73
|
+
nextCursor: resolveDocumentNextCursor(resolved.document)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(resolved.value)) {
|
|
77
|
+
return {
|
|
78
|
+
items: resolved.value,
|
|
79
|
+
nextCursor: null
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (isRecord(resolved.value) && Array.isArray(resolved.value.items)) {
|
|
83
|
+
return {
|
|
84
|
+
items: resolved.value.items,
|
|
85
|
+
nextCursor: normalizeOptionalCursor(resolved.value.nextCursor)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return resolved.value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (resolved.document.kind === "resource") {
|
|
92
|
+
return simplifyJsonApiDocument(resolved.value);
|
|
93
|
+
}
|
|
94
|
+
return resolved.value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createCrudAssistantExtension(resource, namespace, operation) {
|
|
98
|
+
const resourceOperation = CRUD_ASSISTANT_RESOURCE_OPERATION[operation];
|
|
99
|
+
const output = resource?.operations?.[resourceOperation]?.output || null;
|
|
100
|
+
const actionLabel = operation === "list"
|
|
101
|
+
? `List ${namespace} records.`
|
|
102
|
+
: operation === "view"
|
|
103
|
+
? `View a ${namespace} record.`
|
|
104
|
+
: operation === "create"
|
|
105
|
+
? `Create a ${namespace} record.`
|
|
106
|
+
: operation === "update"
|
|
107
|
+
? `Update a ${namespace} record.`
|
|
108
|
+
: `Delete a ${namespace} record.`;
|
|
109
|
+
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
description: actionLabel,
|
|
112
|
+
output,
|
|
113
|
+
transformResult(result, context) {
|
|
114
|
+
return transformCrudAssistantResult(operation, result, context);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
15
118
|
|
|
16
119
|
function createActionInput(
|
|
17
120
|
resource,
|
|
@@ -303,6 +406,9 @@ function createCrudJsonApiActions({
|
|
|
303
406
|
permission: permission(definition.operation),
|
|
304
407
|
input: input(definition.operation),
|
|
305
408
|
output: null,
|
|
409
|
+
extensions: Object.freeze({
|
|
410
|
+
assistant: createCrudAssistantExtension(resource, namespace, definition.operation)
|
|
411
|
+
}),
|
|
306
412
|
idempotency: definition.idempotency,
|
|
307
413
|
audit: { actionName: id },
|
|
308
414
|
observability: {},
|
|
@@ -4,6 +4,8 @@ import { createSchema } from "json-rest-schema";
|
|
|
4
4
|
|
|
5
5
|
import { createActionProvider } from "@jskit-ai/kernel/server/actions";
|
|
6
6
|
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
7
|
+
import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
|
|
8
|
+
import { returnJsonApiDocument } from "@jskit-ai/http-runtime/shared";
|
|
7
9
|
import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
|
|
8
10
|
import { defineCrudJsonApiFeature } from "../src/server/defineCrudJsonApiFeature.js";
|
|
9
11
|
import { createCrudJsonApiActions } from "../src/server/jsonApiModule/actions.js";
|
|
@@ -187,6 +189,84 @@ test("defineCrudJsonApiFeature makes workspace scope and permissions explicit",
|
|
|
187
189
|
await runtime.shutdown();
|
|
188
190
|
});
|
|
189
191
|
|
|
192
|
+
test("generated CRUD actions expose truthful assistant contracts without changing native results", async () => {
|
|
193
|
+
const resource = createBookResource();
|
|
194
|
+
const document = (id, title) => ({
|
|
195
|
+
data: {
|
|
196
|
+
type: "books",
|
|
197
|
+
id,
|
|
198
|
+
attributes: { title }
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
const actions = createCrudJsonApiActions({
|
|
202
|
+
namespace: "books",
|
|
203
|
+
resource,
|
|
204
|
+
service: {
|
|
205
|
+
async queryDocuments() {
|
|
206
|
+
return returnJsonApiDocument({
|
|
207
|
+
data: [document("1", "Kindred").data],
|
|
208
|
+
meta: { page: { nextCursor: "cursor-2" } }
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
async getDocumentById() {
|
|
212
|
+
return returnJsonApiDocument(document("1", "Kindred"));
|
|
213
|
+
},
|
|
214
|
+
async createDocument() {
|
|
215
|
+
return returnJsonApiDocument(document("2", "Parable of the Sower"));
|
|
216
|
+
},
|
|
217
|
+
async patchDocumentById() {
|
|
218
|
+
return returnJsonApiDocument(document("1", "Kindred (updated)"));
|
|
219
|
+
},
|
|
220
|
+
async deleteDocumentById() {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
surface: "admin",
|
|
225
|
+
permissionForOperation: () => ({ require: "authenticated" })
|
|
226
|
+
});
|
|
227
|
+
const byOperation = new Map(actions.map((entry) => [entry.id.split(".").at(-1), entry]));
|
|
228
|
+
|
|
229
|
+
assert.ok(actions.every((entry) => entry.output === null));
|
|
230
|
+
assert.equal(byOperation.get("list").extensions.assistant.output, resource.operations.list.output);
|
|
231
|
+
assert.equal(byOperation.get("view").extensions.assistant.output, resource.operations.view.output);
|
|
232
|
+
assert.equal(byOperation.get("create").extensions.assistant.output, resource.operations.create.output);
|
|
233
|
+
assert.equal(byOperation.get("update").extensions.assistant.output, resource.operations.patch.output);
|
|
234
|
+
assert.equal(byOperation.get("delete").extensions.assistant.output, resource.operations.delete.output);
|
|
235
|
+
|
|
236
|
+
const transform = async (operation, input = {}) => {
|
|
237
|
+
const definition = byOperation.get(operation);
|
|
238
|
+
const nativeResult = await definition.execute(input, {});
|
|
239
|
+
const assistantResult = await definition.extensions.assistant.transformResult(nativeResult, { input });
|
|
240
|
+
return validateSchemaPayload(definition.extensions.assistant.output, assistantResult, {
|
|
241
|
+
phase: "output"
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
assert.deepEqual(await transform("list"), {
|
|
246
|
+
items: [{ id: "1", title: "Kindred" }],
|
|
247
|
+
nextCursor: "cursor-2"
|
|
248
|
+
});
|
|
249
|
+
assert.deepEqual(await transform("view", { recordId: "1" }), {
|
|
250
|
+
id: "1",
|
|
251
|
+
title: "Kindred"
|
|
252
|
+
});
|
|
253
|
+
assert.deepEqual(await transform("create", { title: "Parable of the Sower" }), {
|
|
254
|
+
id: "2",
|
|
255
|
+
title: "Parable of the Sower"
|
|
256
|
+
});
|
|
257
|
+
assert.deepEqual(await transform("update", { recordId: "1", title: "Kindred (updated)" }), {
|
|
258
|
+
id: "1",
|
|
259
|
+
title: "Kindred (updated)"
|
|
260
|
+
});
|
|
261
|
+
assert.deepEqual(await transform("delete", { recordId: "1" }), {
|
|
262
|
+
id: "1",
|
|
263
|
+
deleted: true
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
assert.deepEqual(await byOperation.get("view").execute({ recordId: "1" }, {}),
|
|
267
|
+
returnJsonApiDocument(document("1", "Kindred")));
|
|
268
|
+
});
|
|
269
|
+
|
|
190
270
|
test("defineCrudJsonApiFeature lets product services name exact capability dependencies", async () => {
|
|
191
271
|
const routes = [];
|
|
192
272
|
const access = {
|