@jskit-ai/json-rest-api-core 0.1.37 → 0.1.38
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
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/json-rest-api-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"hooked-api": "1.x.x",
|
|
13
13
|
"json-rest-api": "1.x.x",
|
|
14
|
-
"@jskit-ai/kernel": "0.1.
|
|
14
|
+
"@jskit-ai/kernel": "0.1.93"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Api } from "hooked-api";
|
|
2
|
-
import { AutoFilterPlugin, RestApiKnexPlugin, RestApiPlugin } from "json-rest-api";
|
|
2
|
+
import { AutoFilterPlugin, QueryProjectionsPlugin, RestApiKnexPlugin, RestApiPlugin } from "json-rest-api";
|
|
3
3
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
4
|
import { resolveCrudResourceScopeName } from "@jskit-ai/kernel/shared/support/crudLookup";
|
|
5
5
|
|
|
@@ -125,6 +125,45 @@ function normalizeJsonRestText(value, { fallback = "" } = {}) {
|
|
|
125
125
|
return normalized || fallback;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function normalizeJsonRestFilterValue(value) {
|
|
129
|
+
if (value == null) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
const entries = value
|
|
135
|
+
.map((entry) => normalizeJsonRestFilterValue(entry))
|
|
136
|
+
.filter((entry) => entry !== undefined);
|
|
137
|
+
return entries.length > 0 ? entries : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isPlainJsonRestObject(value)) {
|
|
141
|
+
const entries = Object.entries(value)
|
|
142
|
+
.map(([key, entry]) => [normalizeJsonRestText(key), normalizeJsonRestFilterValue(entry)])
|
|
143
|
+
.filter(([key, entry]) => key && entry !== undefined);
|
|
144
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof value === "string") {
|
|
148
|
+
const normalized = normalizeJsonRestText(value);
|
|
149
|
+
return normalized || undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof value === "number") {
|
|
153
|
+
return Number.isFinite(value) ? value : undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (typeof value === "bigint") {
|
|
157
|
+
return String(value);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof value === "boolean") {
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
128
167
|
function normalizeJsonRestObject(value) {
|
|
129
168
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
130
169
|
return {};
|
|
@@ -183,6 +222,99 @@ function resolveJsonRestCollectionRelationships(resource = {}) {
|
|
|
183
222
|
return relationships;
|
|
184
223
|
}
|
|
185
224
|
|
|
225
|
+
function normalizeJsonRestQueryField(fieldName = "", fieldDefinition = {}, projectionDefinition = null) {
|
|
226
|
+
const normalizedFieldName = normalizeJsonRestText(fieldName);
|
|
227
|
+
const sourceProjection = isPlainJsonRestObject(projectionDefinition)
|
|
228
|
+
? projectionDefinition
|
|
229
|
+
: isPlainJsonRestObject(fieldDefinition?.storage?.queryProjection)
|
|
230
|
+
? fieldDefinition.storage.queryProjection
|
|
231
|
+
: isPlainJsonRestObject(fieldDefinition?.queryProjection)
|
|
232
|
+
? fieldDefinition.queryProjection
|
|
233
|
+
: null;
|
|
234
|
+
if (!normalizedFieldName || !sourceProjection) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const sourceFieldDefinition = isPlainJsonRestObject(fieldDefinition) ? fieldDefinition : {};
|
|
239
|
+
const select = typeof sourceProjection.select === "function"
|
|
240
|
+
? sourceProjection.select
|
|
241
|
+
: typeof sourceProjection.project === "function"
|
|
242
|
+
? sourceProjection.project
|
|
243
|
+
: null;
|
|
244
|
+
const type = normalizeJsonRestText(sourceProjection.type || sourceFieldDefinition.type);
|
|
245
|
+
return {
|
|
246
|
+
...sourceProjection,
|
|
247
|
+
...(type ? { type } : {}),
|
|
248
|
+
...(select ? { select } : {})
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isJsonRestVirtualField(fieldDefinition = null) {
|
|
253
|
+
return fieldDefinition?.virtual === true ||
|
|
254
|
+
fieldDefinition?.storage?.virtual === true ||
|
|
255
|
+
Boolean(fieldDefinition?.storage?.queryProjection || fieldDefinition?.queryProjection);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function applyJsonRestQueryFields(scopeOptions = {}, extraQueryFields = {}) {
|
|
259
|
+
const schema = normalizeJsonRestObject(scopeOptions.schema);
|
|
260
|
+
const queryFields = {
|
|
261
|
+
...normalizeJsonRestObject(scopeOptions.queryFields)
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
for (const [fieldName, fieldDefinition] of Object.entries(schema)) {
|
|
265
|
+
const queryField = normalizeJsonRestQueryField(fieldName, fieldDefinition);
|
|
266
|
+
if (!queryField) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
queryFields[fieldName] = queryField;
|
|
271
|
+
delete schema[fieldName];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const key of Object.keys(queryFields)) {
|
|
275
|
+
const schemaFieldDefinition = schema[key];
|
|
276
|
+
if (!schemaFieldDefinition) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (!isJsonRestVirtualField(schemaFieldDefinition)) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`json-rest-api query field "${key}" conflicts with a column-backed schema field.`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
delete schema[key];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (const [fieldName, projectionDefinition] of Object.entries(normalizeJsonRestObject(extraQueryFields))) {
|
|
288
|
+
const key = normalizeJsonRestText(fieldName);
|
|
289
|
+
if (!key) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const schemaFieldDefinition = schema[key];
|
|
294
|
+
if (schemaFieldDefinition && !isJsonRestVirtualField(schemaFieldDefinition)) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`json-rest-api query field "${key}" conflicts with a column-backed schema field.`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const queryField = normalizeJsonRestQueryField(key, schemaFieldDefinition, projectionDefinition);
|
|
301
|
+
if (!queryField) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
queryFields[key] = queryField;
|
|
306
|
+
if (schemaFieldDefinition) {
|
|
307
|
+
delete schema[key];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (Object.keys(queryFields).length > 0) {
|
|
312
|
+
scopeOptions.queryFields = queryFields;
|
|
313
|
+
} else {
|
|
314
|
+
delete scopeOptions.queryFields;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
186
318
|
function buildJsonRestQueryParams(resourceType = "", query = {}, { include = undefined } = {}) {
|
|
187
319
|
const normalizedResourceType = normalizeJsonRestText(resourceType);
|
|
188
320
|
const source = normalizeJsonRestObject(query);
|
|
@@ -194,8 +326,8 @@ function buildJsonRestQueryParams(resourceType = "", query = {}, { include = und
|
|
|
194
326
|
continue;
|
|
195
327
|
}
|
|
196
328
|
|
|
197
|
-
const normalizedValue =
|
|
198
|
-
if (
|
|
329
|
+
const normalizedValue = normalizeJsonRestFilterValue(rawValue);
|
|
330
|
+
if (normalizedValue === undefined) {
|
|
199
331
|
continue;
|
|
200
332
|
}
|
|
201
333
|
|
|
@@ -327,10 +459,25 @@ function createJsonApiRelationship(resourceType = "", id = null) {
|
|
|
327
459
|
};
|
|
328
460
|
}
|
|
329
461
|
|
|
330
|
-
function createJsonRestResourceScopeOptions(
|
|
462
|
+
function createJsonRestResourceScopeOptions(
|
|
463
|
+
resource = {},
|
|
464
|
+
{
|
|
465
|
+
writeSerializers = {},
|
|
466
|
+
normalizeId = null,
|
|
467
|
+
searchSchema = null,
|
|
468
|
+
queryFields = null
|
|
469
|
+
} = {}
|
|
470
|
+
) {
|
|
331
471
|
const scopeOptions = cloneJsonRestResourceValue(resource, {
|
|
332
472
|
writeSerializers: normalizeJsonRestObject(writeSerializers)
|
|
333
473
|
});
|
|
474
|
+
if (isPlainJsonRestObject(searchSchema)) {
|
|
475
|
+
scopeOptions.searchSchema = {
|
|
476
|
+
...normalizeJsonRestObject(scopeOptions.searchSchema),
|
|
477
|
+
...searchSchema
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
applyJsonRestQueryFields(scopeOptions, queryFields);
|
|
334
481
|
const collectionRelationships = resolveJsonRestCollectionRelationships(scopeOptions);
|
|
335
482
|
if (Object.keys(collectionRelationships).length > 0) {
|
|
336
483
|
if (
|
|
@@ -442,6 +589,7 @@ async function createJsonRestApiHost({ knex }) {
|
|
|
442
589
|
normalizeId: normalizeRecordId
|
|
443
590
|
});
|
|
444
591
|
|
|
592
|
+
await api.use(QueryProjectionsPlugin);
|
|
445
593
|
await api.use(RestApiKnexPlugin, { knex });
|
|
446
594
|
await api.use(AutoFilterPlugin, {
|
|
447
595
|
resolvers: {
|
|
@@ -249,6 +249,38 @@ test("shared query/document helpers build json-rest-api request shapes", () => {
|
|
|
249
249
|
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
+
test("buildJsonRestQueryParams preserves structured filter values", () => {
|
|
253
|
+
assert.deepEqual(
|
|
254
|
+
buildJsonRestQueryParams("receivals", {
|
|
255
|
+
status: ["pending", "open", ""],
|
|
256
|
+
supplierId: [7, " 8 "],
|
|
257
|
+
availability: true,
|
|
258
|
+
weight: {
|
|
259
|
+
min: 12.5,
|
|
260
|
+
max: " 18 "
|
|
261
|
+
},
|
|
262
|
+
emptyList: [],
|
|
263
|
+
emptyObject: {},
|
|
264
|
+
emptyText: " ",
|
|
265
|
+
cursor: "cursor_1"
|
|
266
|
+
}),
|
|
267
|
+
{
|
|
268
|
+
filters: {
|
|
269
|
+
status: ["pending", "open"],
|
|
270
|
+
supplierId: [7, "8"],
|
|
271
|
+
availability: true,
|
|
272
|
+
weight: {
|
|
273
|
+
min: 12.5,
|
|
274
|
+
max: "18"
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
page: {
|
|
278
|
+
after: "cursor_1"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
252
284
|
test("createJsonRestResourceScopeOptions clones canonical resource metadata and resolves symbolic write serializers", () => {
|
|
253
285
|
const serializer = (value) => value;
|
|
254
286
|
const normalizeId = (value) => String(value || "").trim() || null;
|
|
@@ -356,6 +388,109 @@ test("createJsonRestResourceScopeOptions clones canonical resource metadata and
|
|
|
356
388
|
assert.equal(source.schema.createdAt.indexed, undefined);
|
|
357
389
|
});
|
|
358
390
|
|
|
391
|
+
test("createJsonRestResourceScopeOptions maps server query projections into json-rest-api queryFields", () => {
|
|
392
|
+
const select = ({ knex }) => knex.raw("1");
|
|
393
|
+
const resource = {
|
|
394
|
+
namespace: "receivals",
|
|
395
|
+
schema: {
|
|
396
|
+
id: { type: "id", primary: true },
|
|
397
|
+
remainingProcessableWeight: {
|
|
398
|
+
type: "number",
|
|
399
|
+
storage: {
|
|
400
|
+
virtual: true,
|
|
401
|
+
queryProjection: {
|
|
402
|
+
sortable: true,
|
|
403
|
+
select
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
availableCapacityWeight: {
|
|
408
|
+
type: "number",
|
|
409
|
+
storage: {
|
|
410
|
+
virtual: true
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const result = createJsonRestResourceScopeOptions(resource, {
|
|
417
|
+
queryFields: {
|
|
418
|
+
availableCapacityWeight: {
|
|
419
|
+
type: "number",
|
|
420
|
+
select
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
searchSchema: {
|
|
424
|
+
status: { type: "string", filterOperator: "=" }
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
assert.deepEqual(Object.keys(result.schema), ["id"]);
|
|
429
|
+
assert.equal(result.queryFields.remainingProcessableWeight.type, "number");
|
|
430
|
+
assert.equal(result.queryFields.remainingProcessableWeight.sortable, true);
|
|
431
|
+
assert.equal(result.queryFields.remainingProcessableWeight.select, select);
|
|
432
|
+
assert.equal(result.queryFields.availableCapacityWeight.select, select);
|
|
433
|
+
assert.deepEqual(result.searchSchema, {
|
|
434
|
+
status: { type: "string", filterOperator: "=" }
|
|
435
|
+
});
|
|
436
|
+
assert.equal(resource.schema.remainingProcessableWeight.storage.virtual, true);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("createJsonRestResourceScopeOptions rejects query field names for column-backed schema fields", () => {
|
|
440
|
+
assert.throws(
|
|
441
|
+
() => createJsonRestResourceScopeOptions({
|
|
442
|
+
namespace: "receivals",
|
|
443
|
+
schema: {
|
|
444
|
+
id: { type: "id", primary: true },
|
|
445
|
+
status: { type: "string" }
|
|
446
|
+
}
|
|
447
|
+
}, {
|
|
448
|
+
queryFields: {
|
|
449
|
+
status: {
|
|
450
|
+
type: "string",
|
|
451
|
+
select() {}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}),
|
|
455
|
+
/query field "status" conflicts with a column-backed schema field/
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("createJsonRestApiHost installs json-rest-api query projections", async () => {
|
|
460
|
+
const fakeKnex = Object.assign(() => {}, {
|
|
461
|
+
client: {
|
|
462
|
+
config: {
|
|
463
|
+
client: "sqlite3"
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
raw(sql) {
|
|
467
|
+
if (String(sql || "").includes("sqlite_version")) {
|
|
468
|
+
return [{ version: "3.35.5" }];
|
|
469
|
+
}
|
|
470
|
+
return { sql };
|
|
471
|
+
},
|
|
472
|
+
async transaction() {}
|
|
473
|
+
});
|
|
474
|
+
const api = await createJsonRestApiHost({ knex: fakeKnex });
|
|
475
|
+
|
|
476
|
+
await api.addResource("projectionContacts", {
|
|
477
|
+
tableName: "projection_contacts",
|
|
478
|
+
schema: {
|
|
479
|
+
id: { type: "id", primary: true }
|
|
480
|
+
},
|
|
481
|
+
queryFields: {
|
|
482
|
+
displayName: {
|
|
483
|
+
type: "string",
|
|
484
|
+
select({ knex }) {
|
|
485
|
+
return knex.raw("'Display'");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
assert.equal(typeof api.resources.projectionContacts.vars.queryFields.displayName.select, "function");
|
|
492
|
+
});
|
|
493
|
+
|
|
359
494
|
test("returnNullWhenJsonRestResourceMissing only swallows missing-resource errors", async () => {
|
|
360
495
|
await assert.doesNotReject(async () => {
|
|
361
496
|
const result = await returnNullWhenJsonRestResourceMissing(async () => {
|