@plasius/schema 1.1.0 → 1.2.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.
Files changed (59) hide show
  1. package/README.md +31 -18
  2. package/dist/index.cjs +2370 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +627 -0
  5. package/dist/index.d.ts +627 -0
  6. package/dist/index.js +2308 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +18 -6
  9. package/.eslintrc.cjs +0 -7
  10. package/.github/workflows/cd.yml +0 -236
  11. package/.github/workflows/ci.yml +0 -16
  12. package/.nvmrc +0 -1
  13. package/.vscode/launch.json +0 -15
  14. package/CHANGELOG.md +0 -120
  15. package/CODE_OF_CONDUCT.md +0 -79
  16. package/CONTRIBUTING.md +0 -201
  17. package/CONTRIBUTORS.md +0 -27
  18. package/SECURITY.md +0 -17
  19. package/docs/adrs/adr-0001: schema.md +0 -45
  20. package/docs/adrs/adr-template.md +0 -67
  21. package/legal/CLA-REGISTRY.csv +0 -2
  22. package/legal/CLA.md +0 -22
  23. package/legal/CORPORATE_CLA.md +0 -57
  24. package/legal/INDIVIDUAL_CLA.md +0 -91
  25. package/sbom.cdx.json +0 -66
  26. package/src/components.ts +0 -39
  27. package/src/field.builder.ts +0 -239
  28. package/src/field.ts +0 -153
  29. package/src/index.ts +0 -7
  30. package/src/infer.ts +0 -34
  31. package/src/pii.ts +0 -165
  32. package/src/schema.ts +0 -893
  33. package/src/types.ts +0 -156
  34. package/src/validation/countryCode.ISO3166.ts +0 -256
  35. package/src/validation/currencyCode.ISO4217.ts +0 -191
  36. package/src/validation/dateTime.ISO8601.ts +0 -60
  37. package/src/validation/email.RFC5322.ts +0 -9
  38. package/src/validation/generalText.OWASP.ts +0 -39
  39. package/src/validation/index.ts +0 -13
  40. package/src/validation/languageCode.BCP47.ts +0 -299
  41. package/src/validation/name.OWASP.ts +0 -25
  42. package/src/validation/percentage.ISO80000-1.ts +0 -8
  43. package/src/validation/phone.E.164.ts +0 -9
  44. package/src/validation/richtext.OWASP.ts +0 -34
  45. package/src/validation/url.WHATWG.ts +0 -16
  46. package/src/validation/user.MS-GOOGLE-APPLE.ts +0 -31
  47. package/src/validation/uuid.RFC4122.ts +0 -10
  48. package/src/validation/version.SEMVER2.0.0.ts +0 -10
  49. package/tests/field.builder.test.ts +0 -81
  50. package/tests/fields.test.ts +0 -213
  51. package/tests/pii.test.ts +0 -139
  52. package/tests/schema.test.ts +0 -501
  53. package/tests/test-utils.ts +0 -97
  54. package/tests/validate.test.ts +0 -97
  55. package/tests/validation.test.ts +0 -98
  56. package/tsconfig.build.json +0 -19
  57. package/tsconfig.json +0 -7
  58. package/tsup.config.ts +0 -10
  59. package/vitest.config.js +0 -20
@@ -1,501 +0,0 @@
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.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.0", piiEnforcement: "strict" }
33
- );
34
-
35
- const NumberField = createSchema({ n: field.number() }, "NumberField", {
36
- version: "1.0.0",
37
- piiEnforcement: "strict",
38
- });
39
- const BooleanField = createSchema({ b: field.boolean() }, "BooleanField", {
40
- version: "1.0.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.0", piiEnforcement: "strict" }
59
- );
60
-
61
- const ArrayOfStrings = createSchema(
62
- { tags: field.array(field.string().enum(["alpha", "beta"])) },
63
- "ArrayOfStrings",
64
- { version: "1.0.0", piiEnforcement: "strict" }
65
- );
66
- const ArrayOfNumbers = createSchema(
67
- { nums: field.array(field.number()) },
68
- "ArrayOfNumbers",
69
- { version: "1.0.0", piiEnforcement: "strict" }
70
- );
71
- const ArrayOfBooleans = createSchema(
72
- { flags: field.array(field.boolean()) },
73
- "ArrayOfBooleans",
74
- { version: "1.0.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.0", piiEnforcement: "strict" }
497
- );
498
- const r = S.validate({ ok: "ok" });
499
- expectValid(r);
500
- });
501
- });
@@ -1,97 +0,0 @@
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.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.0"),
21
-
22
- bestFriend: field
23
- .string()
24
- .optional()
25
- .description("User's best friend ID")
26
- .version("1.0.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.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.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.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.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
- }
@@ -1,97 +0,0 @@
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.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.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.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.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.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.0",
91
- name: "Al",
92
- bestFriend: { type: "user", id: "user-123" },
93
- characters: [],
94
- };
95
- expect(isValidUser(input)).toBe(true);
96
- });
97
- });