@jskit-ai/crud-core 0.1.63 → 0.1.65

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,18 +1,17 @@
1
- import { Type } from "typebox";
2
1
  import {
3
2
  normalizeObjectInput,
4
- mergeObjectSchemas,
5
3
  RECORD_ID_PATTERN
6
4
  } from "@jskit-ai/kernel/shared/validators";
5
+ import { createSchema } from "json-rest-schema";
7
6
  import {
8
- normalizeBoolean,
9
- normalizeCanonicalRecordIdText,
7
+ isRecord as isPlainObject,
10
8
  normalizeObject,
11
- normalizeText,
12
- normalizeUniqueTextList
9
+ normalizeText
13
10
  } from "@jskit-ai/kernel/shared/support/normalize";
14
11
  import {
15
12
  defineCrudListFilters,
13
+ CRUD_LIST_FILTER_INVALID_VALUES_REJECT,
14
+ CRUD_LIST_FILTER_INVALID_VALUES_DISCARD,
16
15
  CRUD_LIST_FILTER_TYPE_FLAG,
17
16
  CRUD_LIST_FILTER_TYPE_ENUM,
18
17
  CRUD_LIST_FILTER_TYPE_ENUM_MANY,
@@ -21,127 +20,109 @@ import {
21
20
  CRUD_LIST_FILTER_TYPE_DATE,
22
21
  CRUD_LIST_FILTER_TYPE_DATE_RANGE,
23
22
  CRUD_LIST_FILTER_TYPE_NUMBER_RANGE,
24
- CRUD_LIST_FILTER_TYPE_PRESENCE
23
+ CRUD_LIST_FILTER_TYPE_PRESENCE,
24
+ INVALID_CRUD_LIST_FILTER_QUERY_VALUE,
25
+ normalizeCrudListFilterInvalidValues,
26
+ parseCrudListFilterQueryValue
25
27
  } from "@jskit-ai/kernel/shared/support/crudListFilters";
26
28
 
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
- }
29
+ const DATE_FILTER_VALUE_PATTERN_SOURCE = "\\d{4}-\\d{2}-\\d{2}";
30
+ const DATE_FILTER_PATTERN_SOURCE = `^${DATE_FILTER_VALUE_PATTERN_SOURCE}$`;
31
+ const DATE_RANGE_FILTER_PATTERN_SOURCE =
32
+ `^(?:${DATE_FILTER_VALUE_PATTERN_SOURCE}(?:\\.\\.(?:${DATE_FILTER_VALUE_PATTERN_SOURCE})?)?|\\.\\.${DATE_FILTER_VALUE_PATTERN_SOURCE})$`;
33
+ const NUMBER_FILTER_VALUE_PATTERN_SOURCE = "[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?";
34
+ const NUMBER_RANGE_FILTER_PATTERN_SOURCE =
35
+ `^(?:${NUMBER_FILTER_VALUE_PATTERN_SOURCE}(?:\\.\\.(?:${NUMBER_FILTER_VALUE_PATTERN_SOURCE})?)?|\\.\\.${NUMBER_FILTER_VALUE_PATTERN_SOURCE})$`;
36
+ const CRUD_LIST_FILTER_QUERY_TYPE = "crudListFilterQuery";
37
+ const crudListFilterSchemaFactory = createSchema.createFactory();
38
+ const looseTextTransportSchema = Object.freeze({
39
+ type: "string",
40
+ minLength: 0
41
+ });
42
+ const strictNumberRangeTransportSchema = Object.freeze({
43
+ anyOf: [
44
+ {
45
+ type: "string",
46
+ pattern: NUMBER_RANGE_FILTER_PATTERN_SOURCE
47
+ },
48
+ {
49
+ type: "number"
50
+ }
51
+ ]
52
+ });
53
+ const looseStringOrNumberTransportSchema = Object.freeze({
54
+ anyOf: [
55
+ looseTextTransportSchema,
56
+ {
57
+ type: "number"
58
+ }
59
+ ]
60
+ });
61
+ const recordIdTransportSchema = Object.freeze({
62
+ anyOf: [
63
+ {
64
+ type: "string",
65
+ pattern: RECORD_ID_PATTERN
66
+ },
67
+ {
68
+ type: "number",
69
+ minimum: 1
70
+ }
71
+ ]
72
+ });
73
+ const flagTransportSchema = Object.freeze({
74
+ anyOf: [
75
+ {
76
+ type: "string",
77
+ minLength: 0
78
+ },
79
+ {
80
+ type: "boolean"
81
+ },
82
+ {
83
+ type: "number"
84
+ }
85
+ ]
86
+ });
76
87
 
77
- function firstValue(value) {
88
+ function cloneTransportSchema(value) {
78
89
  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 "";
90
+ return value.map((entry) => cloneTransportSchema(entry));
89
91
  }
90
92
 
91
- return normalized;
92
- }
93
-
94
- function normalizeNumberFilterValue(value) {
95
- if (value == null || value === "") {
96
- return null;
93
+ if (!value || typeof value !== "object") {
94
+ return value;
97
95
  }
98
96
 
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)
97
+ return Object.fromEntries(
98
+ Object.entries(value).map(([key, entry]) => [key, cloneTransportSchema(entry)])
124
99
  );
125
100
  }
126
101
 
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));
102
+ function buildSingleOrMultiTransportSchema(itemSchema) {
103
+ return {
104
+ anyOf: [
105
+ cloneTransportSchema(itemSchema),
106
+ {
107
+ type: "array",
108
+ items: cloneTransportSchema(itemSchema),
109
+ minItems: 1
110
+ }
111
+ ]
112
+ };
140
113
  }
141
114
 
142
115
  function addDaysToDateFilterValue(value = "", days = 0) {
143
- const normalizedValue = normalizeDateFilterValue(value);
144
- if (!normalizedValue || !Number.isInteger(days)) {
116
+ const normalizedValue = parseCrudListFilterQueryValue(
117
+ { type: CRUD_LIST_FILTER_TYPE_DATE },
118
+ value,
119
+ { invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT }
120
+ );
121
+ if (
122
+ normalizedValue === INVALID_CRUD_LIST_FILTER_QUERY_VALUE ||
123
+ !normalizedValue ||
124
+ !Number.isInteger(days)
125
+ ) {
145
126
  return "";
146
127
  }
147
128
 
@@ -157,216 +138,312 @@ function addDaysToDateFilterValue(value = "", days = 0) {
157
138
  return `${year}-${month}-${day}`;
158
139
  }
159
140
 
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
- }
141
+ const FILTER_TYPE_SERVER_HANDLERS = Object.freeze({
142
+ [CRUD_LIST_FILTER_TYPE_FLAG]: Object.freeze({
143
+ buildTransportSchema() {
144
+ return cloneTransportSchema(flagTransportSchema);
145
+ },
146
+ applyQuery(queryBuilder, value, column = "") {
147
+ if (column && value === true) {
148
+ queryBuilder.where(column, true);
149
+ }
150
+ return queryBuilder;
151
+ }
152
+ }),
153
+ [CRUD_LIST_FILTER_TYPE_ENUM]: Object.freeze({
154
+ buildTransportSchema({ discardInvalidValues, allowedValues }) {
155
+ return discardInvalidValues
156
+ ? cloneTransportSchema(looseTextTransportSchema)
157
+ : {
158
+ type: "string",
159
+ enum: allowedValues
160
+ };
161
+ },
162
+ applyQuery(queryBuilder, value, column = "") {
163
+ if (column && value !== undefined) {
164
+ queryBuilder.where(column, value);
165
+ }
166
+ return queryBuilder;
167
+ }
168
+ }),
169
+ [CRUD_LIST_FILTER_TYPE_ENUM_MANY]: Object.freeze({
170
+ buildTransportSchema({ discardInvalidValues, allowedValues }) {
171
+ return buildSingleOrMultiTransportSchema(
172
+ discardInvalidValues
173
+ ? looseTextTransportSchema
174
+ : {
175
+ type: "string",
176
+ enum: allowedValues
177
+ }
178
+ );
179
+ },
180
+ applyQuery(queryBuilder, value, column = "") {
181
+ if (column && Array.isArray(value) && value.length > 0) {
182
+ queryBuilder.whereIn(column, value);
183
+ }
184
+ return queryBuilder;
185
+ }
186
+ }),
187
+ [CRUD_LIST_FILTER_TYPE_RECORD_ID]: Object.freeze({
188
+ buildTransportSchema({ discardInvalidValues }) {
189
+ return discardInvalidValues
190
+ ? cloneTransportSchema(looseStringOrNumberTransportSchema)
191
+ : cloneTransportSchema(recordIdTransportSchema);
192
+ },
193
+ applyQuery(queryBuilder, value, column = "") {
194
+ if (column && value !== undefined) {
195
+ queryBuilder.where(column, value);
196
+ }
197
+ return queryBuilder;
198
+ }
199
+ }),
200
+ [CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY]: Object.freeze({
201
+ buildTransportSchema({ discardInvalidValues }) {
202
+ return buildSingleOrMultiTransportSchema(
203
+ discardInvalidValues
204
+ ? looseStringOrNumberTransportSchema
205
+ : recordIdTransportSchema
206
+ );
207
+ },
208
+ applyQuery(queryBuilder, value, column = "") {
209
+ if (column && Array.isArray(value) && value.length > 0) {
210
+ queryBuilder.whereIn(column, value);
211
+ }
212
+ return queryBuilder;
213
+ }
214
+ }),
215
+ [CRUD_LIST_FILTER_TYPE_DATE]: Object.freeze({
216
+ buildTransportSchema({ discardInvalidValues }) {
217
+ return discardInvalidValues
218
+ ? cloneTransportSchema(looseTextTransportSchema)
219
+ : {
220
+ type: "string",
221
+ pattern: DATE_FILTER_PATTERN_SOURCE
222
+ };
223
+ },
224
+ applyQuery(queryBuilder, value, column = "") {
225
+ if (!column || !value) {
226
+ return queryBuilder;
227
+ }
173
228
 
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
- }
229
+ const nextDate = addDaysToDateFilterValue(value, 1);
230
+ queryBuilder.where(column, ">=", `${value} 00:00:00`);
231
+ if (nextDate) {
232
+ queryBuilder.where(column, "<", `${nextDate} 00:00:00`);
233
+ }
234
+ return queryBuilder;
235
+ }
236
+ }),
237
+ [CRUD_LIST_FILTER_TYPE_DATE_RANGE]: Object.freeze({
238
+ buildTransportSchema({ discardInvalidValues }) {
239
+ return discardInvalidValues
240
+ ? cloneTransportSchema(looseTextTransportSchema)
241
+ : {
242
+ type: "string",
243
+ pattern: DATE_RANGE_FILTER_PATTERN_SOURCE
244
+ };
245
+ },
246
+ applyQuery(queryBuilder, value, column = "") {
247
+ if (!column) {
248
+ return queryBuilder;
249
+ }
186
250
 
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
- }
251
+ if (value?.from) {
252
+ queryBuilder.where(column, ">=", `${value.from} 00:00:00`);
253
+ }
254
+ if (value?.to) {
255
+ const nextDate = addDaysToDateFilterValue(value.to, 1);
256
+ if (nextDate) {
257
+ queryBuilder.where(column, "<", `${nextDate} 00:00:00`);
258
+ }
259
+ }
260
+ return queryBuilder;
261
+ }
262
+ }),
263
+ [CRUD_LIST_FILTER_TYPE_NUMBER_RANGE]: Object.freeze({
264
+ buildTransportSchema({ discardInvalidValues }) {
265
+ return discardInvalidValues
266
+ ? cloneTransportSchema(looseStringOrNumberTransportSchema)
267
+ : cloneTransportSchema(strictNumberRangeTransportSchema);
268
+ },
269
+ applyQuery(queryBuilder, value, column = "") {
270
+ if (!column) {
271
+ return queryBuilder;
272
+ }
199
273
 
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
- }
274
+ if (value?.min != null) {
275
+ queryBuilder.where(column, ">=", value.min);
276
+ }
277
+ if (value?.max != null) {
278
+ queryBuilder.where(column, "<=", value.max);
279
+ }
280
+ return queryBuilder;
281
+ }
282
+ }),
283
+ [CRUD_LIST_FILTER_TYPE_PRESENCE]: Object.freeze({
284
+ buildTransportSchema({ discardInvalidValues, allowedValues }) {
285
+ return discardInvalidValues
286
+ ? cloneTransportSchema(looseTextTransportSchema)
287
+ : {
288
+ type: "string",
289
+ enum: allowedValues
290
+ };
291
+ },
292
+ applyQuery(queryBuilder, value, column = "") {
293
+ if (!column) {
294
+ return queryBuilder;
295
+ }
212
296
 
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
- }
297
+ if (value === "present") {
298
+ queryBuilder.whereNotNull(column);
299
+ }
300
+ if (value === "missing") {
301
+ queryBuilder.whereNull(column);
302
+ }
303
+ return queryBuilder;
304
+ }
305
+ })
306
+ });
225
307
 
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
- );
308
+ function normalizeCrudListFilterQueryFieldInput(filterDefinition = null) {
309
+ if (!filterDefinition || typeof filterDefinition !== "object" || Array.isArray(filterDefinition)) {
310
+ throw new TypeError("createCrudListFilterQueryField requires a normalized filter definition object.");
237
311
  }
238
312
 
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 }
313
+ const key = normalizeText(filterDefinition.key);
314
+ const type = normalizeText(filterDefinition.type);
315
+ const queryKey = normalizeText(filterDefinition.queryKey);
316
+ if (!key || !type || !queryKey) {
317
+ throw new TypeError(
318
+ "createCrudListFilterQueryField requires a normalized filter definition from defineCrudListFilters(...)."
254
319
  );
255
320
  }
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
- );
321
+ if (!FILTER_TYPE_SERVER_HANDLERS[type]) {
322
+ throw new TypeError(`Unsupported CRUD list filter type "${type}".`);
273
323
  }
274
324
 
275
- return Type.Object({}, { additionalProperties: false });
325
+ return filterDefinition;
276
326
  }
277
327
 
278
- function normalizeFilterValue(filter = {}, source = {}) {
279
- const allowedValues = resolveAllowedOptionValues(filter);
328
+ function createCrudListFilterQueryField(filterDefinition = {}, {
329
+ invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT
330
+ } = {}) {
331
+ const invalidValueMode = normalizeCrudListFilterInvalidValues(invalidValues);
332
+ const filter = normalizeCrudListFilterQueryFieldInput(filterDefinition);
333
+ const filterContract = Object.freeze({
334
+ filter,
335
+ invalidValues: invalidValueMode
336
+ });
280
337
 
281
- if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
282
- if (!Object.hasOwn(source, filter.queryKey)) {
283
- return undefined;
284
- }
338
+ return Object.freeze({
339
+ type: CRUD_LIST_FILTER_QUERY_TYPE,
340
+ filterContract
341
+ });
342
+ }
285
343
 
286
- const sourceValue = source[filter.queryKey];
287
- if (sourceValue === null || sourceValue === "") {
288
- return true;
289
- }
344
+ function buildFilterQueryFieldDefinition(filterDefinition = {}, { invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT } = {}) {
345
+ const filter = normalizeCrudListFilterQueryFieldInput(filterDefinition);
346
+ return {
347
+ [filter.queryKey]: createCrudListFilterQueryField(filter, {
348
+ invalidValues
349
+ })
350
+ };
351
+ }
290
352
 
291
- try {
292
- return normalizeBoolean(firstValue(sourceValue));
293
- } catch {
294
- return undefined;
295
- }
296
- }
353
+ function buildFilterQueryTransportSchema(filter = {}, { invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT } = {}) {
354
+ const invalidValueMode = normalizeCrudListFilterInvalidValues(invalidValues);
355
+ const discardInvalidValues = invalidValueMode === CRUD_LIST_FILTER_INVALID_VALUES_DISCARD;
356
+ const allowedValues = (Array.isArray(filter.options) ? filter.options : []).map((entry) => entry.value);
357
+ const handler = FILTER_TYPE_SERVER_HANDLERS[filter.type];
358
+ return handler
359
+ ? handler.buildTransportSchema({ discardInvalidValues, allowedValues })
360
+ : {};
361
+ }
362
+
363
+ crudListFilterSchemaFactory.addType(CRUD_LIST_FILTER_QUERY_TYPE, Object.assign(
364
+ (context) => {
365
+ const contract = isPlainObject(context.definition.filterContract)
366
+ ? context.definition.filterContract
367
+ : null;
368
+ const filter = contract?.filter;
369
+ const invalidValues = contract?.invalidValues;
297
370
 
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;
371
+ if (!filter || !invalidValues) {
372
+ context.throwTypeError();
301
373
  }
302
374
 
303
- const normalizedValue = normalizeAllowedTextValue(source[filter.queryKey], allowedValues);
304
- return normalizedValue || undefined;
305
- }
375
+ const parsedValue = parseCrudListFilterQueryValue(filter, context.value, {
376
+ invalidValues
377
+ });
306
378
 
307
- if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY) {
308
- if (!Object.hasOwn(source, filter.queryKey)) {
309
- return undefined;
379
+ if (parsedValue === INVALID_CRUD_LIST_FILTER_QUERY_VALUE) {
380
+ context.throwTypeError();
310
381
  }
311
382
 
312
- const normalizedValues = normalizeAllowedTextValues(source[filter.queryKey], allowedValues);
313
- return normalizedValues.length > 0 ? normalizedValues : undefined;
314
- }
383
+ return parsedValue;
384
+ },
385
+ {
386
+ toJsonSchema({ definition }) {
387
+ const contract = isPlainObject(definition?.filterContract)
388
+ ? definition.filterContract
389
+ : null;
390
+ const filter = contract?.filter;
391
+ const invalidValues = contract?.invalidValues;
315
392
 
316
- if (filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID) {
317
- if (!Object.hasOwn(source, filter.queryKey)) {
318
- return undefined;
319
- }
393
+ if (!filter || !invalidValues) {
394
+ throw new Error(`Type "${CRUD_LIST_FILTER_QUERY_TYPE}" requires definition.filterContract for transport export.`);
395
+ }
320
396
 
321
- const normalizedValue = normalizeRecordIdFilterValue(source[filter.queryKey]);
322
- return normalizedValue || undefined;
397
+ return buildFilterQueryTransportSchema(filter, {
398
+ invalidValues
399
+ });
400
+ }
323
401
  }
402
+ ));
324
403
 
325
- if (filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
326
- if (!Object.hasOwn(source, filter.queryKey)) {
327
- return undefined;
328
- }
404
+ function createCrudListFilterQuerySchema(structure = {}) {
405
+ return crudListFilterSchemaFactory(structure);
406
+ }
407
+
408
+ function buildFilterQuerySchemaDefinition(filterEntries = [], {
409
+ invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT
410
+ } = {}) {
411
+ const structure = {};
329
412
 
330
- const normalizedValues = normalizeRecordIdFilterValues(source[filter.queryKey]);
331
- return normalizedValues.length > 0 ? normalizedValues : undefined;
413
+ for (const filter of filterEntries) {
414
+ Object.assign(structure, buildFilterQueryFieldDefinition(filter, {
415
+ invalidValues
416
+ }));
332
417
  }
333
418
 
334
- if (filter.type === CRUD_LIST_FILTER_TYPE_DATE) {
335
- if (!Object.hasOwn(source, filter.queryKey)) {
336
- return undefined;
337
- }
419
+ return Object.freeze({
420
+ schema: createCrudListFilterQuerySchema(structure),
421
+ mode: "patch"
422
+ });
423
+ }
338
424
 
339
- const normalizedValue = normalizeDateFilterValue(source[filter.queryKey]);
340
- return normalizedValue || undefined;
341
- }
425
+ function projectNormalizedFilterValues(filterEntries = [], source = {}, errors = {}) {
426
+ const normalizedSource = normalizeObjectInput(source);
427
+ const errorFieldKeys = new Set(Object.keys(normalizeObject(errors)));
428
+ const normalized = {};
342
429
 
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;
430
+ for (const filter of filterEntries) {
431
+ if (errorFieldKeys.has(filter.queryKey)) {
432
+ continue;
433
+ }
434
+ if (!Object.hasOwn(normalizedSource, filter.queryKey)) {
435
+ continue;
348
436
  }
349
437
 
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;
438
+ const value = normalizedSource[filter.queryKey];
439
+ if (value === null || value === undefined) {
440
+ continue;
361
441
  }
362
442
 
363
- return Object.freeze({
364
- ...(min != null ? { min } : {}),
365
- ...(max != null ? { max } : {})
366
- });
443
+ normalized[filter.key] = value;
367
444
  }
368
445
 
369
- return undefined;
446
+ return Object.freeze(normalized);
370
447
  }
371
448
 
372
449
  function normalizeColumnsMap(columns = {}) {
@@ -386,124 +463,41 @@ function normalizeColumnsMap(columns = {}) {
386
463
  }
387
464
 
388
465
  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;
466
+ const handler = FILTER_TYPE_SERVER_HANDLERS[filter.type];
467
+ return handler
468
+ ? handler.applyQuery(queryBuilder, value, column)
469
+ : queryBuilder;
459
470
  }
460
471
 
461
472
  function createCrudListFilters(definitions = {}, { columns = {}, apply = {} } = {}) {
462
473
  const normalizedFilters = defineCrudListFilters(definitions);
463
474
  const normalizedColumns = normalizeColumnsMap(columns);
464
475
  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({
476
+ const queryValidatorsByInvalidValueMode = Object.freeze({
483
477
  [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
478
+ ...buildFilterQuerySchemaDefinition(filterEntries, {
479
+ invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT
480
+ })
490
481
  }),
491
482
  [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
483
+ ...buildFilterQuerySchemaDefinition(filterEntries, {
484
+ invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_DISCARD
485
+ })
498
486
  })
499
487
  });
500
488
 
489
+ function parseFilterPayload(payload = {}) {
490
+ const discardValidator = queryValidatorsByInvalidValueMode[CRUD_LIST_FILTER_INVALID_VALUES_DISCARD];
491
+ const result = discardValidator.schema.patch(normalizeObjectInput(payload));
492
+ return projectNormalizedFilterValues(filterEntries, result.validatedObject, result.errors);
493
+ }
494
+
501
495
  function applyQuery(queryBuilder, payload = {}) {
502
496
  if (!queryBuilder || typeof queryBuilder.where !== "function") {
503
497
  throw new TypeError("createCrudListFilters.applyQuery requires query builder.");
504
498
  }
505
499
 
506
- const normalized = normalize(payload);
500
+ const normalized = parseFilterPayload(payload);
507
501
  for (const filter of filterEntries) {
508
502
  if (!Object.hasOwn(normalized, filter.key)) {
509
503
  continue;
@@ -529,13 +523,12 @@ function createCrudListFilters(definitions = {}, { columns = {}, apply = {} } =
529
523
 
530
524
  function createQueryValidator({ invalidValues } = {}) {
531
525
  const invalidValueMode = normalizeCrudListFilterInvalidValues(invalidValues);
532
- return queryValidators[invalidValueMode];
526
+ return queryValidatorsByInvalidValueMode[invalidValueMode];
533
527
  }
534
528
 
535
529
  return Object.freeze({
536
530
  filters: normalizedFilters,
537
531
  createQueryValidator,
538
- normalize,
539
532
  applyQuery
540
533
  });
541
534
  }
@@ -543,5 +536,7 @@ function createCrudListFilters(definitions = {}, { columns = {}, apply = {} } =
543
536
  export {
544
537
  CRUD_LIST_FILTER_INVALID_VALUES_REJECT,
545
538
  CRUD_LIST_FILTER_INVALID_VALUES_DISCARD,
539
+ createCrudListFilterQueryField,
540
+ createCrudListFilterQuerySchema,
546
541
  createCrudListFilters
547
542
  };