@jskit-ai/kernel 0.1.55 → 0.1.56
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 +3 -2
- package/server/actions/ActionRuntimeServiceProvider.test.js +23 -15
- package/server/http/lib/kernel.test.js +447 -0
- package/server/http/lib/routeRegistration.js +236 -15
- package/server/http/lib/routeTransport.js +126 -0
- package/server/http/lib/routeValidator.js +133 -198
- package/server/http/lib/routeValidator.test.js +385 -278
- package/server/http/lib/router.js +17 -2
- package/server/platform/providerRuntime.test.js +7 -7
- package/server/runtime/bootBootstrapRoutes.js +2 -18
- package/server/runtime/bootBootstrapRoutes.test.js +5 -14
- package/server/runtime/fastifyBootstrap.js +119 -0
- package/server/runtime/fastifyBootstrap.test.js +119 -1
- package/server/runtime/moduleConfig.js +32 -62
- package/server/runtime/moduleConfig.test.js +48 -24
- package/server/support/pageTargets.js +15 -9
- package/server/support/pageTargets.test.js +1 -1
- package/shared/actions/actionContributorHelpers.js +5 -11
- package/shared/actions/actionDefinitions.js +37 -150
- package/shared/actions/actionDefinitions.test.js +117 -136
- package/shared/actions/policies.js +25 -169
- package/shared/actions/policies.test.js +76 -87
- package/shared/actions/registry.test.js +24 -50
- package/shared/support/crudFieldContract.js +322 -0
- package/shared/support/crudFieldContract.test.js +67 -0
- package/shared/support/crudListFilters.js +582 -38
- package/shared/support/crudListFilters.test.js +178 -8
- package/shared/support/crudLookup.js +14 -7
- package/shared/support/crudLookup.test.js +91 -66
- package/shared/support/shellLayoutTargets.test.js +1 -1
- package/shared/validators/composeSchemaDefinitions.js +53 -0
- package/shared/validators/composeSchemaDefinitions.test.js +156 -0
- package/shared/validators/createCursorListValidator.js +22 -35
- package/shared/validators/createCursorListValidator.test.js +22 -23
- package/shared/validators/cursorPaginationQueryValidator.js +14 -24
- package/shared/validators/cursorPaginationQueryValidator.test.js +18 -8
- package/shared/validators/htmlTimeSchemas.js +6 -4
- package/shared/validators/index.js +15 -7
- package/shared/validators/jsonRestSchemaSupport.js +139 -0
- package/shared/validators/mergeObjectSchemas.js +44 -6
- package/shared/validators/mergeObjectSchemas.test.js +60 -35
- package/shared/validators/recordIdParamsValidator.js +19 -52
- package/shared/validators/recordIdParamsValidator.test.js +13 -8
- package/shared/validators/resourceRequiredMetadata.js +3 -3
- package/shared/validators/resourceRequiredMetadata.test.js +29 -16
- package/shared/validators/schemaDefinitions.js +126 -0
- package/shared/validators/schemaDefinitions.test.js +51 -0
- package/shared/validators/schemaPayloadValidation.js +65 -0
- package/test/barrelExposure.test.js +30 -0
- package/test/routeInputContractGuard.test.js +10 -6
- package/shared/validators/mergeValidators.js +0 -89
- package/shared/validators/mergeValidators.test.js +0 -116
- package/shared/validators/nestValidator.js +0 -53
- package/shared/validators/nestValidator.test.js +0 -60
- package/shared/validators/settingsFieldNormalization.js +0 -40
|
@@ -2,6 +2,21 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import {
|
|
4
4
|
defineCrudListFilters,
|
|
5
|
+
CRUD_LIST_FILTER_INVALID_VALUES_REJECT,
|
|
6
|
+
CRUD_LIST_FILTER_INVALID_VALUES_DISCARD,
|
|
7
|
+
INVALID_CRUD_LIST_FILTER_QUERY_VALUE,
|
|
8
|
+
parseCrudListRangeQueryExpression,
|
|
9
|
+
formatCrudListRangeQueryExpression,
|
|
10
|
+
createCrudListFilterInitialValue,
|
|
11
|
+
isCrudListFilterMultiValue,
|
|
12
|
+
isCrudListFilterStructuredValue,
|
|
13
|
+
normalizeCrudListFilterUiValue,
|
|
14
|
+
areCrudListFilterUiValuesEqual,
|
|
15
|
+
hasCrudListFilterUiValue,
|
|
16
|
+
listCrudListFilterChipValues,
|
|
17
|
+
formatCrudListFilterDefaultChipLabel,
|
|
18
|
+
formatCrudListFilterQueryValue,
|
|
19
|
+
parseCrudListFilterQueryValue,
|
|
5
20
|
resolveCrudListFilterQueryKeys,
|
|
6
21
|
resolveCrudListFilterOptionLabel
|
|
7
22
|
} from "./crudListFilters.js";
|
|
@@ -52,8 +67,7 @@ test("defineCrudListFilters normalizes common filter shapes", () => {
|
|
|
52
67
|
key: "arrivalDate",
|
|
53
68
|
type: "dateRange",
|
|
54
69
|
label: "Arrival Date",
|
|
55
|
-
|
|
56
|
-
toKey: "arrivalDateTo",
|
|
70
|
+
queryKey: "arrivalDate",
|
|
57
71
|
options: [],
|
|
58
72
|
lookup: null,
|
|
59
73
|
chipLabel: null,
|
|
@@ -64,8 +78,7 @@ test("defineCrudListFilters normalizes common filter shapes", () => {
|
|
|
64
78
|
key: "weight",
|
|
65
79
|
type: "numberRange",
|
|
66
80
|
label: "Weight",
|
|
67
|
-
|
|
68
|
-
maxKey: "weightMax",
|
|
81
|
+
queryKey: "weight",
|
|
69
82
|
options: [],
|
|
70
83
|
lookup: null,
|
|
71
84
|
chipLabel: null,
|
|
@@ -104,12 +117,37 @@ test("defineCrudListFilters rejects duplicate query keys", () => {
|
|
|
104
117
|
type: "dateRange",
|
|
105
118
|
label: "Arrival Date"
|
|
106
119
|
},
|
|
107
|
-
|
|
120
|
+
arrivalDateExact: {
|
|
108
121
|
type: "date",
|
|
109
|
-
label: "Arrival Date From"
|
|
122
|
+
label: "Arrival Date From",
|
|
123
|
+
queryKey: "arrivalDate"
|
|
110
124
|
}
|
|
111
125
|
}),
|
|
112
|
-
/both use query key "
|
|
126
|
+
/both use query key "arrivalDate"/
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("defineCrudListFilters rejects split range keys", () => {
|
|
131
|
+
assert.throws(
|
|
132
|
+
() => defineCrudListFilters({
|
|
133
|
+
arrivalDate: {
|
|
134
|
+
type: "dateRange",
|
|
135
|
+
label: "Arrival Date",
|
|
136
|
+
fromKey: "arrivalDateFrom"
|
|
137
|
+
}
|
|
138
|
+
}),
|
|
139
|
+
/unsupported split range keys/
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
assert.throws(
|
|
143
|
+
() => defineCrudListFilters({
|
|
144
|
+
weight: {
|
|
145
|
+
type: "numberRange",
|
|
146
|
+
label: "Weight",
|
|
147
|
+
minKey: "weightMin"
|
|
148
|
+
}
|
|
149
|
+
}),
|
|
150
|
+
/unsupported split range keys/
|
|
113
151
|
);
|
|
114
152
|
});
|
|
115
153
|
|
|
@@ -129,7 +167,139 @@ test("resolveCrudListFilter helpers expose query keys and option labels", () =>
|
|
|
129
167
|
});
|
|
130
168
|
|
|
131
169
|
assert.deepEqual(resolveCrudListFilterQueryKeys(filters.status), ["status"]);
|
|
132
|
-
assert.deepEqual(resolveCrudListFilterQueryKeys(filters.arrivalDate), ["
|
|
170
|
+
assert.deepEqual(resolveCrudListFilterQueryKeys(filters.arrivalDate), ["arrivalDate"]);
|
|
133
171
|
assert.equal(resolveCrudListFilterOptionLabel(filters.status, "active"), "Active");
|
|
134
172
|
assert.equal(resolveCrudListFilterOptionLabel(filters.status, "missing", { fallback: "Unknown" }), "Unknown");
|
|
135
173
|
});
|
|
174
|
+
|
|
175
|
+
test("crud list range helpers parse and format single-key range expressions", () => {
|
|
176
|
+
assert.deepEqual(parseCrudListRangeQueryExpression("2026-04-01"), {
|
|
177
|
+
exact: true,
|
|
178
|
+
start: "2026-04-01",
|
|
179
|
+
end: "2026-04-01"
|
|
180
|
+
});
|
|
181
|
+
assert.deepEqual(parseCrudListRangeQueryExpression("2026-04-01..2026-04-30"), {
|
|
182
|
+
exact: false,
|
|
183
|
+
start: "2026-04-01",
|
|
184
|
+
end: "2026-04-30"
|
|
185
|
+
});
|
|
186
|
+
assert.deepEqual(parseCrudListRangeQueryExpression("..2026-04-30"), {
|
|
187
|
+
exact: false,
|
|
188
|
+
start: "",
|
|
189
|
+
end: "2026-04-30"
|
|
190
|
+
});
|
|
191
|
+
assert.equal(parseCrudListRangeQueryExpression(".."), null);
|
|
192
|
+
|
|
193
|
+
assert.equal(formatCrudListRangeQueryExpression("2026-04-01", ""), "2026-04-01..");
|
|
194
|
+
assert.equal(
|
|
195
|
+
formatCrudListRangeQueryExpression("2026-04-01", "2026-04-01", { collapseExact: true }),
|
|
196
|
+
"2026-04-01"
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("crud list filter helpers share canonical UI and query normalization", () => {
|
|
201
|
+
const filters = defineCrudListFilters({
|
|
202
|
+
status: {
|
|
203
|
+
type: "enumMany",
|
|
204
|
+
label: "Status",
|
|
205
|
+
options: [
|
|
206
|
+
{ value: "active", label: "Active" },
|
|
207
|
+
{ value: "archived", label: "Archived" }
|
|
208
|
+
]
|
|
209
|
+
},
|
|
210
|
+
supplierContactId: {
|
|
211
|
+
type: "recordIdMany",
|
|
212
|
+
label: "Supplier"
|
|
213
|
+
},
|
|
214
|
+
arrivalDate: {
|
|
215
|
+
type: "dateRange",
|
|
216
|
+
label: "Arrival Date"
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
assert.deepEqual(createCrudListFilterInitialValue(filters.status), []);
|
|
221
|
+
assert.equal(isCrudListFilterMultiValue(filters.status), true);
|
|
222
|
+
assert.equal(isCrudListFilterMultiValue(filters.arrivalDate), false);
|
|
223
|
+
assert.equal(isCrudListFilterStructuredValue(filters.arrivalDate), true);
|
|
224
|
+
assert.equal(isCrudListFilterStructuredValue(filters.status), false);
|
|
225
|
+
assert.deepEqual(
|
|
226
|
+
normalizeCrudListFilterUiValue(filters.status, ["active", "unexpected"]),
|
|
227
|
+
["active"]
|
|
228
|
+
);
|
|
229
|
+
assert.deepEqual(
|
|
230
|
+
parseCrudListFilterQueryValue(filters.status, ["active", "unexpected"], {
|
|
231
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_DISCARD
|
|
232
|
+
}),
|
|
233
|
+
["active"]
|
|
234
|
+
);
|
|
235
|
+
assert.equal(
|
|
236
|
+
parseCrudListFilterQueryValue(filters.status, ["active", "unexpected"], {
|
|
237
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT
|
|
238
|
+
}),
|
|
239
|
+
INVALID_CRUD_LIST_FILTER_QUERY_VALUE
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
assert.deepEqual(
|
|
243
|
+
normalizeCrudListFilterUiValue(filters.supplierContactId, ["7", "bad", 4]),
|
|
244
|
+
["7", "4"]
|
|
245
|
+
);
|
|
246
|
+
assert.deepEqual(
|
|
247
|
+
parseCrudListFilterQueryValue(filters.supplierContactId, ["7", "bad", 4], {
|
|
248
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_DISCARD
|
|
249
|
+
}),
|
|
250
|
+
["7", "4"]
|
|
251
|
+
);
|
|
252
|
+
assert.equal(
|
|
253
|
+
parseCrudListFilterQueryValue(filters.supplierContactId, { bad: true }, {
|
|
254
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT
|
|
255
|
+
}),
|
|
256
|
+
INVALID_CRUD_LIST_FILTER_QUERY_VALUE
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
assert.deepEqual(
|
|
260
|
+
normalizeCrudListFilterUiValue(filters.arrivalDate, "..2026-04-30"),
|
|
261
|
+
{
|
|
262
|
+
from: "",
|
|
263
|
+
to: "2026-04-30"
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
assert.equal(
|
|
267
|
+
formatCrudListFilterQueryValue(filters.arrivalDate, {
|
|
268
|
+
from: "2026-04-30",
|
|
269
|
+
to: "2026-04-30"
|
|
270
|
+
}),
|
|
271
|
+
"2026-04-30"
|
|
272
|
+
);
|
|
273
|
+
assert.equal(
|
|
274
|
+
areCrudListFilterUiValuesEqual(
|
|
275
|
+
filters.arrivalDate,
|
|
276
|
+
{
|
|
277
|
+
from: "2026-04-30",
|
|
278
|
+
to: "2026-04-30"
|
|
279
|
+
},
|
|
280
|
+
"2026-04-30"
|
|
281
|
+
),
|
|
282
|
+
true
|
|
283
|
+
);
|
|
284
|
+
assert.equal(hasCrudListFilterUiValue(filters.status, ["active", "unexpected"]), true);
|
|
285
|
+
assert.equal(hasCrudListFilterUiValue(filters.arrivalDate, { from: "", to: "" }), false);
|
|
286
|
+
assert.deepEqual(listCrudListFilterChipValues(filters.status, ["active", "unexpected"]), ["active"]);
|
|
287
|
+
assert.deepEqual(
|
|
288
|
+
listCrudListFilterChipValues(filters.arrivalDate, {
|
|
289
|
+
from: "2026-04-30",
|
|
290
|
+
to: "2026-04-30"
|
|
291
|
+
}),
|
|
292
|
+
[{
|
|
293
|
+
from: "2026-04-30",
|
|
294
|
+
to: "2026-04-30"
|
|
295
|
+
}]
|
|
296
|
+
);
|
|
297
|
+
assert.equal(
|
|
298
|
+
formatCrudListFilterDefaultChipLabel(filters.status, "active", {
|
|
299
|
+
resolveAtomicValue(value) {
|
|
300
|
+
return resolveCrudListFilterOptionLabel(filters.status, value);
|
|
301
|
+
}
|
|
302
|
+
}),
|
|
303
|
+
"Status: Active"
|
|
304
|
+
);
|
|
305
|
+
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { normalizeText } from "./normalize.js";
|
|
2
2
|
import { normalizePathname } from "../surface/paths.js";
|
|
3
|
+
import {
|
|
4
|
+
buildCrudFieldContractMap,
|
|
5
|
+
resolveCrudFieldSchemaProperties
|
|
6
|
+
} from "./crudFieldContract.js";
|
|
3
7
|
|
|
4
8
|
const DEFAULT_CRUD_LOOKUP_CONTAINER_KEY = "lookups";
|
|
5
9
|
|
|
@@ -66,7 +70,9 @@ function resolveCrudLookupContainerKey(resource = {}, options = {}) {
|
|
|
66
70
|
|
|
67
71
|
function resolveCrudLookupFieldEntries(resource = {}, { allowKeys = [] } = {}) {
|
|
68
72
|
const source = resource && typeof resource === "object" && !Array.isArray(resource) ? resource : {};
|
|
69
|
-
const entries =
|
|
73
|
+
const entries = Object.values(buildCrudFieldContractMap(source, {
|
|
74
|
+
context: "crud lookup field entries"
|
|
75
|
+
}));
|
|
70
76
|
const allowedKeySet = new Set(
|
|
71
77
|
(Array.isArray(allowKeys) ? allowKeys : [])
|
|
72
78
|
.map((entry) => normalizeText(entry))
|
|
@@ -109,12 +115,13 @@ function resolveCrudLookupFieldEntries(resource = {}, { allowKeys = [] } = {}) {
|
|
|
109
115
|
}
|
|
110
116
|
|
|
111
117
|
function resolveCrudLookupCreateSchemaKeys(resource = {}) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
return Object.freeze(
|
|
119
|
+
Object.keys(
|
|
120
|
+
resolveCrudFieldSchemaProperties(resource?.operations?.create?.body, {
|
|
121
|
+
context: "crud lookup create schema"
|
|
122
|
+
})
|
|
123
|
+
)
|
|
124
|
+
);
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
function resolveCrudLookupFieldKeys(resource = {}, { allowKeys = [] } = {}) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { createSchema } from "json-rest-schema";
|
|
3
4
|
import {
|
|
4
5
|
DEFAULT_CRUD_LOOKUP_CONTAINER_KEY,
|
|
5
6
|
normalizeCrudLookupApiPath,
|
|
@@ -13,6 +14,34 @@ import {
|
|
|
13
14
|
resolveCrudParentFilterFieldKeyFromRouteParam
|
|
14
15
|
} from "./crudLookup.js";
|
|
15
16
|
|
|
17
|
+
function createCrudResource({
|
|
18
|
+
viewFields = {},
|
|
19
|
+
createFields = {},
|
|
20
|
+
patchFields = {},
|
|
21
|
+
contract = {}
|
|
22
|
+
} = {}) {
|
|
23
|
+
return {
|
|
24
|
+
...(Object.keys(contract).length > 0 ? { contract } : {}),
|
|
25
|
+
operations: {
|
|
26
|
+
view: {
|
|
27
|
+
output: {
|
|
28
|
+
schema: createSchema(viewFields)
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
create: {
|
|
32
|
+
body: {
|
|
33
|
+
schema: createSchema(createFields)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
patch: {
|
|
37
|
+
body: {
|
|
38
|
+
schema: createSchema(patchFields)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
16
45
|
test("normalizeCrudLookupApiPath normalizes and rejects root", () => {
|
|
17
46
|
assert.equal(normalizeCrudLookupApiPath("vets"), "/vets");
|
|
18
47
|
assert.equal(normalizeCrudLookupApiPath("/vets//"), "/vets");
|
|
@@ -57,21 +86,21 @@ test("resolveCrudLookupContainerKey throws for invalid contract shape", () => {
|
|
|
57
86
|
});
|
|
58
87
|
|
|
59
88
|
test("resolveCrudLookupFieldKeys returns lookup field keys with optional allow-list", () => {
|
|
60
|
-
const resource = {
|
|
61
|
-
|
|
62
|
-
{
|
|
63
|
-
|
|
89
|
+
const resource = createCrudResource({
|
|
90
|
+
viewFields: {
|
|
91
|
+
contactId: {
|
|
92
|
+
type: "integer",
|
|
64
93
|
relation: {
|
|
65
94
|
kind: "lookup",
|
|
66
95
|
apiPath: "/contacts",
|
|
67
96
|
valueKey: "id"
|
|
68
97
|
}
|
|
69
98
|
},
|
|
70
|
-
{
|
|
71
|
-
|
|
99
|
+
status: {
|
|
100
|
+
type: "string"
|
|
72
101
|
},
|
|
73
|
-
{
|
|
74
|
-
|
|
102
|
+
vetId: {
|
|
103
|
+
type: "integer",
|
|
75
104
|
parentRouteParamKey: "primaryVetId",
|
|
76
105
|
relation: {
|
|
77
106
|
kind: "lookup",
|
|
@@ -79,18 +108,18 @@ test("resolveCrudLookupFieldKeys returns lookup field keys with optional allow-l
|
|
|
79
108
|
valueKey: "id"
|
|
80
109
|
}
|
|
81
110
|
}
|
|
82
|
-
|
|
83
|
-
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
84
113
|
|
|
85
114
|
assert.deepEqual(resolveCrudLookupFieldKeys(resource), ["contactId", "vetId"]);
|
|
86
115
|
assert.deepEqual(resolveCrudLookupFieldKeys(resource, { allowKeys: ["vetId", "missing"] }), ["vetId"]);
|
|
87
116
|
});
|
|
88
117
|
|
|
89
118
|
test("resolveCrudLookupFieldKeyFromRouteParam matches parent route param aliases to canonical lookup field keys", () => {
|
|
90
|
-
const resource = {
|
|
91
|
-
|
|
92
|
-
{
|
|
93
|
-
|
|
119
|
+
const resource = createCrudResource({
|
|
120
|
+
viewFields: {
|
|
121
|
+
staffContactId: {
|
|
122
|
+
type: "integer",
|
|
94
123
|
parentRouteParamKey: "contactId",
|
|
95
124
|
relation: {
|
|
96
125
|
kind: "lookup",
|
|
@@ -98,16 +127,16 @@ test("resolveCrudLookupFieldKeyFromRouteParam matches parent route param aliases
|
|
|
98
127
|
valueKey: "id"
|
|
99
128
|
}
|
|
100
129
|
},
|
|
101
|
-
{
|
|
102
|
-
|
|
130
|
+
serviceId: {
|
|
131
|
+
type: "integer",
|
|
103
132
|
relation: {
|
|
104
133
|
kind: "lookup",
|
|
105
134
|
apiPath: "/services",
|
|
106
135
|
valueKey: "id"
|
|
107
136
|
}
|
|
108
137
|
}
|
|
109
|
-
|
|
110
|
-
};
|
|
138
|
+
}
|
|
139
|
+
});
|
|
111
140
|
|
|
112
141
|
assert.equal(resolveCrudLookupFieldKeyFromRouteParam(resource, "contactId"), "staffContactId");
|
|
113
142
|
assert.equal(resolveCrudLookupFieldKeyFromRouteParam(resource, "staffContactId"), "staffContactId");
|
|
@@ -120,10 +149,10 @@ test("resolveCrudLookupFieldKeyFromRouteParam matches parent route param aliases
|
|
|
120
149
|
});
|
|
121
150
|
|
|
122
151
|
test("resolveCrudLookupFieldKeyFromRouteParam prefers exact field keys before alias matches", () => {
|
|
123
|
-
const resource = {
|
|
124
|
-
|
|
125
|
-
{
|
|
126
|
-
|
|
152
|
+
const resource = createCrudResource({
|
|
153
|
+
viewFields: {
|
|
154
|
+
staffContactId: {
|
|
155
|
+
type: "integer",
|
|
127
156
|
parentRouteParamKey: "contactId",
|
|
128
157
|
relation: {
|
|
129
158
|
kind: "lookup",
|
|
@@ -131,37 +160,25 @@ test("resolveCrudLookupFieldKeyFromRouteParam prefers exact field keys before al
|
|
|
131
160
|
valueKey: "id"
|
|
132
161
|
}
|
|
133
162
|
},
|
|
134
|
-
{
|
|
135
|
-
|
|
163
|
+
contactId: {
|
|
164
|
+
type: "integer",
|
|
136
165
|
relation: {
|
|
137
166
|
kind: "lookup",
|
|
138
167
|
apiPath: "/contacts",
|
|
139
168
|
valueKey: "id"
|
|
140
169
|
}
|
|
141
170
|
}
|
|
142
|
-
|
|
143
|
-
};
|
|
171
|
+
}
|
|
172
|
+
});
|
|
144
173
|
|
|
145
174
|
assert.equal(resolveCrudLookupFieldKeyFromRouteParam(resource, "contactId"), "contactId");
|
|
146
175
|
});
|
|
147
176
|
|
|
148
177
|
test("resolveCrudParentFilterKeys keeps only lookup keys that are writable through create", () => {
|
|
149
|
-
const resource = {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
schema: {
|
|
154
|
-
type: "object",
|
|
155
|
-
properties: {
|
|
156
|
-
serviceId: { type: "integer" }
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
fieldMeta: [
|
|
163
|
-
{
|
|
164
|
-
key: "staffContactId",
|
|
178
|
+
const resource = createCrudResource({
|
|
179
|
+
viewFields: {
|
|
180
|
+
staffContactId: {
|
|
181
|
+
type: "integer",
|
|
165
182
|
parentRouteParamKey: "contactId",
|
|
166
183
|
relation: {
|
|
167
184
|
kind: "lookup",
|
|
@@ -169,37 +186,35 @@ test("resolveCrudParentFilterKeys keeps only lookup keys that are writable throu
|
|
|
169
186
|
valueKey: "id"
|
|
170
187
|
}
|
|
171
188
|
},
|
|
172
|
-
{
|
|
173
|
-
|
|
189
|
+
serviceId: {
|
|
190
|
+
type: "integer",
|
|
174
191
|
relation: {
|
|
175
192
|
kind: "lookup",
|
|
176
193
|
apiPath: "/services",
|
|
177
194
|
valueKey: "id"
|
|
178
195
|
}
|
|
179
196
|
}
|
|
180
|
-
|
|
181
|
-
|
|
197
|
+
},
|
|
198
|
+
createFields: {
|
|
199
|
+
serviceId: {
|
|
200
|
+
type: "integer",
|
|
201
|
+
relation: {
|
|
202
|
+
kind: "lookup",
|
|
203
|
+
apiPath: "/services",
|
|
204
|
+
valueKey: "id"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
182
209
|
|
|
183
210
|
assert.deepEqual(resolveCrudParentFilterKeys(resource), ["serviceId"]);
|
|
184
211
|
});
|
|
185
212
|
|
|
186
213
|
test("resolveCrudParentFilterFieldKeyFromRouteParam uses the same allowed keys as server parent filters", () => {
|
|
187
|
-
const resource = {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
schema: {
|
|
192
|
-
type: "object",
|
|
193
|
-
properties: {
|
|
194
|
-
serviceId: { type: "integer" }
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
fieldMeta: [
|
|
201
|
-
{
|
|
202
|
-
key: "staffContactId",
|
|
214
|
+
const resource = createCrudResource({
|
|
215
|
+
viewFields: {
|
|
216
|
+
staffContactId: {
|
|
217
|
+
type: "integer",
|
|
203
218
|
parentRouteParamKey: "contactId",
|
|
204
219
|
relation: {
|
|
205
220
|
kind: "lookup",
|
|
@@ -207,16 +222,26 @@ test("resolveCrudParentFilterFieldKeyFromRouteParam uses the same allowed keys a
|
|
|
207
222
|
valueKey: "id"
|
|
208
223
|
}
|
|
209
224
|
},
|
|
210
|
-
{
|
|
211
|
-
|
|
225
|
+
serviceId: {
|
|
226
|
+
type: "integer",
|
|
212
227
|
relation: {
|
|
213
228
|
kind: "lookup",
|
|
214
229
|
apiPath: "/services",
|
|
215
230
|
valueKey: "id"
|
|
216
231
|
}
|
|
217
232
|
}
|
|
218
|
-
|
|
219
|
-
|
|
233
|
+
},
|
|
234
|
+
createFields: {
|
|
235
|
+
serviceId: {
|
|
236
|
+
type: "integer",
|
|
237
|
+
relation: {
|
|
238
|
+
kind: "lookup",
|
|
239
|
+
apiPath: "/services",
|
|
240
|
+
valueKey: "id"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
220
245
|
|
|
221
246
|
assert.equal(resolveCrudParentFilterFieldKeyFromRouteParam(resource, "contactId"), "");
|
|
222
247
|
assert.equal(resolveCrudParentFilterFieldKeyFromRouteParam(resource, "serviceId"), "serviceId");
|
|
@@ -88,7 +88,7 @@ test("discoverShellOutletTargetsFromVueSource ignores disabled default markers",
|
|
|
88
88
|
assert.equal(discovered.defaultTargetId, "");
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
test("discoverShellOutletTargetsFromVueSource rejects
|
|
91
|
+
test("discoverShellOutletTargetsFromVueSource rejects split outlet attributes", () => {
|
|
92
92
|
const source = `
|
|
93
93
|
<template>
|
|
94
94
|
<ShellOutlet target="shell-layout:primary-menu" host="other-host" position="primary-menu" />
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createSchema } from "json-rest-schema";
|
|
2
|
+
import { normalizeText } from "../support/normalize.js";
|
|
3
|
+
import { deepFreeze } from "../support/deepFreeze.js";
|
|
4
|
+
import { normalizeSingleSchemaDefinition } from "./schemaDefinitions.js";
|
|
5
|
+
|
|
6
|
+
function composeSchemaDefinitions(definitions, {
|
|
7
|
+
mode,
|
|
8
|
+
context = "schema definitions"
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (!Array.isArray(definitions) || definitions.length < 1) {
|
|
11
|
+
throw new TypeError(`${context} must be a non-empty array of schema definitions.`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const normalizedDefinitions = definitions.map((definition, index) =>
|
|
15
|
+
normalizeSingleSchemaDefinition(definition, {
|
|
16
|
+
context: `${context}[${index}]`
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const mergedStructure = {};
|
|
21
|
+
for (const normalizedDefinition of normalizedDefinitions) {
|
|
22
|
+
for (const [fieldName, fieldDefinition] of Object.entries(normalizedDefinition.schema.getFieldDefinitions())) {
|
|
23
|
+
if (Object.prototype.hasOwnProperty.call(mergedStructure, fieldName)) {
|
|
24
|
+
throw new Error(`${context} cannot compose duplicate field "${fieldName}".`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
mergedStructure[fieldName] = fieldDefinition;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let resolvedMode = normalizeText(mode).toLowerCase();
|
|
32
|
+
if (!resolvedMode) {
|
|
33
|
+
const uniqueModes = Array.from(new Set(
|
|
34
|
+
normalizedDefinitions.map((definition) => normalizeText(definition.mode).toLowerCase())
|
|
35
|
+
)).filter(Boolean);
|
|
36
|
+
if (uniqueModes.length === 1 && uniqueModes[0] === "patch") {
|
|
37
|
+
resolvedMode = "patch";
|
|
38
|
+
} else {
|
|
39
|
+
throw new TypeError(`${context} requires an explicit mode unless all schema definitions use patch mode.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const schemaFactory = createSchema.createFactory(
|
|
44
|
+
normalizedDefinitions.map((definition) => definition.schema)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return deepFreeze({
|
|
48
|
+
schema: schemaFactory(mergedStructure),
|
|
49
|
+
mode: resolvedMode
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { composeSchemaDefinitions };
|