@jskit-ai/kernel 0.1.54 → 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
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/kernel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"
|
|
6
|
+
"json-rest-schema": "1.x.x"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"test": "node --test"
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"./shared/support/normalize": "./shared/support/normalize.js",
|
|
41
41
|
"./shared/support/permissions": "./shared/support/permissions.js",
|
|
42
42
|
"./shared/support/crudListFilters": "./shared/support/crudListFilters.js",
|
|
43
|
+
"./shared/support/crudFieldContract": "./shared/support/crudFieldContract.js",
|
|
43
44
|
"./shared/support/crudLookup": "./shared/support/crudLookup.js",
|
|
44
45
|
"./shared/support/deepFreeze": "./shared/support/deepFreeze.js",
|
|
45
46
|
"./shared/support/listenerSet": "./shared/support/listenerSet.js",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { createSchema } from "json-rest-schema";
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
OBJECT_INPUT_VALIDATOR
|
|
6
|
+
emptyInputValidator
|
|
7
7
|
} from "../../shared/actions/actionContributorHelpers.js";
|
|
8
8
|
import {
|
|
9
9
|
ActionRuntimeServiceProvider,
|
|
@@ -109,7 +109,15 @@ test("ActionRuntimeServiceProvider materializes dependencies and surfaces for ap
|
|
|
109
109
|
dependencies: {
|
|
110
110
|
echoService: "test.echo.service"
|
|
111
111
|
},
|
|
112
|
-
|
|
112
|
+
input: {
|
|
113
|
+
schema: createSchema({
|
|
114
|
+
value: {
|
|
115
|
+
type: "string",
|
|
116
|
+
required: false
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
mode: "patch"
|
|
120
|
+
},
|
|
113
121
|
idempotency: "none",
|
|
114
122
|
audit: { actionName: "test.echo" },
|
|
115
123
|
observability: {},
|
|
@@ -154,7 +162,7 @@ test("ActionRuntimeServiceProvider registers SurfaceRuntime from appConfig when
|
|
|
154
162
|
kind: "query",
|
|
155
163
|
channels: ["internal"],
|
|
156
164
|
surfacesFrom: "enabled",
|
|
157
|
-
|
|
165
|
+
input: emptyInputValidator,
|
|
158
166
|
idempotency: "none",
|
|
159
167
|
audit: { actionName: "test.surfaces.from.appconfig" },
|
|
160
168
|
observability: {},
|
|
@@ -190,7 +198,7 @@ test("ActionRuntimeServiceProvider materializes custom surfacesFrom aliases regi
|
|
|
190
198
|
kind: "query",
|
|
191
199
|
channels: ["internal"],
|
|
192
200
|
surfacesFrom: "workspace",
|
|
193
|
-
|
|
201
|
+
input: emptyInputValidator,
|
|
194
202
|
idempotency: "none",
|
|
195
203
|
audit: { actionName: "test.workspace.alias" },
|
|
196
204
|
observability: {},
|
|
@@ -227,7 +235,7 @@ test("ActionRuntimeServiceProvider does not infer service method bindings from a
|
|
|
227
235
|
dependencies: {
|
|
228
236
|
customerService: "test.customer.service"
|
|
229
237
|
},
|
|
230
|
-
|
|
238
|
+
input: emptyInputValidator,
|
|
231
239
|
idempotency: "optional",
|
|
232
240
|
audit: { actionName: "test.customer.create" },
|
|
233
241
|
observability: {},
|
|
@@ -260,7 +268,7 @@ test("app.actions + resolveActionContributors provide canonical contributor wiri
|
|
|
260
268
|
kind: "query",
|
|
261
269
|
channels: ["internal"],
|
|
262
270
|
surfaces: ["app"],
|
|
263
|
-
|
|
271
|
+
input: emptyInputValidator,
|
|
264
272
|
idempotency: "none",
|
|
265
273
|
audit: { actionName: "alpha.one" },
|
|
266
274
|
observability: {},
|
|
@@ -277,7 +285,7 @@ test("app.actions + resolveActionContributors provide canonical contributor wiri
|
|
|
277
285
|
kind: "query",
|
|
278
286
|
channels: ["internal"],
|
|
279
287
|
surfaces: ["app"],
|
|
280
|
-
|
|
288
|
+
input: emptyInputValidator,
|
|
281
289
|
idempotency: "none",
|
|
282
290
|
audit: { actionName: "beta.one" },
|
|
283
291
|
observability: {},
|
|
@@ -320,7 +328,7 @@ test("action runtime execute merges static and per-execution dependencies", asyn
|
|
|
320
328
|
dependencies: {
|
|
321
329
|
staticService: "test.static.service"
|
|
322
330
|
},
|
|
323
|
-
|
|
331
|
+
input: emptyInputValidator,
|
|
324
332
|
idempotency: "none",
|
|
325
333
|
audit: { actionName: "test.deps.merge" },
|
|
326
334
|
observability: {},
|
|
@@ -364,7 +372,7 @@ test("app.actions accepts custom action domains", () => {
|
|
|
364
372
|
kind: "query",
|
|
365
373
|
channels: ["internal"],
|
|
366
374
|
surfaces: ["app"],
|
|
367
|
-
|
|
375
|
+
input: emptyInputValidator,
|
|
368
376
|
idempotency: "none",
|
|
369
377
|
audit: { actionName: "custom.domain.check" },
|
|
370
378
|
observability: {},
|
|
@@ -391,7 +399,7 @@ test("app.action registers a single action with default contributor id", () => {
|
|
|
391
399
|
kind: "query",
|
|
392
400
|
channels: ["internal"],
|
|
393
401
|
surfaces: ["app"],
|
|
394
|
-
|
|
402
|
+
input: emptyInputValidator,
|
|
395
403
|
idempotency: "none",
|
|
396
404
|
audit: { actionName: "test.single" },
|
|
397
405
|
observability: {},
|
|
@@ -415,7 +423,7 @@ test("app.actions requires an array", () => {
|
|
|
415
423
|
assert.throws(() => app.actions({}), /requires an array/);
|
|
416
424
|
});
|
|
417
425
|
|
|
418
|
-
test("
|
|
426
|
+
test("emptyInputValidator allows empty input and rejects unexpected fields", async () => {
|
|
419
427
|
const app = createSingletonApp();
|
|
420
428
|
const provider = new ActionRuntimeServiceProvider();
|
|
421
429
|
provider.register(app);
|
|
@@ -434,7 +442,7 @@ test("EMPTY_INPUT_VALIDATOR allows empty input and rejects unexpected fields", a
|
|
|
434
442
|
kind: "query",
|
|
435
443
|
channels: ["internal"],
|
|
436
444
|
surfaces: ["app"],
|
|
437
|
-
|
|
445
|
+
input: emptyInputValidator,
|
|
438
446
|
idempotency: "none",
|
|
439
447
|
audit: { actionName: "test.empty-input" },
|
|
440
448
|
observability: {},
|
|
@@ -501,7 +509,7 @@ test("app.actions rejects invalid domain identifiers", () => {
|
|
|
501
509
|
kind: "query",
|
|
502
510
|
channels: ["internal"],
|
|
503
511
|
surfaces: ["app"],
|
|
504
|
-
|
|
512
|
+
input: emptyInputValidator,
|
|
505
513
|
idempotency: "none",
|
|
506
514
|
audit: { actionName: "invalid.domain" },
|
|
507
515
|
observability: {},
|
|
@@ -533,7 +541,7 @@ test("app.actions rejects unsupported surfacesFrom aliases", () => {
|
|
|
533
541
|
kind: "query",
|
|
534
542
|
channels: ["internal"],
|
|
535
543
|
surfacesFrom: "workspace",
|
|
536
|
-
|
|
544
|
+
input: emptyInputValidator,
|
|
537
545
|
idempotency: "none",
|
|
538
546
|
audit: { actionName: "workspace.alias.invalid" },
|
|
539
547
|
observability: {},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { createSchema } from "json-rest-schema";
|
|
3
4
|
import { registerActionContextContributor } from "../../actions/ActionRuntimeServiceProvider.js";
|
|
4
5
|
import { createApplication } from "../../kernel/index.js";
|
|
5
6
|
import { createRouter } from "./router.js";
|
|
@@ -12,12 +13,22 @@ function createFastifyStub() {
|
|
|
12
13
|
routes,
|
|
13
14
|
setErrorHandlerCalls: 0,
|
|
14
15
|
errorHandler: null,
|
|
16
|
+
contentTypeParsers: new Map(),
|
|
15
17
|
route(definition) {
|
|
16
18
|
routes.push(definition);
|
|
17
19
|
},
|
|
18
20
|
setErrorHandler(handler) {
|
|
19
21
|
this.errorHandler = handler;
|
|
20
22
|
this.setErrorHandlerCalls += 1;
|
|
23
|
+
},
|
|
24
|
+
hasContentTypeParser(contentType) {
|
|
25
|
+
return this.contentTypeParsers.has(String(contentType || "").trim().toLowerCase());
|
|
26
|
+
},
|
|
27
|
+
addContentTypeParser(contentType, options, parser) {
|
|
28
|
+
this.contentTypeParsers.set(String(contentType || "").trim().toLowerCase(), {
|
|
29
|
+
options,
|
|
30
|
+
parser
|
|
31
|
+
});
|
|
21
32
|
}
|
|
22
33
|
};
|
|
23
34
|
}
|
|
@@ -508,6 +519,38 @@ test("registerHttpRuntime installs API error handling once by default", () => {
|
|
|
508
519
|
|
|
509
520
|
assert.equal(fastify.setErrorHandlerCalls, 1);
|
|
510
521
|
assert.equal(typeof fastify.errorHandler, "function");
|
|
522
|
+
assert.equal(fastify.contentTypeParsers.size, 0);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("registerHttpRuntime installs the JSON:API parser only when a registered route accepts JSON:API request bodies", () => {
|
|
526
|
+
const app = createApplication();
|
|
527
|
+
const fastify = createFastifyStub();
|
|
528
|
+
const router = createRouter();
|
|
529
|
+
|
|
530
|
+
router.post(
|
|
531
|
+
"/jsonapi-body",
|
|
532
|
+
{
|
|
533
|
+
body: {
|
|
534
|
+
schema: createSchema({})
|
|
535
|
+
},
|
|
536
|
+
transport: {
|
|
537
|
+
kind: "jsonapi-resource",
|
|
538
|
+
contentType: "application/vnd.api+json"
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
async (_request, reply) => {
|
|
542
|
+
reply.code(204).send();
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
app.instance("jskit.fastify", fastify);
|
|
547
|
+
app.instance("jskit.http.router", router);
|
|
548
|
+
|
|
549
|
+
registerHttpRuntime(app);
|
|
550
|
+
registerHttpRuntime(app);
|
|
551
|
+
|
|
552
|
+
assert.equal(fastify.contentTypeParsers.has("application/vnd.api+json"), true);
|
|
553
|
+
assert.equal(fastify.contentTypeParsers.size, 1);
|
|
511
554
|
});
|
|
512
555
|
|
|
513
556
|
test("registerHttpRuntime can disable automatic API error handling", () => {
|
|
@@ -717,6 +760,109 @@ test("registerRoutes attaches request.input when route input transforms are conf
|
|
|
717
760
|
assert.equal(reply.statusCode, 200);
|
|
718
761
|
});
|
|
719
762
|
|
|
763
|
+
test("registerRoutes applies transport request transforms before route input normalization", async () => {
|
|
764
|
+
const fastify = createFastifyStub();
|
|
765
|
+
|
|
766
|
+
registerRoutes(fastify, {
|
|
767
|
+
routes: [
|
|
768
|
+
{
|
|
769
|
+
method: "POST",
|
|
770
|
+
path: "/transport-input",
|
|
771
|
+
transport: {
|
|
772
|
+
kind: "jsonapi-resource",
|
|
773
|
+
request: {
|
|
774
|
+
body(body) {
|
|
775
|
+
return body?.data?.attributes || {};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
input: {
|
|
780
|
+
body: (body) => ({
|
|
781
|
+
name: String(body?.name || "").trim()
|
|
782
|
+
})
|
|
783
|
+
},
|
|
784
|
+
handler: async (request, reply) => {
|
|
785
|
+
assert.equal(request.routeOptions.config.transport.kind, "jsonapi-resource");
|
|
786
|
+
assert.deepEqual(request.input, {
|
|
787
|
+
body: {
|
|
788
|
+
name: "Alice"
|
|
789
|
+
},
|
|
790
|
+
query: undefined,
|
|
791
|
+
params: undefined
|
|
792
|
+
});
|
|
793
|
+
reply.code(200).send({ ok: true });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
]
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const request = {
|
|
800
|
+
body: {
|
|
801
|
+
data: {
|
|
802
|
+
type: "contacts",
|
|
803
|
+
attributes: {
|
|
804
|
+
name: " Alice "
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
const reply = createReplyStub();
|
|
810
|
+
|
|
811
|
+
await fastify.routes[0].handler(request, reply);
|
|
812
|
+
|
|
813
|
+
assert.equal(reply.statusCode, 200);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test("registerRoutes does not invoke transport body transforms for bodyless requests", async () => {
|
|
817
|
+
const fastify = createFastifyStub();
|
|
818
|
+
let bodyTransformCalls = 0;
|
|
819
|
+
|
|
820
|
+
registerRoutes(fastify, {
|
|
821
|
+
routes: [
|
|
822
|
+
{
|
|
823
|
+
method: "GET",
|
|
824
|
+
path: "/transport-input-no-body",
|
|
825
|
+
transport: {
|
|
826
|
+
kind: "jsonapi-resource",
|
|
827
|
+
request: {
|
|
828
|
+
body() {
|
|
829
|
+
bodyTransformCalls += 1;
|
|
830
|
+
throw new Error("transport body transform should not run");
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
input: {
|
|
835
|
+
query: (query) => ({
|
|
836
|
+
page: Number(query?.page || 1)
|
|
837
|
+
})
|
|
838
|
+
},
|
|
839
|
+
handler: async (request, reply) => {
|
|
840
|
+
assert.deepEqual(request.input, {
|
|
841
|
+
body: undefined,
|
|
842
|
+
query: {
|
|
843
|
+
page: 2
|
|
844
|
+
},
|
|
845
|
+
params: undefined
|
|
846
|
+
});
|
|
847
|
+
reply.code(200).send({ ok: true });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
]
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const request = {
|
|
854
|
+
query: {
|
|
855
|
+
page: "2"
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
const reply = createReplyStub();
|
|
859
|
+
|
|
860
|
+
await fastify.routes[0].handler(request, reply);
|
|
861
|
+
|
|
862
|
+
assert.equal(reply.statusCode, 200);
|
|
863
|
+
assert.equal(bodyTransformCalls, 0);
|
|
864
|
+
});
|
|
865
|
+
|
|
720
866
|
test("registerRoutes leaves request.input undefined when route input is not configured", async () => {
|
|
721
867
|
const fastify = createFastifyStub();
|
|
722
868
|
|
|
@@ -740,6 +886,214 @@ test("registerRoutes leaves request.input undefined when route input is not conf
|
|
|
740
886
|
assert.equal(reply.statusCode, 200);
|
|
741
887
|
});
|
|
742
888
|
|
|
889
|
+
test("registerRoutes applies output transform on reply.send without changing handler shape", async () => {
|
|
890
|
+
const fastify = createFastifyStub();
|
|
891
|
+
|
|
892
|
+
registerRoutes(fastify, {
|
|
893
|
+
routes: [
|
|
894
|
+
{
|
|
895
|
+
method: "GET",
|
|
896
|
+
path: "/transport-output",
|
|
897
|
+
transport: {
|
|
898
|
+
kind: "jsonapi-resource"
|
|
899
|
+
},
|
|
900
|
+
output(payload) {
|
|
901
|
+
return {
|
|
902
|
+
data: payload
|
|
903
|
+
};
|
|
904
|
+
},
|
|
905
|
+
handler: async (request, reply) => {
|
|
906
|
+
assert.equal(request.routeOptions.config.transport.kind, "jsonapi-resource");
|
|
907
|
+
reply.code(200).send({
|
|
908
|
+
id: "7",
|
|
909
|
+
name: "Alice"
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
]
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
const request = {};
|
|
917
|
+
const reply = createReplyStub();
|
|
918
|
+
|
|
919
|
+
await fastify.routes[0].handler(request, reply);
|
|
920
|
+
|
|
921
|
+
assert.equal(reply.statusCode, 200);
|
|
922
|
+
assert.equal(reply.headers["Content-Type"], undefined);
|
|
923
|
+
assert.deepEqual(reply.payload, {
|
|
924
|
+
data: {
|
|
925
|
+
id: "7",
|
|
926
|
+
name: "Alice"
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test("registerRoutes applies transport response transforms before reply serialization", async () => {
|
|
932
|
+
const fastify = createFastifyStub();
|
|
933
|
+
|
|
934
|
+
registerRoutes(fastify, {
|
|
935
|
+
routes: [
|
|
936
|
+
{
|
|
937
|
+
method: "GET",
|
|
938
|
+
path: "/transport-response",
|
|
939
|
+
transport: {
|
|
940
|
+
kind: "jsonapi-resource",
|
|
941
|
+
contentType: "application/vnd.api+json",
|
|
942
|
+
response(payload) {
|
|
943
|
+
return {
|
|
944
|
+
data: {
|
|
945
|
+
type: "contacts",
|
|
946
|
+
id: String(payload?.id || ""),
|
|
947
|
+
attributes: {
|
|
948
|
+
name: String(payload?.name || "")
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
handler: async (_request, reply) => {
|
|
955
|
+
reply.code(200).send({
|
|
956
|
+
id: 7,
|
|
957
|
+
name: "Alice"
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
]
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
const request = {};
|
|
965
|
+
const reply = createReplyStub();
|
|
966
|
+
|
|
967
|
+
await fastify.routes[0].handler(request, reply);
|
|
968
|
+
|
|
969
|
+
assert.equal(reply.statusCode, 200);
|
|
970
|
+
assert.equal(reply.headers["Content-Type"], "application/vnd.api+json");
|
|
971
|
+
assert.deepEqual(reply.payload, {
|
|
972
|
+
data: {
|
|
973
|
+
type: "contacts",
|
|
974
|
+
id: "7",
|
|
975
|
+
attributes: {
|
|
976
|
+
name: "Alice"
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
test("registerRoutes exposes full transport runtime on Fastify route config for pre-handler error serialization", () => {
|
|
983
|
+
const fastify = createFastifyStub();
|
|
984
|
+
const transport = {
|
|
985
|
+
kind: "jsonapi-resource",
|
|
986
|
+
contentType: "application/vnd.api+json",
|
|
987
|
+
error() {
|
|
988
|
+
return {
|
|
989
|
+
errors: []
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
registerRoutes(fastify, {
|
|
995
|
+
routes: [
|
|
996
|
+
{
|
|
997
|
+
method: "GET",
|
|
998
|
+
path: "/transport-runtime",
|
|
999
|
+
transport,
|
|
1000
|
+
handler: async (_request, reply) => {
|
|
1001
|
+
reply.code(200).send({ ok: true });
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
]
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
assert.equal(fastify.routes[0].config.transport.kind, "jsonapi-resource");
|
|
1008
|
+
assert.equal(fastify.routes[0].config.transport.contentType, "application/vnd.api+json");
|
|
1009
|
+
assert.deepEqual(fastify.routes[0].config.transport.runtime, transport);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
test("registerRoutes keeps transport response payload when output transform returns undefined", async () => {
|
|
1013
|
+
const fastify = createFastifyStub();
|
|
1014
|
+
|
|
1015
|
+
registerRoutes(fastify, {
|
|
1016
|
+
routes: [
|
|
1017
|
+
{
|
|
1018
|
+
method: "GET",
|
|
1019
|
+
path: "/transport-response-output-undefined",
|
|
1020
|
+
transport: {
|
|
1021
|
+
kind: "jsonapi-resource",
|
|
1022
|
+
contentType: "application/vnd.api+json",
|
|
1023
|
+
response(payload) {
|
|
1024
|
+
return {
|
|
1025
|
+
data: {
|
|
1026
|
+
type: "contacts",
|
|
1027
|
+
id: String(payload?.id || ""),
|
|
1028
|
+
attributes: {
|
|
1029
|
+
name: String(payload?.name || "")
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
output() {
|
|
1036
|
+
return undefined;
|
|
1037
|
+
},
|
|
1038
|
+
handler: async (_request, reply) => {
|
|
1039
|
+
reply.code(200).send({
|
|
1040
|
+
id: 7,
|
|
1041
|
+
name: "Alice"
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
]
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
const request = {};
|
|
1049
|
+
const reply = createReplyStub();
|
|
1050
|
+
|
|
1051
|
+
await fastify.routes[0].handler(request, reply);
|
|
1052
|
+
|
|
1053
|
+
assert.equal(reply.statusCode, 200);
|
|
1054
|
+
assert.equal(reply.headers["Content-Type"], "application/vnd.api+json");
|
|
1055
|
+
assert.deepEqual(reply.payload, {
|
|
1056
|
+
data: {
|
|
1057
|
+
type: "contacts",
|
|
1058
|
+
id: "7",
|
|
1059
|
+
attributes: {
|
|
1060
|
+
name: "Alice"
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
test("registerRoutes sets transport content type on successful replies", async () => {
|
|
1067
|
+
const fastify = createFastifyStub();
|
|
1068
|
+
|
|
1069
|
+
registerRoutes(fastify, {
|
|
1070
|
+
routes: [
|
|
1071
|
+
{
|
|
1072
|
+
method: "GET",
|
|
1073
|
+
path: "/transport-content-type",
|
|
1074
|
+
transport: {
|
|
1075
|
+
kind: "jsonapi-resource",
|
|
1076
|
+
contentType: "application/vnd.api+json"
|
|
1077
|
+
},
|
|
1078
|
+
handler: async (_request, reply) => {
|
|
1079
|
+
reply.code(200).send({
|
|
1080
|
+
data: {
|
|
1081
|
+
type: "contacts",
|
|
1082
|
+
id: "7"
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
]
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
const reply = createReplyStub();
|
|
1091
|
+
await fastify.routes[0].handler({}, reply);
|
|
1092
|
+
|
|
1093
|
+
assert.equal(reply.statusCode, 200);
|
|
1094
|
+
assert.equal(reply.headers["Content-Type"], "application/vnd.api+json");
|
|
1095
|
+
});
|
|
1096
|
+
|
|
743
1097
|
test("registerRoutes rejects invalid route input transform definitions", () => {
|
|
744
1098
|
const fastify = createFastifyStub();
|
|
745
1099
|
|
|
@@ -761,6 +1115,99 @@ test("registerRoutes rejects invalid route input transform definitions", () => {
|
|
|
761
1115
|
);
|
|
762
1116
|
});
|
|
763
1117
|
|
|
1118
|
+
test("registerRoutes rejects invalid transport definitions", () => {
|
|
1119
|
+
const fastify = createFastifyStub();
|
|
1120
|
+
|
|
1121
|
+
assert.throws(
|
|
1122
|
+
() =>
|
|
1123
|
+
registerRoutes(fastify, {
|
|
1124
|
+
routes: [
|
|
1125
|
+
{
|
|
1126
|
+
method: "GET",
|
|
1127
|
+
path: "/invalid-transport",
|
|
1128
|
+
transport: {
|
|
1129
|
+
kind: "weird"
|
|
1130
|
+
},
|
|
1131
|
+
handler: async () => {}
|
|
1132
|
+
}
|
|
1133
|
+
]
|
|
1134
|
+
}),
|
|
1135
|
+
/transport\.kind must be one of: command, jsonapi-resource/
|
|
1136
|
+
);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
test("registerRoutes enforces request content type for unsafe transport routes", async () => {
|
|
1140
|
+
const fastify = createFastifyStub();
|
|
1141
|
+
|
|
1142
|
+
registerRoutes(fastify, {
|
|
1143
|
+
routes: [
|
|
1144
|
+
{
|
|
1145
|
+
method: "POST",
|
|
1146
|
+
path: "/transport-content-type-enforced",
|
|
1147
|
+
body: {
|
|
1148
|
+
type: "object",
|
|
1149
|
+
additionalProperties: true
|
|
1150
|
+
},
|
|
1151
|
+
transport: {
|
|
1152
|
+
kind: "jsonapi-resource",
|
|
1153
|
+
contentType: "application/vnd.api+json"
|
|
1154
|
+
},
|
|
1155
|
+
handler: async (_request, reply) => {
|
|
1156
|
+
reply.code(200).send({ ok: true });
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
]
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
await assert.rejects(
|
|
1163
|
+
() =>
|
|
1164
|
+
fastify.routes[0].handler(
|
|
1165
|
+
{
|
|
1166
|
+
headers: {
|
|
1167
|
+
"content-type": "application/json"
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
createReplyStub()
|
|
1171
|
+
),
|
|
1172
|
+
(error) => {
|
|
1173
|
+
assert.equal(error?.status, 415);
|
|
1174
|
+
assert.equal(error?.code, "unsupported_media_type");
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
test("registerRoutes does not enforce request content type for bodyless unsafe transport routes", async () => {
|
|
1181
|
+
const fastify = createFastifyStub();
|
|
1182
|
+
|
|
1183
|
+
registerRoutes(fastify, {
|
|
1184
|
+
routes: [
|
|
1185
|
+
{
|
|
1186
|
+
method: "POST",
|
|
1187
|
+
path: "/transport-content-type-bodyless",
|
|
1188
|
+
transport: {
|
|
1189
|
+
kind: "jsonapi-resource",
|
|
1190
|
+
contentType: "application/vnd.api+json"
|
|
1191
|
+
},
|
|
1192
|
+
handler: async (_request, reply) => {
|
|
1193
|
+
reply.code(204).send();
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
]
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
const reply = createReplyStub();
|
|
1200
|
+
await fastify.routes[0].handler(
|
|
1201
|
+
{
|
|
1202
|
+
headers: {}
|
|
1203
|
+
},
|
|
1204
|
+
reply
|
|
1205
|
+
);
|
|
1206
|
+
|
|
1207
|
+
assert.equal(reply.statusCode, 204);
|
|
1208
|
+
assert.equal(reply.payload, undefined);
|
|
1209
|
+
});
|
|
1210
|
+
|
|
764
1211
|
test("registerRoutes resolves middleware aliases and groups", async () => {
|
|
765
1212
|
const fastify = createFastifyStub();
|
|
766
1213
|
const observed = {
|