@jskit-ai/crud-core 0.1.180 → 0.1.182
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.182",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"./server/routeContracts": "./src/server/routeContracts.js"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
27
|
-
"@jskit-ai/json-rest-api-core": "0.1.
|
|
28
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
26
|
+
"@jskit-ai/database-runtime": "0.1.171",
|
|
27
|
+
"@jskit-ai/json-rest-api-core": "0.1.115",
|
|
28
|
+
"@jskit-ai/resource-crud-core": "0.1.113",
|
|
29
29
|
"json-rest-schema": "^1.0.17"
|
|
30
30
|
},
|
|
31
31
|
"description": "Shared server-side CRUD service, repository, route, and query helpers.",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
49
|
-
"@jskit-ai/kernel": "0.1.
|
|
48
|
+
"@jskit-ai/http-runtime": "0.1.169",
|
|
49
|
+
"@jskit-ai/kernel": "0.1.171"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -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.182",
|
|
12
|
+
"@jskit-ai/resource-crud-core": "0.1.113"
|
|
13
13
|
},
|
|
14
14
|
"jskit": {
|
|
15
15
|
"kind": "runtime",
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createSchema,
|
|
4
4
|
recordIdParamsValidator
|
|
5
5
|
} from "@jskit-ai/kernel/shared/validators";
|
|
6
|
+
import { normalizeJsonApiFieldsets } from "@jskit-ai/kernel/shared/support/jsonApiFieldsets";
|
|
6
7
|
import { createEntityChangedActionEvent } from "@jskit-ai/kernel/server/actions";
|
|
7
8
|
import {
|
|
8
9
|
decodeJsonApiResourceResponse,
|
|
@@ -166,6 +167,102 @@ function createCrudAssistantTransport(resource, operation, relationshipEntries,
|
|
|
166
167
|
});
|
|
167
168
|
}
|
|
168
169
|
|
|
170
|
+
function projectCrudAssistantRecordFields(record, selectedFields = null, {
|
|
171
|
+
preserveKeys = []
|
|
172
|
+
} = {}) {
|
|
173
|
+
if (!isRecord(record) || !Array.isArray(selectedFields)) {
|
|
174
|
+
return record;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const allowedKeys = new Set(["id", ...preserveKeys, ...selectedFields]);
|
|
178
|
+
return Object.fromEntries(
|
|
179
|
+
Object.entries(record).filter(([key]) => allowedKeys.has(key))
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function projectCrudAssistantLookupValue(value, selectedFields = null) {
|
|
184
|
+
if (Array.isArray(value)) {
|
|
185
|
+
return value.map((entry) => projectCrudAssistantRecordFields(entry, selectedFields));
|
|
186
|
+
}
|
|
187
|
+
return projectCrudAssistantRecordFields(value, selectedFields);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function projectCrudAssistantReadRecord(record, {
|
|
191
|
+
lookupContainerKey = "",
|
|
192
|
+
primaryFields = null,
|
|
193
|
+
relatedFieldsByLookupKey = new Map()
|
|
194
|
+
} = {}) {
|
|
195
|
+
if (!isRecord(record)) {
|
|
196
|
+
return record;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const primaryProjection = projectCrudAssistantRecordFields(record, primaryFields, {
|
|
200
|
+
preserveKeys: lookupContainerKey && isRecord(record[lookupContainerKey])
|
|
201
|
+
? [lookupContainerKey]
|
|
202
|
+
: []
|
|
203
|
+
});
|
|
204
|
+
const projectedRecord = primaryProjection === record ? { ...record } : primaryProjection;
|
|
205
|
+
if (!lookupContainerKey || !isRecord(projectedRecord[lookupContainerKey])) {
|
|
206
|
+
return projectedRecord;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
projectedRecord[lookupContainerKey] = Object.fromEntries(
|
|
210
|
+
Object.entries(projectedRecord[lookupContainerKey]).map(([lookupKey, value]) => {
|
|
211
|
+
return [
|
|
212
|
+
lookupKey,
|
|
213
|
+
projectCrudAssistantLookupValue(value, relatedFieldsByLookupKey.get(lookupKey))
|
|
214
|
+
];
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
return projectedRecord;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function createCrudAssistantResultProjection(input = {}, resource = {}, relationshipEntries = []) {
|
|
221
|
+
const primaryType = String(resource?.namespace || "").trim();
|
|
222
|
+
const fieldsets = normalizeJsonApiFieldsets(input?.fields, {
|
|
223
|
+
primaryType
|
|
224
|
+
});
|
|
225
|
+
if (Object.keys(fieldsets).length < 1) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const relatedFieldsByLookupKey = new Map();
|
|
230
|
+
for (const entry of relationshipEntries) {
|
|
231
|
+
const selectedFields = fieldsets[entry.relationshipType];
|
|
232
|
+
if (!Array.isArray(selectedFields)) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
relatedFieldsByLookupKey.set(entry.relationshipName, selectedFields);
|
|
236
|
+
relatedFieldsByLookupKey.set(entry.attributeKey, selectedFields);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
primaryFields: fieldsets[primaryType],
|
|
241
|
+
relatedFieldsByLookupKey
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function projectCrudAssistantReadResult(result, projection = null, {
|
|
246
|
+
lookupContainerKey = ""
|
|
247
|
+
} = {}) {
|
|
248
|
+
if (!projection) {
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const projectionOptions = {
|
|
253
|
+
...projection,
|
|
254
|
+
lookupContainerKey
|
|
255
|
+
};
|
|
256
|
+
if (isRecord(result) && Array.isArray(result.items)) {
|
|
257
|
+
return {
|
|
258
|
+
...result,
|
|
259
|
+
items: result.items.map((entry) => projectCrudAssistantReadRecord(entry, projectionOptions))
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return projectCrudAssistantReadRecord(result, projectionOptions);
|
|
264
|
+
}
|
|
265
|
+
|
|
169
266
|
function transformCrudAssistantResult(operation, result, {
|
|
170
267
|
input = {},
|
|
171
268
|
resource,
|
|
@@ -184,39 +281,45 @@ function transformCrudAssistantResult(operation, result, {
|
|
|
184
281
|
}
|
|
185
282
|
|
|
186
283
|
const resolved = resolveAssistantResultValue(result);
|
|
284
|
+
const projection = operation === "list" || operation === "view"
|
|
285
|
+
? createCrudAssistantResultProjection(input, resource, relationshipEntries)
|
|
286
|
+
: null;
|
|
287
|
+
const projectReadResult = (value) => projectCrudAssistantReadResult(value, projection, {
|
|
288
|
+
lookupContainerKey
|
|
289
|
+
});
|
|
187
290
|
if (operation === "list") {
|
|
188
291
|
if (resolved.document.kind === "collection") {
|
|
189
292
|
const decoded = decodeJsonApiResourceResponse(
|
|
190
293
|
resolved.value,
|
|
191
294
|
createCrudAssistantTransport(resource, operation, relationshipEntries, lookupContainerKey)
|
|
192
295
|
);
|
|
193
|
-
return {
|
|
296
|
+
return projectReadResult({
|
|
194
297
|
items: decoded.items,
|
|
195
298
|
nextCursor: normalizeOptionalCursor(decoded.nextCursor)
|
|
196
|
-
};
|
|
299
|
+
});
|
|
197
300
|
}
|
|
198
301
|
if (Array.isArray(resolved.value)) {
|
|
199
|
-
return {
|
|
302
|
+
return projectReadResult({
|
|
200
303
|
items: resolved.value,
|
|
201
304
|
nextCursor: null
|
|
202
|
-
};
|
|
305
|
+
});
|
|
203
306
|
}
|
|
204
307
|
if (isRecord(resolved.value) && Array.isArray(resolved.value.items)) {
|
|
205
|
-
return {
|
|
308
|
+
return projectReadResult({
|
|
206
309
|
items: resolved.value.items,
|
|
207
310
|
nextCursor: normalizeOptionalCursor(resolved.value.nextCursor)
|
|
208
|
-
};
|
|
311
|
+
});
|
|
209
312
|
}
|
|
210
313
|
return resolved.value;
|
|
211
314
|
}
|
|
212
315
|
|
|
213
316
|
if (resolved.document.kind === "resource") {
|
|
214
|
-
return decodeJsonApiResourceResponse(
|
|
317
|
+
return projectReadResult(decodeJsonApiResourceResponse(
|
|
215
318
|
resolved.value,
|
|
216
319
|
createCrudAssistantTransport(resource, operation, relationshipEntries, lookupContainerKey)
|
|
217
|
-
);
|
|
320
|
+
));
|
|
218
321
|
}
|
|
219
|
-
return resolved.value;
|
|
322
|
+
return projectReadResult(resolved.value);
|
|
220
323
|
}
|
|
221
324
|
|
|
222
325
|
function createCrudAssistantReadDescription({
|
|
@@ -11,14 +11,46 @@ import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudReso
|
|
|
11
11
|
import { createCrudJsonApiActions } from "../src/server/jsonApiModule/actions.js";
|
|
12
12
|
|
|
13
13
|
const BOOKINGS = Object.freeze([
|
|
14
|
-
Object.freeze({
|
|
15
|
-
|
|
14
|
+
Object.freeze({
|
|
15
|
+
id: "41",
|
|
16
|
+
serviceId: "11",
|
|
17
|
+
productId: "21",
|
|
18
|
+
contactId: "31",
|
|
19
|
+
petId: "7",
|
|
20
|
+
summary: "Wash and trim"
|
|
21
|
+
}),
|
|
22
|
+
Object.freeze({
|
|
23
|
+
id: "42",
|
|
24
|
+
serviceId: "12",
|
|
25
|
+
productId: "22",
|
|
26
|
+
contactId: "32",
|
|
27
|
+
petId: "8",
|
|
28
|
+
summary: "Nail trim"
|
|
29
|
+
})
|
|
16
30
|
]);
|
|
17
31
|
const PETS = Object.freeze({
|
|
18
|
-
"7": Object.freeze({ id: "7", name: "Fido", species: "dog" }),
|
|
19
|
-
"8": Object.freeze({ id: "8", name: "Mabel", species: "cat" })
|
|
32
|
+
"7": Object.freeze({ id: "7", contactId: "31", breedId: "91", name: "Fido", species: "dog" }),
|
|
33
|
+
"8": Object.freeze({ id: "8", contactId: "32", breedId: "92", name: "Mabel", species: "cat" })
|
|
20
34
|
});
|
|
21
35
|
|
|
36
|
+
function lookupField(namespace, relationshipName) {
|
|
37
|
+
return {
|
|
38
|
+
type: "id",
|
|
39
|
+
required: true,
|
|
40
|
+
belongsTo: namespace,
|
|
41
|
+
as: relationshipName,
|
|
42
|
+
relation: {
|
|
43
|
+
kind: "lookup",
|
|
44
|
+
namespace,
|
|
45
|
+
valueKey: "id",
|
|
46
|
+
labelKey: "name"
|
|
47
|
+
},
|
|
48
|
+
operations: {
|
|
49
|
+
output: { required: true }
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
22
54
|
function createBookingsResource() {
|
|
23
55
|
return defineCrudResource({
|
|
24
56
|
namespace: "bookings",
|
|
@@ -30,21 +62,10 @@ function createBookingsResource() {
|
|
|
30
62
|
}
|
|
31
63
|
},
|
|
32
64
|
schema: {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
as: "pet",
|
|
38
|
-
relation: {
|
|
39
|
-
kind: "lookup",
|
|
40
|
-
namespace: "pets",
|
|
41
|
-
valueKey: "id",
|
|
42
|
-
labelKey: "name"
|
|
43
|
-
},
|
|
44
|
-
operations: {
|
|
45
|
-
output: { required: true }
|
|
46
|
-
}
|
|
47
|
-
},
|
|
65
|
+
serviceId: lookupField("services", "service"),
|
|
66
|
+
productId: lookupField("products", "product"),
|
|
67
|
+
contactId: lookupField("contacts", "contact"),
|
|
68
|
+
petId: lookupField("pets", "pet"),
|
|
48
69
|
summary: {
|
|
49
70
|
type: "string",
|
|
50
71
|
required: true,
|
|
@@ -77,20 +98,24 @@ function createBookingDocument(query = {}) {
|
|
|
77
98
|
.includes("pet");
|
|
78
99
|
const rows = BOOKINGS.slice(0, limit);
|
|
79
100
|
const data = rows.map((booking) => {
|
|
80
|
-
const includePetLinkage = !selectedBookings || selectedBookings.includes("petId");
|
|
81
101
|
return {
|
|
82
102
|
type: "bookings",
|
|
83
103
|
id: booking.id,
|
|
84
104
|
attributes: selectFields({ summary: booking.summary }, selectedBookings),
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
relationships: {
|
|
106
|
+
service: {
|
|
107
|
+
data: { type: "services", id: booking.serviceId }
|
|
108
|
+
},
|
|
109
|
+
product: {
|
|
110
|
+
data: { type: "products", id: booking.productId }
|
|
111
|
+
},
|
|
112
|
+
contact: {
|
|
113
|
+
data: { type: "contacts", id: booking.contactId }
|
|
114
|
+
},
|
|
115
|
+
pet: {
|
|
116
|
+
data: { type: "pets", id: booking.petId }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
94
119
|
};
|
|
95
120
|
});
|
|
96
121
|
const included = includesPet
|
|
@@ -100,7 +125,15 @@ function createBookingDocument(query = {}) {
|
|
|
100
125
|
attributes: selectFields({
|
|
101
126
|
name: PETS[booking.petId].name,
|
|
102
127
|
species: PETS[booking.petId].species
|
|
103
|
-
}, selectedPets)
|
|
128
|
+
}, selectedPets),
|
|
129
|
+
relationships: {
|
|
130
|
+
contact: {
|
|
131
|
+
data: { type: "contacts", id: PETS[booking.petId].contactId }
|
|
132
|
+
},
|
|
133
|
+
breed: {
|
|
134
|
+
data: { type: "breeds", id: PETS[booking.petId].breedId }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
104
137
|
}))
|
|
105
138
|
: [];
|
|
106
139
|
|
|
@@ -123,8 +156,12 @@ function createFixture() {
|
|
|
123
156
|
calls.push({ query, context });
|
|
124
157
|
return returnJsonApiDocument(createBookingDocument(query));
|
|
125
158
|
},
|
|
126
|
-
async getDocumentById() {
|
|
127
|
-
|
|
159
|
+
async getDocumentById(_recordId, query) {
|
|
160
|
+
const document = createBookingDocument(query);
|
|
161
|
+
return returnJsonApiDocument({
|
|
162
|
+
data: document.data[0],
|
|
163
|
+
...(document.included ? { included: document.included } : {})
|
|
164
|
+
});
|
|
128
165
|
}
|
|
129
166
|
};
|
|
130
167
|
const definitions = createCrudJsonApiActions({
|
|
@@ -210,14 +247,23 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
210
247
|
contract.result.inputSchema,
|
|
211
248
|
contract.result.inputSchema.properties.fields
|
|
212
249
|
);
|
|
213
|
-
assert.deepEqual(
|
|
250
|
+
assert.deepEqual(
|
|
251
|
+
Object.keys(fieldsetInputSchema.properties),
|
|
252
|
+
["bookings", "services", "products", "contacts", "pets"]
|
|
253
|
+
);
|
|
214
254
|
assert.equal(fieldsetInputSchema.additionalProperties, false);
|
|
215
|
-
assert.deepEqual(
|
|
255
|
+
assert.deepEqual(
|
|
256
|
+
fieldsetInputSchema.properties.bookings.items.enum,
|
|
257
|
+
["id", "serviceId", "productId", "contactId", "petId", "summary"]
|
|
258
|
+
);
|
|
216
259
|
assert.equal(Object.hasOwn(fieldsetInputSchema.properties.pets.items, "enum"), false);
|
|
217
260
|
assert.equal(Object.hasOwn(contract.result.inputSchema.properties, "workspaceSlug"), false);
|
|
218
261
|
assert.match(contract.result.description, /include must be a comma-separated string/u);
|
|
219
|
-
assert.match(contract.result.description, /\{"bookings":\["
|
|
220
|
-
assert.match(
|
|
262
|
+
assert.match(contract.result.description, /\{"bookings":\["serviceId"\],"services":\["name"\]\}/u);
|
|
263
|
+
assert.match(
|
|
264
|
+
contract.result.description,
|
|
265
|
+
/fields keys must be JSON:API resource types: "bookings", "services", "products", "contacts", "pets"/u
|
|
266
|
+
);
|
|
221
267
|
assert.match(contract.result.description, /use "pets" instead of "pet"/u);
|
|
222
268
|
assert.match(
|
|
223
269
|
contract.result.description,
|
|
@@ -241,21 +287,35 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
241
287
|
const response = await executeList(fixture, toolSet, { limit: 5 });
|
|
242
288
|
assert.equal(response.ok, true);
|
|
243
289
|
assert.deepEqual(response.result.result.items, [
|
|
244
|
-
{
|
|
245
|
-
|
|
290
|
+
{
|
|
291
|
+
id: "41",
|
|
292
|
+
serviceId: 11,
|
|
293
|
+
productId: 21,
|
|
294
|
+
contactId: 31,
|
|
295
|
+
petId: 7,
|
|
296
|
+
summary: "Wash and trim"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
id: "42",
|
|
300
|
+
serviceId: 12,
|
|
301
|
+
productId: 22,
|
|
302
|
+
contactId: 32,
|
|
303
|
+
petId: 8,
|
|
304
|
+
summary: "Nail trim"
|
|
305
|
+
}
|
|
246
306
|
]);
|
|
247
307
|
assert.equal(response.result.result.nextCursor, null);
|
|
248
308
|
});
|
|
249
309
|
|
|
250
310
|
await t.test("sparse primary fields only", async () => {
|
|
251
311
|
const response = await executeList(fixture, toolSet, {
|
|
252
|
-
fields: { bookings: ["
|
|
312
|
+
fields: { bookings: ["serviceId"] },
|
|
253
313
|
limit: 5
|
|
254
314
|
});
|
|
255
315
|
assert.equal(response.ok, true);
|
|
256
316
|
assert.deepEqual(response.result.result.items, [
|
|
257
|
-
{ id: "41",
|
|
258
|
-
{ id: "42",
|
|
317
|
+
{ id: "41", serviceId: 11 },
|
|
318
|
+
{ id: "42", serviceId: 12 }
|
|
259
319
|
]);
|
|
260
320
|
});
|
|
261
321
|
|
|
@@ -264,6 +324,8 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
264
324
|
assert.equal(response.ok, true);
|
|
265
325
|
assert.equal(response.result.result.items[0].lookups.pet.name, "Fido");
|
|
266
326
|
assert.equal(response.result.result.items[0].lookups.pet.species, "dog");
|
|
327
|
+
assert.equal(response.result.result.items[0].lookups.pet.contactId, "31");
|
|
328
|
+
assert.equal(response.result.result.items[0].lookups.pet.breedId, "91");
|
|
267
329
|
assert.equal(response.result.result.items[1].lookups.pet.name, "Mabel");
|
|
268
330
|
});
|
|
269
331
|
|
|
@@ -271,7 +333,7 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
271
333
|
const response = await executeList(fixture, toolSet, {
|
|
272
334
|
workspaceSlug: "model-controlled-workspace",
|
|
273
335
|
include: "pet",
|
|
274
|
-
fields: { bookings: ["
|
|
336
|
+
fields: { bookings: ["id", "serviceId"], pets: ["name"] },
|
|
275
337
|
limit: 5
|
|
276
338
|
});
|
|
277
339
|
assert.equal(response.ok, true);
|
|
@@ -279,7 +341,7 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
279
341
|
assert.deepEqual(JSON.parse(JSON.stringify(response.result.result.items)), [
|
|
280
342
|
{
|
|
281
343
|
id: "41",
|
|
282
|
-
|
|
344
|
+
serviceId: 11,
|
|
283
345
|
lookups: {
|
|
284
346
|
petId: { id: "7", name: "Fido" },
|
|
285
347
|
pet: { id: "7", name: "Fido" }
|
|
@@ -287,7 +349,7 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
287
349
|
},
|
|
288
350
|
{
|
|
289
351
|
id: "42",
|
|
290
|
-
|
|
352
|
+
serviceId: 12,
|
|
291
353
|
lookups: {
|
|
292
354
|
petId: { id: "8", name: "Mabel" },
|
|
293
355
|
pet: { id: "8", name: "Mabel" }
|
|
@@ -295,7 +357,12 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
295
357
|
}
|
|
296
358
|
]);
|
|
297
359
|
assert.equal(Object.hasOwn(response.result.result.items[0], "summary"), false);
|
|
360
|
+
assert.equal(Object.hasOwn(response.result.result.items[0], "productId"), false);
|
|
361
|
+
assert.equal(Object.hasOwn(response.result.result.items[0], "contactId"), false);
|
|
362
|
+
assert.equal(Object.hasOwn(response.result.result.items[0], "petId"), false);
|
|
298
363
|
assert.equal(Object.hasOwn(response.result.result.items[0].lookups.pet, "species"), false);
|
|
364
|
+
assert.equal(Object.hasOwn(response.result.result.items[0].lookups.pet, "contactId"), false);
|
|
365
|
+
assert.equal(Object.hasOwn(response.result.result.items[0].lookups.pet, "breedId"), false);
|
|
299
366
|
assert.deepEqual(
|
|
300
367
|
validateSchemaPayload(listDefinition.extensions.assistant.output, response.result.result, {
|
|
301
368
|
phase: "output"
|
|
@@ -303,9 +370,47 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
303
370
|
response.result.result
|
|
304
371
|
);
|
|
305
372
|
assert.equal(fixture.calls.at(-1).query.workspaceSlug, undefined);
|
|
373
|
+
assert.deepEqual(fixture.calls.at(-1).query.fields, {
|
|
374
|
+
bookings: ["id", "serviceId"],
|
|
375
|
+
pets: ["name"]
|
|
376
|
+
});
|
|
306
377
|
assert.equal(fixture.calls.at(-1).context.workspace.slug, "north-clinic");
|
|
307
378
|
});
|
|
308
379
|
|
|
380
|
+
await t.test("sparse view projection removes unselected relationship linkage", async () => {
|
|
381
|
+
const viewContract = await fixture.catalog.executeToolCall({
|
|
382
|
+
toolName: "assistant_action_contract",
|
|
383
|
+
argumentsText: JSON.stringify({ actionId: "crud.bookings.view", version: 1 }),
|
|
384
|
+
context: fixture.context,
|
|
385
|
+
toolSet
|
|
386
|
+
});
|
|
387
|
+
assert.equal(viewContract.ok, true);
|
|
388
|
+
|
|
389
|
+
const response = await fixture.catalog.executeToolCall({
|
|
390
|
+
toolName: "assistant_action_execute",
|
|
391
|
+
argumentsText: JSON.stringify({
|
|
392
|
+
actionId: "crud.bookings.view",
|
|
393
|
+
version: 1,
|
|
394
|
+
input: {
|
|
395
|
+
recordId: "41",
|
|
396
|
+
include: "pet",
|
|
397
|
+
fields: { bookings: ["serviceId"], pets: ["name"] }
|
|
398
|
+
}
|
|
399
|
+
}),
|
|
400
|
+
context: fixture.context,
|
|
401
|
+
toolSet
|
|
402
|
+
});
|
|
403
|
+
assert.equal(response.ok, true);
|
|
404
|
+
assert.deepEqual(JSON.parse(JSON.stringify(response.result.result)), {
|
|
405
|
+
id: "41",
|
|
406
|
+
serviceId: 11,
|
|
407
|
+
lookups: {
|
|
408
|
+
petId: { id: "7", name: "Fido" },
|
|
409
|
+
pet: { id: "7", name: "Fido" }
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
309
414
|
await t.test("limit and cursor truncation remain bounded", async () => {
|
|
310
415
|
const response = await executeList(fixture, toolSet, { limit: 1 });
|
|
311
416
|
assert.equal(response.ok, true);
|
|
@@ -329,7 +434,7 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
329
434
|
assert.equal(response.error.status, 400);
|
|
330
435
|
assert.match(
|
|
331
436
|
response.error.message,
|
|
332
|
-
/fields expects an object keyed by JSON:API resource type, such as \{"bookings":\["
|
|
437
|
+
/fields expects an object keyed by JSON:API resource type, such as \{"bookings":\["serviceId"\],"services":\["name"\]\}/u
|
|
333
438
|
);
|
|
334
439
|
});
|
|
335
440
|
|
|
@@ -344,7 +449,10 @@ test("generated CRUD list contracts conform through native assistant discovery a
|
|
|
344
449
|
assert.equal(response.error.code, "ACTION_VALIDATION_FAILED");
|
|
345
450
|
assert.equal(response.error.status, 400);
|
|
346
451
|
assert.match(response.error.message, /fields\.pet: fields keys must be JSON:API resource types/u);
|
|
347
|
-
assert.match(
|
|
452
|
+
assert.match(
|
|
453
|
+
response.error.message,
|
|
454
|
+
/Allowed keys: "bookings", "services", "products", "contacts", "pets"/u
|
|
455
|
+
);
|
|
348
456
|
assert.match(response.error.message, /use "pets" instead of "pet"/u);
|
|
349
457
|
assert.equal(fixture.calls.length, callCount);
|
|
350
458
|
});
|