@jskit-ai/crud-core 0.1.52 → 0.1.54
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.descriptor.mjs +2 -2
- package/package.json +9 -8
- package/src/server/listFilters.js +547 -0
- package/test/listFilters.test.js +379 -0
package/package.descriptor.mjs
CHANGED
|
@@ -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.
|
|
4
|
+
version: "0.1.54",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Shared CRUD helpers used by CRUD modules.",
|
|
7
7
|
dependsOn: [
|
|
@@ -26,7 +26,7 @@ export default Object.freeze({
|
|
|
26
26
|
mutations: {
|
|
27
27
|
dependencies: {
|
|
28
28
|
runtime: {
|
|
29
|
-
"@jskit-ai/crud-core": "0.1.
|
|
29
|
+
"@jskit-ai/crud-core": "0.1.54"
|
|
30
30
|
},
|
|
31
31
|
dev: {}
|
|
32
32
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/crud-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -22,16 +22,17 @@
|
|
|
22
22
|
"./server/fieldAccess": "./src/server/fieldAccess.js",
|
|
23
23
|
"./server/createCrudServiceFromResource": "./src/server/createCrudServiceFromResource.js",
|
|
24
24
|
"./server/crudModuleConfig": "./src/server/crudModuleConfig.js",
|
|
25
|
-
"./server/listQueryValidators": "./src/server/listQueryValidators.js"
|
|
25
|
+
"./server/listQueryValidators": "./src/server/listQueryValidators.js",
|
|
26
|
+
"./server/listFilters": "./src/server/listFilters.js"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"@tanstack/vue-query": "^5.90.5",
|
|
29
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
30
|
-
"@jskit-ai/kernel": "0.1.
|
|
31
|
-
"@jskit-ai/realtime": "0.1.
|
|
32
|
-
"@jskit-ai/shell-web": "0.1.
|
|
33
|
-
"@jskit-ai/users-core": "0.1.
|
|
34
|
-
"@jskit-ai/users-web": "0.1.
|
|
30
|
+
"@jskit-ai/database-runtime": "0.1.46",
|
|
31
|
+
"@jskit-ai/kernel": "0.1.46",
|
|
32
|
+
"@jskit-ai/realtime": "0.1.45",
|
|
33
|
+
"@jskit-ai/shell-web": "0.1.45",
|
|
34
|
+
"@jskit-ai/users-core": "0.1.56",
|
|
35
|
+
"@jskit-ai/users-web": "0.1.61",
|
|
35
36
|
"typebox": "^1.0.81"
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import {
|
|
3
|
+
normalizeObjectInput,
|
|
4
|
+
mergeObjectSchemas,
|
|
5
|
+
RECORD_ID_PATTERN
|
|
6
|
+
} from "@jskit-ai/kernel/shared/validators";
|
|
7
|
+
import {
|
|
8
|
+
normalizeBoolean,
|
|
9
|
+
normalizeCanonicalRecordIdText,
|
|
10
|
+
normalizeObject,
|
|
11
|
+
normalizeText,
|
|
12
|
+
normalizeUniqueTextList
|
|
13
|
+
} from "@jskit-ai/kernel/shared/support/normalize";
|
|
14
|
+
import {
|
|
15
|
+
defineCrudListFilters,
|
|
16
|
+
CRUD_LIST_FILTER_TYPE_FLAG,
|
|
17
|
+
CRUD_LIST_FILTER_TYPE_ENUM,
|
|
18
|
+
CRUD_LIST_FILTER_TYPE_ENUM_MANY,
|
|
19
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID,
|
|
20
|
+
CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY,
|
|
21
|
+
CRUD_LIST_FILTER_TYPE_DATE,
|
|
22
|
+
CRUD_LIST_FILTER_TYPE_DATE_RANGE,
|
|
23
|
+
CRUD_LIST_FILTER_TYPE_NUMBER_RANGE,
|
|
24
|
+
CRUD_LIST_FILTER_TYPE_PRESENCE
|
|
25
|
+
} from "@jskit-ai/kernel/shared/support/crudListFilters";
|
|
26
|
+
|
|
27
|
+
const DATE_FILTER_PATTERN_SOURCE = "^\\d{4}-\\d{2}-\\d{2}$";
|
|
28
|
+
const DATE_FILTER_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
|
|
29
|
+
const NUMBER_FILTER_PATTERN_SOURCE = "^[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$";
|
|
30
|
+
const CRUD_LIST_FILTER_INVALID_VALUES_REJECT = "reject";
|
|
31
|
+
const CRUD_LIST_FILTER_INVALID_VALUES_DISCARD = "discard";
|
|
32
|
+
const CRUD_LIST_FILTER_INVALID_VALUES_MODES = Object.freeze([
|
|
33
|
+
CRUD_LIST_FILTER_INVALID_VALUES_REJECT,
|
|
34
|
+
CRUD_LIST_FILTER_INVALID_VALUES_DISCARD
|
|
35
|
+
]);
|
|
36
|
+
const looseTextInputSchema = Type.String({ minLength: 0 });
|
|
37
|
+
const strictNumberInputSchema = Type.Union([
|
|
38
|
+
Type.String({ pattern: NUMBER_FILTER_PATTERN_SOURCE }),
|
|
39
|
+
Type.Number()
|
|
40
|
+
]);
|
|
41
|
+
const looseStringOrNumberSchema = Type.Union([
|
|
42
|
+
looseTextInputSchema,
|
|
43
|
+
Type.Number()
|
|
44
|
+
]);
|
|
45
|
+
const recordIdInputSchema = Type.Union([
|
|
46
|
+
Type.String({ pattern: RECORD_ID_PATTERN }),
|
|
47
|
+
Type.Number({ minimum: 1 })
|
|
48
|
+
]);
|
|
49
|
+
const flagInputSchema = Type.Union([
|
|
50
|
+
Type.String({ minLength: 0 }),
|
|
51
|
+
Type.Boolean(),
|
|
52
|
+
Type.Number()
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function normalizeCrudListFilterInvalidValues(value = "") {
|
|
56
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
57
|
+
if (CRUD_LIST_FILTER_INVALID_VALUES_MODES.includes(normalized)) {
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new TypeError(
|
|
62
|
+
`Unsupported CRUD list filter invalidValues mode "${value}". Expected one of: ${CRUD_LIST_FILTER_INVALID_VALUES_MODES.join(", ")}.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createSingleOrMultiValueSchema(itemSchema) {
|
|
67
|
+
return Type.Optional(
|
|
68
|
+
Type.Union([
|
|
69
|
+
itemSchema,
|
|
70
|
+
Type.Array(itemSchema, {
|
|
71
|
+
minItems: 1
|
|
72
|
+
})
|
|
73
|
+
])
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function firstValue(value) {
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
return value[0];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizeDateFilterValue(value) {
|
|
86
|
+
const normalized = normalizeText(firstValue(value));
|
|
87
|
+
if (!normalized || !DATE_FILTER_PATTERN.test(normalized)) {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return normalized;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeNumberFilterValue(value) {
|
|
95
|
+
if (value == null || value === "") {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const normalized = typeof value === "number"
|
|
100
|
+
? value
|
|
101
|
+
: Number(normalizeText(firstValue(value)));
|
|
102
|
+
return Number.isFinite(normalized) ? normalized : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeRecordIdFilterValue(value) {
|
|
106
|
+
return normalizeCanonicalRecordIdText(firstValue(value), {
|
|
107
|
+
fallback: ""
|
|
108
|
+
}) || "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeRecordIdFilterValues(value) {
|
|
112
|
+
return normalizeUniqueTextList(value, {
|
|
113
|
+
acceptSingle: true
|
|
114
|
+
})
|
|
115
|
+
.map((entry) => normalizeCanonicalRecordIdText(entry, { fallback: "" }))
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveAllowedOptionValues(filter = {}) {
|
|
120
|
+
return new Set(
|
|
121
|
+
(Array.isArray(filter.options) ? filter.options : [])
|
|
122
|
+
.map((entry) => normalizeText(entry?.value))
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeAllowedTextValue(value, allowedValues = new Set()) {
|
|
128
|
+
const normalized = normalizeText(firstValue(value));
|
|
129
|
+
if (!normalized || !allowedValues.has(normalized)) {
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeAllowedTextValues(value, allowedValues = new Set()) {
|
|
137
|
+
return normalizeUniqueTextList(value, {
|
|
138
|
+
acceptSingle: true
|
|
139
|
+
}).filter((entry) => allowedValues.has(entry));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function addDaysToDateFilterValue(value = "", days = 0) {
|
|
143
|
+
const normalizedValue = normalizeDateFilterValue(value);
|
|
144
|
+
if (!normalizedValue || !Number.isInteger(days)) {
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const date = new Date(`${normalizedValue}T00:00:00.000Z`);
|
|
149
|
+
if (Number.isNaN(date.getTime())) {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
date.setUTCDate(date.getUTCDate() + days);
|
|
154
|
+
const year = date.getUTCFullYear();
|
|
155
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
156
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
157
|
+
return `${year}-${month}-${day}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createFilterQuerySchema(filter = {}, { invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT } = {}) {
|
|
161
|
+
const invalidValueMode = normalizeCrudListFilterInvalidValues(invalidValues);
|
|
162
|
+
const discardInvalidValues = invalidValueMode === CRUD_LIST_FILTER_INVALID_VALUES_DISCARD;
|
|
163
|
+
const allowedValues = (Array.isArray(filter.options) ? filter.options : []).map((entry) => entry.value);
|
|
164
|
+
|
|
165
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
166
|
+
return Type.Object(
|
|
167
|
+
{
|
|
168
|
+
[filter.queryKey]: Type.Optional(flagInputSchema)
|
|
169
|
+
},
|
|
170
|
+
{ additionalProperties: false }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM || filter.type === CRUD_LIST_FILTER_TYPE_PRESENCE) {
|
|
175
|
+
return Type.Object(
|
|
176
|
+
{
|
|
177
|
+
[filter.queryKey]: Type.Optional(
|
|
178
|
+
discardInvalidValues
|
|
179
|
+
? looseTextInputSchema
|
|
180
|
+
: Type.String({ enum: allowedValues })
|
|
181
|
+
)
|
|
182
|
+
},
|
|
183
|
+
{ additionalProperties: false }
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY) {
|
|
188
|
+
return Type.Object(
|
|
189
|
+
{
|
|
190
|
+
[filter.queryKey]: createSingleOrMultiValueSchema(
|
|
191
|
+
discardInvalidValues
|
|
192
|
+
? looseTextInputSchema
|
|
193
|
+
: Type.String({ enum: allowedValues })
|
|
194
|
+
)
|
|
195
|
+
},
|
|
196
|
+
{ additionalProperties: false }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID) {
|
|
201
|
+
return Type.Object(
|
|
202
|
+
{
|
|
203
|
+
[filter.queryKey]: Type.Optional(
|
|
204
|
+
discardInvalidValues
|
|
205
|
+
? looseStringOrNumberSchema
|
|
206
|
+
: recordIdInputSchema
|
|
207
|
+
)
|
|
208
|
+
},
|
|
209
|
+
{ additionalProperties: false }
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
214
|
+
return Type.Object(
|
|
215
|
+
{
|
|
216
|
+
[filter.queryKey]: createSingleOrMultiValueSchema(
|
|
217
|
+
discardInvalidValues
|
|
218
|
+
? looseStringOrNumberSchema
|
|
219
|
+
: recordIdInputSchema
|
|
220
|
+
)
|
|
221
|
+
},
|
|
222
|
+
{ additionalProperties: false }
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE) {
|
|
227
|
+
return Type.Object(
|
|
228
|
+
{
|
|
229
|
+
[filter.queryKey]: Type.Optional(
|
|
230
|
+
discardInvalidValues
|
|
231
|
+
? looseTextInputSchema
|
|
232
|
+
: Type.String({ pattern: DATE_FILTER_PATTERN_SOURCE })
|
|
233
|
+
)
|
|
234
|
+
},
|
|
235
|
+
{ additionalProperties: false }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
240
|
+
return Type.Object(
|
|
241
|
+
{
|
|
242
|
+
[filter.fromKey]: Type.Optional(
|
|
243
|
+
discardInvalidValues
|
|
244
|
+
? looseTextInputSchema
|
|
245
|
+
: Type.String({ pattern: DATE_FILTER_PATTERN_SOURCE })
|
|
246
|
+
),
|
|
247
|
+
[filter.toKey]: Type.Optional(
|
|
248
|
+
discardInvalidValues
|
|
249
|
+
? looseTextInputSchema
|
|
250
|
+
: Type.String({ pattern: DATE_FILTER_PATTERN_SOURCE })
|
|
251
|
+
)
|
|
252
|
+
},
|
|
253
|
+
{ additionalProperties: false }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
258
|
+
return Type.Object(
|
|
259
|
+
{
|
|
260
|
+
[filter.minKey]: Type.Optional(
|
|
261
|
+
discardInvalidValues
|
|
262
|
+
? looseStringOrNumberSchema
|
|
263
|
+
: strictNumberInputSchema
|
|
264
|
+
),
|
|
265
|
+
[filter.maxKey]: Type.Optional(
|
|
266
|
+
discardInvalidValues
|
|
267
|
+
? looseStringOrNumberSchema
|
|
268
|
+
: strictNumberInputSchema
|
|
269
|
+
)
|
|
270
|
+
},
|
|
271
|
+
{ additionalProperties: false }
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return Type.Object({}, { additionalProperties: false });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function normalizeFilterValue(filter = {}, source = {}) {
|
|
279
|
+
const allowedValues = resolveAllowedOptionValues(filter);
|
|
280
|
+
|
|
281
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
282
|
+
if (!Object.hasOwn(source, filter.queryKey)) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const sourceValue = source[filter.queryKey];
|
|
287
|
+
if (sourceValue === null || sourceValue === "") {
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
return normalizeBoolean(firstValue(sourceValue));
|
|
293
|
+
} catch {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM || filter.type === CRUD_LIST_FILTER_TYPE_PRESENCE) {
|
|
299
|
+
if (!Object.hasOwn(source, filter.queryKey)) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const normalizedValue = normalizeAllowedTextValue(source[filter.queryKey], allowedValues);
|
|
304
|
+
return normalizedValue || undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY) {
|
|
308
|
+
if (!Object.hasOwn(source, filter.queryKey)) {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const normalizedValues = normalizeAllowedTextValues(source[filter.queryKey], allowedValues);
|
|
313
|
+
return normalizedValues.length > 0 ? normalizedValues : undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID) {
|
|
317
|
+
if (!Object.hasOwn(source, filter.queryKey)) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const normalizedValue = normalizeRecordIdFilterValue(source[filter.queryKey]);
|
|
322
|
+
return normalizedValue || undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
326
|
+
if (!Object.hasOwn(source, filter.queryKey)) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const normalizedValues = normalizeRecordIdFilterValues(source[filter.queryKey]);
|
|
331
|
+
return normalizedValues.length > 0 ? normalizedValues : undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE) {
|
|
335
|
+
if (!Object.hasOwn(source, filter.queryKey)) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const normalizedValue = normalizeDateFilterValue(source[filter.queryKey]);
|
|
340
|
+
return normalizedValue || undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
344
|
+
const from = normalizeDateFilterValue(source[filter.fromKey]);
|
|
345
|
+
const to = normalizeDateFilterValue(source[filter.toKey]);
|
|
346
|
+
if (!from && !to) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return Object.freeze({
|
|
351
|
+
...(from ? { from } : {}),
|
|
352
|
+
...(to ? { to } : {})
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
357
|
+
const min = normalizeNumberFilterValue(source[filter.minKey]);
|
|
358
|
+
const max = normalizeNumberFilterValue(source[filter.maxKey]);
|
|
359
|
+
if (min == null && max == null) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return Object.freeze({
|
|
364
|
+
...(min != null ? { min } : {}),
|
|
365
|
+
...(max != null ? { max } : {})
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function normalizeColumnsMap(columns = {}) {
|
|
373
|
+
const source = normalizeObject(columns);
|
|
374
|
+
const normalized = {};
|
|
375
|
+
for (const [key, value] of Object.entries(source)) {
|
|
376
|
+
const normalizedKey = normalizeText(key);
|
|
377
|
+
const normalizedValue = normalizeText(value);
|
|
378
|
+
if (!normalizedKey || !normalizedValue) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
normalized[normalizedKey] = normalizedValue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return Object.freeze(normalized);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function applyDefaultFilterQuery(queryBuilder, filter = {}, value, column = "") {
|
|
389
|
+
if (!column) {
|
|
390
|
+
return queryBuilder;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
394
|
+
if (value === true) {
|
|
395
|
+
queryBuilder.where(column, true);
|
|
396
|
+
}
|
|
397
|
+
return queryBuilder;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID) {
|
|
401
|
+
if (value !== undefined) {
|
|
402
|
+
queryBuilder.where(column, value);
|
|
403
|
+
}
|
|
404
|
+
return queryBuilder;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
408
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
409
|
+
queryBuilder.whereIn(column, value);
|
|
410
|
+
}
|
|
411
|
+
return queryBuilder;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_PRESENCE) {
|
|
415
|
+
if (value === "present") {
|
|
416
|
+
queryBuilder.whereNotNull(column);
|
|
417
|
+
}
|
|
418
|
+
if (value === "missing") {
|
|
419
|
+
queryBuilder.whereNull(column);
|
|
420
|
+
}
|
|
421
|
+
return queryBuilder;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE) {
|
|
425
|
+
if (value) {
|
|
426
|
+
const nextDate = addDaysToDateFilterValue(value, 1);
|
|
427
|
+
queryBuilder.where(column, ">=", `${value} 00:00:00`);
|
|
428
|
+
if (nextDate) {
|
|
429
|
+
queryBuilder.where(column, "<", `${nextDate} 00:00:00`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return queryBuilder;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
436
|
+
if (value?.from) {
|
|
437
|
+
queryBuilder.where(column, ">=", `${value.from} 00:00:00`);
|
|
438
|
+
}
|
|
439
|
+
if (value?.to) {
|
|
440
|
+
const nextDate = addDaysToDateFilterValue(value.to, 1);
|
|
441
|
+
if (nextDate) {
|
|
442
|
+
queryBuilder.where(column, "<", `${nextDate} 00:00:00`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return queryBuilder;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
449
|
+
if (value?.min != null) {
|
|
450
|
+
queryBuilder.where(column, ">=", value.min);
|
|
451
|
+
}
|
|
452
|
+
if (value?.max != null) {
|
|
453
|
+
queryBuilder.where(column, "<=", value.max);
|
|
454
|
+
}
|
|
455
|
+
return queryBuilder;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return queryBuilder;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function createCrudListFilters(definitions = {}, { columns = {}, apply = {} } = {}) {
|
|
462
|
+
const normalizedFilters = defineCrudListFilters(definitions);
|
|
463
|
+
const normalizedColumns = normalizeColumnsMap(columns);
|
|
464
|
+
const filterEntries = Object.values(normalizedFilters);
|
|
465
|
+
|
|
466
|
+
function normalize(payload = {}) {
|
|
467
|
+
const source = normalizeObjectInput(payload);
|
|
468
|
+
const normalized = {};
|
|
469
|
+
|
|
470
|
+
for (const filter of filterEntries) {
|
|
471
|
+
const value = normalizeFilterValue(filter, source);
|
|
472
|
+
if (value === undefined) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
normalized[filter.key] = value;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return Object.freeze(normalized);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const queryValidators = Object.freeze({
|
|
483
|
+
[CRUD_LIST_FILTER_INVALID_VALUES_REJECT]: Object.freeze({
|
|
484
|
+
schema: mergeObjectSchemas(
|
|
485
|
+
filterEntries.map((filter) => createFilterQuerySchema(filter, {
|
|
486
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT
|
|
487
|
+
}))
|
|
488
|
+
),
|
|
489
|
+
normalize
|
|
490
|
+
}),
|
|
491
|
+
[CRUD_LIST_FILTER_INVALID_VALUES_DISCARD]: Object.freeze({
|
|
492
|
+
schema: mergeObjectSchemas(
|
|
493
|
+
filterEntries.map((filter) => createFilterQuerySchema(filter, {
|
|
494
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_DISCARD
|
|
495
|
+
}))
|
|
496
|
+
),
|
|
497
|
+
normalize
|
|
498
|
+
})
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
function applyQuery(queryBuilder, payload = {}) {
|
|
502
|
+
if (!queryBuilder || typeof queryBuilder.where !== "function") {
|
|
503
|
+
throw new TypeError("createCrudListFilters.applyQuery requires query builder.");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const normalized = normalize(payload);
|
|
507
|
+
for (const filter of filterEntries) {
|
|
508
|
+
if (!Object.hasOwn(normalized, filter.key)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const value = normalized[filter.key];
|
|
513
|
+
const customApply = typeof apply?.[filter.key] === "function"
|
|
514
|
+
? apply[filter.key]
|
|
515
|
+
: null;
|
|
516
|
+
if (customApply) {
|
|
517
|
+
customApply(queryBuilder, value, {
|
|
518
|
+
filter,
|
|
519
|
+
filters: normalized
|
|
520
|
+
});
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
applyDefaultFilterQuery(queryBuilder, filter, value, normalizedColumns[filter.key] || "");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return queryBuilder;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function createQueryValidator({ invalidValues } = {}) {
|
|
531
|
+
const invalidValueMode = normalizeCrudListFilterInvalidValues(invalidValues);
|
|
532
|
+
return queryValidators[invalidValueMode];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return Object.freeze({
|
|
536
|
+
filters: normalizedFilters,
|
|
537
|
+
createQueryValidator,
|
|
538
|
+
normalize,
|
|
539
|
+
applyQuery
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export {
|
|
544
|
+
CRUD_LIST_FILTER_INVALID_VALUES_REJECT,
|
|
545
|
+
CRUD_LIST_FILTER_INVALID_VALUES_DISCARD,
|
|
546
|
+
createCrudListFilters
|
|
547
|
+
};
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Check } from "typebox/value";
|
|
4
|
+
import { compileRouteValidator } from "@jskit-ai/kernel/_testable";
|
|
5
|
+
import { cursorPaginationQueryValidator } from "@jskit-ai/kernel/shared/validators";
|
|
6
|
+
import { listSearchQueryValidator } from "../src/server/listQueryValidators.js";
|
|
7
|
+
import {
|
|
8
|
+
CRUD_LIST_FILTER_INVALID_VALUES_REJECT,
|
|
9
|
+
CRUD_LIST_FILTER_INVALID_VALUES_DISCARD,
|
|
10
|
+
createCrudListFilters
|
|
11
|
+
} from "../src/server/listFilters.js";
|
|
12
|
+
|
|
13
|
+
test("crud-core exposes createCrudListFilters through the public package export", async () => {
|
|
14
|
+
const module = await import("@jskit-ai/crud-core/server/listFilters");
|
|
15
|
+
assert.equal(typeof module.createCrudListFilters, "function");
|
|
16
|
+
assert.equal(module.CRUD_LIST_FILTER_INVALID_VALUES_REJECT, CRUD_LIST_FILTER_INVALID_VALUES_REJECT);
|
|
17
|
+
assert.equal(module.CRUD_LIST_FILTER_INVALID_VALUES_DISCARD, CRUD_LIST_FILTER_INVALID_VALUES_DISCARD);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function createQueryDouble() {
|
|
21
|
+
const calls = [];
|
|
22
|
+
const nestedQuery = {
|
|
23
|
+
where(...args) {
|
|
24
|
+
calls.push(["innerWhere", ...args]);
|
|
25
|
+
return nestedQuery;
|
|
26
|
+
},
|
|
27
|
+
orWhere(...args) {
|
|
28
|
+
calls.push(["innerOrWhere", ...args]);
|
|
29
|
+
return nestedQuery;
|
|
30
|
+
},
|
|
31
|
+
whereNull(...args) {
|
|
32
|
+
calls.push(["innerWhereNull", ...args]);
|
|
33
|
+
return nestedQuery;
|
|
34
|
+
},
|
|
35
|
+
whereNotNull(...args) {
|
|
36
|
+
calls.push(["innerWhereNotNull", ...args]);
|
|
37
|
+
return nestedQuery;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const query = {
|
|
42
|
+
where(...args) {
|
|
43
|
+
if (args.length === 1 && typeof args[0] === "function") {
|
|
44
|
+
calls.push(["whereGroup"]);
|
|
45
|
+
args[0](nestedQuery);
|
|
46
|
+
return query;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
calls.push(["where", ...args]);
|
|
50
|
+
return query;
|
|
51
|
+
},
|
|
52
|
+
whereIn(...args) {
|
|
53
|
+
calls.push(["whereIn", ...args]);
|
|
54
|
+
return query;
|
|
55
|
+
},
|
|
56
|
+
whereNull(...args) {
|
|
57
|
+
calls.push(["whereNull", ...args]);
|
|
58
|
+
return query;
|
|
59
|
+
},
|
|
60
|
+
whereNotNull(...args) {
|
|
61
|
+
calls.push(["whereNotNull", ...args]);
|
|
62
|
+
return query;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
query,
|
|
68
|
+
calls
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test("createCrudListFilters normalizes filters into semantic values", () => {
|
|
73
|
+
const runtime = createCrudListFilters({
|
|
74
|
+
onlyStaff: {
|
|
75
|
+
type: "flag",
|
|
76
|
+
label: "Staff"
|
|
77
|
+
},
|
|
78
|
+
status: {
|
|
79
|
+
type: "enumMany",
|
|
80
|
+
label: "Status",
|
|
81
|
+
options: [
|
|
82
|
+
{ value: "active", label: "Active" },
|
|
83
|
+
{ value: "archived", label: "Archived" }
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
arrivalDate: {
|
|
87
|
+
type: "dateRange",
|
|
88
|
+
label: "Arrival Date"
|
|
89
|
+
},
|
|
90
|
+
supplierContactId: {
|
|
91
|
+
type: "recordIdMany",
|
|
92
|
+
label: "Supplier"
|
|
93
|
+
},
|
|
94
|
+
weight: {
|
|
95
|
+
type: "numberRange",
|
|
96
|
+
label: "Weight"
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const normalized = runtime.normalize({
|
|
101
|
+
onlyStaff: "",
|
|
102
|
+
status: ["active", "ignored", "archived"],
|
|
103
|
+
arrivalDateFrom: "2026-04-01",
|
|
104
|
+
arrivalDateTo: "2026-04-30",
|
|
105
|
+
supplierContactId: ["7", "bad", "4"],
|
|
106
|
+
weightMin: "12.5",
|
|
107
|
+
weightMax: 18
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
assert.deepEqual(normalized, {
|
|
111
|
+
onlyStaff: true,
|
|
112
|
+
status: ["active", "archived"],
|
|
113
|
+
arrivalDate: {
|
|
114
|
+
from: "2026-04-01",
|
|
115
|
+
to: "2026-04-30"
|
|
116
|
+
},
|
|
117
|
+
supplierContactId: ["7", "4"],
|
|
118
|
+
weight: {
|
|
119
|
+
min: 12.5,
|
|
120
|
+
max: 18
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("createCrudListFilters applies default column filters by type", () => {
|
|
126
|
+
const runtime = createCrudListFilters(
|
|
127
|
+
{
|
|
128
|
+
onlyStaff: {
|
|
129
|
+
type: "flag",
|
|
130
|
+
label: "Staff"
|
|
131
|
+
},
|
|
132
|
+
status: {
|
|
133
|
+
type: "enumMany",
|
|
134
|
+
label: "Status",
|
|
135
|
+
options: [
|
|
136
|
+
{ value: "active", label: "Active" },
|
|
137
|
+
{ value: "archived", label: "Archived" }
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
supplierContactId: {
|
|
141
|
+
type: "recordId",
|
|
142
|
+
label: "Supplier"
|
|
143
|
+
},
|
|
144
|
+
arrivalDate: {
|
|
145
|
+
type: "dateRange",
|
|
146
|
+
label: "Arrival Date"
|
|
147
|
+
},
|
|
148
|
+
weight: {
|
|
149
|
+
type: "numberRange",
|
|
150
|
+
label: "Weight"
|
|
151
|
+
},
|
|
152
|
+
locationAssignment: {
|
|
153
|
+
type: "presence",
|
|
154
|
+
label: "Storage"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
columns: {
|
|
159
|
+
onlyStaff: "is_staff",
|
|
160
|
+
status: "status",
|
|
161
|
+
supplierContactId: "supplier_contact_id",
|
|
162
|
+
arrivalDate: "arrival_datetime",
|
|
163
|
+
weight: "weight_received",
|
|
164
|
+
locationAssignment: "location_id"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
const { query, calls } = createQueryDouble();
|
|
169
|
+
|
|
170
|
+
runtime.applyQuery(query, {
|
|
171
|
+
onlyStaff: "",
|
|
172
|
+
status: ["active", "archived"],
|
|
173
|
+
supplierContactId: "7",
|
|
174
|
+
arrivalDateFrom: "2026-04-01",
|
|
175
|
+
arrivalDateTo: "2026-04-30",
|
|
176
|
+
weightMin: "12.5",
|
|
177
|
+
weightMax: "18",
|
|
178
|
+
locationAssignment: "missing"
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
assert.deepEqual(calls, [
|
|
182
|
+
["where", "is_staff", true],
|
|
183
|
+
["whereIn", "status", ["active", "archived"]],
|
|
184
|
+
["where", "supplier_contact_id", "7"],
|
|
185
|
+
["where", "arrival_datetime", ">=", "2026-04-01 00:00:00"],
|
|
186
|
+
["where", "arrival_datetime", "<", "2026-05-01 00:00:00"],
|
|
187
|
+
["where", "weight_received", ">=", 12.5],
|
|
188
|
+
["where", "weight_received", "<=", 18],
|
|
189
|
+
["whereNull", "location_id"]
|
|
190
|
+
]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("createCrudListFilters supports custom apply overrides for complex filters", () => {
|
|
194
|
+
const runtime = createCrudListFilters(
|
|
195
|
+
{
|
|
196
|
+
ccp1Status: {
|
|
197
|
+
type: "enumMany",
|
|
198
|
+
label: "CCP1",
|
|
199
|
+
options: [
|
|
200
|
+
{ value: "pending", label: "Pending" },
|
|
201
|
+
{ value: "passed", label: "Passed" },
|
|
202
|
+
{ value: "failed", label: "Failed" }
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
apply: {
|
|
208
|
+
ccp1Status(queryBuilder, values) {
|
|
209
|
+
queryBuilder.where((statusQuery) => {
|
|
210
|
+
if (values.includes("pending")) {
|
|
211
|
+
statusQuery.whereNull("ccp1_passed");
|
|
212
|
+
}
|
|
213
|
+
if (values.includes("failed")) {
|
|
214
|
+
statusQuery.orWhere("ccp1_passed", false);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
const { query, calls } = createQueryDouble();
|
|
222
|
+
|
|
223
|
+
runtime.applyQuery(query, {
|
|
224
|
+
ccp1Status: ["pending", "failed"]
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
assert.deepEqual(calls, [
|
|
228
|
+
["whereGroup"],
|
|
229
|
+
["innerWhereNull", "ccp1_passed"],
|
|
230
|
+
["innerOrWhere", "ccp1_passed", false]
|
|
231
|
+
]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("createCrudListFilters query validator stays mergeable with search and cursor validators", () => {
|
|
235
|
+
const runtime = createCrudListFilters({
|
|
236
|
+
status: {
|
|
237
|
+
type: "enum",
|
|
238
|
+
label: "Status",
|
|
239
|
+
options: [
|
|
240
|
+
{ value: "active", label: "Active" },
|
|
241
|
+
{ value: "archived", label: "Archived" }
|
|
242
|
+
]
|
|
243
|
+
},
|
|
244
|
+
arrivalDate: {
|
|
245
|
+
type: "dateRange",
|
|
246
|
+
label: "Arrival Date"
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const compiled = compileRouteValidator({
|
|
251
|
+
queryValidator: [
|
|
252
|
+
cursorPaginationQueryValidator,
|
|
253
|
+
listSearchQueryValidator,
|
|
254
|
+
runtime.createQueryValidator({
|
|
255
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT
|
|
256
|
+
})
|
|
257
|
+
]
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
assert.deepEqual(compiled.schema.querystring.required || [], []);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("createCrudListFilters requires explicit invalid-value mode for new query validators", () => {
|
|
264
|
+
const runtime = createCrudListFilters({});
|
|
265
|
+
|
|
266
|
+
assert.throws(
|
|
267
|
+
() => runtime.createQueryValidator(),
|
|
268
|
+
/invalidValues mode/
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("createCrudListFilters reject validator keeps strict filter schemas", () => {
|
|
273
|
+
const runtime = createCrudListFilters({
|
|
274
|
+
arrivalDate: {
|
|
275
|
+
type: "dateRange",
|
|
276
|
+
label: "Arrival Date"
|
|
277
|
+
},
|
|
278
|
+
status: {
|
|
279
|
+
type: "enumMany",
|
|
280
|
+
label: "Status",
|
|
281
|
+
options: [
|
|
282
|
+
{ value: "active", label: "Active" },
|
|
283
|
+
{ value: "archived", label: "Archived" }
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
supplierContactId: {
|
|
287
|
+
type: "recordIdMany",
|
|
288
|
+
label: "Supplier"
|
|
289
|
+
},
|
|
290
|
+
weight: {
|
|
291
|
+
type: "numberRange",
|
|
292
|
+
label: "Weight"
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const validator = runtime.createQueryValidator({
|
|
297
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
assert.equal(Check(validator.schema, {
|
|
301
|
+
arrivalDateFrom: "2026-04-01",
|
|
302
|
+
status: ["active"],
|
|
303
|
+
supplierContactId: ["7"],
|
|
304
|
+
weightMin: "12.5"
|
|
305
|
+
}), true);
|
|
306
|
+
assert.equal(Check(validator.schema, {
|
|
307
|
+
arrivalDateFrom: "bad-date"
|
|
308
|
+
}), false);
|
|
309
|
+
assert.equal(Check(validator.schema, {
|
|
310
|
+
status: ["active", "unexpected"]
|
|
311
|
+
}), false);
|
|
312
|
+
assert.equal(Check(validator.schema, {
|
|
313
|
+
supplierContactId: ["7", "bad"]
|
|
314
|
+
}), false);
|
|
315
|
+
assert.equal(Check(validator.schema, {
|
|
316
|
+
weightMin: "bad"
|
|
317
|
+
}), false);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("createCrudListFilters discard validator accepts malformed values and lets normalize drop them", () => {
|
|
321
|
+
const runtime = createCrudListFilters({
|
|
322
|
+
arrivalDate: {
|
|
323
|
+
type: "dateRange",
|
|
324
|
+
label: "Arrival Date"
|
|
325
|
+
},
|
|
326
|
+
status: {
|
|
327
|
+
type: "enumMany",
|
|
328
|
+
label: "Status",
|
|
329
|
+
options: [
|
|
330
|
+
{ value: "active", label: "Active" },
|
|
331
|
+
{ value: "archived", label: "Archived" }
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
supplierContactId: {
|
|
335
|
+
type: "recordIdMany",
|
|
336
|
+
label: "Supplier"
|
|
337
|
+
},
|
|
338
|
+
weight: {
|
|
339
|
+
type: "numberRange",
|
|
340
|
+
label: "Weight"
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const validator = runtime.createQueryValidator({
|
|
345
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_DISCARD
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
assert.equal(Check(validator.schema, {
|
|
349
|
+
arrivalDateFrom: "bad-date",
|
|
350
|
+
status: ["active", "unexpected"],
|
|
351
|
+
supplierContactId: ["7", "bad"],
|
|
352
|
+
weightMin: "bad"
|
|
353
|
+
}), true);
|
|
354
|
+
assert.deepEqual(validator.normalize({
|
|
355
|
+
arrivalDateFrom: "bad-date",
|
|
356
|
+
arrivalDateTo: "2026-04-30",
|
|
357
|
+
status: ["active", "unexpected"],
|
|
358
|
+
supplierContactId: ["7", "bad"],
|
|
359
|
+
weightMin: "bad"
|
|
360
|
+
}), {
|
|
361
|
+
arrivalDate: {
|
|
362
|
+
to: "2026-04-30"
|
|
363
|
+
},
|
|
364
|
+
status: ["active"],
|
|
365
|
+
supplierContactId: ["7"]
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("createCrudListFilters exposes no default query validator alias", () => {
|
|
370
|
+
const runtime = createCrudListFilters({
|
|
371
|
+
arrivalDate: {
|
|
372
|
+
type: "dateRange",
|
|
373
|
+
label: "Arrival Date"
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
assert.equal(Object.hasOwn(runtime, "queryValidator"), false);
|
|
378
|
+
assert.equal(runtime.queryValidator, undefined);
|
|
379
|
+
});
|