@jskit-ai/crud-core 0.1.144 → 0.1.146

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/crud-core",
4
- version: "0.1.144",
4
+ version: "0.1.146",
5
5
  kind: "runtime",
6
6
  description: "Shared CRUD helpers used by CRUD modules.",
7
7
  dependsOn: [
@@ -28,7 +28,7 @@ export default Object.freeze({
28
28
  mutations: {
29
29
  dependencies: {
30
30
  runtime: {
31
- "@jskit-ai/crud-core": "0.1.144"
31
+ "@jskit-ai/crud-core": "0.1.146"
32
32
  },
33
33
  dev: {}
34
34
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/crud-core",
3
- "version": "0.1.144",
3
+ "version": "0.1.146",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -27,15 +27,15 @@
27
27
  "./server/routeContracts": "./src/server/routeContracts.js"
28
28
  },
29
29
  "dependencies": {
30
- "@jskit-ai/database-runtime": "0.1.134",
31
- "@jskit-ai/http-runtime": "0.1.133",
32
- "@jskit-ai/kernel": "0.1.135",
30
+ "@jskit-ai/database-runtime": "0.1.135",
31
+ "@jskit-ai/http-runtime": "0.1.134",
32
+ "@jskit-ai/kernel": "0.1.136",
33
33
  "json-rest-schema": "1.x.x",
34
- "@jskit-ai/realtime": "0.1.132",
35
- "@jskit-ai/resource-crud-core": "0.1.78",
36
- "@jskit-ai/shell-web": "0.1.136",
37
- "@jskit-ai/users-core": "0.1.146",
38
- "@jskit-ai/users-web": "0.1.152"
34
+ "@jskit-ai/realtime": "0.1.133",
35
+ "@jskit-ai/resource-crud-core": "0.1.79",
36
+ "@jskit-ai/shell-web": "0.1.137",
37
+ "@jskit-ai/users-core": "0.1.149",
38
+ "@jskit-ai/users-web": "0.1.153"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@tanstack/vue-query": "^5.90.5",
@@ -25,6 +25,23 @@ const lookupIncludeQueryValidator = Object.freeze({
25
25
  mode: "patch"
26
26
  });
27
27
 
28
+ const jsonApiFieldsetsQueryValidator = Object.freeze({
29
+ schema: createSchema({
30
+ fields: {
31
+ type: "object",
32
+ required: false,
33
+ values: {
34
+ type: "array",
35
+ items: {
36
+ type: "string",
37
+ minLength: 1
38
+ }
39
+ }
40
+ }
41
+ }),
42
+ mode: "patch"
43
+ });
44
+
28
45
  function resolveCrudListUsesOrderedCursor(list = {}) {
29
46
  const entries = Array.isArray(list?.orderBy)
30
47
  ? list.orderBy
@@ -88,10 +105,47 @@ function createCrudParentFilterQueryValidator(resource = {}) {
88
105
  });
89
106
  }
90
107
 
108
+ function createStandardCrudListQueryValidators({
109
+ resource = {},
110
+ listFilterQueryValidator = null,
111
+ searchQueryValidator = listSearchQueryValidator,
112
+ includeQueryValidator = lookupIncludeQueryValidator
113
+ } = {}) {
114
+ const resolvedListFilterQueryValidator =
115
+ listFilterQueryValidator
116
+ ?? resource?.contract?.listFilters?.queryValidator
117
+ ?? null;
118
+
119
+ return [
120
+ createCrudCursorPaginationQueryValidator({
121
+ orderBy: resource?.defaultSort
122
+ }),
123
+ searchQueryValidator,
124
+ createCrudParentFilterQueryValidator(resource),
125
+ ...(resolvedListFilterQueryValidator
126
+ ? [resolvedListFilterQueryValidator]
127
+ : []),
128
+ includeQueryValidator,
129
+ jsonApiFieldsetsQueryValidator
130
+ ];
131
+ }
132
+
133
+ function createStandardCrudViewQueryValidators({
134
+ includeQueryValidator = lookupIncludeQueryValidator
135
+ } = {}) {
136
+ return [
137
+ includeQueryValidator,
138
+ jsonApiFieldsetsQueryValidator
139
+ ];
140
+ }
141
+
91
142
  export {
92
143
  createCrudCursorPaginationQueryValidator,
93
144
  listSearchQueryValidator,
94
145
  lookupIncludeQueryValidator,
146
+ jsonApiFieldsetsQueryValidator,
95
147
  resolveCrudParentFilterKeys,
96
- createCrudParentFilterQueryValidator
148
+ createCrudParentFilterQueryValidator,
149
+ createStandardCrudListQueryValidators,
150
+ createStandardCrudViewQueryValidators
97
151
  };
@@ -8,10 +8,10 @@ import {
8
8
  } from "@jskit-ai/kernel/shared/validators";
9
9
  import { resolveCrudResourceScopeName } from "@jskit-ai/kernel/shared/support/crudLookup";
10
10
  import {
11
- createCrudCursorPaginationQueryValidator,
11
+ createStandardCrudListQueryValidators,
12
+ createStandardCrudViewQueryValidators,
12
13
  listSearchQueryValidator as defaultListSearchQueryValidator,
13
- lookupIncludeQueryValidator as defaultLookupIncludeQueryValidator,
14
- createCrudParentFilterQueryValidator
14
+ lookupIncludeQueryValidator as defaultLookupIncludeQueryValidator
15
15
  } from "./listQueryValidators.js";
16
16
 
17
17
  function isRecord(value) {
@@ -444,18 +444,19 @@ function createCrudJsonApiRouteContracts({
444
444
  lookupIncludeQueryValidator = defaultLookupIncludeQueryValidator,
445
445
  listFilterQueryValidator = null
446
446
  } = {}) {
447
- const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator({
448
- orderBy: resource?.defaultSort
449
- });
450
- const listParentFilterQueryValidator = createCrudParentFilterQueryValidator(resource);
451
- const resolvedListFilterQueryValidator = listFilterQueryValidator || resource?.contract?.listFilters?.queryValidator || null;
452
- const listRouteQueryValidator = composeSchemaDefinitions([
453
- listCursorPaginationQueryValidator,
454
- listSearchQueryValidator,
455
- listParentFilterQueryValidator,
456
- ...(resolvedListFilterQueryValidator ? [resolvedListFilterQueryValidator] : []),
457
- lookupIncludeQueryValidator
458
- ]);
447
+ const listRouteQueryValidator = composeSchemaDefinitions(
448
+ createStandardCrudListQueryValidators({
449
+ resource,
450
+ listFilterQueryValidator,
451
+ searchQueryValidator: listSearchQueryValidator,
452
+ includeQueryValidator: lookupIncludeQueryValidator
453
+ })
454
+ );
455
+ const viewRouteQueryValidator = composeSchemaDefinitions(
456
+ createStandardCrudViewQueryValidators({
457
+ includeQueryValidator: lookupIncludeQueryValidator
458
+ })
459
+ );
459
460
  const recordRouteParamsValidator = routeParamsValidator
460
461
  ? composeSchemaDefinitions([
461
462
  routeParamsValidator,
@@ -511,19 +512,23 @@ function createCrudJsonApiRouteContracts({
511
512
  query: listRouteQueryValidator,
512
513
  output: viewOutput,
513
514
  outputKind: "collection",
515
+ includeValidation400: true,
514
516
  outputAttributeExcludeKeys,
515
517
  outputRelationshipEntries: viewOutputRelationships,
518
+ allowSparseFields: true,
516
519
  getRecordAttributes: viewRecordAttributes,
517
520
  getRecordRelationships: viewRecordRelationships,
518
521
  getIncluded: viewIncluded
519
522
  }),
520
523
  viewRouteContract: createJsonApiResourceRouteContract({
521
524
  type: routeType,
522
- query: lookupIncludeQueryValidator,
525
+ query: viewRouteQueryValidator,
523
526
  output: viewOutput,
524
527
  outputKind: "record",
528
+ includeValidation400: true,
525
529
  outputAttributeExcludeKeys,
526
530
  outputRelationshipEntries: viewOutputRelationships,
531
+ allowSparseFields: true,
527
532
  getRecordAttributes: viewRecordAttributes,
528
533
  getRecordRelationships: viewRecordRelationships,
529
534
  getIncluded: viewIncluded
@@ -11,6 +11,8 @@ import {
11
11
  lookupIncludeQueryValidator,
12
12
  createCrudCursorPaginationQueryValidator,
13
13
  createCrudParentFilterQueryValidator,
14
+ createStandardCrudListQueryValidators,
15
+ createStandardCrudViewQueryValidators,
14
16
  resolveCrudParentFilterKeys
15
17
  } from "../src/server/listQueryValidators.js";
16
18
 
@@ -254,3 +256,49 @@ test("createCrudParentFilterQueryValidator keeps parent filters optional when me
254
256
  });
255
257
  assert.deepEqual(compiled.schema.querystring.required || [], []);
256
258
  });
259
+
260
+ test("standard CRUD query validator groups remain plain and extensible", () => {
261
+ const listFilterQueryValidator = {
262
+ schema: createSchema({
263
+ currentness: {
264
+ type: "string",
265
+ required: false
266
+ }
267
+ }),
268
+ mode: "patch"
269
+ };
270
+ const resource = {
271
+ ...createCrudResource(),
272
+ defaultSort: ["-createdAt"],
273
+ contract: {
274
+ listFilters: {
275
+ queryValidator: listFilterQueryValidator
276
+ }
277
+ }
278
+ };
279
+
280
+ const listQueryValidator = composeSchemaDefinition(
281
+ ...createStandardCrudListQueryValidators({ resource }),
282
+ {
283
+ schema: createSchema({
284
+ archived: {
285
+ type: "boolean",
286
+ required: false
287
+ }
288
+ }),
289
+ mode: "patch"
290
+ }
291
+ );
292
+ assert.deepEqual(
293
+ Object.keys(listQueryValidator.schema.getFieldDefinitions()).sort(),
294
+ ["archived", "currentness", "cursor", "fields", "include", "limit", "q"]
295
+ );
296
+
297
+ const viewQueryValidator = composeSchemaDefinition(
298
+ ...createStandardCrudViewQueryValidators()
299
+ );
300
+ assert.deepEqual(
301
+ Object.keys(viewQueryValidator.schema.getFieldDefinitions()).sort(),
302
+ ["fields", "include"]
303
+ );
304
+ });
@@ -177,7 +177,11 @@ test("createCrudJsonApiRouteContracts builds default CRUD JSON:API contracts", a
177
177
  include: " ownerId ",
178
178
  contactId: " 42 ",
179
179
  cursor: " offset:2 ",
180
- limit: "25"
180
+ limit: "25",
181
+ fields: {
182
+ contacts: ["id", "name"],
183
+ userProfiles: ["id", "name"]
184
+ }
181
185
  }, { phase: "input" });
182
186
 
183
187
  assert.deepEqual(normalizedListQuery, {
@@ -185,9 +189,39 @@ test("createCrudJsonApiRouteContracts builds default CRUD JSON:API contracts", a
185
189
  include: "ownerId",
186
190
  contactId: "42",
187
191
  cursor: "offset:2",
188
- limit: 25
192
+ limit: 25,
193
+ fields: {
194
+ contacts: ["id", "name"],
195
+ userProfiles: ["id", "name"]
196
+ }
189
197
  });
190
198
 
199
+ assert.deepEqual(
200
+ await validateSchemaPayload(contracts.viewRouteContract.query, {
201
+ include: "owner",
202
+ fields: {
203
+ contacts: ["id", "name"],
204
+ userProfiles: ["id", "name"]
205
+ }
206
+ }, { phase: "input" }),
207
+ {
208
+ include: "owner",
209
+ fields: {
210
+ contacts: ["id", "name"],
211
+ userProfiles: ["id", "name"]
212
+ }
213
+ }
214
+ );
215
+
216
+ const sparseResourceSchema =
217
+ contracts.listRouteContract.responses[200].transportSchema.definitions.contactsSuccessResource;
218
+ const sparseAttributesSchemaName = sparseResourceSchema.properties.attributes.allOf[0].$ref
219
+ .replace("#/definitions/", "");
220
+ const sparseAttributesSchema =
221
+ contracts.listRouteContract.responses[200].transportSchema.definitions[sparseAttributesSchemaName];
222
+ assert.deepEqual(sparseResourceSchema.required, ["type", "id"]);
223
+ assert.equal(Object.hasOwn(sparseAttributesSchema, "required"), false);
224
+
191
225
  const decodedCreateBody = contracts.createRouteContract.transport.request.body({
192
226
  data: {
193
227
  type: "contacts",