@jskit-ai/http-runtime 0.1.54 → 0.1.55

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.
Files changed (32) hide show
  1. package/package.descriptor.mjs +2 -4
  2. package/package.json +5 -5
  3. package/src/shared/clientRuntime/client.js +126 -9
  4. package/src/shared/clientRuntime/errors.js +6 -0
  5. package/src/shared/clientRuntime/jsonApiResourceTransport.js +241 -0
  6. package/src/shared/index.js +54 -5
  7. package/src/shared/validators/command.js +5 -4
  8. package/src/shared/validators/errorResponses.js +125 -62
  9. package/src/shared/validators/httpValidatorsApi.js +83 -12
  10. package/src/shared/validators/jsonApiQueryTransport.js +211 -0
  11. package/src/shared/validators/jsonApiResponses.js +3 -0
  12. package/src/shared/validators/jsonApiResult.js +83 -0
  13. package/src/shared/validators/jsonApiRouteTransport.js +800 -0
  14. package/src/shared/validators/jsonApiTransport.js +484 -0
  15. package/src/shared/validators/operationValidation.js +62 -101
  16. package/src/shared/validators/paginationQuery.js +14 -19
  17. package/src/shared/validators/resource.js +15 -17
  18. package/src/shared/validators/schemaUtils.js +18 -5
  19. package/src/shared/validators/transportSchemaEmbedding.js +81 -0
  20. package/test/client.test.js +279 -0
  21. package/test/command.test.js +38 -21
  22. package/test/entrypoints.boundary.test.js +8 -0
  23. package/test/errorResponses.test.js +49 -13
  24. package/test/jsonApiRouteTransport.test.js +349 -0
  25. package/test/jsonApiTransport.test.js +231 -0
  26. package/test/operationMessages.test.js +115 -66
  27. package/test/operationValidation.test.js +147 -159
  28. package/test/paginationQuery.test.js +4 -8
  29. package/test/resource.test.js +89 -55
  30. package/test/validationErrors.test.js +33 -0
  31. package/src/shared/validators/typeboxFormats.js +0 -43
  32. package/test/typeboxFormats.test.js +0 -42
@@ -1,4 +1,4 @@
1
- import { Type } from "@fastify/type-provider-typebox";
1
+ import { createSchema } from "json-rest-schema";
2
2
 
3
3
  function createPaginationQuerySchema({
4
4
  defaultPage = 1,
@@ -7,26 +7,21 @@ function createPaginationQuerySchema({
7
7
  minPageSize = 1,
8
8
  maxPageSize = 100
9
9
  } = {}) {
10
- return Type.Object(
11
- {
12
- page: Type.Optional(
13
- Type.Integer({
14
- minimum: minPage,
15
- default: defaultPage
16
- })
17
- ),
18
- pageSize: Type.Optional(
19
- Type.Integer({
20
- minimum: minPageSize,
21
- maximum: maxPageSize,
22
- default: defaultPageSize
23
- })
24
- )
10
+ return createSchema({
11
+ page: {
12
+ type: "integer",
13
+ required: false,
14
+ min: minPage,
15
+ default: defaultPage
25
16
  },
26
- {
27
- additionalProperties: false
17
+ pageSize: {
18
+ type: "integer",
19
+ required: false,
20
+ min: minPageSize,
21
+ max: maxPageSize,
22
+ default: defaultPageSize
28
23
  }
29
- );
24
+ });
30
25
  }
31
26
 
32
27
  export { createPaginationQuerySchema };
@@ -1,15 +1,9 @@
1
- import { Type } from "@fastify/type-provider-typebox";
2
- import { asSchema } from "./schemaUtils.js";
1
+ import { asSchemaDefinition } from "./schemaUtils.js";
2
+ import { createCursorListValidator } from "@jskit-ai/kernel/shared/validators";
3
+ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
4
 
4
5
  function createCursorPagedListResponseSchema(itemSchema) {
5
- const normalizedItemSchema = asSchema(itemSchema, "itemSchema");
6
- return Type.Object(
7
- {
8
- items: Type.Array(normalizedItemSchema),
9
- nextCursor: Type.Union([Type.String({ minLength: 1 }), Type.Null()])
10
- },
11
- { additionalProperties: false }
12
- );
6
+ return createCursorListValidator(itemSchema);
13
7
  }
14
8
 
15
9
  function createResource({
@@ -20,14 +14,18 @@ function createResource({
20
14
  list = null,
21
15
  listItem = null
22
16
  } = {}) {
23
- const normalizedRecordSchema = asSchema(record, "record");
24
- const normalizedCreateSchema = asSchema(create, "create");
25
- const normalizedReplaceSchema = asSchema(replace, "replace");
26
- const normalizedPatchSchema = asSchema(patch, "patch");
27
- const normalizedListItemSchema = listItem ? asSchema(listItem, "listItem") : normalizedRecordSchema;
28
- const normalizedListSchema = list ? asSchema(list, "list") : createCursorPagedListResponseSchema(normalizedListItemSchema);
17
+ const normalizedRecordSchema = asSchemaDefinition(record, "record", "replace");
18
+ const normalizedCreateSchema = asSchemaDefinition(create, "create", "create");
19
+ const normalizedReplaceSchema = asSchemaDefinition(replace, "replace", "replace");
20
+ const normalizedPatchSchema = asSchemaDefinition(patch, "patch", "patch");
21
+ const normalizedListItemSchema = listItem
22
+ ? asSchemaDefinition(listItem, "listItem", "replace")
23
+ : normalizedRecordSchema;
24
+ const normalizedListSchema = list
25
+ ? asSchemaDefinition(list, "list", "replace")
26
+ : createCursorPagedListResponseSchema(normalizedListItemSchema);
29
27
 
30
- return Object.freeze({
28
+ return deepFreeze({
31
29
  record: normalizedRecordSchema,
32
30
  create: normalizedCreateSchema,
33
31
  replace: normalizedReplaceSchema,
@@ -1,9 +1,22 @@
1
- function asSchema(value, label) {
2
- if (!value || typeof value !== "object" || Array.isArray(value)) {
3
- throw new TypeError(`${label} must be a TypeBox schema object.`);
1
+ import { normalizeSingleSchemaDefinition } from "@jskit-ai/kernel/shared/validators";
2
+
3
+ function asSchemaDefinition(value, label, defaultMode, { required = true } = {}) {
4
+ if (value == null) {
5
+ if (!required) {
6
+ return null;
7
+ }
8
+
9
+ throw new TypeError(`${label} is required.`);
4
10
  }
5
11
 
6
- return value;
12
+ try {
13
+ return normalizeSingleSchemaDefinition(value, {
14
+ context: label,
15
+ defaultMode
16
+ });
17
+ } catch (error) {
18
+ throw new TypeError(error?.message || `${label} must be a schema definition object.`);
19
+ }
7
20
  }
8
21
 
9
- export { asSchema };
22
+ export { asSchemaDefinition };
@@ -0,0 +1,81 @@
1
+ function rewriteEmbeddedTransportSchemaRefs(value, {
2
+ rootRef = "#",
3
+ definitionRefByName = {}
4
+ } = {}) {
5
+ if (Array.isArray(value)) {
6
+ return value.map((entry) => rewriteEmbeddedTransportSchemaRefs(entry, {
7
+ rootRef,
8
+ definitionRefByName
9
+ }));
10
+ }
11
+
12
+ if (!value || typeof value !== "object") {
13
+ return value;
14
+ }
15
+
16
+ const rewritten = {};
17
+
18
+ for (const [key, entry] of Object.entries(value)) {
19
+ if (key === "$ref" && typeof entry === "string") {
20
+ if (entry === "#") {
21
+ rewritten[key] = rootRef;
22
+ continue;
23
+ }
24
+
25
+ if (entry.startsWith("#/definitions/")) {
26
+ const definitionName = entry.slice("#/definitions/".length);
27
+ rewritten[key] = definitionRefByName[definitionName] || entry;
28
+ continue;
29
+ }
30
+ }
31
+
32
+ rewritten[key] = rewriteEmbeddedTransportSchemaRefs(entry, {
33
+ rootRef,
34
+ definitionRefByName
35
+ });
36
+ }
37
+
38
+ return rewritten;
39
+ }
40
+
41
+ function createEmbeddableTransportSchemaDocument(schemaDocument = {}, rootDefinitionName = "TransportSchema") {
42
+ const {
43
+ $schema: _jsonSchemaDraft,
44
+ definitions: sourceDefinitions = {},
45
+ ...rootSchema
46
+ } = schemaDocument || {};
47
+
48
+ const rootRef = `#/definitions/${rootDefinitionName}`;
49
+ const definitionRefByName = {};
50
+ const definitions = {};
51
+
52
+ for (const definitionName of Object.keys(sourceDefinitions)) {
53
+ definitionRefByName[definitionName] = `#/definitions/${rootDefinitionName}__${definitionName}`;
54
+ }
55
+
56
+ definitions[rootDefinitionName] = rewriteEmbeddedTransportSchemaRefs(rootSchema, {
57
+ rootRef,
58
+ definitionRefByName
59
+ });
60
+
61
+ for (const [definitionName, definitionSchema] of Object.entries(sourceDefinitions)) {
62
+ definitions[`${rootDefinitionName}__${definitionName}`] = rewriteEmbeddedTransportSchemaRefs(definitionSchema, {
63
+ rootRef,
64
+ definitionRefByName
65
+ });
66
+ }
67
+
68
+ return {
69
+ schema: {
70
+ allOf: [{
71
+ $ref: rootRef
72
+ }]
73
+ },
74
+ definitions
75
+ };
76
+ }
77
+
78
+ export {
79
+ rewriteEmbeddedTransportSchemaRefs,
80
+ createEmbeddableTransportSchemaDocument
81
+ };
@@ -56,6 +56,285 @@ test("request serializes json body and injects csrf token for unsafe methods", a
56
56
  assert.equal(calls[1][1].body, JSON.stringify({ demo: true }));
57
57
  });
58
58
 
59
+ test("request parses json:api responses as json payloads", async () => {
60
+ const fetchImpl = async () =>
61
+ mockResponse({
62
+ contentType: "application/vnd.api+json",
63
+ data: {
64
+ data: {
65
+ type: "contacts",
66
+ id: "2",
67
+ attributes: {
68
+ name: "ddd"
69
+ }
70
+ }
71
+ }
72
+ });
73
+
74
+ const client = createHttpClient({ fetchImpl });
75
+ const payload = await client.request("/api/contacts/2");
76
+
77
+ assert.deepEqual(payload, {
78
+ data: {
79
+ type: "contacts",
80
+ id: "2",
81
+ attributes: {
82
+ name: "ddd"
83
+ }
84
+ }
85
+ });
86
+ });
87
+
88
+ test("request encodes and decodes json:api resource transport for records", async () => {
89
+ const calls = [];
90
+ const fetchImpl = async (url, options) => {
91
+ calls.push([url, options]);
92
+ if (url === "/api/session") {
93
+ return mockResponse({
94
+ data: {
95
+ csrfToken: "csrf-jsonapi"
96
+ }
97
+ });
98
+ }
99
+
100
+ return mockResponse({
101
+ contentType: "application/vnd.api+json",
102
+ data: {
103
+ data: {
104
+ type: "contacts",
105
+ id: "2",
106
+ attributes: {
107
+ name: "ddd",
108
+ subscribed: false
109
+ }
110
+ }
111
+ }
112
+ });
113
+ };
114
+
115
+ const client = createHttpClient({ fetchImpl });
116
+ const payload = await client.request("/api/contacts/2", {
117
+ method: "PATCH",
118
+ body: {
119
+ name: "ddd",
120
+ subscribed: false
121
+ },
122
+ transport: {
123
+ kind: "jsonapi-resource",
124
+ requestType: "contact-updates",
125
+ responseType: "contacts",
126
+ responseKind: "record"
127
+ }
128
+ });
129
+
130
+ assert.deepEqual(payload, {
131
+ id: "2",
132
+ name: "ddd",
133
+ subscribed: false
134
+ });
135
+ assert.equal(calls[1][1].headers.Accept, "application/vnd.api+json");
136
+ assert.equal(calls[1][1].headers["Content-Type"], "application/vnd.api+json");
137
+ assert.equal(
138
+ calls[1][1].body,
139
+ JSON.stringify({
140
+ data: {
141
+ type: "contact-updates",
142
+ attributes: {
143
+ name: "ddd",
144
+ subscribed: false
145
+ }
146
+ }
147
+ })
148
+ );
149
+ });
150
+
151
+ test("request decodes json:api collection responses into JSKIT paged-list shape", async () => {
152
+ const fetchImpl = async () =>
153
+ mockResponse({
154
+ contentType: "application/vnd.api+json",
155
+ data: {
156
+ data: [
157
+ {
158
+ type: "contacts",
159
+ id: "2",
160
+ attributes: {
161
+ name: "ddd"
162
+ }
163
+ }
164
+ ],
165
+ meta: {
166
+ page: {
167
+ nextCursor: "cursor_2"
168
+ }
169
+ },
170
+ links: {
171
+ next: "/api/contacts?page[cursor]=cursor_2"
172
+ }
173
+ }
174
+ });
175
+
176
+ const client = createHttpClient({ fetchImpl });
177
+ const payload = await client.request("/api/contacts", {
178
+ method: "GET",
179
+ transport: {
180
+ kind: "jsonapi-resource",
181
+ responseType: "contacts",
182
+ responseKind: "collection"
183
+ }
184
+ });
185
+
186
+ assert.deepEqual(payload, {
187
+ items: [
188
+ {
189
+ id: "2",
190
+ name: "ddd"
191
+ }
192
+ ],
193
+ nextCursor: "cursor_2",
194
+ meta: {
195
+ page: {
196
+ nextCursor: "cursor_2"
197
+ }
198
+ },
199
+ links: {
200
+ next: "/api/contacts?page[cursor]=cursor_2"
201
+ }
202
+ });
203
+ });
204
+
205
+ test("request decodes native json-rest-api collection pagination metadata into JSKIT nextCursor", async () => {
206
+ const fetchImpl = async () =>
207
+ mockResponse({
208
+ contentType: "application/vnd.api+json",
209
+ data: {
210
+ data: [
211
+ {
212
+ type: "contacts",
213
+ id: "2",
214
+ attributes: {
215
+ name: "ddd"
216
+ }
217
+ }
218
+ ],
219
+ meta: {
220
+ pagination: {
221
+ cursor: {
222
+ next: "cursor_2"
223
+ }
224
+ }
225
+ },
226
+ links: {
227
+ next: "/api/contacts?page[after]=cursor_2&page[size]=20"
228
+ }
229
+ }
230
+ });
231
+
232
+ const client = createHttpClient({ fetchImpl });
233
+ const payload = await client.request("/api/contacts", {
234
+ method: "GET",
235
+ transport: {
236
+ kind: "jsonapi-resource",
237
+ responseType: "contacts",
238
+ responseKind: "collection"
239
+ }
240
+ });
241
+
242
+ assert.deepEqual(payload, {
243
+ items: [
244
+ {
245
+ id: "2",
246
+ name: "ddd"
247
+ }
248
+ ],
249
+ nextCursor: "cursor_2",
250
+ meta: {
251
+ pagination: {
252
+ cursor: {
253
+ next: "cursor_2"
254
+ }
255
+ }
256
+ },
257
+ links: {
258
+ next: "/api/contacts?page[after]=cursor_2&page[size]=20"
259
+ }
260
+ });
261
+ });
262
+
263
+ test("request encodes JSON:API query params for resource collections", async () => {
264
+ const calls = [];
265
+ const fetchImpl = async (url) => {
266
+ calls.push(url);
267
+ return mockResponse({
268
+ contentType: "application/vnd.api+json",
269
+ data: {
270
+ data: [],
271
+ meta: {
272
+ page: {
273
+ nextCursor: null
274
+ }
275
+ }
276
+ }
277
+ });
278
+ };
279
+
280
+ const client = createHttpClient({ fetchImpl });
281
+ await client.request("/api/contacts", {
282
+ method: "GET",
283
+ query: {
284
+ cursor: "cursor_2",
285
+ limit: 10,
286
+ q: "Merc",
287
+ include: "workspace,user",
288
+ workspaceId: "7"
289
+ },
290
+ transport: {
291
+ kind: "jsonapi-resource",
292
+ responseType: "contacts",
293
+ responseKind: "collection"
294
+ }
295
+ });
296
+
297
+ assert.equal(
298
+ calls[0],
299
+ "/api/contacts?page%5Bcursor%5D=cursor_2&page%5Blimit%5D=10&filter%5Bq%5D=Merc&include=workspace%2Cuser&filter%5BworkspaceId%5D=7"
300
+ );
301
+ });
302
+
303
+ test("request rejects json:api responses whose primary data type does not match the transport contract", async () => {
304
+ const fetchImpl = async () =>
305
+ mockResponse({
306
+ contentType: "application/vnd.api+json",
307
+ data: {
308
+ data: {
309
+ type: "user-settings",
310
+ id: "2",
311
+ attributes: {
312
+ name: "ddd"
313
+ }
314
+ }
315
+ }
316
+ });
317
+
318
+ const client = createHttpClient({ fetchImpl });
319
+
320
+ await assert.rejects(
321
+ () =>
322
+ client.request("/api/contacts/2", {
323
+ method: "GET",
324
+ transport: {
325
+ kind: "jsonapi-resource",
326
+ responseType: "contacts",
327
+ responseKind: "record"
328
+ }
329
+ }),
330
+ (error) => {
331
+ assert.equal(error?.message, "JSON:API response decoding failed.");
332
+ assert.equal(error?.cause?.message, "Expected JSON:API resource type contacts, received user-settings.");
333
+ return true;
334
+ }
335
+ );
336
+ });
337
+
59
338
  test("request retries once on retryable csrf failure and preserves stateful headers", async () => {
60
339
  const calls = [];
61
340
  const fetchImpl = async (url, options) => {
@@ -1,48 +1,65 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { Type } from "@fastify/type-provider-typebox";
3
+ import { createSchema } from "json-rest-schema";
4
4
  import { createCommand } from "../src/shared/validators/command.js";
5
5
 
6
- test("createCommand requires input/output TypeBox schemas", () => {
7
- assert.throws(() => createCommand({}), /input must be a TypeBox schema object/);
6
+ test("createCommand requires input/output schema definitions", () => {
7
+ assert.throws(() => createCommand({}), /input is required/);
8
8
 
9
9
  assert.throws(
10
10
  () =>
11
11
  createCommand({
12
- input: Type.Object({}, { additionalProperties: false })
12
+ input: {
13
+ schema: createSchema({})
14
+ }
13
15
  }),
14
- /output must be a TypeBox schema object/
16
+ /output is required/
17
+ );
18
+
19
+ assert.throws(
20
+ () =>
21
+ createCommand({
22
+ input: createSchema({}),
23
+ output: {
24
+ schema: createSchema({})
25
+ }
26
+ }),
27
+ /input must be a schema definition object/
15
28
  );
16
29
  });
17
30
 
18
31
  test("createCommand normalizes invalidates and preserves idempotent flag", () => {
19
32
  const command = createCommand({
20
- input: Type.Object(
21
- {
22
- token: Type.String({ minLength: 1 })
23
- },
24
- { additionalProperties: false }
25
- ),
26
- output: Type.Object(
27
- {
28
- ok: Type.Boolean()
29
- },
30
- { additionalProperties: false }
31
- ),
33
+ input: {
34
+ schema: createSchema({
35
+ token: { type: "string", required: true, minLength: 1 }
36
+ })
37
+ },
38
+ output: {
39
+ schema: createSchema({
40
+ ok: { type: "boolean", required: true }
41
+ })
42
+ },
32
43
  idempotent: true,
33
44
  invalidates: ["users-web", "users-web", "", "workspace.members"]
34
45
  });
35
46
 
36
47
  assert.equal(command.idempotent, true);
37
48
  assert.deepEqual(command.invalidates, ["users-web", "workspace.members"]);
38
- assert.equal(command.input.type, "object");
39
- assert.equal(command.output.type, "object");
49
+ assert.equal(command.input.mode, "patch");
50
+ assert.equal(command.output.mode, "replace");
51
+ assert.equal(typeof command.input.schema.toJsonSchema, "function");
52
+ assert.equal(typeof command.output.schema.toJsonSchema, "function");
40
53
  });
41
54
 
42
55
  test("createCommand omits idempotent when not explicitly boolean", () => {
43
56
  const command = createCommand({
44
- input: Type.Object({}, { additionalProperties: false }),
45
- output: Type.Object({}, { additionalProperties: false })
57
+ input: {
58
+ schema: createSchema({})
59
+ },
60
+ output: {
61
+ schema: createSchema({})
62
+ }
46
63
  });
47
64
 
48
65
  assert.equal(Object.hasOwn(command, "idempotent"), false);
@@ -32,6 +32,14 @@ test("shared entrypoint exports shared validators only", () => {
32
32
  assert.equal(typeof sharedApi.enumSchema, "function");
33
33
  assert.equal(typeof sharedApi.createResource, "function");
34
34
  assert.equal(typeof sharedApi.createCommand, "function");
35
+ assert.equal(typeof sharedApi.createJsonApiDocument, "function");
36
+ assert.equal(typeof sharedApi.createJsonApiErrorDocumentFromFailure, "function");
37
+ assert.equal(typeof sharedApi.normalizeJsonApiDocument, "function");
38
+ assert.equal(typeof sharedApi.returnJsonApiDocument, "function");
39
+ assert.equal(typeof sharedApi.returnJsonApiData, "function");
40
+ assert.equal(typeof sharedApi.returnJsonApiMeta, "function");
41
+ assert.equal(typeof sharedApi.createJsonApiResourceRouteContract, "function");
42
+ assert.equal(typeof sharedApi.withJsonApiErrorResponses, "function");
35
43
  assert.equal(typeof sharedApi.createHttpClient, "undefined");
36
44
  assert.equal(typeof sharedApi.HttpValidatorsServiceProvider, "undefined");
37
45
  });
@@ -3,10 +3,13 @@ import test from "node:test";
3
3
 
4
4
  import {
5
5
  STANDARD_ERROR_STATUS_CODES,
6
- apiErrorResponseSchema,
7
- apiValidationErrorResponseSchema,
8
- fastifyDefaultErrorResponseSchema,
6
+ apiErrorOutputValidator,
7
+ apiValidationErrorOutputValidator,
8
+ apiErrorTransportSchema,
9
+ apiValidationErrorTransportSchema,
10
+ fastifyDefaultErrorTransportSchema,
9
11
  enumSchema,
12
+ createTransportResponseSchema,
10
13
  withStandardErrorResponses
11
14
  } from "../src/shared/validators/errorResponses.js";
12
15
 
@@ -25,7 +28,17 @@ test("withStandardErrorResponses includes standard statuses", () => {
25
28
  for (const statusCode of STANDARD_ERROR_STATUS_CODES) {
26
29
  assert.ok(responses[statusCode], `missing status ${statusCode}`);
27
30
  }
28
- assert.equal(responses[400].schema.anyOf.length, 2);
31
+ assert.equal(responses[400].transportSchema.anyOf.length, 2);
32
+ assert.deepEqual(responses[400].transportSchema.anyOf[0], {
33
+ allOf: [{
34
+ $ref: "#/definitions/ApiErrorOutput"
35
+ }]
36
+ });
37
+ assert.equal(responses[400].transportSchema.definitions.ApiErrorOutput.type, "object");
38
+ assert.equal(
39
+ responses[400].transportSchema.definitions.ApiErrorOutput.properties.details.allOf[0].$ref,
40
+ "#/definitions/ApiErrorOutput__SchemaNode_1_replace"
41
+ );
29
42
  });
30
43
 
31
44
  test("withStandardErrorResponses uses validation union for 400 when enabled", () => {
@@ -40,11 +53,20 @@ test("withStandardErrorResponses uses validation union for 400 when enabled", ()
40
53
  { includeValidation400: true }
41
54
  );
42
55
 
43
- assert.equal(responses[400].schema.anyOf.length, 3);
44
- assert.deepEqual(
45
- responses[400].schema.anyOf.map((schema) => schema.type),
46
- [apiValidationErrorResponseSchema.type, apiErrorResponseSchema.type, fastifyDefaultErrorResponseSchema.type]
47
- );
56
+ assert.equal(responses[400].transportSchema.anyOf.length, 3);
57
+ assert.deepEqual(responses[400].transportSchema.anyOf[0], {
58
+ allOf: [{
59
+ $ref: "#/definitions/ApiValidationErrorOutput"
60
+ }]
61
+ });
62
+ assert.deepEqual(responses[400].transportSchema.anyOf[1], {
63
+ allOf: [{
64
+ $ref: "#/definitions/ApiErrorOutput"
65
+ }]
66
+ });
67
+ assert.equal(responses[400].transportSchema.anyOf[2].type, fastifyDefaultErrorTransportSchema.type);
68
+ assert.equal(responses[400].transportSchema.definitions.ApiValidationErrorOutput.type, "object");
69
+ assert.equal(responses[400].transportSchema.definitions.ApiErrorOutput.type, "object");
48
70
  });
49
71
 
50
72
  test("withStandardErrorResponses does not override existing error schemas", () => {
@@ -63,14 +85,12 @@ test("withStandardErrorResponses does not override existing error schemas", () =
63
85
  type: "string"
64
86
  }
65
87
  },
66
- 400: {
67
- schema: custom400
68
- }
88
+ 400: createTransportResponseSchema(custom400)
69
89
  },
70
90
  { includeValidation400: true }
71
91
  );
72
92
 
73
- assert.equal(responses[400].schema, custom400);
93
+ assert.equal(responses[400].transportSchema, custom400);
74
94
  });
75
95
 
76
96
  test("enumSchema creates a literal union", () => {
@@ -82,3 +102,19 @@ test("enumSchema creates a literal union", () => {
82
102
  ["one", "two", "three"]
83
103
  );
84
104
  });
105
+
106
+ test("error response validators export transport schemas from the same contracts", () => {
107
+ assert.equal(apiErrorOutputValidator.mode, "replace");
108
+ assert.equal(apiValidationErrorOutputValidator.mode, "replace");
109
+ assert.equal(apiErrorTransportSchema.type, "object");
110
+ assert.equal(apiValidationErrorTransportSchema.type, "object");
111
+ assert.equal(apiErrorTransportSchema.properties.details.allOf[0].$ref, "#/definitions/SchemaNode_1_replace");
112
+ assert.equal(
113
+ apiValidationErrorTransportSchema.properties.details.allOf[0].$ref,
114
+ "#/definitions/SchemaNode_1_replace"
115
+ );
116
+ assert.equal(
117
+ apiValidationErrorTransportSchema.definitions.SchemaNode_1_replace.required.includes("fieldErrors"),
118
+ true
119
+ );
120
+ });