@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,38 @@
|
|
|
1
|
+
const DEFAULT_RETRYABLE_CSRF_ERROR_CODES = Object.freeze(["FST_CSRF_INVALID_TOKEN", "FST_CSRF_MISSING_SECRET"]);
|
|
2
|
+
|
|
3
|
+
function toUpperStringSet(values, fallback = []) {
|
|
4
|
+
const source = Array.isArray(values) ? values : fallback;
|
|
5
|
+
return new Set(
|
|
6
|
+
source
|
|
7
|
+
.map((value) =>
|
|
8
|
+
String(value || "")
|
|
9
|
+
.trim()
|
|
10
|
+
.toUpperCase()
|
|
11
|
+
)
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shouldRetryForCsrfFailure({
|
|
17
|
+
response,
|
|
18
|
+
method,
|
|
19
|
+
state,
|
|
20
|
+
data,
|
|
21
|
+
unsafeMethods,
|
|
22
|
+
retryableErrorCodes = DEFAULT_RETRYABLE_CSRF_ERROR_CODES
|
|
23
|
+
}) {
|
|
24
|
+
const methodValue = String(method || "")
|
|
25
|
+
.trim()
|
|
26
|
+
.toUpperCase();
|
|
27
|
+
if (Number(response?.status) !== 403 || !unsafeMethods?.has(methodValue) || state?.csrfRetried) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const code = String(data?.details?.code || "")
|
|
32
|
+
.trim()
|
|
33
|
+
.toUpperCase();
|
|
34
|
+
const retryableCodes = toUpperStringSet(retryableErrorCodes, DEFAULT_RETRYABLE_CSRF_ERROR_CODES);
|
|
35
|
+
return retryableCodes.has(code);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { DEFAULT_RETRYABLE_CSRF_ERROR_CODES, shouldRetryForCsrfFailure };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { createPaginationQuerySchema } from "./validators/paginationQuery.js";
|
|
2
|
+
export { registerTypeBoxFormats, __testables } from "./validators/typeboxFormats.js";
|
|
3
|
+
export {
|
|
4
|
+
fieldErrorsSchema,
|
|
5
|
+
apiErrorDetailsSchema,
|
|
6
|
+
apiErrorResponseSchema,
|
|
7
|
+
apiValidationErrorResponseSchema,
|
|
8
|
+
fastifyDefaultErrorResponseSchema,
|
|
9
|
+
STANDARD_ERROR_STATUS_CODES,
|
|
10
|
+
passthroughErrorResponses,
|
|
11
|
+
withStandardErrorResponses,
|
|
12
|
+
enumSchema
|
|
13
|
+
} from "./validators/errorResponses.js";
|
|
14
|
+
export {
|
|
15
|
+
createCursorPagedListResponseSchema,
|
|
16
|
+
createResource
|
|
17
|
+
} from "./validators/resource.js";
|
|
18
|
+
export { createCommand } from "./validators/command.js";
|
|
19
|
+
export {
|
|
20
|
+
resolveSchemaMessages,
|
|
21
|
+
resolveFieldSchema,
|
|
22
|
+
resolveIssueField,
|
|
23
|
+
resolveMissingRequiredFields,
|
|
24
|
+
resolveIssueMessageFromSchema,
|
|
25
|
+
mapOperationIssues
|
|
26
|
+
} from "./validators/operationMessages.js";
|
|
27
|
+
export {
|
|
28
|
+
validateOperationSection,
|
|
29
|
+
validateOperationInput
|
|
30
|
+
} from "./validators/operationValidation.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class SingletonApiProvider {
|
|
2
|
+
static bindingToken = "";
|
|
3
|
+
|
|
4
|
+
static api = null;
|
|
5
|
+
|
|
6
|
+
static providerName = "SingletonApiProvider";
|
|
7
|
+
|
|
8
|
+
register(app) {
|
|
9
|
+
if (!app || typeof app.singleton !== "function") {
|
|
10
|
+
throw new Error(`${this.constructor.providerName} requires application singleton().`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const bindingToken = String(this.constructor.bindingToken || "").trim();
|
|
14
|
+
if (!bindingToken) {
|
|
15
|
+
throw new Error(`${this.constructor.providerName} requires static bindingToken.`);
|
|
16
|
+
}
|
|
17
|
+
if (!this.constructor.api || typeof this.constructor.api !== "object") {
|
|
18
|
+
throw new Error(`${this.constructor.providerName} requires static api object.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
app.singleton(bindingToken, () => this.constructor.api);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
boot() {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { SingletonApiProvider };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function normalizeFieldErrors(value) {
|
|
4
|
+
const source = isRecord(value) ? value : {};
|
|
5
|
+
const normalized = {};
|
|
6
|
+
|
|
7
|
+
for (const [field, message] of Object.entries(source)) {
|
|
8
|
+
const normalizedField = String(field || "").trim();
|
|
9
|
+
if (!normalizedField) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
normalized[normalizedField] = String(message || "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveFieldErrors(value = null) {
|
|
19
|
+
const source = isRecord(value) ? value : {};
|
|
20
|
+
if (isRecord(source.fieldErrors)) {
|
|
21
|
+
return normalizeFieldErrors(source.fieldErrors);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (isRecord(source.details?.fieldErrors)) {
|
|
25
|
+
return normalizeFieldErrors(source.details.fieldErrors);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { isRecord, normalizeFieldErrors, resolveFieldErrors };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { asSchema } from "./schemaUtils.js";
|
|
2
|
+
|
|
3
|
+
function normalizeInvalidates(value) {
|
|
4
|
+
if (!Array.isArray(value)) {
|
|
5
|
+
return Object.freeze([]);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const normalized = value
|
|
9
|
+
.map((entry) => String(entry || "").trim())
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
|
|
12
|
+
return Object.freeze(Array.from(new Set(normalized)));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createCommand({
|
|
16
|
+
input,
|
|
17
|
+
output,
|
|
18
|
+
idempotent = null,
|
|
19
|
+
invalidates = []
|
|
20
|
+
} = {}) {
|
|
21
|
+
const command = {
|
|
22
|
+
input: asSchema(input, "input"),
|
|
23
|
+
output: asSchema(output, "output"),
|
|
24
|
+
invalidates: normalizeInvalidates(invalidates)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (typeof idempotent === "boolean") {
|
|
28
|
+
command.idempotent = idempotent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Object.freeze(command);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { createCommand };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
2
|
+
|
|
3
|
+
const fieldErrorsSchema = Type.Record(Type.String(), Type.String());
|
|
4
|
+
|
|
5
|
+
const apiErrorDetailsSchema = Type.Object(
|
|
6
|
+
{
|
|
7
|
+
fieldErrors: Type.Optional(fieldErrorsSchema)
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
additionalProperties: true
|
|
11
|
+
}
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const apiErrorResponseSchema = Type.Object(
|
|
15
|
+
{
|
|
16
|
+
error: Type.String({ minLength: 1 }),
|
|
17
|
+
code: Type.Optional(Type.String({ minLength: 1 })),
|
|
18
|
+
details: Type.Optional(apiErrorDetailsSchema),
|
|
19
|
+
fieldErrors: Type.Optional(fieldErrorsSchema)
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
additionalProperties: false
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const apiValidationErrorResponseSchema = Type.Object(
|
|
27
|
+
{
|
|
28
|
+
error: Type.String({ minLength: 1 }),
|
|
29
|
+
code: Type.Optional(Type.String({ minLength: 1 })),
|
|
30
|
+
fieldErrors: fieldErrorsSchema,
|
|
31
|
+
details: Type.Object(
|
|
32
|
+
{
|
|
33
|
+
fieldErrors: fieldErrorsSchema
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
additionalProperties: true
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
additionalProperties: false
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const fastifyDefaultErrorResponseSchema = Type.Object(
|
|
46
|
+
{
|
|
47
|
+
statusCode: Type.Integer({ minimum: 400, maximum: 599 }),
|
|
48
|
+
error: Type.String({ minLength: 1 }),
|
|
49
|
+
message: Type.String({ minLength: 1 }),
|
|
50
|
+
code: Type.Optional(Type.String({ minLength: 1 })),
|
|
51
|
+
details: Type.Optional(Type.Unknown()),
|
|
52
|
+
fieldErrors: Type.Optional(fieldErrorsSchema)
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
additionalProperties: true
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const STANDARD_ERROR_STATUS_CODES = [400, 401, 403, 404, 409, 422, 429, 500, 503];
|
|
60
|
+
|
|
61
|
+
function passthroughErrorResponses(successResponses) {
|
|
62
|
+
return successResponses;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function withStandardErrorResponses(successResponses, { includeValidation400 = false } = {}) {
|
|
66
|
+
const responses = {
|
|
67
|
+
...successResponses
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const statusCode of STANDARD_ERROR_STATUS_CODES) {
|
|
71
|
+
if (responses[statusCode]) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (statusCode === 400 && includeValidation400) {
|
|
76
|
+
responses[statusCode] = {
|
|
77
|
+
schema: Type.Union([
|
|
78
|
+
apiValidationErrorResponseSchema,
|
|
79
|
+
apiErrorResponseSchema,
|
|
80
|
+
fastifyDefaultErrorResponseSchema
|
|
81
|
+
])
|
|
82
|
+
};
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
responses[statusCode] = {
|
|
87
|
+
schema: Type.Union([apiErrorResponseSchema, fastifyDefaultErrorResponseSchema])
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return responses;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function enumSchema(values) {
|
|
95
|
+
return Type.Union(values.map((value) => Type.Literal(value)));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
fieldErrorsSchema,
|
|
100
|
+
apiErrorDetailsSchema,
|
|
101
|
+
apiErrorResponseSchema,
|
|
102
|
+
apiValidationErrorResponseSchema,
|
|
103
|
+
fastifyDefaultErrorResponseSchema,
|
|
104
|
+
STANDARD_ERROR_STATUS_CODES,
|
|
105
|
+
passthroughErrorResponses,
|
|
106
|
+
withStandardErrorResponses,
|
|
107
|
+
enumSchema
|
|
108
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createPaginationQuerySchema } from "./paginationQuery.js";
|
|
2
|
+
import { registerTypeBoxFormats, __testables } from "./typeboxFormats.js";
|
|
3
|
+
import {
|
|
4
|
+
fieldErrorsSchema,
|
|
5
|
+
apiErrorDetailsSchema,
|
|
6
|
+
apiErrorResponseSchema,
|
|
7
|
+
apiValidationErrorResponseSchema,
|
|
8
|
+
fastifyDefaultErrorResponseSchema,
|
|
9
|
+
STANDARD_ERROR_STATUS_CODES,
|
|
10
|
+
passthroughErrorResponses,
|
|
11
|
+
withStandardErrorResponses,
|
|
12
|
+
enumSchema
|
|
13
|
+
} from "./errorResponses.js";
|
|
14
|
+
import {
|
|
15
|
+
createCursorPagedListResponseSchema,
|
|
16
|
+
createResource
|
|
17
|
+
} from "./resource.js";
|
|
18
|
+
import { createCommand } from "./command.js";
|
|
19
|
+
import {
|
|
20
|
+
resolveSchemaMessages,
|
|
21
|
+
resolveFieldSchema,
|
|
22
|
+
resolveIssueField,
|
|
23
|
+
resolveMissingRequiredFields,
|
|
24
|
+
resolveIssueMessageFromSchema,
|
|
25
|
+
mapOperationIssues
|
|
26
|
+
} from "./operationMessages.js";
|
|
27
|
+
import {
|
|
28
|
+
validateOperationSection,
|
|
29
|
+
validateOperationInput
|
|
30
|
+
} from "./operationValidation.js";
|
|
31
|
+
|
|
32
|
+
const HTTP_VALIDATORS_API = Object.freeze({
|
|
33
|
+
createPaginationQuerySchema,
|
|
34
|
+
registerTypeBoxFormats,
|
|
35
|
+
__testables,
|
|
36
|
+
fieldErrorsSchema,
|
|
37
|
+
apiErrorDetailsSchema,
|
|
38
|
+
apiErrorResponseSchema,
|
|
39
|
+
apiValidationErrorResponseSchema,
|
|
40
|
+
fastifyDefaultErrorResponseSchema,
|
|
41
|
+
STANDARD_ERROR_STATUS_CODES,
|
|
42
|
+
passthroughErrorResponses,
|
|
43
|
+
withStandardErrorResponses,
|
|
44
|
+
enumSchema,
|
|
45
|
+
createCursorPagedListResponseSchema,
|
|
46
|
+
createResource,
|
|
47
|
+
createCommand,
|
|
48
|
+
resolveSchemaMessages,
|
|
49
|
+
resolveFieldSchema,
|
|
50
|
+
resolveIssueField,
|
|
51
|
+
resolveMissingRequiredFields,
|
|
52
|
+
resolveIssueMessageFromSchema,
|
|
53
|
+
mapOperationIssues,
|
|
54
|
+
validateOperationSection,
|
|
55
|
+
validateOperationInput
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export { HTTP_VALIDATORS_API };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { isRecord, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function resolveIssueField(issue = {}) {
|
|
4
|
+
const instancePath = normalizeText(issue.instancePath);
|
|
5
|
+
if (instancePath) {
|
|
6
|
+
const segments = instancePath
|
|
7
|
+
.replace(/^\//, "")
|
|
8
|
+
.split("/")
|
|
9
|
+
.map((entry) => normalizeText(entry))
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
|
|
12
|
+
if (segments.length > 0) {
|
|
13
|
+
return segments[0];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const params = isRecord(issue.params) ? issue.params : {};
|
|
18
|
+
const missingProperty = normalizeText(params.missingProperty);
|
|
19
|
+
if (missingProperty) {
|
|
20
|
+
return missingProperty;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const requiredProperties = Array.isArray(params.requiredProperties)
|
|
24
|
+
? params.requiredProperties
|
|
25
|
+
: [];
|
|
26
|
+
if (requiredProperties.length > 0) {
|
|
27
|
+
return normalizeText(requiredProperties[0]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const additionalProperties = Array.isArray(params.additionalProperties)
|
|
31
|
+
? params.additionalProperties
|
|
32
|
+
: [];
|
|
33
|
+
if (additionalProperties.length > 0) {
|
|
34
|
+
return normalizeText(additionalProperties[0]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const additionalProperty = normalizeText(params.additionalProperty);
|
|
38
|
+
if (additionalProperty) {
|
|
39
|
+
return additionalProperty;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveMissingRequiredFields(issue = {}) {
|
|
46
|
+
const params = isRecord(issue.params) ? issue.params : {};
|
|
47
|
+
const requiredProperties = Array.isArray(params.requiredProperties)
|
|
48
|
+
? params.requiredProperties
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
return requiredProperties
|
|
52
|
+
.map((entry) => normalizeText(entry))
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveSchemaMessages(schema = {}) {
|
|
57
|
+
const source = isRecord(schema) ? schema : {};
|
|
58
|
+
const messages = source.messages;
|
|
59
|
+
return isRecord(messages) ? messages : {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveFieldSchema(schema = {}, field = "") {
|
|
63
|
+
const source = isRecord(schema) ? schema : {};
|
|
64
|
+
const properties = isRecord(source.properties) ? source.properties : {};
|
|
65
|
+
const fieldSchema = properties[field];
|
|
66
|
+
return isRecord(fieldSchema) ? fieldSchema : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveIssueMessageFromSchema(field, issue, schema = {}) {
|
|
70
|
+
const keyword = normalizeText(issue?.keyword);
|
|
71
|
+
|
|
72
|
+
if (field) {
|
|
73
|
+
const fieldSchema = resolveFieldSchema(schema, field);
|
|
74
|
+
const fieldMessages = resolveSchemaMessages(fieldSchema);
|
|
75
|
+
|
|
76
|
+
const fieldKeywordMessage = normalizeText(fieldMessages[keyword]);
|
|
77
|
+
if (fieldKeywordMessage) {
|
|
78
|
+
return fieldKeywordMessage;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const fieldDefaultMessage = normalizeText(fieldMessages.default);
|
|
82
|
+
if (fieldDefaultMessage) {
|
|
83
|
+
return fieldDefaultMessage;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const schemaMessages = resolveSchemaMessages(schema);
|
|
88
|
+
const schemaKeywordMessage = normalizeText(schemaMessages[keyword]);
|
|
89
|
+
if (schemaKeywordMessage) {
|
|
90
|
+
return schemaKeywordMessage;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const schemaDefaultMessage = normalizeText(schemaMessages.default);
|
|
94
|
+
if (schemaDefaultMessage) {
|
|
95
|
+
return schemaDefaultMessage;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const issueMessage = normalizeText(issue?.message);
|
|
99
|
+
if (issueMessage) {
|
|
100
|
+
return issueMessage;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return "Invalid value.";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mapOperationIssues(issues = [], schema = {}) {
|
|
107
|
+
const source = Array.isArray(issues) ? issues : [];
|
|
108
|
+
const fieldErrors = {};
|
|
109
|
+
const globalErrors = [];
|
|
110
|
+
|
|
111
|
+
for (const issue of source) {
|
|
112
|
+
const missingRequiredFields = resolveMissingRequiredFields(issue);
|
|
113
|
+
if (missingRequiredFields.length > 0) {
|
|
114
|
+
for (const field of missingRequiredFields) {
|
|
115
|
+
if (Object.hasOwn(fieldErrors, field)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fieldErrors[field] = resolveIssueMessageFromSchema(field, issue, schema);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const field = resolveIssueField(issue);
|
|
126
|
+
if (field) {
|
|
127
|
+
if (!Object.hasOwn(fieldErrors, field)) {
|
|
128
|
+
fieldErrors[field] = resolveIssueMessageFromSchema(field, issue, schema);
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
globalErrors.push(resolveIssueMessageFromSchema("", issue, schema));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
fieldErrors,
|
|
138
|
+
globalErrors
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
resolveSchemaMessages,
|
|
144
|
+
resolveFieldSchema,
|
|
145
|
+
resolveIssueField,
|
|
146
|
+
resolveMissingRequiredFields,
|
|
147
|
+
resolveIssueMessageFromSchema,
|
|
148
|
+
mapOperationIssues
|
|
149
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Check, Errors } from "typebox/value";
|
|
2
|
+
import { mapOperationIssues } from "./operationMessages.js";
|
|
3
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
|
|
5
|
+
function defaultNormalize(value) {
|
|
6
|
+
if (!isRecord(value)) {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
...value
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolveOperationSection(operation = {}, section = "bodyValidator") {
|
|
16
|
+
const source = isRecord(operation) ? operation : {};
|
|
17
|
+
const value = source[section];
|
|
18
|
+
if (!isRecord(value)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateOperationSection({
|
|
26
|
+
operation = {},
|
|
27
|
+
section = "bodyValidator",
|
|
28
|
+
value,
|
|
29
|
+
context = {}
|
|
30
|
+
} = {}) {
|
|
31
|
+
const sectionDefinition = resolveOperationSection(operation, section);
|
|
32
|
+
if (!sectionDefinition) {
|
|
33
|
+
return {
|
|
34
|
+
ok: true,
|
|
35
|
+
value,
|
|
36
|
+
normalized: value,
|
|
37
|
+
fieldErrors: {},
|
|
38
|
+
globalErrors: [],
|
|
39
|
+
issues: []
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const schema = sectionDefinition.schema;
|
|
44
|
+
if (!isRecord(schema)) {
|
|
45
|
+
throw new TypeError(`Operation section \"${section}\" requires a schema object.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const normalize = typeof sectionDefinition.normalize === "function"
|
|
49
|
+
? sectionDefinition.normalize
|
|
50
|
+
: defaultNormalize;
|
|
51
|
+
|
|
52
|
+
const normalized = normalize(value, context);
|
|
53
|
+
const issues = Check(schema, normalized) ? [] : [...Errors(schema, normalized)];
|
|
54
|
+
const mapped = mapOperationIssues(issues, schema);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ok: issues.length < 1,
|
|
58
|
+
value: issues.length < 1 ? normalized : null,
|
|
59
|
+
normalized,
|
|
60
|
+
fieldErrors: mapped.fieldErrors,
|
|
61
|
+
globalErrors: mapped.globalErrors,
|
|
62
|
+
issues
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateOperationInput({
|
|
67
|
+
operation = {},
|
|
68
|
+
input = {},
|
|
69
|
+
context = {}
|
|
70
|
+
} = {}) {
|
|
71
|
+
const source = isRecord(input) ? input : {};
|
|
72
|
+
const sectionResults = {
|
|
73
|
+
paramsValidator: validateOperationSection({
|
|
74
|
+
operation,
|
|
75
|
+
section: "paramsValidator",
|
|
76
|
+
value: source.params,
|
|
77
|
+
context
|
|
78
|
+
}),
|
|
79
|
+
queryValidator: validateOperationSection({
|
|
80
|
+
operation,
|
|
81
|
+
section: "queryValidator",
|
|
82
|
+
value: source.query,
|
|
83
|
+
context
|
|
84
|
+
}),
|
|
85
|
+
bodyValidator: validateOperationSection({
|
|
86
|
+
operation,
|
|
87
|
+
section: "bodyValidator",
|
|
88
|
+
value: source.body,
|
|
89
|
+
context
|
|
90
|
+
})
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const fieldErrors = {
|
|
94
|
+
...sectionResults.paramsValidator.fieldErrors,
|
|
95
|
+
...sectionResults.queryValidator.fieldErrors,
|
|
96
|
+
...sectionResults.bodyValidator.fieldErrors
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const globalErrors = [
|
|
100
|
+
...sectionResults.paramsValidator.globalErrors,
|
|
101
|
+
...sectionResults.queryValidator.globalErrors,
|
|
102
|
+
...sectionResults.bodyValidator.globalErrors
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
ok: sectionResults.paramsValidator.ok && sectionResults.queryValidator.ok && sectionResults.bodyValidator.ok,
|
|
107
|
+
value: {
|
|
108
|
+
params: sectionResults.paramsValidator.value,
|
|
109
|
+
query: sectionResults.queryValidator.value,
|
|
110
|
+
body: sectionResults.bodyValidator.value
|
|
111
|
+
},
|
|
112
|
+
normalized: {
|
|
113
|
+
params: sectionResults.paramsValidator.normalized,
|
|
114
|
+
query: sectionResults.queryValidator.normalized,
|
|
115
|
+
body: sectionResults.bodyValidator.normalized
|
|
116
|
+
},
|
|
117
|
+
fieldErrors,
|
|
118
|
+
globalErrors,
|
|
119
|
+
sections: sectionResults
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
validateOperationSection,
|
|
125
|
+
validateOperationInput
|
|
126
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
2
|
+
|
|
3
|
+
function createPaginationQuerySchema({
|
|
4
|
+
defaultPage = 1,
|
|
5
|
+
defaultPageSize = 10,
|
|
6
|
+
minPage = 1,
|
|
7
|
+
minPageSize = 1,
|
|
8
|
+
maxPageSize = 100
|
|
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
|
+
)
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
additionalProperties: false
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { createPaginationQuerySchema };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
2
|
+
import { asSchema } from "./schemaUtils.js";
|
|
3
|
+
|
|
4
|
+
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
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createResource({
|
|
16
|
+
record,
|
|
17
|
+
create,
|
|
18
|
+
replace,
|
|
19
|
+
patch,
|
|
20
|
+
list = null,
|
|
21
|
+
listItem = null
|
|
22
|
+
} = {}) {
|
|
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);
|
|
29
|
+
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
record: normalizedRecordSchema,
|
|
32
|
+
create: normalizedCreateSchema,
|
|
33
|
+
replace: normalizedReplaceSchema,
|
|
34
|
+
patch: normalizedPatchSchema,
|
|
35
|
+
listItem: normalizedListItemSchema,
|
|
36
|
+
list: normalizedListSchema
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
createCursorPagedListResponseSchema,
|
|
42
|
+
createResource
|
|
43
|
+
};
|