@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.
- 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,7 +1,9 @@
|
|
|
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
|
-
cursorPaginationQueryValidator
|
|
5
|
+
cursorPaginationQueryValidator,
|
|
6
|
+
validateSchemaPayload
|
|
5
7
|
} from "@jskit-ai/kernel/shared/validators";
|
|
6
8
|
import { compileRouteValidator } from "@jskit-ai/kernel/_testable";
|
|
7
9
|
import {
|
|
@@ -12,10 +14,48 @@ import {
|
|
|
12
14
|
resolveCrudParentFilterKeys
|
|
13
15
|
} from "../src/server/listQueryValidators.js";
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
function composeSchemaDefinition(...definitions) {
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
schema: createSchema(
|
|
20
|
+
Object.assign({}, ...definitions.map((definition) => definition.schema.getFieldDefinitions()))
|
|
21
|
+
),
|
|
22
|
+
mode: "patch"
|
|
18
23
|
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createCrudResource({
|
|
27
|
+
viewFields = {},
|
|
28
|
+
createFields = {},
|
|
29
|
+
patchFields = {}
|
|
30
|
+
} = {}) {
|
|
31
|
+
return {
|
|
32
|
+
operations: {
|
|
33
|
+
view: {
|
|
34
|
+
output: {
|
|
35
|
+
schema: createSchema(viewFields),
|
|
36
|
+
mode: "replace"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
create: {
|
|
40
|
+
body: {
|
|
41
|
+
schema: createSchema(createFields),
|
|
42
|
+
mode: "create"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
patch: {
|
|
46
|
+
body: {
|
|
47
|
+
schema: createSchema(patchFields),
|
|
48
|
+
mode: "patch"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test("listSearchQueryValidator normalizes q", async () => {
|
|
56
|
+
const normalized = await validateSchemaPayload(listSearchQueryValidator, {
|
|
57
|
+
q: " ani "
|
|
58
|
+
}, { phase: "input" });
|
|
19
59
|
|
|
20
60
|
assert.deepEqual(normalized, {
|
|
21
61
|
q: "ani"
|
|
@@ -24,16 +64,16 @@ test("listSearchQueryValidator normalizes q", () => {
|
|
|
24
64
|
|
|
25
65
|
test("listSearchQueryValidator keeps q optional when merged with pagination query validator", () => {
|
|
26
66
|
const compiled = compileRouteValidator({
|
|
27
|
-
|
|
67
|
+
query: composeSchemaDefinition(cursorPaginationQueryValidator, listSearchQueryValidator)
|
|
28
68
|
});
|
|
29
69
|
|
|
30
70
|
assert.deepEqual(compiled.schema.querystring.required || [], []);
|
|
31
71
|
});
|
|
32
72
|
|
|
33
|
-
test("lookupIncludeQueryValidator normalizes include", () => {
|
|
34
|
-
const normalized = lookupIncludeQueryValidator
|
|
73
|
+
test("lookupIncludeQueryValidator normalizes include", async () => {
|
|
74
|
+
const normalized = await validateSchemaPayload(lookupIncludeQueryValidator, {
|
|
35
75
|
include: " vetId,ownerId "
|
|
36
|
-
});
|
|
76
|
+
}, { phase: "input" });
|
|
37
77
|
|
|
38
78
|
assert.deepEqual(normalized, {
|
|
39
79
|
include: "vetId,ownerId"
|
|
@@ -42,7 +82,7 @@ test("lookupIncludeQueryValidator normalizes include", () => {
|
|
|
42
82
|
|
|
43
83
|
test("lookupIncludeQueryValidator keeps include optional when merged with pagination and search", () => {
|
|
44
84
|
const compiled = compileRouteValidator({
|
|
45
|
-
|
|
85
|
+
query: composeSchemaDefinition(cursorPaginationQueryValidator, listSearchQueryValidator, lookupIncludeQueryValidator)
|
|
46
86
|
});
|
|
47
87
|
|
|
48
88
|
assert.deepEqual(compiled.schema.querystring.required || [], []);
|
|
@@ -56,120 +96,117 @@ test("createCrudCursorPaginationQueryValidator keeps numeric cursor validation f
|
|
|
56
96
|
|
|
57
97
|
test("createCrudCursorPaginationQueryValidator allows opaque cursor strings for ordered lists", () => {
|
|
58
98
|
const validator = createCrudCursorPaginationQueryValidator({
|
|
59
|
-
orderBy: [
|
|
60
|
-
{
|
|
61
|
-
column: "created_at",
|
|
62
|
-
direction: "desc"
|
|
63
|
-
}
|
|
64
|
-
]
|
|
99
|
+
orderBy: ["-createdAt"]
|
|
65
100
|
});
|
|
66
101
|
|
|
67
102
|
assert.notEqual(validator, cursorPaginationQueryValidator);
|
|
68
|
-
|
|
103
|
+
const normalized = validateSchemaPayload(validator, { cursor: " offset:3 ", limit: "25" }, { phase: "input" });
|
|
104
|
+
assert.deepEqual(normalized, {
|
|
69
105
|
cursor: "offset:3",
|
|
70
106
|
limit: 25
|
|
71
107
|
});
|
|
72
108
|
});
|
|
73
109
|
|
|
74
110
|
test("resolveCrudParentFilterKeys returns lookup keys that exist in create schema", () => {
|
|
75
|
-
const resource = {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
schema: {
|
|
80
|
-
type: "object",
|
|
81
|
-
properties: {
|
|
82
|
-
contactId: { type: "integer" },
|
|
83
|
-
name: { type: "string" },
|
|
84
|
-
vetId: { type: "integer" }
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
fieldMeta: [
|
|
91
|
-
{
|
|
92
|
-
key: "contactId",
|
|
111
|
+
const resource = createCrudResource({
|
|
112
|
+
viewFields: {
|
|
113
|
+
contactId: {
|
|
114
|
+
type: "integer",
|
|
93
115
|
relation: {
|
|
94
116
|
kind: "lookup",
|
|
95
117
|
apiPath: "/contacts",
|
|
96
118
|
valueKey: "id"
|
|
97
119
|
}
|
|
98
120
|
},
|
|
99
|
-
{
|
|
100
|
-
|
|
121
|
+
vetId: {
|
|
122
|
+
type: "integer",
|
|
101
123
|
relation: {
|
|
102
124
|
kind: "lookup",
|
|
103
125
|
apiPath: "/vets",
|
|
104
126
|
valueKey: "id"
|
|
105
127
|
}
|
|
106
128
|
},
|
|
107
|
-
{
|
|
108
|
-
|
|
129
|
+
ignoredLookup: {
|
|
130
|
+
type: "integer",
|
|
109
131
|
relation: {
|
|
110
132
|
kind: "lookup",
|
|
111
133
|
apiPath: "/ignored",
|
|
112
134
|
valueKey: "id"
|
|
113
135
|
}
|
|
114
136
|
}
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
},
|
|
138
|
+
createFields: {
|
|
139
|
+
contactId: {
|
|
140
|
+
type: "integer",
|
|
141
|
+
relation: {
|
|
142
|
+
kind: "lookup",
|
|
143
|
+
apiPath: "/contacts",
|
|
144
|
+
valueKey: "id"
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
name: { type: "string" },
|
|
148
|
+
vetId: {
|
|
149
|
+
type: "integer",
|
|
150
|
+
relation: {
|
|
151
|
+
kind: "lookup",
|
|
152
|
+
apiPath: "/vets",
|
|
153
|
+
valueKey: "id"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
117
158
|
|
|
118
159
|
assert.deepEqual(resolveCrudParentFilterKeys(resource), ["contactId", "vetId"]);
|
|
119
160
|
});
|
|
120
161
|
|
|
121
|
-
test("createCrudParentFilterQueryValidator normalizes configured parent filters", () => {
|
|
122
|
-
const validator = createCrudParentFilterQueryValidator({
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
}
|
|
162
|
+
test("createCrudParentFilterQueryValidator normalizes configured parent filters", async () => {
|
|
163
|
+
const validator = createCrudParentFilterQueryValidator(createCrudResource({
|
|
164
|
+
viewFields: {
|
|
165
|
+
contactId: {
|
|
166
|
+
type: "integer",
|
|
167
|
+
relation: {
|
|
168
|
+
kind: "lookup",
|
|
169
|
+
apiPath: "/contacts",
|
|
170
|
+
valueKey: "id"
|
|
132
171
|
}
|
|
133
172
|
}
|
|
134
173
|
},
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
|
|
174
|
+
createFields: {
|
|
175
|
+
contactId: {
|
|
176
|
+
type: "integer",
|
|
138
177
|
relation: {
|
|
139
178
|
kind: "lookup",
|
|
140
179
|
apiPath: "/contacts",
|
|
141
180
|
valueKey: "id"
|
|
142
181
|
}
|
|
143
182
|
}
|
|
144
|
-
|
|
145
|
-
});
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
146
185
|
|
|
147
|
-
const normalized = validator
|
|
148
|
-
contactId: " 42 "
|
|
149
|
-
|
|
150
|
-
});
|
|
186
|
+
const normalized = await validateSchemaPayload(validator, {
|
|
187
|
+
contactId: " 42 "
|
|
188
|
+
}, { phase: "input" });
|
|
151
189
|
assert.deepEqual(normalized, {
|
|
152
190
|
contactId: "42"
|
|
153
191
|
});
|
|
154
192
|
});
|
|
155
193
|
|
|
156
|
-
test("createCrudParentFilterQueryValidator keeps canonical field keys when
|
|
157
|
-
const validator = createCrudParentFilterQueryValidator({
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
194
|
+
test("createCrudParentFilterQueryValidator keeps canonical field keys when schema declares parent route aliases", async () => {
|
|
195
|
+
const validator = createCrudParentFilterQueryValidator(createCrudResource({
|
|
196
|
+
viewFields: {
|
|
197
|
+
staffContactId: {
|
|
198
|
+
type: "integer",
|
|
199
|
+
parentRouteParamKey: "contactId",
|
|
200
|
+
relation: {
|
|
201
|
+
kind: "lookup",
|
|
202
|
+
apiPath: "/contacts",
|
|
203
|
+
valueKey: "id"
|
|
167
204
|
}
|
|
168
205
|
}
|
|
169
206
|
},
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
|
|
207
|
+
createFields: {
|
|
208
|
+
staffContactId: {
|
|
209
|
+
type: "integer",
|
|
173
210
|
parentRouteParamKey: "contactId",
|
|
174
211
|
relation: {
|
|
175
212
|
kind: "lookup",
|
|
@@ -177,46 +214,43 @@ test("createCrudParentFilterQueryValidator keeps canonical field keys when field
|
|
|
177
214
|
valueKey: "id"
|
|
178
215
|
}
|
|
179
216
|
}
|
|
180
|
-
|
|
181
|
-
});
|
|
217
|
+
}
|
|
218
|
+
}));
|
|
182
219
|
|
|
183
|
-
assert.deepEqual(Object.keys(validator.schema.
|
|
184
|
-
assert.deepEqual(validator
|
|
185
|
-
staffContactId: " 42 "
|
|
186
|
-
|
|
187
|
-
}), {
|
|
220
|
+
assert.deepEqual(Object.keys(validator.schema.getFieldDefinitions()), ["staffContactId"]);
|
|
221
|
+
assert.deepEqual(await validateSchemaPayload(validator, {
|
|
222
|
+
staffContactId: " 42 "
|
|
223
|
+
}, { phase: "input" }), {
|
|
188
224
|
staffContactId: "42"
|
|
189
225
|
});
|
|
190
226
|
});
|
|
191
227
|
|
|
192
228
|
test("createCrudParentFilterQueryValidator keeps parent filters optional when merged", () => {
|
|
193
|
-
const parentValidator = createCrudParentFilterQueryValidator({
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
}
|
|
229
|
+
const parentValidator = createCrudParentFilterQueryValidator(createCrudResource({
|
|
230
|
+
viewFields: {
|
|
231
|
+
contactId: {
|
|
232
|
+
type: "integer",
|
|
233
|
+
relation: {
|
|
234
|
+
kind: "lookup",
|
|
235
|
+
apiPath: "/contacts",
|
|
236
|
+
valueKey: "id"
|
|
203
237
|
}
|
|
204
238
|
}
|
|
205
239
|
},
|
|
206
|
-
|
|
207
|
-
{
|
|
208
|
-
|
|
240
|
+
createFields: {
|
|
241
|
+
contactId: {
|
|
242
|
+
type: "integer",
|
|
209
243
|
relation: {
|
|
210
244
|
kind: "lookup",
|
|
211
245
|
apiPath: "/contacts",
|
|
212
246
|
valueKey: "id"
|
|
213
247
|
}
|
|
214
248
|
}
|
|
215
|
-
|
|
216
|
-
});
|
|
249
|
+
}
|
|
250
|
+
}));
|
|
217
251
|
|
|
218
252
|
const compiled = compileRouteValidator({
|
|
219
|
-
|
|
253
|
+
query: composeSchemaDefinition(cursorPaginationQueryValidator, listSearchQueryValidator, parentValidator)
|
|
220
254
|
});
|
|
221
255
|
assert.deepEqual(compiled.schema.querystring.required || [], []);
|
|
222
256
|
});
|