@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.
- package/package.descriptor.mjs +2 -4
- package/package.json +5 -5
- package/src/shared/clientRuntime/client.js +126 -9
- package/src/shared/clientRuntime/errors.js +6 -0
- package/src/shared/clientRuntime/jsonApiResourceTransport.js +241 -0
- package/src/shared/index.js +54 -5
- package/src/shared/validators/command.js +5 -4
- package/src/shared/validators/errorResponses.js +125 -62
- package/src/shared/validators/httpValidatorsApi.js +83 -12
- package/src/shared/validators/jsonApiQueryTransport.js +211 -0
- package/src/shared/validators/jsonApiResponses.js +3 -0
- package/src/shared/validators/jsonApiResult.js +83 -0
- package/src/shared/validators/jsonApiRouteTransport.js +800 -0
- package/src/shared/validators/jsonApiTransport.js +484 -0
- package/src/shared/validators/operationValidation.js +62 -101
- package/src/shared/validators/paginationQuery.js +14 -19
- package/src/shared/validators/resource.js +15 -17
- package/src/shared/validators/schemaUtils.js +18 -5
- package/src/shared/validators/transportSchemaEmbedding.js +81 -0
- package/test/client.test.js +279 -0
- package/test/command.test.js +38 -21
- package/test/entrypoints.boundary.test.js +8 -0
- package/test/errorResponses.test.js +49 -13
- package/test/jsonApiRouteTransport.test.js +349 -0
- package/test/jsonApiTransport.test.js +231 -0
- package/test/operationMessages.test.js +115 -66
- package/test/operationValidation.test.js +147 -159
- package/test/paginationQuery.test.js +4 -8
- package/test/resource.test.js +89 -55
- package/test/validationErrors.test.js +33 -0
- package/src/shared/validators/typeboxFormats.js +0 -43
- package/test/typeboxFormats.test.js +0 -42
package/test/resource.test.js
CHANGED
|
@@ -1,86 +1,115 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import {
|
|
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 =
|
|
11
|
-
{
|
|
12
|
-
id:
|
|
13
|
-
}
|
|
14
|
-
|
|
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.
|
|
19
|
-
assert.equal(
|
|
20
|
-
assert.equal(
|
|
21
|
-
assert.equal(
|
|
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
|
|
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 =
|
|
33
|
-
{
|
|
34
|
-
id:
|
|
35
|
-
name:
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
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 =
|
|
59
|
-
{
|
|
60
|
-
id:
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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,
|
|
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
|
-
});
|