@jskit-ai/http-runtime 0.1.133 → 0.1.135

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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  "packageVersion": 1,
3
3
  "packageId": "@jskit-ai/http-runtime",
4
- "version": "0.1.133",
4
+ "version": "0.1.135",
5
5
  "kind": "runtime",
6
6
  "dependsOn": [],
7
7
  "capabilities": {
@@ -67,7 +67,7 @@ export default Object.freeze({
67
67
  "mutations": {
68
68
  "dependencies": {
69
69
  "runtime": {
70
- "@jskit-ai/kernel": "0.1.135"
70
+ "@jskit-ai/kernel": "0.1.137"
71
71
  },
72
72
  "dev": {}
73
73
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/http-runtime",
3
- "version": "0.1.133",
3
+ "version": "0.1.135",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -18,7 +18,7 @@
18
18
  "./shared/validators/operationValidation": "./src/shared/validators/operationValidation.js"
19
19
  },
20
20
  "dependencies": {
21
- "@jskit-ai/kernel": "0.1.135",
21
+ "@jskit-ai/kernel": "0.1.137",
22
22
  "json-rest-schema": "1.x.x"
23
23
  }
24
24
  }
@@ -1,4 +1,8 @@
1
1
  import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import {
3
+ normalizeJsonApiFieldList,
4
+ normalizeJsonApiFieldsets
5
+ } from "@jskit-ai/kernel/shared/support/jsonApiFieldsets";
2
6
  import { resolveSchemaTransportSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
3
7
 
4
8
  const JSON_API_QUERY_PAGE_CURSOR_KEY = "page[cursor]";
@@ -21,6 +25,50 @@ function buildFieldsTransportKey(responseType = "") {
21
25
  return normalizedResponseType ? `${JSON_API_FIELDS_PREFIX}${normalizedResponseType}]` : "";
22
26
  }
23
27
 
28
+ function parseFieldsTransportType(key = "") {
29
+ const normalizedKey = normalizeQueryKey(key);
30
+ if (!normalizedKey.startsWith(JSON_API_FIELDS_PREFIX) || !normalizedKey.endsWith("]")) {
31
+ return "";
32
+ }
33
+
34
+ return normalizeText(normalizedKey.slice(JSON_API_FIELDS_PREFIX.length, -1));
35
+ }
36
+
37
+ function addJsonApiFieldset(fieldsets, type = "", value = []) {
38
+ const normalizedType = normalizeText(type);
39
+ const fields = normalizeJsonApiFieldList(value);
40
+ if (!normalizedType || fields.length < 1) {
41
+ return;
42
+ }
43
+
44
+ fieldsets.set(
45
+ normalizedType,
46
+ normalizeJsonApiFieldList([
47
+ ...(fieldsets.get(normalizedType) || []),
48
+ ...fields
49
+ ])
50
+ );
51
+ }
52
+
53
+ function createJsonApiFieldsTransportValueSchema() {
54
+ return {
55
+ anyOf: [
56
+ {
57
+ type: "string",
58
+ minLength: 1
59
+ },
60
+ {
61
+ type: "array",
62
+ minItems: 1,
63
+ items: {
64
+ type: "string",
65
+ minLength: 1
66
+ }
67
+ }
68
+ ]
69
+ };
70
+ }
71
+
24
72
  function mapPlainQueryKeyToTransportKey(key = "", { responseType = "" } = {}) {
25
73
  const normalizedKey = normalizeQueryKey(key);
26
74
  if (!normalizedKey) {
@@ -110,6 +158,15 @@ function encodeJsonApiResourceQueryObject(query = {}, { responseType = "" } = {}
110
158
  const encoded = {};
111
159
 
112
160
  for (const [rawKey, rawValue] of Object.entries(source)) {
161
+ if (normalizeQueryKey(rawKey) === "fields") {
162
+ for (const [type, fields] of Object.entries(normalizeJsonApiFieldsets(rawValue, {
163
+ primaryType: responseType
164
+ }))) {
165
+ encoded[buildFieldsTransportKey(type)] = fields.join(",");
166
+ }
167
+ continue;
168
+ }
169
+
113
170
  const transportKey = mapPlainQueryKeyToTransportKey(rawKey, {
114
171
  responseType
115
172
  });
@@ -138,8 +195,20 @@ function decodeJsonApiResourceQueryObject(query = {}, { responseType = "" } = {}
138
195
 
139
196
  const source = normalizeObject(query);
140
197
  const decoded = {};
198
+ const fieldsets = new Map();
141
199
 
142
200
  for (const [rawKey, rawValue] of Object.entries(source)) {
201
+ if (normalizeQueryKey(rawKey) === "fields") {
202
+ addJsonApiFieldset(fieldsets, responseType, rawValue);
203
+ continue;
204
+ }
205
+
206
+ const fieldsetType = parseFieldsTransportType(rawKey);
207
+ if (fieldsetType) {
208
+ addJsonApiFieldset(fieldsets, fieldsetType, rawValue);
209
+ continue;
210
+ }
211
+
143
212
  const plainKey = mapTransportQueryKeyToPlainKey(rawKey, {
144
213
  responseType
145
214
  });
@@ -155,6 +224,11 @@ function decodeJsonApiResourceQueryObject(query = {}, { responseType = "" } = {}
155
224
  decoded[plainKey] = normalizedValue;
156
225
  }
157
226
 
227
+ const normalizedFieldsets = normalizeJsonApiFieldsets(Object.fromEntries(fieldsets));
228
+ if (Object.keys(normalizedFieldsets).length > 0) {
229
+ decoded.fields = normalizedFieldsets;
230
+ }
231
+
158
232
  return Object.freeze(decoded);
159
233
  }
160
234
 
@@ -174,8 +248,16 @@ function createJsonApiResourceQueryTransportSchema({
174
248
  const sourceSchema = normalizeObject(transportSchema);
175
249
  const sourceProperties = normalizeObject(sourceSchema.properties);
176
250
  const properties = {};
251
+ const patternProperties = {};
177
252
 
178
253
  for (const [plainKey, propertySchema] of Object.entries(sourceProperties)) {
254
+ if (plainKey === "fields") {
255
+ properties.fields = createJsonApiFieldsTransportValueSchema();
256
+ patternProperties["^fields\\[[^\\[\\]]+\\]$"] =
257
+ createJsonApiFieldsTransportValueSchema();
258
+ continue;
259
+ }
260
+
179
261
  const transportKey = mapPlainQueryKeyToTransportKey(plainKey, {
180
262
  responseType
181
263
  });
@@ -190,6 +272,9 @@ function createJsonApiResourceQueryTransportSchema({
190
272
  additionalProperties: false,
191
273
  properties
192
274
  };
275
+ if (Object.keys(patternProperties).length > 0) {
276
+ schema.patternProperties = patternProperties;
277
+ }
193
278
 
194
279
  if (isRecord(sourceSchema.definitions) && Object.keys(sourceSchema.definitions).length > 0) {
195
280
  schema.definitions = normalizeObject(sourceSchema.definitions);
@@ -169,7 +169,8 @@ function resolveEmbeddedAttributesTransportSchema(definition, {
169
169
  context = "JSON:API resource",
170
170
  defaultMode = "replace",
171
171
  removeId = false,
172
- removeKeys = []
172
+ removeKeys = [],
173
+ allowSparseFields = false
173
174
  } = {}) {
174
175
  const transportSchema = resolveSchemaTransportSchemaDefinition(definition, {
175
176
  context,
@@ -204,7 +205,7 @@ function resolveEmbeddedAttributesTransportSchema(definition, {
204
205
  return isRecord(fieldDefinitions) ? fieldDefinitions : {};
205
206
  })();
206
207
 
207
- const required = Array.isArray(sourceSchema.required)
208
+ const required = allowSparseFields !== true && Array.isArray(sourceSchema.required)
208
209
  ? sourceSchema.required.filter((entry) => {
209
210
  const normalizedEntry = String(entry || "").trim();
210
211
  if (!normalizedEntry || excludedKeys.has(normalizedEntry)) {
@@ -341,7 +342,8 @@ function createJsonApiResourceObjectTransportSchema({
341
342
  includeMeta = false,
342
343
  excludeAttributeKeys = [],
343
344
  relationshipEntries = [],
344
- relationshipMembersRequired = false
345
+ relationshipMembersRequired = false,
346
+ allowSparseFields = false
345
347
  } = {}) {
346
348
  const normalizedType = resolveRouteType(type);
347
349
  const relationshipsTransport = createJsonApiRelationshipsTransportSchema(relationshipEntries, {
@@ -357,7 +359,8 @@ function createJsonApiResourceObjectTransportSchema({
357
359
  removeKeys: [
358
360
  ...normalizeArray(excludeAttributeKeys),
359
361
  ...relationFieldKeys,
360
- ]
362
+ ],
363
+ allowSparseFields
361
364
  });
362
365
 
363
366
  const properties = {
@@ -366,7 +369,7 @@ function createJsonApiResourceObjectTransportSchema({
366
369
  },
367
370
  attributes: embeddedAttributes.schema
368
371
  };
369
- const required = ["type", "attributes"];
372
+ const required = allowSparseFields === true ? ["type"] : ["type", "attributes"];
370
373
 
371
374
  if (requireId) {
372
375
  properties.id = JSON_API_ID_SCHEMA;
@@ -457,6 +460,7 @@ function createJsonApiResourceSuccessTransportSchema({
457
460
  includeLinks = false,
458
461
  includeMeta = false,
459
462
  includeIncluded = false,
463
+ allowSparseFields = false,
460
464
  excludeAttributeKeys = [],
461
465
  relationshipEntries = []
462
466
  } = {}) {
@@ -475,6 +479,7 @@ function createJsonApiResourceSuccessTransportSchema({
475
479
  type,
476
480
  attributes,
477
481
  requireId: true,
482
+ allowSparseFields,
478
483
  excludeAttributeKeys,
479
484
  relationshipEntries
480
485
  });
@@ -890,6 +895,7 @@ function createJsonApiResourceRouteContract({
890
895
  outputKind = "record",
891
896
  successStatus = 200,
892
897
  includeValidation400 = false,
898
+ allowSparseFields = false,
893
899
  allowBodyId = false,
894
900
  pointerPrefix = "/data/attributes",
895
901
  bodyAttributeExcludeKeys = [],
@@ -984,6 +990,7 @@ function createJsonApiResourceRouteContract({
984
990
  includeLinks: typeof getDocumentLinks === "function",
985
991
  includeMeta: typeof getDocumentMeta === "function" || normalizedOutputKind === "collection",
986
992
  includeIncluded: typeof getIncluded === "function",
993
+ allowSparseFields,
987
994
  excludeAttributeKeys: outputAttributeExcludeKeys,
988
995
  relationshipEntries: outputRelationshipEntries
989
996
  })
@@ -3,6 +3,7 @@ import test from "node:test";
3
3
 
4
4
  import {
5
5
  JSON_API_CONTENT_TYPE,
6
+ encodeJsonApiResourceQueryObject,
6
7
  createJsonApiResourceQueryTransportSchema,
7
8
  createJsonApiResourceRequestBodyTransportSchema,
8
9
  createJsonApiResourceRouteContract,
@@ -70,6 +71,17 @@ const CONTACT_LIST_QUERY_SCHEMA = Object.freeze({
70
71
  type: "string",
71
72
  required: false
72
73
  },
74
+ fields: {
75
+ type: "object",
76
+ required: false,
77
+ values: {
78
+ type: "array",
79
+ items: {
80
+ type: "string",
81
+ minLength: 1
82
+ }
83
+ }
84
+ },
73
85
  workspaceId: {
74
86
  type: "string",
75
87
  required: false,
@@ -198,6 +210,23 @@ test("createJsonApiResourceQueryTransportSchema and route transport map list que
198
210
  assert.ok(Object.hasOwn(schema.properties, "filter[q]"));
199
211
  assert.ok(Object.hasOwn(schema.properties, "include"));
200
212
  assert.ok(Object.hasOwn(schema.properties, "filter[workspaceId]"));
213
+ assert.ok(Object.hasOwn(schema.properties, "fields"));
214
+ assert.ok(Object.hasOwn(schema.patternProperties, "^fields\\[[^\\[\\]]+\\]$"));
215
+
216
+ assert.deepEqual(
217
+ encodeJsonApiResourceQueryObject({
218
+ fields: {
219
+ contacts: ["id", "name"],
220
+ workspaces: ["id", "slug"]
221
+ }
222
+ }, {
223
+ responseType: "contacts"
224
+ }),
225
+ {
226
+ "fields[contacts]": "id,name",
227
+ "fields[workspaces]": "id,slug"
228
+ }
229
+ );
201
230
 
202
231
  const transport = createJsonApiResourceRouteTransport({
203
232
  type: "contacts",
@@ -210,7 +239,9 @@ test("createJsonApiResourceQueryTransportSchema and route transport map list que
210
239
  "page[limit]": "10",
211
240
  "filter[q]": "Merc",
212
241
  include: "workspace",
213
- "filter[workspaceId]": "7"
242
+ "filter[workspaceId]": "7",
243
+ "fields[contacts]": "id,name",
244
+ "fields[workspaces]": "id,slug"
214
245
  });
215
246
 
216
247
  assert.deepEqual(plainQuery, {
@@ -218,8 +249,41 @@ test("createJsonApiResourceQueryTransportSchema and route transport map list que
218
249
  limit: "10",
219
250
  q: "Merc",
220
251
  include: "workspace",
221
- workspaceId: "7"
252
+ workspaceId: "7",
253
+ fields: {
254
+ contacts: ["id", "name"],
255
+ workspaces: ["id", "slug"]
256
+ }
222
257
  });
258
+
259
+ assert.deepEqual(
260
+ transport.request.query({
261
+ fields: "name,id"
262
+ }),
263
+ {
264
+ fields: {
265
+ contacts: ["id", "name"]
266
+ }
267
+ }
268
+ );
269
+ });
270
+
271
+ test("sparse JSON:API response contracts preserve field types without requiring every attribute", () => {
272
+ const schema = createJsonApiResourceSuccessTransportSchema({
273
+ type: "contacts",
274
+ attributes: CONTACT_RECORD_SCHEMA,
275
+ kind: "collection",
276
+ allowSparseFields: true
277
+ });
278
+ const resourceSchema = schema.definitions.contactsSuccessResource;
279
+ const attributesSchemaName = resourceSchema.properties.attributes.allOf[0].$ref
280
+ .replace("#/definitions/", "");
281
+ const attributesSchema = schema.definitions[attributesSchemaName];
282
+
283
+ assert.deepEqual(resourceSchema.required, ["type", "id"]);
284
+ assert.equal(Object.hasOwn(attributesSchema, "required"), false);
285
+ assert.equal(attributesSchema.additionalProperties, false);
286
+ assert.equal(attributesSchema.properties.subscribed.anyOf[0].type, "boolean");
223
287
  });
224
288
 
225
289
  test("createJsonApiResourceRouteContract produces route options compatible with kernel route validator", () => {