@jskit-ai/kernel 0.1.154 → 0.1.156

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,323 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import {
4
- defineCrudListFilters,
5
- CRUD_LIST_FILTER_INVALID_VALUES_REJECT,
6
- CRUD_LIST_FILTER_INVALID_VALUES_DISCARD,
7
- INVALID_CRUD_LIST_FILTER_QUERY_VALUE,
8
- parseCrudListRangeQueryExpression,
9
- formatCrudListRangeQueryExpression,
10
- createCrudListFilterEmptyValue,
11
- createCrudListFilterInitialValue,
12
- isCrudListFilterMultiValue,
13
- isCrudListFilterStructuredValue,
14
- normalizeCrudListFilterUiValue,
15
- areCrudListFilterUiValuesEqual,
16
- hasCrudListFilterUiValue,
17
- listCrudListFilterChipValues,
18
- formatCrudListFilterDefaultChipLabel,
19
- formatCrudListFilterQueryValue,
20
- parseCrudListFilterQueryValue,
21
- resolveCrudListFilterQueryKeys,
22
- resolveCrudListFilterOptionLabel
23
- } from "./crudListFilters.js";
24
-
25
- test("defineCrudListFilters normalizes common filter shapes", () => {
26
- const filters = defineCrudListFilters({
27
- onlyStaff: {
28
- type: "flag",
29
- label: "Staff"
30
- },
31
- status: {
32
- type: "enumMany",
33
- label: "Status",
34
- options: [
35
- { value: "active", label: "Active" },
36
- { value: "archived", label: "Archived" }
37
- ]
38
- },
39
- arrivalDate: {
40
- type: "dateRange",
41
- label: "Arrival Date"
42
- },
43
- weight: {
44
- type: "numberRange",
45
- label: "Weight"
46
- },
47
- supplierContactId: {
48
- type: "recordIdMany",
49
- label: "Supplier",
50
- lookup: {
51
- namespace: "contacts"
52
- }
53
- }
54
- });
55
-
56
- assert.deepEqual(filters.onlyStaff, {
57
- key: "onlyStaff",
58
- type: "flag",
59
- label: "Staff",
60
- queryKey: "onlyStaff",
61
- options: [],
62
- lookup: null,
63
- chipLabel: null,
64
- ui: null,
65
- meta: null
66
- });
67
- assert.deepEqual(filters.arrivalDate, {
68
- key: "arrivalDate",
69
- type: "dateRange",
70
- label: "Arrival Date",
71
- queryKey: "arrivalDate",
72
- options: [],
73
- lookup: null,
74
- chipLabel: null,
75
- ui: null,
76
- meta: null
77
- });
78
- assert.deepEqual(filters.weight, {
79
- key: "weight",
80
- type: "numberRange",
81
- label: "Weight",
82
- queryKey: "weight",
83
- options: [],
84
- lookup: null,
85
- chipLabel: null,
86
- ui: null,
87
- meta: null
88
- });
89
- assert.deepEqual(filters.supplierContactId.lookup, {
90
- namespace: "contacts",
91
- apiSuffix: "/contacts",
92
- valueKey: "id"
93
- });
94
- });
95
-
96
- test("defineCrudListFilters uses fixed presence semantics with optional label overrides", () => {
97
- const filters = defineCrudListFilters({
98
- locationAssignment: {
99
- type: "presence",
100
- label: "Storage",
101
- options: [
102
- { value: "present", label: "Assigned" },
103
- { value: "missing", label: "Unassigned" }
104
- ]
105
- }
106
- });
107
-
108
- assert.deepEqual(filters.locationAssignment.options, [
109
- { value: "present", label: "Assigned" },
110
- { value: "missing", label: "Unassigned" }
111
- ]);
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
-
131
- test("defineCrudListFilters rejects duplicate query keys", () => {
132
- assert.throws(
133
- () => defineCrudListFilters({
134
- arrivalDate: {
135
- type: "dateRange",
136
- label: "Arrival Date"
137
- },
138
- arrivalDateExact: {
139
- type: "date",
140
- label: "Arrival Date From",
141
- queryKey: "arrivalDate"
142
- }
143
- }),
144
- /both use query key "arrivalDate"/
145
- );
146
- });
147
-
148
- test("defineCrudListFilters rejects split range keys", () => {
149
- assert.throws(
150
- () => defineCrudListFilters({
151
- arrivalDate: {
152
- type: "dateRange",
153
- label: "Arrival Date",
154
- fromKey: "arrivalDateFrom"
155
- }
156
- }),
157
- /unsupported split range keys/
158
- );
159
-
160
- assert.throws(
161
- () => defineCrudListFilters({
162
- weight: {
163
- type: "numberRange",
164
- label: "Weight",
165
- minKey: "weightMin"
166
- }
167
- }),
168
- /unsupported split range keys/
169
- );
170
- });
171
-
172
- test("resolveCrudListFilter helpers expose query keys and option labels", () => {
173
- const filters = defineCrudListFilters({
174
- status: {
175
- type: "enum",
176
- label: "Status",
177
- options: [
178
- { value: "active", label: "Active" }
179
- ]
180
- },
181
- arrivalDate: {
182
- type: "dateRange",
183
- label: "Arrival Date"
184
- }
185
- });
186
-
187
- assert.deepEqual(resolveCrudListFilterQueryKeys(filters.status), ["status"]);
188
- assert.deepEqual(resolveCrudListFilterQueryKeys(filters.arrivalDate), ["arrivalDate"]);
189
- assert.equal(resolveCrudListFilterOptionLabel(filters.status, "active"), "Active");
190
- assert.equal(resolveCrudListFilterOptionLabel(filters.status, "missing", { fallback: "Unknown" }), "Unknown");
191
- });
192
-
193
- test("crud list range helpers parse and format single-key range expressions", () => {
194
- assert.deepEqual(parseCrudListRangeQueryExpression("2026-04-01"), {
195
- exact: true,
196
- start: "2026-04-01",
197
- end: "2026-04-01"
198
- });
199
- assert.deepEqual(parseCrudListRangeQueryExpression("2026-04-01..2026-04-30"), {
200
- exact: false,
201
- start: "2026-04-01",
202
- end: "2026-04-30"
203
- });
204
- assert.deepEqual(parseCrudListRangeQueryExpression("..2026-04-30"), {
205
- exact: false,
206
- start: "",
207
- end: "2026-04-30"
208
- });
209
- assert.equal(parseCrudListRangeQueryExpression(".."), null);
210
-
211
- assert.equal(formatCrudListRangeQueryExpression("2026-04-01", ""), "2026-04-01..");
212
- assert.equal(
213
- formatCrudListRangeQueryExpression("2026-04-01", "2026-04-01", { collapseExact: true }),
214
- "2026-04-01"
215
- );
216
- });
217
-
218
- test("crud list filter helpers share canonical UI and query normalization", () => {
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
- arrivalDate: {
233
- type: "dateRange",
234
- label: "Arrival Date"
235
- }
236
- });
237
-
238
- assert.deepEqual(createCrudListFilterInitialValue(filters.status), []);
239
- assert.equal(isCrudListFilterMultiValue(filters.status), true);
240
- assert.equal(isCrudListFilterMultiValue(filters.arrivalDate), false);
241
- assert.equal(isCrudListFilterStructuredValue(filters.arrivalDate), true);
242
- assert.equal(isCrudListFilterStructuredValue(filters.status), false);
243
- assert.deepEqual(
244
- normalizeCrudListFilterUiValue(filters.status, ["active", "unexpected"]),
245
- ["active"]
246
- );
247
- assert.deepEqual(
248
- parseCrudListFilterQueryValue(filters.status, ["active", "unexpected"], {
249
- invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_DISCARD
250
- }),
251
- ["active"]
252
- );
253
- assert.equal(
254
- parseCrudListFilterQueryValue(filters.status, ["active", "unexpected"], {
255
- invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT
256
- }),
257
- INVALID_CRUD_LIST_FILTER_QUERY_VALUE
258
- );
259
-
260
- assert.deepEqual(
261
- normalizeCrudListFilterUiValue(filters.supplierContactId, ["7", "bad", 4]),
262
- ["7", "4"]
263
- );
264
- assert.deepEqual(
265
- parseCrudListFilterQueryValue(filters.supplierContactId, ["7", "bad", 4], {
266
- invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_DISCARD
267
- }),
268
- ["7", "4"]
269
- );
270
- assert.equal(
271
- parseCrudListFilterQueryValue(filters.supplierContactId, { bad: true }, {
272
- invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT
273
- }),
274
- INVALID_CRUD_LIST_FILTER_QUERY_VALUE
275
- );
276
-
277
- assert.deepEqual(
278
- normalizeCrudListFilterUiValue(filters.arrivalDate, "..2026-04-30"),
279
- {
280
- from: "",
281
- to: "2026-04-30"
282
- }
283
- );
284
- assert.equal(
285
- formatCrudListFilterQueryValue(filters.arrivalDate, {
286
- from: "2026-04-30",
287
- to: "2026-04-30"
288
- }),
289
- "2026-04-30"
290
- );
291
- assert.equal(
292
- areCrudListFilterUiValuesEqual(
293
- filters.arrivalDate,
294
- {
295
- from: "2026-04-30",
296
- to: "2026-04-30"
297
- },
298
- "2026-04-30"
299
- ),
300
- true
301
- );
302
- assert.equal(hasCrudListFilterUiValue(filters.status, ["active", "unexpected"]), true);
303
- assert.equal(hasCrudListFilterUiValue(filters.arrivalDate, { from: "", to: "" }), false);
304
- assert.deepEqual(listCrudListFilterChipValues(filters.status, ["active", "unexpected"]), ["active"]);
305
- assert.deepEqual(
306
- listCrudListFilterChipValues(filters.arrivalDate, {
307
- from: "2026-04-30",
308
- to: "2026-04-30"
309
- }),
310
- [{
311
- from: "2026-04-30",
312
- to: "2026-04-30"
313
- }]
314
- );
315
- assert.equal(
316
- formatCrudListFilterDefaultChipLabel(filters.status, "active", {
317
- resolveAtomicValue(value) {
318
- return resolveCrudListFilterOptionLabel(filters.status, value);
319
- }
320
- }),
321
- "Status: Active"
322
- );
323
- });
@@ -1,190 +0,0 @@
1
- import { normalizeText } from "./normalize.js";
2
- import { toCamelCase } from "./stringCase.js";
3
- import { normalizePathname } from "../surface/paths.js";
4
- import {
5
- buildCrudFieldContractMap,
6
- resolveCrudFieldSchemaProperties
7
- } from "./crudFieldContract.js";
8
-
9
- const DEFAULT_CRUD_LOOKUP_CONTAINER_KEY = "lookups";
10
-
11
- function normalizeCrudLookupApiPath(value = "") {
12
- const normalized = normalizePathname(normalizeText(value));
13
- if (!normalized || normalized === "/") {
14
- return "";
15
- }
16
-
17
- return normalized;
18
- }
19
-
20
- function normalizeCrudLookupNamespace(value = "") {
21
- const normalizedApiPath = normalizeCrudLookupApiPath(value);
22
- if (!normalizedApiPath) {
23
- return "";
24
- }
25
-
26
- return normalizedApiPath.slice(1);
27
- }
28
-
29
- function resolveCrudLookupApiPathFromNamespace(value = "") {
30
- const normalizedNamespace = normalizeCrudLookupNamespace(value);
31
- if (!normalizedNamespace) {
32
- return "";
33
- }
34
-
35
- return `/${normalizedNamespace}`;
36
- }
37
-
38
- function resolveCrudResourceScopeName(value = "") {
39
- const normalizedNamespace = normalizeCrudLookupNamespace(value);
40
- if (!normalizedNamespace) {
41
- return "";
42
- }
43
-
44
- return toCamelCase(normalizedNamespace.replace(/\//g, "-"));
45
- }
46
-
47
- function normalizeCrudLookupContainerKey(
48
- value,
49
- {
50
- defaultValue = DEFAULT_CRUD_LOOKUP_CONTAINER_KEY,
51
- context = "crud lookup container key"
52
- } = {}
53
- ) {
54
- if (value === undefined || value === null || value === "") {
55
- return normalizeText(defaultValue) || DEFAULT_CRUD_LOOKUP_CONTAINER_KEY;
56
- }
57
-
58
- const normalized = normalizeText(value);
59
- if (!normalized) {
60
- throw new TypeError(`${context} must be a non-empty string.`);
61
- }
62
-
63
- return normalized;
64
- }
65
-
66
- function resolveCrudLookupContainerKey(resource = {}, options = {}) {
67
- const source = resource && typeof resource === "object" && !Array.isArray(resource) ? resource : {};
68
- const contract = source.contract;
69
- if (contract !== undefined && contract !== null && (typeof contract !== "object" || Array.isArray(contract))) {
70
- throw new TypeError("crud resource contract must be an object when provided.");
71
- }
72
-
73
- const lookup = contract?.lookup;
74
- if (lookup !== undefined && lookup !== null && (typeof lookup !== "object" || Array.isArray(lookup))) {
75
- throw new TypeError("crud resource contract.lookup must be an object when provided.");
76
- }
77
-
78
- return normalizeCrudLookupContainerKey(lookup?.containerKey, options);
79
- }
80
-
81
- function resolveCrudLookupFieldEntries(resource = {}, { allowKeys = [] } = {}) {
82
- const source = resource && typeof resource === "object" && !Array.isArray(resource) ? resource : {};
83
- const entries = Object.values(buildCrudFieldContractMap(source, {
84
- context: "crud lookup field entries"
85
- }));
86
- const allowedKeySet = new Set(
87
- (Array.isArray(allowKeys) ? allowKeys : [])
88
- .map((entry) => normalizeText(entry))
89
- .filter(Boolean)
90
- );
91
-
92
- const keys = [];
93
- const seenKeys = new Set();
94
- for (const entry of entries) {
95
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
96
- continue;
97
- }
98
-
99
- const key = normalizeText(entry.key);
100
- if (!key || seenKeys.has(key)) {
101
- continue;
102
- }
103
- if (allowedKeySet.size > 0 && !allowedKeySet.has(key)) {
104
- continue;
105
- }
106
-
107
- const relation = entry.relation;
108
- if (!relation || typeof relation !== "object" || Array.isArray(relation)) {
109
- continue;
110
- }
111
- if (normalizeText(relation.kind).toLowerCase() !== "lookup") {
112
- continue;
113
- }
114
-
115
- seenKeys.add(key);
116
- keys.push(
117
- Object.freeze({
118
- key,
119
- parentRouteParamKey: normalizeText(entry.parentRouteParamKey)
120
- })
121
- );
122
- }
123
-
124
- return Object.freeze(keys);
125
- }
126
-
127
- function resolveCrudLookupCreateSchemaKeys(resource = {}) {
128
- return Object.freeze(
129
- Object.keys(
130
- resolveCrudFieldSchemaProperties(resource?.operations?.create?.body, {
131
- context: "crud lookup create schema"
132
- })
133
- )
134
- );
135
- }
136
-
137
- function resolveCrudLookupFieldKeys(resource = {}, { allowKeys = [] } = {}) {
138
- return Object.freeze(
139
- resolveCrudLookupFieldEntries(resource, { allowKeys })
140
- .map(({ key }) => key)
141
- );
142
- }
143
-
144
- function resolveCrudParentFilterKeys(resource = {}) {
145
- return resolveCrudLookupFieldKeys(resource, {
146
- allowKeys: resolveCrudLookupCreateSchemaKeys(resource)
147
- });
148
- }
149
-
150
- function resolveCrudLookupFieldKeyFromRouteParam(resource = {}, routeParamKey = "", { allowKeys = [] } = {}) {
151
- const normalizedRouteParamKey = normalizeText(routeParamKey);
152
- if (!normalizedRouteParamKey) {
153
- return "";
154
- }
155
-
156
- const entries = resolveCrudLookupFieldEntries(resource, { allowKeys });
157
- for (const entry of entries) {
158
- if (entry.key === normalizedRouteParamKey) {
159
- return entry.key;
160
- }
161
- }
162
-
163
- for (const entry of entries) {
164
- if (entry.parentRouteParamKey === normalizedRouteParamKey) {
165
- return entry.key;
166
- }
167
- }
168
-
169
- return "";
170
- }
171
-
172
- function resolveCrudParentFilterFieldKeyFromRouteParam(resource = {}, routeParamKey = "") {
173
- return resolveCrudLookupFieldKeyFromRouteParam(resource, routeParamKey, {
174
- allowKeys: resolveCrudLookupCreateSchemaKeys(resource)
175
- });
176
- }
177
-
178
- export {
179
- DEFAULT_CRUD_LOOKUP_CONTAINER_KEY,
180
- normalizeCrudLookupApiPath,
181
- normalizeCrudLookupNamespace,
182
- resolveCrudLookupApiPathFromNamespace,
183
- resolveCrudResourceScopeName,
184
- normalizeCrudLookupContainerKey,
185
- resolveCrudLookupContainerKey,
186
- resolveCrudLookupFieldKeys,
187
- resolveCrudParentFilterKeys,
188
- resolveCrudLookupFieldKeyFromRouteParam,
189
- resolveCrudParentFilterFieldKeyFromRouteParam
190
- };