@jskit-ai/kernel 0.1.154 → 0.1.156
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -4
- package/shared/support/crudFieldContract.js +0 -322
- package/shared/support/crudFieldContract.test.js +0 -67
- package/shared/support/crudListFilters.js +0 -855
- package/shared/support/crudListFilters.test.js +0 -323
- package/shared/support/crudLookup.js +0 -190
- package/shared/support/crudLookup.test.js +0 -256
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/kernel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.156",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"json-rest-schema": "^1.0.17"
|
|
@@ -44,9 +44,6 @@
|
|
|
44
44
|
"./shared/support/tokens": "./shared/support/tokens.js",
|
|
45
45
|
"./shared/support/normalize": "./shared/support/normalize.js",
|
|
46
46
|
"./shared/support/permissions": "./shared/support/permissions.js",
|
|
47
|
-
"./shared/support/crudListFilters": "./shared/support/crudListFilters.js",
|
|
48
|
-
"./shared/support/crudFieldContract": "./shared/support/crudFieldContract.js",
|
|
49
|
-
"./shared/support/crudLookup": "./shared/support/crudLookup.js",
|
|
50
47
|
"./shared/support/jsonApiFieldsets": "./shared/support/jsonApiFieldsets.js",
|
|
51
48
|
"./shared/support/generatedUiContract": "./shared/support/generatedUiContract.js",
|
|
52
49
|
"./shared/support/deepFreeze": "./shared/support/deepFreeze.js",
|
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
import { normalizeSchemaDefinition } from "../validators/schemaDefinitions.js";
|
|
2
|
-
import { normalizeObject, normalizeText } from "./normalize.js";
|
|
3
|
-
|
|
4
|
-
const CRUD_FIELD_STORAGE_COLUMN = "column";
|
|
5
|
-
const CRUD_FIELD_STORAGE_VIRTUAL = "virtual";
|
|
6
|
-
const CRUD_FIELD_WRITE_SERIALIZER_DATETIME_UTC = "datetime-utc";
|
|
7
|
-
const CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE = "autocomplete";
|
|
8
|
-
const CRUD_LOOKUP_FORM_CONTROL_SELECT = "select";
|
|
9
|
-
|
|
10
|
-
function checkCrudLookupFormControl(
|
|
11
|
-
value,
|
|
12
|
-
{
|
|
13
|
-
context = "crud field ui.formControl",
|
|
14
|
-
defaultValue = CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE
|
|
15
|
-
} = {}
|
|
16
|
-
) {
|
|
17
|
-
const resolvedValue = value === undefined || value === null || value === "" ? defaultValue : value;
|
|
18
|
-
if (resolvedValue === "") {
|
|
19
|
-
return "";
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
resolvedValue === CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE ||
|
|
24
|
-
resolvedValue === CRUD_LOOKUP_FORM_CONTROL_SELECT
|
|
25
|
-
) {
|
|
26
|
-
return resolvedValue;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
throw new Error(
|
|
30
|
-
`${context} must be "${CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE}" or "${CRUD_LOOKUP_FORM_CONTROL_SELECT}". ` +
|
|
31
|
-
`Received: ${JSON.stringify(resolvedValue)}.`
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function cloneStructuredFieldMetadata(value = {}) {
|
|
36
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const normalized = {};
|
|
41
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
42
|
-
if (entry === undefined) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (Array.isArray(entry)) {
|
|
47
|
-
normalized[key] = entry.map((item) =>
|
|
48
|
-
item && typeof item === "object" && !Array.isArray(item)
|
|
49
|
-
? cloneStructuredFieldMetadata(item) || {}
|
|
50
|
-
: item
|
|
51
|
-
);
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (entry && typeof entry === "object") {
|
|
56
|
-
normalized[key] = cloneStructuredFieldMetadata(entry) || {};
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
normalized[key] = entry;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return Object.keys(normalized).length > 0 ? Object.freeze(normalized) : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function resolveCrudFieldSchemaProperties(value, { context = "crud resource field definitions" } = {}) {
|
|
67
|
-
if (value == null) {
|
|
68
|
-
return {};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const normalized = normalizeSchemaDefinition(value, {
|
|
72
|
-
context,
|
|
73
|
-
defaultMode: "patch"
|
|
74
|
-
});
|
|
75
|
-
if (!normalized) {
|
|
76
|
-
return {};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return normalizeObject(normalized.schema.getFieldDefinitions());
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function normalizeCrudFieldStorageConfig(
|
|
83
|
-
fieldDefinition = {},
|
|
84
|
-
{
|
|
85
|
-
context = "crud field storage",
|
|
86
|
-
fieldKey = ""
|
|
87
|
-
} = {}
|
|
88
|
-
) {
|
|
89
|
-
const normalizedFieldKey = normalizeText(fieldKey);
|
|
90
|
-
const actualField = normalizeText(fieldDefinition.actualField);
|
|
91
|
-
const storage = fieldDefinition?.storage;
|
|
92
|
-
|
|
93
|
-
if (storage === undefined || storage === null) {
|
|
94
|
-
return Object.freeze({
|
|
95
|
-
mode: CRUD_FIELD_STORAGE_COLUMN,
|
|
96
|
-
column: actualField,
|
|
97
|
-
writeSerializer: ""
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!storage || typeof storage !== "object" || Array.isArray(storage)) {
|
|
102
|
-
throw new TypeError(
|
|
103
|
-
`${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} must be an object when provided.`
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const storageKey of Object.keys(storage)) {
|
|
108
|
-
if (storageKey !== "column" && storageKey !== "virtual" && storageKey !== "writeSerializer") {
|
|
109
|
-
throw new Error(
|
|
110
|
-
`${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} does not support storage.${storageKey}.`
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const column = normalizeText(storage.column) || actualField;
|
|
116
|
-
const virtual = storage.virtual === true;
|
|
117
|
-
const writeSerializer = normalizeText(storage.writeSerializer).toLowerCase();
|
|
118
|
-
|
|
119
|
-
if (actualField && normalizeText(storage.column) && normalizeText(storage.column) !== actualField) {
|
|
120
|
-
throw new Error(
|
|
121
|
-
`${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} actualField and storage.column must match when both are provided.`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (writeSerializer && writeSerializer !== CRUD_FIELD_WRITE_SERIALIZER_DATETIME_UTC) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} storage.writeSerializer must be ` +
|
|
128
|
-
`"${CRUD_FIELD_WRITE_SERIALIZER_DATETIME_UTC}" when provided.`
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (virtual && column) {
|
|
133
|
-
throw new Error(
|
|
134
|
-
`${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} virtual fields cannot define actualField or storage.column.`
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (virtual && writeSerializer) {
|
|
139
|
-
throw new Error(
|
|
140
|
-
`${context}${normalizedFieldKey ? `["${normalizedFieldKey}"]` : ""} virtual fields cannot define storage.writeSerializer.`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return Object.freeze({
|
|
145
|
-
mode: virtual ? CRUD_FIELD_STORAGE_VIRTUAL : CRUD_FIELD_STORAGE_COLUMN,
|
|
146
|
-
column,
|
|
147
|
-
writeSerializer
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function mergeFieldContractEntry(target, source, { context = "crud field contract", fieldKey = "" } = {}) {
|
|
152
|
-
if (!source || typeof source !== "object") {
|
|
153
|
-
return target;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const next = target ? { ...target } : {};
|
|
157
|
-
const normalizedFieldKey = normalizeText(fieldKey);
|
|
158
|
-
|
|
159
|
-
const mergeScalar = (key) => {
|
|
160
|
-
const value = normalizeText(source[key]);
|
|
161
|
-
if (!value) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (next[key] && next[key] !== value) {
|
|
165
|
-
throw new Error(`${context}["${normalizedFieldKey}"] has conflicting ${key} metadata.`);
|
|
166
|
-
}
|
|
167
|
-
next[key] = value;
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
mergeScalar("actualField");
|
|
171
|
-
mergeScalar("parentRouteParamKey");
|
|
172
|
-
|
|
173
|
-
const storage = source.storage && typeof source.storage === "object" ? source.storage : null;
|
|
174
|
-
if (storage) {
|
|
175
|
-
const currentStorage = next.storage || {};
|
|
176
|
-
if (storage.mode && currentStorage.mode && currentStorage.mode !== storage.mode) {
|
|
177
|
-
throw new Error(`${context}["${normalizedFieldKey}"] has conflicting storage.mode metadata.`);
|
|
178
|
-
}
|
|
179
|
-
if (storage.column && currentStorage.column && currentStorage.column !== storage.column) {
|
|
180
|
-
throw new Error(`${context}["${normalizedFieldKey}"] has conflicting storage.column metadata.`);
|
|
181
|
-
}
|
|
182
|
-
if (storage.writeSerializer &&
|
|
183
|
-
currentStorage.writeSerializer &&
|
|
184
|
-
currentStorage.writeSerializer !== storage.writeSerializer
|
|
185
|
-
) {
|
|
186
|
-
throw new Error(`${context}["${normalizedFieldKey}"] has conflicting storage.writeSerializer metadata.`);
|
|
187
|
-
}
|
|
188
|
-
next.storage = {
|
|
189
|
-
...currentStorage,
|
|
190
|
-
...storage
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (source.relation && typeof source.relation === "object") {
|
|
195
|
-
next.relation = {
|
|
196
|
-
...(next.relation && typeof next.relation === "object" ? next.relation : {}),
|
|
197
|
-
...source.relation
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (source.ui && typeof source.ui === "object") {
|
|
202
|
-
next.ui = {
|
|
203
|
-
...(next.ui && typeof next.ui === "object" ? next.ui : {}),
|
|
204
|
-
...source.ui
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return next;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function buildCrudFieldContractMap(resource = {}, { context = "crud resource field contract" } = {}) {
|
|
212
|
-
const sections = [
|
|
213
|
-
resolveCrudFieldSchemaProperties(resource?.operations?.view?.output, {
|
|
214
|
-
context: `${context}.operations.view.output`
|
|
215
|
-
}),
|
|
216
|
-
resolveCrudFieldSchemaProperties(resource?.operations?.create?.body, {
|
|
217
|
-
context: `${context}.operations.create.body`
|
|
218
|
-
}),
|
|
219
|
-
resolveCrudFieldSchemaProperties(resource?.operations?.patch?.body, {
|
|
220
|
-
context: `${context}.operations.patch.body`
|
|
221
|
-
})
|
|
222
|
-
];
|
|
223
|
-
|
|
224
|
-
const entries = {};
|
|
225
|
-
for (const definitions of sections) {
|
|
226
|
-
for (const [rawKey, rawDefinition] of Object.entries(definitions)) {
|
|
227
|
-
const key = normalizeText(rawKey);
|
|
228
|
-
if (!key) {
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
const definition = normalizeObject(rawDefinition);
|
|
232
|
-
const storage = normalizeCrudFieldStorageConfig(definition, {
|
|
233
|
-
context: `${context}.storage`,
|
|
234
|
-
fieldKey: key
|
|
235
|
-
});
|
|
236
|
-
const relation = cloneStructuredFieldMetadata(definition.relation);
|
|
237
|
-
const ui = cloneStructuredFieldMetadata(definition.ui);
|
|
238
|
-
const parentRouteParamKey =
|
|
239
|
-
normalizeText(definition.parentRouteParamKey) ||
|
|
240
|
-
normalizeText(definition?.relation?.parentRouteParamKey);
|
|
241
|
-
|
|
242
|
-
entries[key] = mergeFieldContractEntry(entries[key], {
|
|
243
|
-
actualField: normalizeText(definition.actualField),
|
|
244
|
-
parentRouteParamKey,
|
|
245
|
-
storage,
|
|
246
|
-
relation,
|
|
247
|
-
ui
|
|
248
|
-
}, {
|
|
249
|
-
context,
|
|
250
|
-
fieldKey: key
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return Object.freeze(
|
|
256
|
-
Object.fromEntries(
|
|
257
|
-
Object.entries(entries).map(([key, value]) => [
|
|
258
|
-
key,
|
|
259
|
-
Object.freeze({
|
|
260
|
-
key,
|
|
261
|
-
actualField: normalizeText(value.actualField),
|
|
262
|
-
parentRouteParamKey: normalizeText(value.parentRouteParamKey),
|
|
263
|
-
storage: Object.freeze({
|
|
264
|
-
mode: normalizeText(value?.storage?.mode) || CRUD_FIELD_STORAGE_COLUMN,
|
|
265
|
-
column: normalizeText(value?.storage?.column),
|
|
266
|
-
writeSerializer: normalizeText(value?.storage?.writeSerializer).toLowerCase()
|
|
267
|
-
}),
|
|
268
|
-
relation: cloneStructuredFieldMetadata(value.relation),
|
|
269
|
-
ui: cloneStructuredFieldMetadata(value.ui)
|
|
270
|
-
})
|
|
271
|
-
])
|
|
272
|
-
)
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function buildCrudOperationSchemaFields(fields = {}, operationName = "") {
|
|
277
|
-
const definitions = {};
|
|
278
|
-
|
|
279
|
-
for (const [fieldKey, fieldDefinition] of Object.entries(fields)) {
|
|
280
|
-
const operationConfig = fieldDefinition?.operations?.[operationName];
|
|
281
|
-
if (!operationConfig) {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const nextDefinition = {
|
|
286
|
-
...fieldDefinition
|
|
287
|
-
};
|
|
288
|
-
delete nextDefinition.operations;
|
|
289
|
-
|
|
290
|
-
if (operationConfig !== true) {
|
|
291
|
-
Object.assign(nextDefinition, operationConfig);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
definitions[fieldKey] = nextDefinition;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return definitions;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function resolveCrudFieldContractEntry(resource = {}, fieldKey = "", options = {}) {
|
|
301
|
-
const normalizedFieldKey = normalizeText(fieldKey);
|
|
302
|
-
if (!normalizedFieldKey) {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const entries = buildCrudFieldContractMap(resource, options);
|
|
307
|
-
return entries[normalizedFieldKey] || null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export {
|
|
311
|
-
CRUD_FIELD_STORAGE_COLUMN,
|
|
312
|
-
CRUD_FIELD_STORAGE_VIRTUAL,
|
|
313
|
-
CRUD_FIELD_WRITE_SERIALIZER_DATETIME_UTC,
|
|
314
|
-
CRUD_LOOKUP_FORM_CONTROL_AUTOCOMPLETE,
|
|
315
|
-
CRUD_LOOKUP_FORM_CONTROL_SELECT,
|
|
316
|
-
checkCrudLookupFormControl,
|
|
317
|
-
resolveCrudFieldSchemaProperties,
|
|
318
|
-
normalizeCrudFieldStorageConfig,
|
|
319
|
-
buildCrudOperationSchemaFields,
|
|
320
|
-
buildCrudFieldContractMap,
|
|
321
|
-
resolveCrudFieldContractEntry
|
|
322
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
|
|
4
|
-
import { buildCrudOperationSchemaFields } from "./crudFieldContract.js";
|
|
5
|
-
|
|
6
|
-
test("buildCrudOperationSchemaFields projects operation-aware field definitions without losing metadata", () => {
|
|
7
|
-
const serializer = () => "serialized";
|
|
8
|
-
const fields = Object.freeze({
|
|
9
|
-
name: Object.freeze({
|
|
10
|
-
type: "string",
|
|
11
|
-
maxLength: 190,
|
|
12
|
-
search: true,
|
|
13
|
-
ui: Object.freeze({
|
|
14
|
-
label: "Name"
|
|
15
|
-
}),
|
|
16
|
-
operations: Object.freeze({
|
|
17
|
-
output: Object.freeze({
|
|
18
|
-
required: true
|
|
19
|
-
}),
|
|
20
|
-
create: Object.freeze({
|
|
21
|
-
required: true
|
|
22
|
-
}),
|
|
23
|
-
patch: Object.freeze({
|
|
24
|
-
required: false
|
|
25
|
-
})
|
|
26
|
-
})
|
|
27
|
-
}),
|
|
28
|
-
createdAt: Object.freeze({
|
|
29
|
-
type: "dateTime",
|
|
30
|
-
storage: Object.freeze({
|
|
31
|
-
column: "created_at",
|
|
32
|
-
writeSerializer: "datetime-utc",
|
|
33
|
-
serialize: serializer
|
|
34
|
-
}),
|
|
35
|
-
operations: Object.freeze({
|
|
36
|
-
output: Object.freeze({
|
|
37
|
-
required: true
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
}),
|
|
41
|
-
workspaceId: Object.freeze({
|
|
42
|
-
type: "id",
|
|
43
|
-
required: true,
|
|
44
|
-
operations: Object.freeze({})
|
|
45
|
-
})
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const outputFields = buildCrudOperationSchemaFields(fields, "output");
|
|
49
|
-
const createFields = buildCrudOperationSchemaFields(fields, "create");
|
|
50
|
-
|
|
51
|
-
assert.deepEqual(Object.keys(outputFields).sort(), ["createdAt", "name"]);
|
|
52
|
-
assert.deepEqual(Object.keys(createFields).sort(), ["name"]);
|
|
53
|
-
|
|
54
|
-
assert.equal(outputFields.name.required, true);
|
|
55
|
-
assert.equal(outputFields.name.maxLength, 190);
|
|
56
|
-
assert.equal(outputFields.name.search, true);
|
|
57
|
-
assert.equal(outputFields.name.ui.label, "Name");
|
|
58
|
-
assert.equal(Object.hasOwn(outputFields.name, "operations"), false);
|
|
59
|
-
|
|
60
|
-
assert.equal(outputFields.createdAt.storage.column, "created_at");
|
|
61
|
-
assert.equal(outputFields.createdAt.storage.writeSerializer, "datetime-utc");
|
|
62
|
-
assert.equal(outputFields.createdAt.storage.serialize, serializer);
|
|
63
|
-
|
|
64
|
-
assert.equal(createFields.name.required, true);
|
|
65
|
-
assert.equal(fields.name.operations.output.required, true);
|
|
66
|
-
assert.equal(Object.hasOwn(fields.name, "operations"), true);
|
|
67
|
-
});
|