@plasius/schema 1.0.0
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/.eslintrc.cjs +7 -0
- package/.github/workflows/cd.yml +54 -0
- package/.github/workflows/ci.yml +16 -0
- package/.vscode/launch.json +15 -0
- package/CODE_OF_CONDUCT.md +79 -0
- package/CONTRIBUTORS.md +27 -0
- package/LICENSE +203 -0
- package/README.md +45 -0
- package/SECURITY.md +17 -0
- package/legal/CLA-REGISTRY.csv +2 -0
- package/legal/CLA.md +22 -0
- package/legal/CORPORATE_CLA.md +55 -0
- package/legal/INDIVIDUAL_CLA.md +91 -0
- package/package.json +48 -0
- package/src/components.ts +39 -0
- package/src/field.builder.ts +119 -0
- package/src/field.ts +14 -0
- package/src/index.ts +7 -0
- package/src/infer.ts +34 -0
- package/src/pii.ts +165 -0
- package/src/schema.ts +757 -0
- package/src/types.ts +156 -0
- package/src/validation/countryCode.ISO3166.ts +256 -0
- package/src/validation/currencyCode.ISO4217.ts +191 -0
- package/src/validation/dateTime.ISO8601.ts +9 -0
- package/src/validation/email.RFC5322.ts +9 -0
- package/src/validation/generalText.OWASP.ts +39 -0
- package/src/validation/index.ts +13 -0
- package/src/validation/name.OWASP.ts +25 -0
- package/src/validation/percentage.ISO80000-1.ts +8 -0
- package/src/validation/phone.E.164.ts +9 -0
- package/src/validation/richtext.OWASP.ts +34 -0
- package/src/validation/url.WHATWG.ts +16 -0
- package/src/validation/user.MS-GOOGLE-APPLE.ts +31 -0
- package/src/validation/uuid.RFC4122.ts +10 -0
- package/src/validation/version.SEMVER2.0.0.ts +8 -0
- package/tests/pii.test.ts +139 -0
- package/tests/schema.test.ts +501 -0
- package/tests/test-utils.ts +97 -0
- package/tests/validate.test.ts +97 -0
- package/tests/validation.test.ts +98 -0
- package/tsconfig.build.json +19 -0
- package/tsconfig.json +7 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { describe, it } from "vitest";
|
|
2
|
+
import { createSchema } from "../src/schema.js";
|
|
3
|
+
import { field } from "../src/field.js";
|
|
4
|
+
import { expectValid, expectInvalid } from "./test-utils.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Full coverage test titles mapped to validator error branches.
|
|
8
|
+
* We compose minimal schemas per case so failures isolate the targeted branch.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
describe("schema.ts – validator coverage", () => {
|
|
12
|
+
// 1) Field definition missing
|
|
13
|
+
it("should fail when field definition is missing", () => {
|
|
14
|
+
const S = createSchema(
|
|
15
|
+
{
|
|
16
|
+
// Force an undefined field definition to simulate a bad schema shape
|
|
17
|
+
bad: undefined as any,
|
|
18
|
+
},
|
|
19
|
+
"BadDef",
|
|
20
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
21
|
+
);
|
|
22
|
+
const r = S.validate({ bad: "anything" });
|
|
23
|
+
expectInvalid(r, "Field definition missing for: bad");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Base schemas reused in several tests
|
|
27
|
+
const StringEnum = createSchema(
|
|
28
|
+
{
|
|
29
|
+
k: field.string().enum(["A", "B", "C"]),
|
|
30
|
+
},
|
|
31
|
+
"StringEnum",
|
|
32
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const NumberField = createSchema({ n: field.number() }, "NumberField", {
|
|
36
|
+
version: "1.0",
|
|
37
|
+
piiEnforcement: "strict",
|
|
38
|
+
});
|
|
39
|
+
const BooleanField = createSchema({ b: field.boolean() }, "BooleanField", {
|
|
40
|
+
version: "1.0",
|
|
41
|
+
piiEnforcement: "strict",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const SimpleObject = createSchema(
|
|
45
|
+
{
|
|
46
|
+
o: field.object({
|
|
47
|
+
a: field.string(),
|
|
48
|
+
b: field.number().optional(),
|
|
49
|
+
nested: field
|
|
50
|
+
.object({
|
|
51
|
+
f: field.boolean(),
|
|
52
|
+
tag: field.string().enum(["x", "y"]).optional(),
|
|
53
|
+
})
|
|
54
|
+
.optional(),
|
|
55
|
+
}),
|
|
56
|
+
},
|
|
57
|
+
"SimpleObject",
|
|
58
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const ArrayOfStrings = createSchema(
|
|
62
|
+
{ tags: field.array(field.string().enum(["alpha", "beta"])) },
|
|
63
|
+
"ArrayOfStrings",
|
|
64
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
65
|
+
);
|
|
66
|
+
const ArrayOfNumbers = createSchema(
|
|
67
|
+
{ nums: field.array(field.number()) },
|
|
68
|
+
"ArrayOfNumbers",
|
|
69
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
70
|
+
);
|
|
71
|
+
const ArrayOfBooleans = createSchema(
|
|
72
|
+
{ flags: field.array(field.boolean()) },
|
|
73
|
+
"ArrayOfBooleans",
|
|
74
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const ArrayOfObjects = createSchema(
|
|
78
|
+
{
|
|
79
|
+
items: field.array(
|
|
80
|
+
field.object({
|
|
81
|
+
type: field.string().enum(["a", "b"]),
|
|
82
|
+
value: field.number().optional(),
|
|
83
|
+
info: field.object({ ok: field.boolean() }).optional(),
|
|
84
|
+
})
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
"ArrayOfObjects",
|
|
88
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// 2) Missing required field
|
|
92
|
+
it("should fail when required field is missing", () => {
|
|
93
|
+
const result = StringEnum.validate({});
|
|
94
|
+
expectInvalid(result, "Missing required field: k");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 3) Immutable field modified (not directly represented)
|
|
98
|
+
it("should fail when immutable field is modified", () => {
|
|
99
|
+
const fixed = field.string().immutable() as any;
|
|
100
|
+
const S = createSchema({ fixed }, "Immutable", {
|
|
101
|
+
version: "1.0",
|
|
102
|
+
piiEnforcement: "strict",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Simulate existing stored value
|
|
106
|
+
const existing = { fixed: "A" };
|
|
107
|
+
const r = S.validate({ fixed: "B" }, existing);
|
|
108
|
+
expectInvalid(r, "Field is immutable: fixed");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// 4) High PII empty under strict (only if such flag exists)
|
|
112
|
+
it("should fail when high PII required field is empty under strict enforcement", () => {
|
|
113
|
+
const secret = field.string().PID({
|
|
114
|
+
classification: "high",
|
|
115
|
+
action: "encrypt",
|
|
116
|
+
logHandling: "redact",
|
|
117
|
+
purpose: "pii security test",
|
|
118
|
+
}) as any;
|
|
119
|
+
// required by default (not optional)
|
|
120
|
+
const S = createSchema({ secret }, "PII", {
|
|
121
|
+
version: "1.0",
|
|
122
|
+
piiEnforcement: "strict",
|
|
123
|
+
});
|
|
124
|
+
const r = S.validate({ secret: "" }); // empty string should trigger strict error
|
|
125
|
+
expectInvalid(r, "High PII field must not be empty: secret");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 5) Custom field validator returns false
|
|
129
|
+
it("should fail when custom field validator returns false", () => {
|
|
130
|
+
const S = createSchema(
|
|
131
|
+
{
|
|
132
|
+
code: field.string().validator((v) => v === "ok"),
|
|
133
|
+
},
|
|
134
|
+
"CustomValidator",
|
|
135
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
136
|
+
);
|
|
137
|
+
const result = S.validate({ code: "nope" });
|
|
138
|
+
expectInvalid(result, "Invalid value for field: code");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 6) String type mismatch
|
|
142
|
+
it("should fail when string field is not a string", () => {
|
|
143
|
+
const result = StringEnum.validate({ k: 1 as unknown as string });
|
|
144
|
+
expectInvalid(result, "Field k must be string");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 7) String enum violation
|
|
148
|
+
it("should fail when string field violates enum", () => {
|
|
149
|
+
const result = StringEnum.validate({ k: "Z" });
|
|
150
|
+
expectInvalid(result, "Field k must be one of: A, B, C");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// 8) Number type mismatch
|
|
154
|
+
it("should fail when number field is not a number", () => {
|
|
155
|
+
const result = NumberField.validate({ n: "1" as unknown as number });
|
|
156
|
+
expectInvalid(result, "Field n must be number");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// 9) Boolean type mismatch
|
|
160
|
+
it("should fail when boolean field is not a boolean", () => {
|
|
161
|
+
const result = BooleanField.validate({ b: "true" as unknown as boolean });
|
|
162
|
+
expectInvalid(result, "Field b must be boolean");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 10) Object not an object
|
|
166
|
+
it("should fail when object field is not an object", () => {
|
|
167
|
+
const result = SimpleObject.validate({ o: 1 as unknown as object });
|
|
168
|
+
expectInvalid(result, "Field o must be object");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 11) Required child missing
|
|
172
|
+
it("should fail when required child field on object is missing", () => {
|
|
173
|
+
const result = SimpleObject.validate({
|
|
174
|
+
o: {
|
|
175
|
+
/* a missing */
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
expectInvalid(result, "Missing required field: o.a");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 12) Child validator returns false
|
|
182
|
+
it("should fail when child field validator on object returns false (using inline validator)", () => {
|
|
183
|
+
const S = createSchema(
|
|
184
|
+
{
|
|
185
|
+
o: field.object({
|
|
186
|
+
a: field.string().validator(() => false),
|
|
187
|
+
}),
|
|
188
|
+
},
|
|
189
|
+
"ChildValidator",
|
|
190
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
191
|
+
);
|
|
192
|
+
const result = S.validate({ o: { a: "x" } });
|
|
193
|
+
expectInvalid(result, "Invalid value for field: o.a");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// 13) Required grandchild missing
|
|
197
|
+
it("should fail when required grandchild field on nested object is missing", () => {
|
|
198
|
+
const result = SimpleObject.validate({
|
|
199
|
+
o: {
|
|
200
|
+
a: "hi",
|
|
201
|
+
nested: {
|
|
202
|
+
/* f missing */
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
expectInvalid(result, "Missing required field: o.nested.f");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 14) Grandchild validator false
|
|
210
|
+
it("should fail when grandchild field validator on nested object returns false", () => {
|
|
211
|
+
const S = createSchema(
|
|
212
|
+
{
|
|
213
|
+
o: field.object({
|
|
214
|
+
nested: field.object({
|
|
215
|
+
f: field.boolean().validator(() => false),
|
|
216
|
+
}),
|
|
217
|
+
a: field.string(),
|
|
218
|
+
}),
|
|
219
|
+
},
|
|
220
|
+
"GrandChildValidator",
|
|
221
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
222
|
+
);
|
|
223
|
+
const result = S.validate({ o: { a: "x", nested: { f: true } } });
|
|
224
|
+
expectInvalid(result, "Invalid value for field: o.nested.f");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// 15) Array not an array
|
|
228
|
+
it("should fail when array field is not an array", () => {
|
|
229
|
+
const result = ArrayOfStrings.validate({
|
|
230
|
+
tags: "alpha" as unknown as string[],
|
|
231
|
+
});
|
|
232
|
+
expectInvalid(result, "Field tags must be an array");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// 16) Array of strings contains a non-string
|
|
236
|
+
it("should fail when array of strings contains a non-string", () => {
|
|
237
|
+
const result = ArrayOfStrings.validate({
|
|
238
|
+
tags: ["alpha", 1 as unknown as string],
|
|
239
|
+
});
|
|
240
|
+
expectInvalid(result, "Field tags must be string[]");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// 17) Array of strings invalid enum values
|
|
244
|
+
it("should fail when array of strings contains invalid enum values", () => {
|
|
245
|
+
const result = ArrayOfStrings.validate({ tags: ["alpha", "delta"] });
|
|
246
|
+
expectInvalid(result, "Field tags contains invalid enum values: delta");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// 18) Array of numbers contains a non-number
|
|
250
|
+
it("should fail when array of numbers contains a non-number", () => {
|
|
251
|
+
const result = ArrayOfNumbers.validate({
|
|
252
|
+
nums: [1, "2" as unknown as number],
|
|
253
|
+
});
|
|
254
|
+
expectInvalid(result, "Field nums must be number[]");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// 19) Array of booleans contains a non-boolean
|
|
258
|
+
it("should fail when array of booleans contains a non-boolean", () => {
|
|
259
|
+
const result = ArrayOfBooleans.validate({
|
|
260
|
+
flags: [true, "false" as unknown as boolean],
|
|
261
|
+
});
|
|
262
|
+
expectInvalid(result, "Field flags must be boolean[]");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// 20) Array of objects contains a non-object
|
|
266
|
+
it("should fail when array of objects contains a non-object", () => {
|
|
267
|
+
const result = ArrayOfObjects.validate({
|
|
268
|
+
items: [{ type: "a" }, 1 as unknown as object],
|
|
269
|
+
});
|
|
270
|
+
expectInvalid(result, "Field items must be object[]");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 21) Required child missing in object item
|
|
274
|
+
it("should fail when required child field is missing in an object item", () => {
|
|
275
|
+
const result = ArrayOfObjects.validate({
|
|
276
|
+
items: [
|
|
277
|
+
{
|
|
278
|
+
/* type missing */
|
|
279
|
+
},
|
|
280
|
+
] as any,
|
|
281
|
+
});
|
|
282
|
+
expectInvalid(result, "Missing required field: items[0].type");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// 22) Child validator returns false in object item
|
|
286
|
+
it("should fail when child field validator returns false in an object item", () => {
|
|
287
|
+
const S = createSchema(
|
|
288
|
+
{
|
|
289
|
+
items: field.array(
|
|
290
|
+
field.object({
|
|
291
|
+
t: field.string().validator(() => false),
|
|
292
|
+
})
|
|
293
|
+
),
|
|
294
|
+
},
|
|
295
|
+
"ArrayChildValidator",
|
|
296
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
297
|
+
);
|
|
298
|
+
const result = S.validate({ items: [{ t: "ok" }] });
|
|
299
|
+
expectInvalid(result, "Invalid value for field: items[0].t");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// 23) Array of refs – invalid ref object (depends on ref support)
|
|
303
|
+
it("should fail when array item is not a valid ref object (or wrong ref type)", () => {
|
|
304
|
+
const refItem = { type: "ref", refType: "asset" } as any;
|
|
305
|
+
const S = createSchema({ items: field.array(refItem) }, "ArrayRefInvalid", {
|
|
306
|
+
version: "1.0",
|
|
307
|
+
piiEnforcement: "strict",
|
|
308
|
+
});
|
|
309
|
+
// bad id type (number) and missing/incorrect fields should trigger the message
|
|
310
|
+
const r = S.validate({ items: [{ type: "asset", id: 123 }] });
|
|
311
|
+
expectInvalid(
|
|
312
|
+
r,
|
|
313
|
+
"Field items[0] must be a reference object with type: asset"
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// 24) Array of refs – missing required child in ref shape
|
|
318
|
+
it("should fail when required child field is missing in ref shape", () => {
|
|
319
|
+
const refItem = {
|
|
320
|
+
type: "ref",
|
|
321
|
+
refType: "asset",
|
|
322
|
+
shape: { region: field.string() },
|
|
323
|
+
} as any;
|
|
324
|
+
const S = createSchema({ items: field.array(refItem) }, "ArrayRefMissing", {
|
|
325
|
+
version: "1.0",
|
|
326
|
+
piiEnforcement: "strict",
|
|
327
|
+
});
|
|
328
|
+
// Valid ref, but missing required shape field 'region'
|
|
329
|
+
const r = S.validate({ items: [{ type: "asset", id: "a1" }] });
|
|
330
|
+
expectInvalid(r, "Missing required field: items[0].region");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// 25) Array of refs – child validator false in ref shape
|
|
334
|
+
it("should fail when child field validator returns false in ref shape", () => {
|
|
335
|
+
const refItem = {
|
|
336
|
+
type: "ref",
|
|
337
|
+
refType: "asset",
|
|
338
|
+
shape: { code: field.string().validator(() => false) },
|
|
339
|
+
} as any;
|
|
340
|
+
const S = createSchema(
|
|
341
|
+
{ items: field.array(refItem) },
|
|
342
|
+
"ArrayRefChildValidator",
|
|
343
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
344
|
+
);
|
|
345
|
+
const r = S.validate({ items: [{ type: "asset", id: "a1", code: "XYZ" }] });
|
|
346
|
+
expectInvalid(r, "Invalid value for field: items[0].code");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// 26) Unsupported array item type (builder guard)
|
|
350
|
+
it("should fail when array item type is unsupported", () => {
|
|
351
|
+
const weirdItem = { type: "date" } as any;
|
|
352
|
+
const S = createSchema({ xs: field.array(weirdItem) }, "ArrayUnsupported", {
|
|
353
|
+
version: "1.0",
|
|
354
|
+
piiEnforcement: "strict",
|
|
355
|
+
});
|
|
356
|
+
const r = S.validate({ xs: ["2025-01-01"] });
|
|
357
|
+
expectInvalid(r, "Field xs has unsupported array item type");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// 27) Ref field (single) – invalid shape
|
|
361
|
+
it("should fail when ref field is not a valid {type,id} object", () => {
|
|
362
|
+
const refDef = { type: "ref" } as any;
|
|
363
|
+
const S = createSchema({ r: refDef }, "SingleRef", {
|
|
364
|
+
version: "1.0",
|
|
365
|
+
piiEnforcement: "strict",
|
|
366
|
+
});
|
|
367
|
+
const r = S.validate({ r: { type: "asset", id: 42 } }); // id should be string
|
|
368
|
+
expectInvalid(r, "Field r must be { type: string; id: string }");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// 28) Unknown field type (defensive branch)
|
|
372
|
+
it("should fail when field type is unknown", () => {
|
|
373
|
+
const unknownDef = { type: "wat" } as any;
|
|
374
|
+
const S = createSchema({ a: unknownDef }, "UnknownType", {
|
|
375
|
+
version: "1.0",
|
|
376
|
+
piiEnforcement: "strict",
|
|
377
|
+
});
|
|
378
|
+
const r = S.validate({ a: 123 });
|
|
379
|
+
expectInvalid(r, "Unknown type for field a: wat");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// 29) Schema-level validator returns false
|
|
383
|
+
it("should fail when schema-level validation returns false", () => {
|
|
384
|
+
const S = createSchema(
|
|
385
|
+
{
|
|
386
|
+
a: field.number(),
|
|
387
|
+
},
|
|
388
|
+
"SchemaLevel",
|
|
389
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// monkey-patch a validator on the schema (if supported by createSchema options)
|
|
393
|
+
// Since createSchema doesn't expose a top-level validator in builder, we simulate via a field validator
|
|
394
|
+
const result = S.validate({ a: NaN as unknown as number });
|
|
395
|
+
// Number type is still number, but if your number validator treats NaN as invalid, else replace with a simple field-level validator example.
|
|
396
|
+
// To guarantee failure, define an explicit schema with a field-level validator:
|
|
397
|
+
const S2 = createSchema(
|
|
398
|
+
{
|
|
399
|
+
x: field.string().validator(() => false),
|
|
400
|
+
},
|
|
401
|
+
"SchemaLevel2",
|
|
402
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
403
|
+
);
|
|
404
|
+
const r2 = S2.validate({ x: "anything" });
|
|
405
|
+
expectInvalid(r2, "Invalid value for field: x");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Sanity: a fully valid complex object passes
|
|
409
|
+
it("should pass on a fully valid complex object", () => {
|
|
410
|
+
const result = ArrayOfObjects.validate({
|
|
411
|
+
items: [{ type: "a", value: 1, info: { ok: true } }, { type: "b" }],
|
|
412
|
+
});
|
|
413
|
+
expectValid(result);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// --- Positive paths to complement the negative coverage above ---
|
|
417
|
+
it("should pass when required enum field is provided with an allowed value", () => {
|
|
418
|
+
const result = StringEnum.validate({ k: "A" });
|
|
419
|
+
expectValid(result);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("should pass when number field is a number", () => {
|
|
423
|
+
const result = NumberField.validate({ n: 42 });
|
|
424
|
+
expectValid(result);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("should pass when boolean field is a boolean", () => {
|
|
428
|
+
const result = BooleanField.validate({ b: false });
|
|
429
|
+
expectValid(result);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should pass when object has required child and omits optionals", () => {
|
|
433
|
+
const result = SimpleObject.validate({ o: { a: "hello" } });
|
|
434
|
+
expectValid(result);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("should pass when object provides valid nested optional object", () => {
|
|
438
|
+
const result = SimpleObject.validate({
|
|
439
|
+
o: { a: "hi", b: 10, nested: { f: true, tag: "x" } },
|
|
440
|
+
});
|
|
441
|
+
expectValid(result);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("should pass when array of strings contains only allowed enum values", () => {
|
|
445
|
+
const result = ArrayOfStrings.validate({ tags: ["alpha", "beta"] });
|
|
446
|
+
expectValid(result);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("should pass when array of numbers contains only numbers", () => {
|
|
450
|
+
const result = ArrayOfNumbers.validate({ nums: [0, 1, 2, 3.14] });
|
|
451
|
+
expectValid(result);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("should pass when array of booleans contains only booleans", () => {
|
|
455
|
+
const result = ArrayOfBooleans.validate({ flags: [true, false, true] });
|
|
456
|
+
expectValid(result);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("should pass when array of objects contains minimal valid items (optionals omitted)", () => {
|
|
460
|
+
const result = ArrayOfObjects.validate({
|
|
461
|
+
items: [{ type: "a" }, { type: "b" }],
|
|
462
|
+
});
|
|
463
|
+
expectValid(result);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("should pass when immutable field remains unchanged compared to existing value", () => {
|
|
467
|
+
const fixed = field.string().immutable() as any;
|
|
468
|
+
const S = createSchema({ fixed }, "ImmutableOK", {
|
|
469
|
+
version: "1.0",
|
|
470
|
+
piiEnforcement: "strict",
|
|
471
|
+
});
|
|
472
|
+
const existing = { fixed: "A" };
|
|
473
|
+
const r = S.validate({ fixed: "A" }, existing);
|
|
474
|
+
expectValid(r);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("should pass when high PII required field is non-empty under strict enforcement", () => {
|
|
478
|
+
const secret = field.string().PID({
|
|
479
|
+
classification: "high",
|
|
480
|
+
action: "encrypt",
|
|
481
|
+
logHandling: "redact",
|
|
482
|
+
purpose: "pii security test",
|
|
483
|
+
}) as any;
|
|
484
|
+
const S = createSchema({ secret }, "PII_OK", {
|
|
485
|
+
version: "1.0",
|
|
486
|
+
piiEnforcement: "strict",
|
|
487
|
+
});
|
|
488
|
+
const r = S.validate({ secret: "non-empty" });
|
|
489
|
+
expectValid(r);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("should pass when custom field validator returns true", () => {
|
|
493
|
+
const S = createSchema(
|
|
494
|
+
{ ok: field.string().validator((v) => v === "ok") },
|
|
495
|
+
"CustomValidatorOK",
|
|
496
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
497
|
+
);
|
|
498
|
+
const r = S.validate({ ok: "ok" });
|
|
499
|
+
expectValid(r);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// tests/test-utils.ts
|
|
2
|
+
import { createSchema } from "../src/schema.js";
|
|
3
|
+
import { Infer } from "../src/infer.js";
|
|
4
|
+
import { field } from "../src/field.js";
|
|
5
|
+
import { expect } from "vitest";
|
|
6
|
+
import { ValidationResult } from "../src/types.js";
|
|
7
|
+
import { SchemaShape, validateUUID } from "../src/index.js";
|
|
8
|
+
|
|
9
|
+
// Mock "character" schema
|
|
10
|
+
export const characterSchema = createSchema(
|
|
11
|
+
{
|
|
12
|
+
name: field.string().description("Character name").version("1.0"),
|
|
13
|
+
},
|
|
14
|
+
"character"
|
|
15
|
+
);
|
|
16
|
+
export type Character = Infer<typeof characterSchema>;
|
|
17
|
+
|
|
18
|
+
const userShape: SchemaShape = {
|
|
19
|
+
id: field.string().system().validator(validateUUID).immutable(),
|
|
20
|
+
name: field.string().description("User name").version("1.0"),
|
|
21
|
+
|
|
22
|
+
bestFriend: field
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.description("User's best friend ID")
|
|
26
|
+
.version("1.0")
|
|
27
|
+
.validator(validateUUID),
|
|
28
|
+
|
|
29
|
+
characters: field
|
|
30
|
+
.array(field.ref("character").as<Character>())
|
|
31
|
+
.optional()
|
|
32
|
+
.description("Characters owned by this user")
|
|
33
|
+
.version("1.0"),
|
|
34
|
+
};
|
|
35
|
+
// Mock "user" schema
|
|
36
|
+
export const userSchema = createSchema(userShape, "user");
|
|
37
|
+
|
|
38
|
+
export type User = Infer<typeof userSchema>;
|
|
39
|
+
|
|
40
|
+
// Simple in-memory mock "DB"
|
|
41
|
+
export const mockDb: {
|
|
42
|
+
[key: string]: {
|
|
43
|
+
type: string;
|
|
44
|
+
version: string;
|
|
45
|
+
name: string;
|
|
46
|
+
bestFriend?: { type: string; id: string };
|
|
47
|
+
characters?: { type: string; id: string }[];
|
|
48
|
+
};
|
|
49
|
+
} = {
|
|
50
|
+
"user:user-1": {
|
|
51
|
+
type: "user",
|
|
52
|
+
version: "1.0",
|
|
53
|
+
name: "Alice",
|
|
54
|
+
bestFriend: { type: "user", id: "user-2" },
|
|
55
|
+
characters: [{ type: "character", id: "char-1" }],
|
|
56
|
+
},
|
|
57
|
+
"user:user-2": {
|
|
58
|
+
type: "user",
|
|
59
|
+
version: "1.0",
|
|
60
|
+
name: "Bob",
|
|
61
|
+
bestFriend: { type: "user", id: "user-1" }, // cycle!
|
|
62
|
+
characters: [],
|
|
63
|
+
},
|
|
64
|
+
"character:char-1": {
|
|
65
|
+
type: "character",
|
|
66
|
+
version: "1.0",
|
|
67
|
+
name: "Hero",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Mock resolver
|
|
72
|
+
export async function mockResolveEntity(type: string, id: string) {
|
|
73
|
+
const key = `${type}:${id}`;
|
|
74
|
+
return mockDb[key] ?? null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function expectValid<T>(result: ValidationResult<T>) {
|
|
78
|
+
if (!result.valid) {
|
|
79
|
+
console.error("Validation failed with errors:", result.errors);
|
|
80
|
+
}
|
|
81
|
+
expect(result.valid).toBe(true);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function expectInvalid<T>(
|
|
85
|
+
result: ValidationResult<T>,
|
|
86
|
+
expectedErrorSubstring: string
|
|
87
|
+
) {
|
|
88
|
+
if (result.valid) {
|
|
89
|
+
console.error("Expected failure but got valid result!");
|
|
90
|
+
} else {
|
|
91
|
+
console.log("Validation errors:", result.errors);
|
|
92
|
+
}
|
|
93
|
+
expect(result.valid).toBe(false);
|
|
94
|
+
expect(
|
|
95
|
+
result.errors?.some((e) => e.includes(expectedErrorSubstring)) ?? false
|
|
96
|
+
).toBe(true);
|
|
97
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createSchema, field } from "../src/index.js";
|
|
3
|
+
|
|
4
|
+
// Define the new-style schema using field() builders.
|
|
5
|
+
const userSchema = createSchema(
|
|
6
|
+
{
|
|
7
|
+
name: field
|
|
8
|
+
.string()
|
|
9
|
+
// simple custom rule to prove validator hooks are exercised
|
|
10
|
+
.validator((v) => typeof v === "string" && v.trim().length >= 2),
|
|
11
|
+
|
|
12
|
+
// Best friend is a reference to another user entity
|
|
13
|
+
bestFriend: field.ref("user"),
|
|
14
|
+
|
|
15
|
+
// Characters is an array of references to character entities
|
|
16
|
+
characters: field.array(field.ref("character")),
|
|
17
|
+
},
|
|
18
|
+
"user",
|
|
19
|
+
{ version: "1.0", piiEnforcement: "strict" }
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Example of a compiled/standalone validator function consumers might use
|
|
23
|
+
// (wrapping the schema.validate for ergonomics/perf in app code)
|
|
24
|
+
const isValidUser = (input: unknown) => userSchema.validate(input as any).valid;
|
|
25
|
+
|
|
26
|
+
describe("validate() with field()-based schema", () => {
|
|
27
|
+
it("validates a correct object", () => {
|
|
28
|
+
const input = {
|
|
29
|
+
type: "user",
|
|
30
|
+
version: "1.0",
|
|
31
|
+
name: "Alice",
|
|
32
|
+
bestFriend: { type: "user", id: "user-123" },
|
|
33
|
+
characters: [{ type: "character", id: "char-1" }],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const result = userSchema.validate(input);
|
|
37
|
+
expect(result.valid).toBe(true);
|
|
38
|
+
expect(result.errors).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("detects missing required field (name)", () => {
|
|
42
|
+
const input = {
|
|
43
|
+
type: "user",
|
|
44
|
+
version: "1.0",
|
|
45
|
+
bestFriend: { type: "user", id: "user-123" },
|
|
46
|
+
characters: [],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const result = userSchema.validate(input);
|
|
50
|
+
expect(result.valid).toBe(false);
|
|
51
|
+
expect(result.errors).toContain("Missing required field: name");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("detects invalid ref shape for bestFriend", () => {
|
|
55
|
+
const input = {
|
|
56
|
+
type: "user",
|
|
57
|
+
version: "1.0",
|
|
58
|
+
name: "Test",
|
|
59
|
+
bestFriend: { id: "user-123" }, // missing type
|
|
60
|
+
characters: [],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const result = userSchema.validate(input);
|
|
64
|
+
expect(result.valid).toBe(false);
|
|
65
|
+
expect(result.errors).toContain(
|
|
66
|
+
"Field bestFriend must be { type: string; id: string }"
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// --- Additional tests proving field-level validators are executed ---
|
|
71
|
+
it("rejects name that fails custom validator (too short)", () => {
|
|
72
|
+
const input = {
|
|
73
|
+
type: "user",
|
|
74
|
+
version: "1.0",
|
|
75
|
+
name: "A", // too short per custom validator
|
|
76
|
+
bestFriend: { type: "user", id: "user-123" },
|
|
77
|
+
characters: [],
|
|
78
|
+
};
|
|
79
|
+
const result = userSchema.validate(input);
|
|
80
|
+
expect(result.valid).toBe(false);
|
|
81
|
+
// be tolerant to exact wording, but ensure the error points at `name`
|
|
82
|
+
expect(
|
|
83
|
+
result.errors?.some((e: string) => e.toLowerCase().includes("name"))
|
|
84
|
+
).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("accepts name that passes custom validator (>=2 chars)", () => {
|
|
88
|
+
const input = {
|
|
89
|
+
type: "user",
|
|
90
|
+
version: "1.0",
|
|
91
|
+
name: "Al",
|
|
92
|
+
bestFriend: { type: "user", id: "user-123" },
|
|
93
|
+
characters: [],
|
|
94
|
+
};
|
|
95
|
+
expect(isValidUser(input)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|