@jskit-ai/resource-crud-core 0.1.187 → 0.1.188

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/resource-crud-core",
3
- "version": "0.1.187",
3
+ "version": "0.1.188",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1"
@@ -13,7 +13,7 @@
13
13
  "./shared/crudResource": "./src/shared/crudResource.js"
14
14
  },
15
15
  "dependencies": {
16
- "@jskit-ai/resource-core": "0.1.187"
16
+ "@jskit-ai/resource-core": "0.1.188"
17
17
  },
18
18
  "description": "Shared CRUD resource, field, lookup, filter, and namespace contracts.",
19
19
  "jskit": {
@@ -34,6 +34,6 @@
34
34
  }
35
35
  },
36
36
  "peerDependencies": {
37
- "@jskit-ai/kernel": "0.1.245"
37
+ "@jskit-ai/kernel": "0.1.246"
38
38
  }
39
39
  }
@@ -1,4 +1,4 @@
1
- import { normalizeSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
1
+ import { normalizeSingleSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
2
2
  import {
3
3
  normalizeObject,
4
4
  normalizeText
@@ -71,7 +71,7 @@ function resolveCrudFieldSchemaProperties(value, { context = "crud resource fiel
71
71
  return {};
72
72
  }
73
73
 
74
- const normalized = normalizeSchemaDefinition(value, {
74
+ const normalized = normalizeSingleSchemaDefinition(value, {
75
75
  context,
76
76
  defaultMode: "patch"
77
77
  });
@@ -424,6 +424,19 @@ function normalizeCrudListNumberRangeQueryValue(value) {
424
424
  };
425
425
  }
426
426
 
427
+ function parseCrudListFilterMultiQueryValue(value, normalizeEntry, { invalidValues }) {
428
+ const normalizedEntries = (Array.isArray(value) ? value : [value]).map(normalizeEntry);
429
+ const acceptedValues = Array.from(new Set(normalizedEntries.filter(Boolean)));
430
+
431
+ if (invalidValues === CRUD_LIST_FILTER_INVALID_VALUES_DISCARD) {
432
+ return acceptedValues.length > 0 ? acceptedValues : null;
433
+ }
434
+
435
+ return acceptedValues.length > 0 && normalizedEntries.every(Boolean)
436
+ ? acceptedValues
437
+ : INVALID_CRUD_LIST_FILTER_QUERY_VALUE;
438
+ }
439
+
427
440
  function parseCrudListFilterQueryValue(
428
441
  filter = {},
429
442
  value,
@@ -468,16 +481,11 @@ function parseCrudListFilterQueryValue(
468
481
  }
469
482
 
470
483
  if (type === CRUD_LIST_FILTER_TYPE_ENUM_MANY) {
471
- const values = Array.isArray(value) ? value : [value];
472
- const normalizedValues = normalizeAllowedFilterTextValues(value, allowedValues);
473
-
474
- if (invalidValues === CRUD_LIST_FILTER_INVALID_VALUES_DISCARD) {
475
- return normalizedValues.length > 0 ? normalizedValues : null;
476
- }
477
-
478
- return normalizedValues.length > 0 && normalizedValues.length === values.length
479
- ? normalizedValues
480
- : INVALID_CRUD_LIST_FILTER_QUERY_VALUE;
484
+ return parseCrudListFilterMultiQueryValue(
485
+ value,
486
+ (entry) => normalizeAllowedFilterTextValue(entry, allowedValues),
487
+ { invalidValues }
488
+ );
481
489
  }
482
490
 
483
491
  if (type === CRUD_LIST_FILTER_TYPE_RECORD_ID) {
@@ -487,16 +495,11 @@ function parseCrudListFilterQueryValue(
487
495
  }
488
496
 
489
497
  if (type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
490
- const values = Array.isArray(value) ? value : [value];
491
- const normalizedValues = normalizeCanonicalRecordIdList(value);
492
-
493
- if (invalidValues === CRUD_LIST_FILTER_INVALID_VALUES_DISCARD) {
494
- return normalizedValues.length > 0 ? normalizedValues : null;
495
- }
496
-
497
- return normalizedValues.length > 0 && normalizedValues.length === values.length
498
- ? normalizedValues
499
- : INVALID_CRUD_LIST_FILTER_QUERY_VALUE;
498
+ return parseCrudListFilterMultiQueryValue(
499
+ value,
500
+ (entry) => normalizeCanonicalRecordIdText(entry, { fallback: "" }),
501
+ { invalidValues }
502
+ );
500
503
  }
501
504
 
502
505
  if (type === CRUD_LIST_FILTER_TYPE_DATE) {
@@ -3,7 +3,7 @@ import {
3
3
  createCursorListValidator,
4
4
  RECORD_ID_PATTERN
5
5
  } from "@jskit-ai/kernel/shared/validators";
6
- import { buildCrudOperationSchemaFields } from "@jskit-ai/resource-crud-core/shared/crudFieldContract";
6
+ import { buildCrudOperationSchemaFields } from "./crudFieldContract.js";
7
7
  import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
8
8
  import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
9
9
  import {
@@ -76,7 +76,9 @@ function createCrudRecordIdFieldDefinition() {
76
76
  };
77
77
  }
78
78
 
79
- function resolveCrudLookupContainerKey(resource = {}) {
79
+ // Only an explicitly configured container key adds a lookup field to the derived output;
80
+ // unlike crudLookup.js resolveCrudLookupContainerKey, the default "lookups" key is not applied.
81
+ function resolveConfiguredLookupContainerKey(resource = {}) {
80
82
  return normalizeText(resource?.contract?.lookup?.containerKey);
81
83
  }
82
84
 
@@ -96,7 +98,7 @@ function createDerivedCrudRecordOutputDefinition(resource = {}) {
96
98
  id: createCrudRecordIdFieldDefinition(),
97
99
  ...outputFields
98
100
  };
99
- const lookupContainerKey = resolveCrudLookupContainerKey(resource);
101
+ const lookupContainerKey = resolveConfiguredLookupContainerKey(resource);
100
102
 
101
103
  if (lookupContainerKey) {
102
104
  fields[lookupContainerKey] = {
@@ -110,7 +112,7 @@ function createDerivedCrudRecordOutputDefinition(resource = {}) {
110
112
  });
111
113
  }
112
114
 
113
- function createDerivedCrudBodyDefinition(resource = {}, operationName = "patch") {
115
+ function createDerivedCrudBodyDefinition(resource = {}, operationName = "patch", bodyMode = "patch") {
114
116
  let fields = resolveFieldEntries(resource, operationName);
115
117
 
116
118
  if (operationName === "replace" && Object.keys(fields).length === 0) {
@@ -126,13 +128,7 @@ function createDerivedCrudBodyDefinition(resource = {}, operationName = "patch")
126
128
  );
127
129
  }
128
130
 
129
- const defaultMode = operationName === "create"
130
- ? "create"
131
- : operationName === "replace"
132
- ? "replace"
133
- : "patch";
134
-
135
- return createSchemaDefinition(createSchema(fields), defaultMode, {
131
+ return createSchemaDefinition(createSchema(fields), bodyMode, {
136
132
  context: `defineCrudResource derived ${operationName} body`
137
133
  });
138
134
  }
@@ -169,8 +165,7 @@ function requireCrudOperationSpec(operationName = "") {
169
165
  }
170
166
 
171
167
  function resolveCrudOperationNames(resource = {}) {
172
- const hasConfiguredOperations = Array.isArray(resource?.crudOperations);
173
- const configuredOperations = hasConfiguredOperations
168
+ const configuredOperations = Array.isArray(resource?.crudOperations)
174
169
  ? resource.crudOperations
175
170
  : DEFAULT_CRUD_OPERATION_NAMES;
176
171
  const names = [];
@@ -187,11 +182,7 @@ function resolveCrudOperationNames(resource = {}) {
187
182
  names.push(operationName);
188
183
  }
189
184
 
190
- if (hasConfiguredOperations) {
191
- return names;
192
- }
193
-
194
- return names.length > 0 ? names : [...DEFAULT_CRUD_OPERATION_NAMES];
185
+ return names;
195
186
  }
196
187
 
197
188
  function resolveFirstPresentValue(source = {}, keys = []) {
@@ -271,7 +262,7 @@ function resolveCrudBodyDefinition(spec, resource = {}, crudConfig = {}) {
271
262
  return explicitBody;
272
263
  }
273
264
 
274
- return createDerivedCrudBodyDefinition(resource, spec.bodyOperation);
265
+ return createDerivedCrudBodyDefinition(resource, spec.bodyOperation, spec.bodyMode);
275
266
  }
276
267
 
277
268
  function resolveCrudOutputDefinition(spec, resolveRecordOutputDefinition, crudConfig = {}) {
@@ -215,6 +215,35 @@ test("crud list range helpers parse and format single-key range expressions", ()
215
215
  );
216
216
  });
217
217
 
218
+ test("multi-value filters accept repeated valid values in reject mode and return them once", () => {
219
+ const filters = defineCrudListFilters({
220
+ status: {
221
+ type: "enumMany",
222
+ label: "Status",
223
+ options: [
224
+ { value: "active", label: "Active" },
225
+ { value: "archived", label: "Archived" }
226
+ ]
227
+ },
228
+ supplierContactId: {
229
+ type: "recordIdMany",
230
+ label: "Supplier"
231
+ }
232
+ });
233
+ const reject = { invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT };
234
+
235
+ assert.deepEqual(parseCrudListFilterQueryValue(filters.status, ["active", "active"], reject), ["active"]);
236
+ assert.deepEqual(parseCrudListFilterQueryValue(filters.supplierContactId, ["5", "5", 7], reject), ["5", "7"]);
237
+ assert.equal(
238
+ parseCrudListFilterQueryValue(filters.status, ["active", "active", "unexpected"], reject),
239
+ INVALID_CRUD_LIST_FILTER_QUERY_VALUE
240
+ );
241
+ assert.equal(
242
+ parseCrudListFilterQueryValue(filters.supplierContactId, ["5", "", "5"], reject),
243
+ INVALID_CRUD_LIST_FILTER_QUERY_VALUE
244
+ );
245
+ });
246
+
218
247
  test("crud list filter helpers share canonical UI and query normalization", () => {
219
248
  const filters = defineCrudListFilters({
220
249
  status: {