@jskit-ai/crud-core 0.1.63 → 0.1.64
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 +4 -2
- package/package.json +18 -10
- package/src/server/crudModuleConfig.js +25 -5
- package/src/server/fieldAccess.js +9 -39
- package/src/server/listFilters.js +384 -389
- package/src/server/listQueryValidators.js +39 -77
- package/src/server/lookupHydration.js +4 -1
- package/src/server/repositorySupport.js +71 -121
- package/src/server/resourceRuntime/index.js +49 -74
- package/src/server/resourceRuntime/lookupHydration.js +4 -1
- package/src/server/routeContracts.js +74 -0
- package/src/server/serviceEvents.js +75 -4
- package/src/shared/crudFieldSupport.js +54 -0
- package/src/shared/crudNamespaceSupport.js +1 -27
- package/src/shared/crudResource.js +1 -0
- package/test/createCrudServiceFromResource.test.js +30 -28
- package/test/{crudFieldMetaSupport.test.js → crudFieldSupport.test.js} +1 -1
- package/test/crudModuleConfig.test.js +33 -0
- package/test/crudResource.test.js +97 -0
- package/test/listFilters.test.js +221 -59
- package/test/listQueryValidators.test.js +131 -97
- package/test/repositorySupport.test.js +241 -241
- package/test/resourceRuntime.test.js +204 -248
- package/test/routeContracts.test.js +146 -0
- package/test/serviceEvents.test.js +41 -1
- package/test/serviceMethods.test.js +12 -10
- package/src/shared/crudFieldMetaSupport.js +0 -153
|
@@ -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
|
-
|
|
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
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
88
|
+
function cloneTransportSchema(value) {
|
|
78
89
|
if (Array.isArray(value)) {
|
|
79
|
-
return value
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 =
|
|
144
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
{
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
|
325
|
+
return filterDefinition;
|
|
276
326
|
}
|
|
277
327
|
|
|
278
|
-
function
|
|
279
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
338
|
+
return Object.freeze({
|
|
339
|
+
type: CRUD_LIST_FILTER_QUERY_TYPE,
|
|
340
|
+
filterContract
|
|
341
|
+
});
|
|
342
|
+
}
|
|
285
343
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
return undefined;
|
|
371
|
+
if (!filter || !invalidValues) {
|
|
372
|
+
context.throwTypeError();
|
|
301
373
|
}
|
|
302
374
|
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
375
|
+
const parsedValue = parseCrudListFilterQueryValue(filter, context.value, {
|
|
376
|
+
invalidValues
|
|
377
|
+
});
|
|
306
378
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return undefined;
|
|
379
|
+
if (parsedValue === INVALID_CRUD_LIST_FILTER_QUERY_VALUE) {
|
|
380
|
+
context.throwTypeError();
|
|
310
381
|
}
|
|
311
382
|
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
322
|
-
|
|
397
|
+
return buildFilterQueryTransportSchema(filter, {
|
|
398
|
+
invalidValues
|
|
399
|
+
});
|
|
400
|
+
}
|
|
323
401
|
}
|
|
402
|
+
));
|
|
324
403
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
331
|
-
|
|
413
|
+
for (const filter of filterEntries) {
|
|
414
|
+
Object.assign(structure, buildFilterQueryFieldDefinition(filter, {
|
|
415
|
+
invalidValues
|
|
416
|
+
}));
|
|
332
417
|
}
|
|
333
418
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
419
|
+
return Object.freeze({
|
|
420
|
+
schema: createCrudListFilterQuerySchema(structure),
|
|
421
|
+
mode: "patch"
|
|
422
|
+
});
|
|
423
|
+
}
|
|
338
424
|
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
364
|
-
...(min != null ? { min } : {}),
|
|
365
|
-
...(max != null ? { max } : {})
|
|
366
|
-
});
|
|
443
|
+
normalized[filter.key] = value;
|
|
367
444
|
}
|
|
368
445
|
|
|
369
|
-
return
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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 =
|
|
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
|
|
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
|
};
|