@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.176",
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.165",
27
- "@jskit-ai/http-runtime": "0.1.163",
28
- "@jskit-ai/json-rest-api-core": "0.1.109",
29
- "@jskit-ai/kernel": "0.1.165",
30
- "@jskit-ai/resource-crud-core": "0.1.107",
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.176",
12
- "@jskit-ai/resource-crud-core": "0.1.107"
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 = {