@jskit-ai/http-runtime 0.1.4
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 +83 -0
- package/package.json +24 -0
- package/src/client/index.js +14 -0
- package/src/client/providers/HttpClientRuntimeClientProvider.js +21 -0
- package/src/client/providers/HttpValidatorsClientProvider.js +14 -0
- package/src/client/validationErrors.js +23 -0
- package/src/server/providers/HttpClientRuntimeServiceProvider.js +21 -0
- package/src/server/providers/HttpValidatorsServiceProvider.js +14 -0
- package/src/shared/clientRuntime/client.js +632 -0
- package/src/shared/clientRuntime/errors.js +35 -0
- package/src/shared/clientRuntime/headers.js +28 -0
- package/src/shared/clientRuntime/index.js +4 -0
- package/src/shared/clientRuntime/retry.js +38 -0
- package/src/shared/index.js +30 -0
- package/src/shared/providers/singletonApiProvider.js +27 -0
- package/src/shared/support/fieldErrors.js +31 -0
- package/src/shared/validators/command.js +34 -0
- package/src/shared/validators/errorResponses.js +108 -0
- package/src/shared/validators/httpValidatorsApi.js +58 -0
- package/src/shared/validators/operationMessages.js +149 -0
- package/src/shared/validators/operationValidation.js +126 -0
- package/src/shared/validators/paginationQuery.js +32 -0
- package/src/shared/validators/resource.js +43 -0
- package/src/shared/validators/schemaUtils.js +9 -0
- package/src/shared/validators/typeboxFormats.js +43 -0
- package/test/client.test.js +246 -0
- package/test/command.test.js +49 -0
- package/test/entrypoints.boundary.test.js +36 -0
- package/test/errorResponses.test.js +84 -0
- package/test/operationMessages.test.js +93 -0
- package/test/operationValidation.test.js +137 -0
- package/test/paginationQuery.test.js +32 -0
- package/test/providerRuntime.httpClient.test.js +35 -0
- package/test/providerRuntime.validators.test.js +39 -0
- package/test/resource.test.js +94 -0
- package/test/retry.test.js +41 -0
- package/test/typeboxFormats.test.js +42 -0
- package/test/validationErrors.test.js +100 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { createPaginationQuerySchema } from "../src/shared/validators/paginationQuery.js";
|
|
5
|
+
|
|
6
|
+
test("createPaginationQuerySchema uses expected defaults", () => {
|
|
7
|
+
const schema = createPaginationQuerySchema();
|
|
8
|
+
|
|
9
|
+
assert.equal(schema.type, "object");
|
|
10
|
+
assert.equal(schema.additionalProperties, false);
|
|
11
|
+
assert.equal(schema.properties.page.minimum, 1);
|
|
12
|
+
assert.equal(schema.properties.page.default, 1);
|
|
13
|
+
assert.equal(schema.properties.pageSize.minimum, 1);
|
|
14
|
+
assert.equal(schema.properties.pageSize.maximum, 100);
|
|
15
|
+
assert.equal(schema.properties.pageSize.default, 10);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("createPaginationQuerySchema applies custom bounds and defaults", () => {
|
|
19
|
+
const schema = createPaginationQuerySchema({
|
|
20
|
+
defaultPage: 2,
|
|
21
|
+
defaultPageSize: 25,
|
|
22
|
+
minPage: 2,
|
|
23
|
+
minPageSize: 5,
|
|
24
|
+
maxPageSize: 250
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(schema.properties.page.minimum, 2);
|
|
28
|
+
assert.equal(schema.properties.page.default, 2);
|
|
29
|
+
assert.equal(schema.properties.pageSize.minimum, 5);
|
|
30
|
+
assert.equal(schema.properties.pageSize.maximum, 250);
|
|
31
|
+
assert.equal(schema.properties.pageSize.default, 25);
|
|
32
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { HttpClientRuntimeServiceProvider } from "../src/server/providers/HttpClientRuntimeServiceProvider.js";
|
|
5
|
+
import { HttpClientRuntimeClientProvider } from "../src/client/providers/HttpClientRuntimeClientProvider.js";
|
|
6
|
+
|
|
7
|
+
function createSingletonApp() {
|
|
8
|
+
const singletons = new Map();
|
|
9
|
+
return {
|
|
10
|
+
singletons,
|
|
11
|
+
singleton(token, factory) {
|
|
12
|
+
singletons.set(token, factory(this));
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("HttpClientRuntimeServiceProvider registers runtime http client api", () => {
|
|
18
|
+
const app = createSingletonApp();
|
|
19
|
+
const provider = new HttpClientRuntimeServiceProvider();
|
|
20
|
+
provider.register(app);
|
|
21
|
+
|
|
22
|
+
assert.equal(app.singletons.has("runtime.http-client"), true);
|
|
23
|
+
const api = app.singletons.get("runtime.http-client");
|
|
24
|
+
assert.equal(typeof api.createHttpClient, "function");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("HttpClientRuntimeClientProvider registers client http client api", () => {
|
|
28
|
+
const app = createSingletonApp();
|
|
29
|
+
const provider = new HttpClientRuntimeClientProvider();
|
|
30
|
+
provider.register(app);
|
|
31
|
+
|
|
32
|
+
assert.equal(app.singletons.has("runtime.http-client.client"), true);
|
|
33
|
+
const api = app.singletons.get("runtime.http-client.client");
|
|
34
|
+
assert.equal(typeof api.createHttpClient, "function");
|
|
35
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { HttpValidatorsServiceProvider } from "../src/server/providers/HttpValidatorsServiceProvider.js";
|
|
5
|
+
import { HttpValidatorsClientProvider } from "../src/client/providers/HttpValidatorsClientProvider.js";
|
|
6
|
+
|
|
7
|
+
function createSingletonApp() {
|
|
8
|
+
const singletons = new Map();
|
|
9
|
+
return {
|
|
10
|
+
singletons,
|
|
11
|
+
singleton(token, factory) {
|
|
12
|
+
singletons.set(token, factory(this));
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("HttpValidatorsServiceProvider registers shared validators api", () => {
|
|
18
|
+
const app = createSingletonApp();
|
|
19
|
+
const provider = new HttpValidatorsServiceProvider();
|
|
20
|
+
provider.register(app);
|
|
21
|
+
|
|
22
|
+
assert.equal(app.singletons.has("validators.http"), true);
|
|
23
|
+
const api = app.singletons.get("validators.http");
|
|
24
|
+
assert.equal(typeof api.withStandardErrorResponses, "function");
|
|
25
|
+
assert.equal(typeof api.createResource, "function");
|
|
26
|
+
assert.equal(typeof api.createCommand, "function");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("HttpValidatorsClientProvider registers client validators api", () => {
|
|
30
|
+
const app = createSingletonApp();
|
|
31
|
+
const provider = new HttpValidatorsClientProvider();
|
|
32
|
+
provider.register(app);
|
|
33
|
+
|
|
34
|
+
assert.equal(app.singletons.has("validators.http.client"), true);
|
|
35
|
+
const api = app.singletons.get("validators.http.client");
|
|
36
|
+
assert.equal(typeof api.withStandardErrorResponses, "function");
|
|
37
|
+
assert.equal(typeof api.createResource, "function");
|
|
38
|
+
assert.equal(typeof api.createCommand, "function");
|
|
39
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
4
|
+
import {
|
|
5
|
+
createCursorPagedListResponseSchema,
|
|
6
|
+
createResource
|
|
7
|
+
} from "../src/shared/validators/resource.js";
|
|
8
|
+
|
|
9
|
+
test("createCursorPagedListResponseSchema builds items + nextCursor schema", () => {
|
|
10
|
+
const itemSchema = Type.Object(
|
|
11
|
+
{
|
|
12
|
+
id: Type.Integer({ minimum: 1 })
|
|
13
|
+
},
|
|
14
|
+
{ additionalProperties: false }
|
|
15
|
+
);
|
|
16
|
+
const listSchema = createCursorPagedListResponseSchema(itemSchema);
|
|
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);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("createResource requires record/create/replace/patch schemas", () => {
|
|
25
|
+
assert.throws(
|
|
26
|
+
() => createResource({}),
|
|
27
|
+
/record must be a TypeBox schema object/
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
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 });
|
|
47
|
+
const resource = createResource({
|
|
48
|
+
record: recordSchema,
|
|
49
|
+
create: writeSchema,
|
|
50
|
+
replace: writeSchema,
|
|
51
|
+
patch: patchSchema
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
assert.equal(resource.list.properties.items.items.type, "object");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
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
|
+
);
|
|
84
|
+
|
|
85
|
+
const resource = createResource({
|
|
86
|
+
record: recordSchema,
|
|
87
|
+
create: writeSchema,
|
|
88
|
+
replace: writeSchema,
|
|
89
|
+
patch: patchSchema,
|
|
90
|
+
list: explicitListSchema
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
assert.equal(resource.list, explicitListSchema);
|
|
94
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { shouldRetryForCsrfFailure } from "../src/shared/clientRuntime/retry.js";
|
|
5
|
+
|
|
6
|
+
test("shouldRetryForCsrfFailure returns true only for unsafe csrf 403 before retry", () => {
|
|
7
|
+
const unsafeMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
8
|
+
|
|
9
|
+
assert.equal(
|
|
10
|
+
shouldRetryForCsrfFailure({
|
|
11
|
+
response: { status: 403 },
|
|
12
|
+
method: "POST",
|
|
13
|
+
state: { csrfRetried: false },
|
|
14
|
+
data: { details: { code: "FST_CSRF_INVALID_TOKEN" } },
|
|
15
|
+
unsafeMethods
|
|
16
|
+
}),
|
|
17
|
+
true
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
assert.equal(
|
|
21
|
+
shouldRetryForCsrfFailure({
|
|
22
|
+
response: { status: 403 },
|
|
23
|
+
method: "GET",
|
|
24
|
+
state: { csrfRetried: false },
|
|
25
|
+
data: { details: { code: "FST_CSRF_INVALID_TOKEN" } },
|
|
26
|
+
unsafeMethods
|
|
27
|
+
}),
|
|
28
|
+
false
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
assert.equal(
|
|
32
|
+
shouldRetryForCsrfFailure({
|
|
33
|
+
response: { status: 403 },
|
|
34
|
+
method: "POST",
|
|
35
|
+
state: { csrfRetried: true },
|
|
36
|
+
data: { details: { code: "FST_CSRF_INVALID_TOKEN" } },
|
|
37
|
+
unsafeMethods
|
|
38
|
+
}),
|
|
39
|
+
false
|
|
40
|
+
);
|
|
41
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
normalizeFieldErrors,
|
|
6
|
+
resolveFieldErrors,
|
|
7
|
+
createValidationFailure,
|
|
8
|
+
createHttpError
|
|
9
|
+
} from "../src/client/index.js";
|
|
10
|
+
|
|
11
|
+
test("normalizeFieldErrors trims keys and stringifies values", () => {
|
|
12
|
+
const normalized = normalizeFieldErrors({
|
|
13
|
+
" name ": "Required",
|
|
14
|
+
count: 3,
|
|
15
|
+
"": "skip-me",
|
|
16
|
+
" ": "skip-me-too"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
assert.deepEqual(normalized, {
|
|
20
|
+
name: "Required",
|
|
21
|
+
count: "3"
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("resolveFieldErrors reads top-level and nested details field errors", () => {
|
|
26
|
+
assert.deepEqual(
|
|
27
|
+
resolveFieldErrors({
|
|
28
|
+
fieldErrors: {
|
|
29
|
+
email: "Invalid email."
|
|
30
|
+
}
|
|
31
|
+
}),
|
|
32
|
+
{
|
|
33
|
+
email: "Invalid email."
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
assert.deepEqual(
|
|
38
|
+
resolveFieldErrors({
|
|
39
|
+
details: {
|
|
40
|
+
fieldErrors: {
|
|
41
|
+
password: "Too short."
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
{
|
|
46
|
+
password: "Too short."
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("createValidationFailure returns canonical validation envelope", () => {
|
|
52
|
+
const failure = createValidationFailure({
|
|
53
|
+
error: "Validation failed.",
|
|
54
|
+
code: "validation_failed",
|
|
55
|
+
fieldErrors: {
|
|
56
|
+
name: "Name is required."
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
assert.deepEqual(failure, {
|
|
61
|
+
error: "Validation failed.",
|
|
62
|
+
code: "validation_failed",
|
|
63
|
+
fieldErrors: {
|
|
64
|
+
name: "Name is required."
|
|
65
|
+
},
|
|
66
|
+
details: {
|
|
67
|
+
fieldErrors: {
|
|
68
|
+
name: "Name is required."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("createHttpError normalizes code and fieldErrors", () => {
|
|
75
|
+
const error = createHttpError(
|
|
76
|
+
{
|
|
77
|
+
status: 422
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
error: "Validation failed.",
|
|
81
|
+
code: "invalid_input",
|
|
82
|
+
details: {
|
|
83
|
+
fieldErrors: {
|
|
84
|
+
" email ": "Invalid email."
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
assert.equal(error.status, 422);
|
|
91
|
+
assert.equal(error.code, "invalid_input");
|
|
92
|
+
assert.deepEqual(error.fieldErrors, {
|
|
93
|
+
email: "Invalid email."
|
|
94
|
+
});
|
|
95
|
+
assert.deepEqual(error.details, {
|
|
96
|
+
fieldErrors: {
|
|
97
|
+
" email ": "Invalid email."
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|