@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,86 +1,115 @@
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 {
5
5
  createCursorPagedListResponseSchema,
6
6
  createResource
7
7
  } from "../src/shared/validators/resource.js";
8
8
 
9
9
  test("createCursorPagedListResponseSchema builds items + nextCursor schema", () => {
10
- const itemSchema = Type.Object(
11
- {
12
- id: Type.Integer({ minimum: 1 })
13
- },
14
- { additionalProperties: false }
15
- );
10
+ const itemSchema = {
11
+ schema: createSchema({
12
+ id: { type: "integer", required: true, min: 1 }
13
+ })
14
+ };
16
15
  const listSchema = createCursorPagedListResponseSchema(itemSchema);
16
+ const transportSchema = listSchema.schema.toJsonSchema({ mode: listSchema.mode });
17
17
 
18
- assert.equal(listSchema.type, "object");
19
- assert.equal(listSchema.additionalProperties, false);
20
- assert.equal(listSchema.properties.items.type, "array");
21
- assert.equal(listSchema.properties.nextCursor.anyOf.length, 2);
18
+ assert.equal(listSchema.mode, "replace");
19
+ assert.equal(transportSchema.type, "object");
20
+ assert.equal(transportSchema.additionalProperties, false);
21
+ assert.equal(transportSchema.properties.items.type, "array");
22
+ assert.equal(transportSchema.properties.nextCursor.anyOf.length, 2);
22
23
  });
23
24
 
24
25
  test("createResource requires record/create/replace/patch schemas", () => {
25
26
  assert.throws(
26
27
  () => createResource({}),
27
- /record must be a TypeBox schema object/
28
+ /record is required/
29
+ );
30
+
31
+ assert.throws(
32
+ () =>
33
+ createResource({
34
+ record: createSchema({}),
35
+ create: { schema: createSchema({}) },
36
+ replace: { schema: createSchema({}) },
37
+ patch: { schema: createSchema({}) }
38
+ }),
39
+ /record must be a schema definition object/
28
40
  );
29
41
  });
30
42
 
31
43
  test("createResource builds default list schema from record/listItem", () => {
32
- const recordSchema = Type.Object(
33
- {
34
- id: Type.Integer({ minimum: 1 }),
35
- name: Type.String({ minLength: 1 })
36
- },
37
- { additionalProperties: false }
38
- );
39
- const writeSchema = Type.Object(
40
- {
41
- name: Type.String({ minLength: 1 }),
42
- color: Type.String({ minLength: 1 })
43
- },
44
- { additionalProperties: false }
45
- );
46
- const patchSchema = Type.Partial(writeSchema, { additionalProperties: false });
44
+ const recordSchema = {
45
+ schema: createSchema({
46
+ id: { type: "integer", required: true, min: 1 },
47
+ name: { type: "string", required: true, minLength: 1 }
48
+ })
49
+ };
50
+ const writeSchema = {
51
+ schema: createSchema({
52
+ name: { type: "string", required: true, minLength: 1 },
53
+ color: { type: "string", required: true, minLength: 1 }
54
+ })
55
+ };
56
+ const patchSchema = {
57
+ schema: createSchema({
58
+ name: { type: "string", minLength: 1 },
59
+ color: { type: "string", minLength: 1 }
60
+ })
61
+ };
47
62
  const resource = createResource({
48
63
  record: recordSchema,
49
64
  create: writeSchema,
50
65
  replace: writeSchema,
51
66
  patch: patchSchema
52
67
  });
68
+ const listTransportSchema = resource.list.schema.toJsonSchema({ mode: resource.list.mode });
53
69
 
54
- assert.equal(resource.list.properties.items.items.type, "object");
70
+ assert.equal(resource.record.mode, "replace");
71
+ assert.equal(resource.create.mode, "create");
72
+ assert.equal(resource.patch.mode, "patch");
73
+ assert.equal(resource.list.mode, "replace");
74
+ assert.equal(listTransportSchema.properties.items.items["x-json-rest-schema"]?.castType, "object");
75
+ assert.equal(Array.isArray(listTransportSchema.properties.items.items.allOf), true);
76
+ assert.match(listTransportSchema.properties.items.items.allOf[0].$ref, /^#\/definitions\//);
55
77
  });
56
78
 
57
79
  test("createResource accepts explicit list schema override", () => {
58
- const recordSchema = Type.Object(
59
- {
60
- id: Type.Integer({ minimum: 1 })
61
- },
62
- { additionalProperties: false }
63
- );
64
- const writeSchema = Type.Object(
65
- {
66
- id: Type.Integer({ minimum: 1 })
67
- },
68
- { additionalProperties: false }
69
- );
70
- const patchSchema = Type.Partial(writeSchema, { additionalProperties: false });
71
- const explicitListSchema = Type.Object(
72
- {
73
- rows: Type.Array(recordSchema),
74
- meta: Type.Object(
75
- {
76
- page: Type.Integer({ minimum: 1 }),
77
- pageSize: Type.Integer({ minimum: 1 })
78
- },
79
- { additionalProperties: false }
80
- )
81
- },
82
- { additionalProperties: false }
83
- );
80
+ const recordSchema = {
81
+ schema: createSchema({
82
+ id: { type: "integer", required: true, min: 1 }
83
+ })
84
+ };
85
+ const writeSchema = {
86
+ schema: createSchema({
87
+ id: { type: "integer", required: true, min: 1 }
88
+ })
89
+ };
90
+ const patchSchema = {
91
+ schema: createSchema({
92
+ id: { type: "integer", min: 1 }
93
+ })
94
+ };
95
+ const explicitListSchema = {
96
+ schema: createSchema({
97
+ rows: {
98
+ type: "array",
99
+ required: true,
100
+ items: recordSchema.schema
101
+ },
102
+ meta: {
103
+ type: "object",
104
+ required: true,
105
+ schema: createSchema({
106
+ page: { type: "integer", required: true, min: 1 },
107
+ pageSize: { type: "integer", required: true, min: 1 }
108
+ })
109
+ }
110
+ }),
111
+ mode: "replace"
112
+ };
84
113
 
85
114
  const resource = createResource({
86
115
  record: recordSchema,
@@ -89,6 +118,11 @@ test("createResource accepts explicit list schema override", () => {
89
118
  patch: patchSchema,
90
119
  list: explicitListSchema
91
120
  });
121
+ const listTransportSchema = resource.list.schema.toJsonSchema({ mode: resource.list.mode });
92
122
 
93
- assert.equal(resource.list, explicitListSchema);
123
+ assert.equal(resource.list.mode, "replace");
124
+ assert.equal(listTransportSchema.properties.rows.type, "array");
125
+ assert.equal(listTransportSchema.properties.meta["x-json-rest-schema"]?.castType, "object");
126
+ assert.equal(Array.isArray(listTransportSchema.properties.meta.allOf), true);
127
+ assert.match(listTransportSchema.properties.meta.allOf[0].$ref, /^#\/definitions\//);
94
128
  });
@@ -98,3 +98,36 @@ test("createHttpError normalizes code and fieldErrors", () => {
98
98
  }
99
99
  });
100
100
  });
101
+
102
+ test("createHttpError decodes json:api error documents into the existing fieldErrors shape", () => {
103
+ const error = createHttpError(
104
+ {
105
+ status: 422
106
+ },
107
+ {
108
+ errors: [
109
+ {
110
+ status: "422",
111
+ code: "invalid_input",
112
+ title: "Validation failed.",
113
+ detail: "Name is required.",
114
+ source: {
115
+ pointer: "/data/attributes/name"
116
+ }
117
+ }
118
+ ]
119
+ }
120
+ );
121
+
122
+ assert.equal(error.status, 422);
123
+ assert.equal(error.code, "invalid_input");
124
+ assert.equal(error.message, "Name is required.");
125
+ assert.deepEqual(error.fieldErrors, {
126
+ name: "Name is required."
127
+ });
128
+ assert.deepEqual(error.details, {
129
+ fieldErrors: {
130
+ name: "Name is required."
131
+ }
132
+ });
133
+ });
@@ -1,43 +0,0 @@
1
- import { Format } from "typebox/format";
2
-
3
- const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
4
- const ISO_UTC_DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
5
-
6
- function isStrictUuid(value) {
7
- return typeof value === "string" && UUID_PATTERN.test(value);
8
- }
9
-
10
- function isStrictIsoUtcDateTime(value) {
11
- if (typeof value !== "string" || !ISO_UTC_DATE_TIME_PATTERN.test(value)) {
12
- return false;
13
- }
14
-
15
- const parsed = new Date(value);
16
- if (Number.isNaN(parsed.getTime())) {
17
- return false;
18
- }
19
-
20
- return parsed.toISOString() === value;
21
- }
22
-
23
- function registerTypeBoxFormats() {
24
- return registerTypeBoxFormatsWith(Format);
25
- }
26
-
27
- function registerTypeBoxFormatsWith(formatRegistry) {
28
- if (!formatRegistry.Has("uuid")) {
29
- formatRegistry.Set("uuid", isStrictUuid);
30
- }
31
-
32
- if (!formatRegistry.Has("iso-utc-date-time")) {
33
- formatRegistry.Set("iso-utc-date-time", isStrictIsoUtcDateTime);
34
- }
35
- }
36
-
37
- const __testables = {
38
- isStrictUuid,
39
- isStrictIsoUtcDateTime,
40
- registerTypeBoxFormatsWith
41
- };
42
-
43
- export { registerTypeBoxFormats, __testables };
@@ -1,42 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
-
4
- import { registerTypeBoxFormats, __testables } from "../src/shared/validators/typeboxFormats.js";
5
-
6
- test("strict uuid validator accepts canonical lowercase v4/v5 values", () => {
7
- assert.equal(__testables.isStrictUuid("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), true);
8
- assert.equal(__testables.isStrictUuid("aaaaaaaa-aaaa-5aaa-8aaa-aaaaaaaaaaaa"), true);
9
- assert.equal(__testables.isStrictUuid("AAAAAAAA-AAAA-4AAA-8AAA-AAAAAAAAAAAA"), false);
10
- assert.equal(__testables.isStrictUuid("not-a-uuid"), false);
11
- });
12
-
13
- test("strict iso utc date-time validator accepts only canonical millisecond UTC", () => {
14
- assert.equal(__testables.isStrictIsoUtcDateTime("2024-01-01T00:00:00.000Z"), true);
15
- assert.equal(__testables.isStrictIsoUtcDateTime("2024-01-01T00:00:00Z"), false);
16
- assert.equal(__testables.isStrictIsoUtcDateTime("2024-01-01T00:00:00.000+01:00"), false);
17
- assert.equal(__testables.isStrictIsoUtcDateTime("2024-02-30T00:00:00.000Z"), false);
18
- });
19
-
20
- test("registerTypeBoxFormatsWith only sets missing validators", () => {
21
- const setCalls = [];
22
- const registry = {
23
- existing: new Set(["uuid"]),
24
- Has(name) {
25
- return this.existing.has(name);
26
- },
27
- Set(name, fn) {
28
- setCalls.push([name, fn]);
29
- this.existing.add(name);
30
- }
31
- };
32
-
33
- __testables.registerTypeBoxFormatsWith(registry);
34
-
35
- assert.equal(setCalls.length, 1);
36
- assert.equal(setCalls[0][0], "iso-utc-date-time");
37
- });
38
-
39
- test("registerTypeBoxFormats is callable", () => {
40
- registerTypeBoxFormats();
41
- assert.equal(typeof registerTypeBoxFormats, "function");
42
- });