@jskit-ai/kernel 0.1.135 → 0.1.136

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/kernel",
3
- "version": "0.1.135",
3
+ "version": "0.1.136",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "json-rest-schema": "1.x.x"
@@ -47,6 +47,7 @@
47
47
  "./shared/support/crudListFilters": "./shared/support/crudListFilters.js",
48
48
  "./shared/support/crudFieldContract": "./shared/support/crudFieldContract.js",
49
49
  "./shared/support/crudLookup": "./shared/support/crudLookup.js",
50
+ "./shared/support/jsonApiFieldsets": "./shared/support/jsonApiFieldsets.js",
50
51
  "./shared/support/generatedUiContract": "./shared/support/generatedUiContract.js",
51
52
  "./shared/support/deepFreeze": "./shared/support/deepFreeze.js",
52
53
  "./shared/support/listenerSet": "./shared/support/listenerSet.js",
@@ -219,7 +219,7 @@ function normalizeCrudListNumberRangeUiValue(rawValue) {
219
219
  };
220
220
  }
221
221
 
222
- function createCrudListFilterInitialValue(filter = {}) {
222
+ function createCrudListFilterEmptyValue(filter = {}) {
223
223
  if (normalizeCrudListFilterType(filter.type) === CRUD_LIST_FILTER_TYPE_FLAG) {
224
224
  return false;
225
225
  }
@@ -242,6 +242,14 @@ function createCrudListFilterInitialValue(filter = {}) {
242
242
  return "";
243
243
  }
244
244
 
245
+ function createCrudListFilterInitialValue(filter = {}) {
246
+ if (!Object.hasOwn(filter, "defaultValue")) {
247
+ return createCrudListFilterEmptyValue(filter);
248
+ }
249
+
250
+ return normalizeCrudListFilterUiValue(filter, filter.defaultValue);
251
+ }
252
+
245
253
  function isCrudListFilterMultiValue(filter = {}) {
246
254
  const type = normalizeCrudListFilterType(filter.type);
247
255
  return type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY;
@@ -745,6 +753,13 @@ function normalizeCrudListFilterDefinition(rawKey = "", rawDefinition = null) {
745
753
  const meta = source.meta && typeof source.meta === "object" && !Array.isArray(source.meta)
746
754
  ? deepFreeze({ ...source.meta })
747
755
  : null;
756
+ const hasDefaultValue = Object.hasOwn(source, "defaultValue");
757
+ const defaultValue = hasDefaultValue
758
+ ? deepFreeze(normalizeCrudListFilterUiValue({
759
+ type,
760
+ options
761
+ }, source.defaultValue))
762
+ : undefined;
748
763
 
749
764
  if (type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
750
765
  if (normalizeText(source.fromKey) || normalizeText(source.toKey)) {
@@ -767,7 +782,8 @@ function normalizeCrudListFilterDefinition(rawKey = "", rawDefinition = null) {
767
782
  lookup,
768
783
  chipLabel,
769
784
  ui,
770
- meta
785
+ meta,
786
+ ...(hasDefaultValue ? { defaultValue } : {})
771
787
  });
772
788
  }
773
789
 
@@ -823,6 +839,7 @@ export {
823
839
  parseCrudListRangeQueryExpression,
824
840
  formatCrudListRangeQueryExpression,
825
841
  defineCrudListFilters,
842
+ createCrudListFilterEmptyValue,
826
843
  createCrudListFilterInitialValue,
827
844
  isCrudListFilterMultiValue,
828
845
  isCrudListFilterStructuredValue,
@@ -7,6 +7,7 @@ import {
7
7
  INVALID_CRUD_LIST_FILTER_QUERY_VALUE,
8
8
  parseCrudListRangeQueryExpression,
9
9
  formatCrudListRangeQueryExpression,
10
+ createCrudListFilterEmptyValue,
10
11
  createCrudListFilterInitialValue,
11
12
  isCrudListFilterMultiValue,
12
13
  isCrudListFilterStructuredValue,
@@ -110,6 +111,23 @@ test("defineCrudListFilters uses fixed presence semantics with optional label ov
110
111
  ]);
111
112
  });
112
113
 
114
+ test("defineCrudListFilters normalizes declarative defaults without changing the empty state", () => {
115
+ const filters = defineCrudListFilters({
116
+ status: {
117
+ type: "enum",
118
+ options: [
119
+ { value: "active", label: "Active" },
120
+ { value: "archived", label: "Archived" }
121
+ ],
122
+ defaultValue: "active"
123
+ }
124
+ });
125
+
126
+ assert.equal(filters.status.defaultValue, "active");
127
+ assert.equal(createCrudListFilterInitialValue(filters.status), "active");
128
+ assert.equal(createCrudListFilterEmptyValue(filters.status), "");
129
+ });
130
+
113
131
  test("defineCrudListFilters rejects duplicate query keys", () => {
114
132
  assert.throws(
115
133
  () => defineCrudListFilters({
@@ -0,0 +1,56 @@
1
+ import { deepFreeze } from "./deepFreeze.js";
2
+ import { normalizeText } from "./normalize.js";
3
+
4
+ function normalizeJsonApiFieldList(value = []) {
5
+ const values = Array.isArray(value) ? value : [value];
6
+ const fields = new Set();
7
+
8
+ for (const entry of values) {
9
+ const parts = typeof entry === "string" ? entry.split(",") : [entry];
10
+ for (const part of parts) {
11
+ const field = normalizeText(part);
12
+ if (field) {
13
+ fields.add(field);
14
+ }
15
+ }
16
+ }
17
+
18
+ return Object.freeze([...fields].sort((left, right) => left.localeCompare(right)));
19
+ }
20
+
21
+ function normalizeJsonApiFieldsets(value = {}, { primaryType = "" } = {}) {
22
+ const normalizedPrimaryType = normalizeText(primaryType);
23
+ let source = {};
24
+ if (value && typeof value === "object" && !Array.isArray(value)) {
25
+ source = value;
26
+ } else if (normalizedPrimaryType) {
27
+ source = { [normalizedPrimaryType]: value };
28
+ }
29
+ const fieldsets = [];
30
+
31
+ for (const [rawType, rawFields] of Object.entries(source)) {
32
+ const type = normalizeText(rawType);
33
+ const fields = normalizeJsonApiFieldList(rawFields);
34
+ if (!type || fields.length < 1) {
35
+ continue;
36
+ }
37
+ fieldsets.push([type, fields]);
38
+ }
39
+
40
+ return deepFreeze(
41
+ Object.fromEntries(
42
+ fieldsets.sort(([left], [right]) => left.localeCompare(right))
43
+ )
44
+ );
45
+ }
46
+
47
+ function buildJsonApiFieldsetsToken(value = {}) {
48
+ const fieldsets = normalizeJsonApiFieldsets(value);
49
+ return Object.keys(fieldsets).length > 0 ? JSON.stringify(fieldsets) : "";
50
+ }
51
+
52
+ export {
53
+ normalizeJsonApiFieldList,
54
+ normalizeJsonApiFieldsets,
55
+ buildJsonApiFieldsetsToken
56
+ };
@@ -0,0 +1,50 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import {
5
+ normalizeJsonApiFieldList,
6
+ normalizeJsonApiFieldsets,
7
+ buildJsonApiFieldsetsToken
8
+ } from "./jsonApiFieldsets.js";
9
+
10
+ test("JSON:API fieldsets normalize comma lists into a stable typed map", () => {
11
+ assert.deepEqual(
12
+ normalizeJsonApiFieldsets({
13
+ jobs: ["status", "id,status"],
14
+ contacts: "displayName,id"
15
+ }),
16
+ {
17
+ contacts: ["displayName", "id"],
18
+ jobs: ["id", "status"]
19
+ }
20
+ );
21
+ assert.deepEqual(normalizeJsonApiFieldList(["name", "", "id,name"]), ["id", "name"]);
22
+ });
23
+
24
+ test("JSON:API fieldsets produce deterministic query tokens", () => {
25
+ const fieldsets = {
26
+ jobs: ["status", "id"],
27
+ contacts: ["id"]
28
+ };
29
+
30
+ assert.equal(
31
+ buildJsonApiFieldsetsToken(fieldsets),
32
+ buildJsonApiFieldsetsToken({
33
+ contacts: ["id"],
34
+ jobs: ["id", "status"]
35
+ })
36
+ );
37
+ assert.equal(buildJsonApiFieldsetsToken({}), "");
38
+ });
39
+
40
+ test("JSON:API fieldset resource types cannot mutate object prototypes", () => {
41
+ const fieldsets = normalizeJsonApiFieldsets(
42
+ Object.fromEntries([
43
+ ["__proto__", ["id", "name"]]
44
+ ])
45
+ );
46
+
47
+ assert.equal(Object.getPrototypeOf(fieldsets), Object.prototype);
48
+ assert.equal(Object.hasOwn(fieldsets, "__proto__"), true);
49
+ assert.deepEqual(fieldsets.__proto__, ["id", "name"]);
50
+ });