@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
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { Type } from "@fastify/type-provider-typebox";
|
|
4
|
-
import { Errors } from "typebox/value";
|
|
5
3
|
import {
|
|
6
4
|
mapOperationIssues,
|
|
7
5
|
resolveFieldSchema,
|
|
@@ -10,62 +8,72 @@ import {
|
|
|
10
8
|
resolveSchemaMessages
|
|
11
9
|
} from "../src/shared/validators/operationMessages.js";
|
|
12
10
|
|
|
13
|
-
const sampleSchema =
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
const sampleSchema = {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
name: {
|
|
15
|
+
type: "string",
|
|
16
16
|
minLength: 1,
|
|
17
17
|
messages: {
|
|
18
18
|
required: "Workspace name is required.",
|
|
19
19
|
minLength: "Workspace name is required.",
|
|
20
20
|
default: "Invalid workspace name."
|
|
21
21
|
}
|
|
22
|
-
}
|
|
23
|
-
color:
|
|
22
|
+
},
|
|
23
|
+
color: {
|
|
24
|
+
type: "string",
|
|
24
25
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
25
26
|
messages: {
|
|
26
27
|
pattern: "Workspace color must be a hex value."
|
|
27
28
|
}
|
|
28
|
-
}
|
|
29
|
-
invitesEnabled:
|
|
29
|
+
},
|
|
30
|
+
invitesEnabled: {
|
|
31
|
+
type: "boolean",
|
|
30
32
|
messages: {
|
|
31
33
|
default: "invitesEnabled must be true or false."
|
|
32
34
|
}
|
|
33
|
-
})
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
additionalProperties: false,
|
|
37
|
-
messages: {
|
|
38
|
-
additionalProperties: "Unexpected field."
|
|
39
35
|
}
|
|
36
|
+
},
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
messages: {
|
|
39
|
+
additionalProperties: "Unexpected field."
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
};
|
|
42
42
|
|
|
43
43
|
test("resolveIssueField resolves missing and nested fields", () => {
|
|
44
|
-
const
|
|
45
|
-
|
|
44
|
+
const requiredIssue = {
|
|
45
|
+
keyword: "required",
|
|
46
|
+
params: {
|
|
47
|
+
missingProperty: "name",
|
|
48
|
+
requiredProperties: ["name"]
|
|
49
|
+
}
|
|
50
|
+
};
|
|
46
51
|
|
|
47
52
|
assert.equal(resolveIssueField(requiredIssue), "name");
|
|
48
53
|
assert.deepEqual(resolveMissingRequiredFields(requiredIssue), ["name"]);
|
|
49
54
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
displayName: Type.String({ minLength: 1 })
|
|
55
|
-
},
|
|
56
|
-
{ additionalProperties: false }
|
|
57
|
-
)
|
|
58
|
-
},
|
|
59
|
-
{ additionalProperties: false }
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const nestedIssues = [...Errors(nestedSchema, { profile: { displayName: "" } })];
|
|
63
|
-
const minLengthIssue = nestedIssues.find((entry) => entry.keyword === "minLength");
|
|
55
|
+
const minLengthIssue = {
|
|
56
|
+
keyword: "minLength",
|
|
57
|
+
instancePath: "/profile/displayName"
|
|
58
|
+
};
|
|
64
59
|
assert.equal(resolveIssueField(minLengthIssue), "profile");
|
|
65
60
|
});
|
|
66
61
|
|
|
67
62
|
test("mapOperationIssues applies field message overrides by keyword", () => {
|
|
68
|
-
const issues = [
|
|
63
|
+
const issues = [
|
|
64
|
+
{
|
|
65
|
+
keyword: "minLength",
|
|
66
|
+
instancePath: "/name"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
keyword: "pattern",
|
|
70
|
+
instancePath: "/color"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
keyword: "type",
|
|
74
|
+
instancePath: "/invitesEnabled"
|
|
75
|
+
}
|
|
76
|
+
];
|
|
69
77
|
const mapped = mapOperationIssues(issues, sampleSchema);
|
|
70
78
|
|
|
71
79
|
assert.equal(mapped.fieldErrors.name, "Workspace name is required.");
|
|
@@ -75,36 +83,54 @@ test("mapOperationIssues applies field message overrides by keyword", () => {
|
|
|
75
83
|
});
|
|
76
84
|
|
|
77
85
|
test("mapOperationIssues falls back to keyword/global messages", () => {
|
|
78
|
-
const issues = [
|
|
86
|
+
const issues = [
|
|
87
|
+
{
|
|
88
|
+
keyword: "additionalProperties",
|
|
89
|
+
params: {
|
|
90
|
+
additionalProperty: "extra"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
];
|
|
79
94
|
const mapped = mapOperationIssues(issues, sampleSchema);
|
|
80
95
|
|
|
81
96
|
assert.equal(mapped.fieldErrors.extra, "Unexpected field.");
|
|
82
97
|
});
|
|
83
98
|
|
|
84
99
|
test("mapOperationIssues maps conditional schema failures to field errors", () => {
|
|
85
|
-
const conditionalSchema =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
const conditionalSchema = {
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
isVaccinated: {
|
|
104
|
+
type: "boolean"
|
|
105
|
+
},
|
|
106
|
+
adenovirusValidTo: {
|
|
107
|
+
type: "string"
|
|
108
|
+
}
|
|
89
109
|
},
|
|
90
|
-
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const: true
|
|
95
|
-
}
|
|
110
|
+
if: {
|
|
111
|
+
properties: {
|
|
112
|
+
isVaccinated: {
|
|
113
|
+
const: true
|
|
96
114
|
}
|
|
97
|
-
},
|
|
98
|
-
then: {
|
|
99
|
-
required: ["adenovirusValidTo"]
|
|
100
|
-
},
|
|
101
|
-
messages: {
|
|
102
|
-
if: "Adenovirus valid-to date is required when vaccinated."
|
|
103
115
|
}
|
|
116
|
+
},
|
|
117
|
+
then: {
|
|
118
|
+
required: ["adenovirusValidTo"]
|
|
119
|
+
},
|
|
120
|
+
messages: {
|
|
121
|
+
if: "Adenovirus valid-to date is required when vaccinated."
|
|
104
122
|
}
|
|
105
|
-
|
|
123
|
+
};
|
|
106
124
|
|
|
107
|
-
const issues = [
|
|
125
|
+
const issues = [
|
|
126
|
+
{
|
|
127
|
+
keyword: "if",
|
|
128
|
+
schemaPath: "#",
|
|
129
|
+
params: {
|
|
130
|
+
failingKeyword: "then"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
];
|
|
108
134
|
const mapped = mapOperationIssues(issues, conditionalSchema);
|
|
109
135
|
|
|
110
136
|
assert.equal(mapped.fieldErrors.adenovirusValidTo, "Adenovirus valid-to date is required when vaccinated.");
|
|
@@ -112,24 +138,47 @@ test("mapOperationIssues maps conditional schema failures to field errors", () =
|
|
|
112
138
|
});
|
|
113
139
|
|
|
114
140
|
test("mapOperationIssues suppresses redundant root anyOf global issue when field errors exist", () => {
|
|
115
|
-
const unionSchema =
|
|
116
|
-
|
|
141
|
+
const unionSchema = {
|
|
142
|
+
anyOf: [
|
|
117
143
|
{
|
|
118
|
-
|
|
119
|
-
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
kind: {
|
|
147
|
+
const: "dog"
|
|
148
|
+
},
|
|
149
|
+
bark: {
|
|
150
|
+
type: "string",
|
|
151
|
+
minLength: 1
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
additionalProperties: false
|
|
120
155
|
},
|
|
121
|
-
{ additionalProperties: false }
|
|
122
|
-
),
|
|
123
|
-
Type.Object(
|
|
124
156
|
{
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
157
|
+
type: "object",
|
|
158
|
+
properties: {
|
|
159
|
+
kind: {
|
|
160
|
+
const: "cat"
|
|
161
|
+
},
|
|
162
|
+
meow: {
|
|
163
|
+
type: "string",
|
|
164
|
+
minLength: 1
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
additionalProperties: false
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
};
|
|
131
171
|
|
|
132
|
-
const issues = [
|
|
172
|
+
const issues = [
|
|
173
|
+
{
|
|
174
|
+
keyword: "minLength",
|
|
175
|
+
instancePath: "/bark"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
keyword: "anyOf",
|
|
179
|
+
schemaPath: "#"
|
|
180
|
+
}
|
|
181
|
+
];
|
|
133
182
|
const mapped = mapOperationIssues(issues, unionSchema);
|
|
134
183
|
|
|
135
184
|
assert.equal(typeof mapped.fieldErrors.bark, "string");
|
|
@@ -1,67 +1,46 @@
|
|
|
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
5
|
import {
|
|
5
6
|
validateOperationInput,
|
|
6
7
|
validateOperationSection
|
|
7
8
|
} from "../src/shared/validators/operationValidation.js";
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const patchOperation = Object.freeze({
|
|
11
|
+
method: "PATCH",
|
|
12
|
+
body: {
|
|
13
|
+
schema: createSchema({
|
|
14
|
+
name: {
|
|
15
|
+
type: "string",
|
|
13
16
|
minLength: 1,
|
|
14
17
|
maxLength: 160,
|
|
15
18
|
messages: {
|
|
16
19
|
minLength: "Workspace name is required."
|
|
17
20
|
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Type.String({
|
|
21
|
+
},
|
|
22
|
+
color: {
|
|
23
|
+
type: "string",
|
|
22
24
|
pattern: "^#[0-9A-Fa-f]{6}$",
|
|
23
25
|
messages: {
|
|
24
26
|
pattern: "Workspace color must be a hex value."
|
|
25
27
|
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
messages: {
|
|
34
|
-
additionalProperties: "Unexpected field."
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const patchOperation = Object.freeze({
|
|
40
|
-
method: "PATCH",
|
|
41
|
-
bodyValidator: {
|
|
42
|
-
schema: patchSchema,
|
|
43
|
-
normalize: (value) => {
|
|
44
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
45
|
-
return {};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const normalized = {
|
|
49
|
-
...value
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
if (Object.hasOwn(normalized, "name")) {
|
|
53
|
-
normalized.name = String(normalized.name || "").trim();
|
|
28
|
+
},
|
|
29
|
+
invitesEnabled: {
|
|
30
|
+
type: "boolean",
|
|
31
|
+
strictBoolean: true,
|
|
32
|
+
messages: {
|
|
33
|
+
default: "invitesEnabled must be a boolean."
|
|
34
|
+
}
|
|
54
35
|
}
|
|
55
|
-
|
|
56
|
-
return normalized;
|
|
57
|
-
}
|
|
36
|
+
})
|
|
58
37
|
}
|
|
59
38
|
});
|
|
60
39
|
|
|
61
|
-
test("validateOperationSection
|
|
40
|
+
test("validateOperationSection validates one section and returns normalized json-rest-schema output", () => {
|
|
62
41
|
const parsed = validateOperationSection({
|
|
63
42
|
operation: patchOperation,
|
|
64
|
-
section: "
|
|
43
|
+
section: "body",
|
|
65
44
|
value: {
|
|
66
45
|
name: " Acme ",
|
|
67
46
|
color: "#0F6B54"
|
|
@@ -76,7 +55,7 @@ test("validateOperationSection normalizes and validates one section", () => {
|
|
|
76
55
|
test("validateOperationSection returns shared field errors", () => {
|
|
77
56
|
const parsed = validateOperationSection({
|
|
78
57
|
operation: patchOperation,
|
|
79
|
-
section: "
|
|
58
|
+
section: "body",
|
|
80
59
|
value: {
|
|
81
60
|
name: "",
|
|
82
61
|
color: "bad",
|
|
@@ -87,135 +66,129 @@ test("validateOperationSection returns shared field errors", () => {
|
|
|
87
66
|
assert.equal(parsed.ok, false);
|
|
88
67
|
assert.equal(parsed.fieldErrors.name, "Workspace name is required.");
|
|
89
68
|
assert.equal(parsed.fieldErrors.color, "Workspace color must be a hex value.");
|
|
90
|
-
assert.equal(parsed.fieldErrors.rogueField, "
|
|
69
|
+
assert.equal(typeof parsed.fieldErrors.rogueField, "string");
|
|
91
70
|
});
|
|
92
71
|
|
|
93
|
-
test("validateOperationSection
|
|
94
|
-
const
|
|
95
|
-
method: "
|
|
96
|
-
|
|
97
|
-
schema:
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
72
|
+
test("validateOperationSection honors json-rest-schema field message overrides", () => {
|
|
73
|
+
const operation = Object.freeze({
|
|
74
|
+
method: "POST",
|
|
75
|
+
body: {
|
|
76
|
+
schema: createSchema({
|
|
77
|
+
name: {
|
|
78
|
+
type: "string",
|
|
79
|
+
required: true,
|
|
80
|
+
messages: {
|
|
81
|
+
required: "Workspace name is required."
|
|
82
|
+
}
|
|
102
83
|
},
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
84
|
+
invitesEnabled: {
|
|
85
|
+
type: "boolean",
|
|
86
|
+
required: true,
|
|
87
|
+
strictBoolean: true,
|
|
88
|
+
messages: {
|
|
89
|
+
default: "invitesEnabled must be a boolean."
|
|
90
|
+
}
|
|
110
91
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
92
|
+
}),
|
|
93
|
+
mode: "create"
|
|
114
94
|
}
|
|
115
95
|
});
|
|
116
96
|
|
|
117
|
-
const
|
|
118
|
-
operation
|
|
119
|
-
section: "
|
|
97
|
+
const missingFieldParsed = validateOperationSection({
|
|
98
|
+
operation,
|
|
99
|
+
section: "body",
|
|
100
|
+
value: {}
|
|
101
|
+
});
|
|
102
|
+
assert.equal(missingFieldParsed.ok, false);
|
|
103
|
+
assert.equal(missingFieldParsed.fieldErrors.name, "Workspace name is required.");
|
|
104
|
+
|
|
105
|
+
const strictBooleanParsed = validateOperationSection({
|
|
106
|
+
operation,
|
|
107
|
+
section: "body",
|
|
120
108
|
value: {
|
|
121
|
-
|
|
109
|
+
name: "Acme",
|
|
110
|
+
invitesEnabled: "yes"
|
|
122
111
|
}
|
|
123
112
|
});
|
|
124
|
-
|
|
125
|
-
assert.equal(
|
|
126
|
-
assert.equal(typeof parsed.fieldErrors.temperament, "string");
|
|
113
|
+
assert.equal(strictBooleanParsed.ok, false);
|
|
114
|
+
assert.equal(strictBooleanParsed.fieldErrors.invitesEnabled, "invitesEnabled must be a boolean.");
|
|
127
115
|
});
|
|
128
116
|
|
|
129
|
-
test("validateOperationSection
|
|
130
|
-
const
|
|
117
|
+
test("validateOperationSection returns field errors for invalid enum values", () => {
|
|
118
|
+
const operationWithEnumConstraint = Object.freeze({
|
|
131
119
|
method: "PATCH",
|
|
132
|
-
|
|
133
|
-
schema:
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
photoUpdatedAt: Type.Union([
|
|
139
|
-
Type.String({
|
|
140
|
-
format: "date-time",
|
|
141
|
-
minLength: 1
|
|
142
|
-
}),
|
|
143
|
-
Type.Null()
|
|
144
|
-
]),
|
|
145
|
-
adenovirusValidTo: Type.Union([
|
|
146
|
-
Type.String({
|
|
147
|
-
format: "date",
|
|
148
|
-
minLength: 1
|
|
149
|
-
}),
|
|
150
|
-
Type.Null()
|
|
151
|
-
])
|
|
152
|
-
},
|
|
153
|
-
{
|
|
154
|
-
additionalProperties: false
|
|
120
|
+
body: {
|
|
121
|
+
schema: createSchema({
|
|
122
|
+
temperament: {
|
|
123
|
+
type: "string",
|
|
124
|
+
enum: ["calm", "playful"],
|
|
125
|
+
required: true
|
|
155
126
|
}
|
|
156
|
-
),
|
|
157
|
-
|
|
158
|
-
const error = new Error("Invalid pet temperament \"unknowne\".");
|
|
159
|
-
error.details = {
|
|
160
|
-
fieldErrors: {
|
|
161
|
-
temperament: "Invalid pet temperament \"unknowne\"."
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
throw error;
|
|
165
|
-
}
|
|
127
|
+
}),
|
|
128
|
+
mode: "patch"
|
|
166
129
|
}
|
|
167
130
|
});
|
|
168
131
|
|
|
169
132
|
const parsed = validateOperationSection({
|
|
170
|
-
operation:
|
|
171
|
-
section: "
|
|
133
|
+
operation: operationWithEnumConstraint,
|
|
134
|
+
section: "body",
|
|
172
135
|
value: {
|
|
173
|
-
temperament: "unknowne"
|
|
174
|
-
photoUpdatedAt: "",
|
|
175
|
-
adenovirusValidTo: ""
|
|
136
|
+
temperament: "unknowne"
|
|
176
137
|
}
|
|
177
138
|
});
|
|
178
139
|
|
|
179
140
|
assert.equal(parsed.ok, false);
|
|
180
|
-
assert.
|
|
181
|
-
temperament: "Invalid pet temperament \"unknowne\"."
|
|
182
|
-
});
|
|
183
|
-
assert.deepEqual(parsed.globalErrors, []);
|
|
141
|
+
assert.equal(typeof parsed.fieldErrors.temperament, "string");
|
|
184
142
|
});
|
|
185
143
|
|
|
186
|
-
test("validateOperationSection
|
|
144
|
+
test("validateOperationSection rethrows malformed operation contracts", () => {
|
|
145
|
+
assert.throws(
|
|
146
|
+
() =>
|
|
147
|
+
validateOperationSection({
|
|
148
|
+
operation: {
|
|
149
|
+
body: {
|
|
150
|
+
schema: null,
|
|
151
|
+
mode: "patch"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
section: "body",
|
|
155
|
+
value: {}
|
|
156
|
+
}),
|
|
157
|
+
/must be a json-rest-schema schema instance/
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("validateOperationSection surfaces custom validator failures as field errors", () => {
|
|
187
162
|
const operationWithConditionalConstraint = Object.freeze({
|
|
188
163
|
method: "PATCH",
|
|
189
|
-
|
|
190
|
-
schema:
|
|
191
|
-
{
|
|
192
|
-
|
|
193
|
-
|
|
164
|
+
body: {
|
|
165
|
+
schema: createSchema({
|
|
166
|
+
isVaccinated: {
|
|
167
|
+
type: "boolean",
|
|
168
|
+
strictBoolean: true,
|
|
169
|
+
required: false
|
|
194
170
|
},
|
|
195
|
-
{
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
171
|
+
adenovirusValidTo: {
|
|
172
|
+
type: "string",
|
|
173
|
+
required: false,
|
|
174
|
+
validator(value, object = {}) {
|
|
175
|
+
if (object.isVaccinated === true && !String(value || "").trim()) {
|
|
176
|
+
return "Adenovirus valid-to date is required when vaccinated.";
|
|
201
177
|
}
|
|
202
|
-
|
|
203
|
-
then: {
|
|
204
|
-
required: ["adenovirusValidTo"]
|
|
205
|
-
},
|
|
206
|
-
messages: {
|
|
207
|
-
if: "Adenovirus valid-to date is required when vaccinated."
|
|
178
|
+
return undefined;
|
|
208
179
|
}
|
|
209
180
|
}
|
|
210
|
-
)
|
|
181
|
+
}),
|
|
182
|
+
mode: "patch"
|
|
211
183
|
}
|
|
212
184
|
});
|
|
213
185
|
|
|
214
186
|
const parsed = validateOperationSection({
|
|
215
187
|
operation: operationWithConditionalConstraint,
|
|
216
|
-
section: "
|
|
188
|
+
section: "body",
|
|
217
189
|
value: {
|
|
218
|
-
isVaccinated: true
|
|
190
|
+
isVaccinated: true,
|
|
191
|
+
adenovirusValidTo: ""
|
|
219
192
|
}
|
|
220
193
|
});
|
|
221
194
|
|
|
@@ -227,30 +200,17 @@ test("validateOperationSection maps conditional validation failures to field err
|
|
|
227
200
|
test("validateOperationInput validates params/query/body together", () => {
|
|
228
201
|
const viewOperation = Object.freeze({
|
|
229
202
|
method: "GET",
|
|
230
|
-
|
|
231
|
-
schema:
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
{ additionalProperties: false }
|
|
236
|
-
)
|
|
203
|
+
params: {
|
|
204
|
+
schema: createSchema({
|
|
205
|
+
workspaceSlug: { type: "string", required: true, minLength: 1 }
|
|
206
|
+
}),
|
|
207
|
+
mode: "patch"
|
|
237
208
|
},
|
|
238
|
-
|
|
239
|
-
schema:
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
{ additionalProperties: false }
|
|
244
|
-
),
|
|
245
|
-
normalize: (value) => {
|
|
246
|
-
if (!value || typeof value !== "object") {
|
|
247
|
-
return {};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return {
|
|
251
|
-
includeArchived: value.includeArchived === true
|
|
252
|
-
};
|
|
253
|
-
}
|
|
209
|
+
query: {
|
|
210
|
+
schema: createSchema({
|
|
211
|
+
includeArchived: { type: "boolean", strictBoolean: true }
|
|
212
|
+
}),
|
|
213
|
+
mode: "patch"
|
|
254
214
|
}
|
|
255
215
|
});
|
|
256
216
|
|
|
@@ -266,6 +226,34 @@ test("validateOperationInput validates params/query/body together", () => {
|
|
|
266
226
|
|
|
267
227
|
assert.equal(parsed.ok, true);
|
|
268
228
|
assert.equal(parsed.value.params.workspaceSlug, "acme");
|
|
269
|
-
assert.
|
|
229
|
+
assert.deepEqual(parsed.value.query, {});
|
|
270
230
|
assert.equal(parsed.value.body, undefined);
|
|
271
231
|
});
|
|
232
|
+
|
|
233
|
+
test("validateOperationInput collects json-rest-schema field errors", () => {
|
|
234
|
+
const parsed = validateOperationInput({
|
|
235
|
+
operation: {
|
|
236
|
+
body: {
|
|
237
|
+
schema: createSchema({
|
|
238
|
+
name: {
|
|
239
|
+
type: "string",
|
|
240
|
+
required: true,
|
|
241
|
+
minLength: 1,
|
|
242
|
+
messages: {
|
|
243
|
+
minLength: "Name is required."
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}),
|
|
247
|
+
mode: "patch"
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
input: {
|
|
251
|
+
body: {
|
|
252
|
+
name: " "
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
assert.equal(parsed.ok, false);
|
|
258
|
+
assert.equal(parsed.fieldErrors.name, "Name is required.");
|
|
259
|
+
});
|
|
@@ -3,30 +3,26 @@ import test from "node:test";
|
|
|
3
3
|
|
|
4
4
|
import { createPaginationQuerySchema } from "../src/shared/validators/paginationQuery.js";
|
|
5
5
|
|
|
6
|
-
test("createPaginationQuerySchema
|
|
7
|
-
const schema = createPaginationQuerySchema();
|
|
6
|
+
test("createPaginationQuerySchema exports the expected transport bounds", () => {
|
|
7
|
+
const schema = createPaginationQuerySchema().toJsonSchema({ mode: "patch" });
|
|
8
8
|
|
|
9
9
|
assert.equal(schema.type, "object");
|
|
10
10
|
assert.equal(schema.additionalProperties, false);
|
|
11
11
|
assert.equal(schema.properties.page.minimum, 1);
|
|
12
|
-
assert.equal(schema.properties.page.default, 1);
|
|
13
12
|
assert.equal(schema.properties.pageSize.minimum, 1);
|
|
14
13
|
assert.equal(schema.properties.pageSize.maximum, 100);
|
|
15
|
-
assert.equal(schema.properties.pageSize.default, 10);
|
|
16
14
|
});
|
|
17
15
|
|
|
18
|
-
test("createPaginationQuerySchema applies custom bounds
|
|
16
|
+
test("createPaginationQuerySchema applies custom transport bounds", () => {
|
|
19
17
|
const schema = createPaginationQuerySchema({
|
|
20
18
|
defaultPage: 2,
|
|
21
19
|
defaultPageSize: 25,
|
|
22
20
|
minPage: 2,
|
|
23
21
|
minPageSize: 5,
|
|
24
22
|
maxPageSize: 250
|
|
25
|
-
});
|
|
23
|
+
}).toJsonSchema({ mode: "patch" });
|
|
26
24
|
|
|
27
25
|
assert.equal(schema.properties.page.minimum, 2);
|
|
28
|
-
assert.equal(schema.properties.page.default, 2);
|
|
29
26
|
assert.equal(schema.properties.pageSize.minimum, 5);
|
|
30
27
|
assert.equal(schema.properties.pageSize.maximum, 250);
|
|
31
|
-
assert.equal(schema.properties.pageSize.default, 25);
|
|
32
28
|
});
|