@plasius/schema 1.0.17 → 1.1.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.
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { field } from "../src/field";
3
+ import { createSchema } from "../src/schema";
4
+
5
+ /**
6
+ * This test verifies that when an older entity (v1.0.0) is validated against a newer
7
+ * schema (v2.0.0), a field-level `.upgrade()` function is invoked to transform the
8
+ * value into the new shape, and that validation passes with the upgraded value.
9
+ */
10
+
11
+ describe("schema field upgrade flow", () => {
12
+ it("upgrades old displayName string → new object shape", () => {
13
+ const userV2 = createSchema(
14
+ {
15
+ // v2 requires an object { given, family } instead of a plain string
16
+ displayName: field
17
+ .object({
18
+ given: field.string().required(),
19
+ family: field.string().required(),
20
+ })
21
+ .version("2.0.0")
22
+ .upgrade((value, { entityFrom, entityTo, fieldTo, fieldName }) => {
23
+ if (typeof value === "string") {
24
+ const parts = value.trim().split(/\s+/);
25
+ const given = parts.shift() ?? "";
26
+ const family = parts.join(" ") || "Unknown";
27
+ return { ok: true, value: { given, family } };
28
+ }
29
+ return {
30
+ ok: false,
31
+ error: `Cannot upgrade ${fieldName} from non-string`,
32
+ };
33
+ }),
34
+ },
35
+ "User",
36
+ { version: "2.0.0", table: "users" }
37
+ );
38
+
39
+ const oldEntity = {
40
+ version: "1.0.0",
41
+ displayName: "Ada Lovelace",
42
+ } as const;
43
+
44
+ const res = userV2.validate(oldEntity);
45
+ // Expect our validation contract: no errors and transformed value
46
+ expect(Array.isArray(res.errors)).toBe(true);
47
+ expect(res.errors?.length).toBe(0);
48
+ expect(res.value?.displayName).toEqual({
49
+ given: "Ada",
50
+ family: "Lovelace",
51
+ });
52
+ });
53
+
54
+ it("fails validation when upgrade is not possible", () => {
55
+ const userV2 = createSchema(
56
+ {
57
+ age: field
58
+ .number()
59
+ .version("2.0.0")
60
+ .upgrade((value) => {
61
+ // Only upgrade from numeric strings like "42"
62
+ if (typeof value === "string" && /^\d+$/.test(value)) {
63
+ return { ok: true, value: Number(value) };
64
+ }
65
+ return { ok: false, error: "age cannot be upgraded" };
66
+ })
67
+ .required(),
68
+ },
69
+ "User",
70
+ { version: "2.0.0", table: "users" }
71
+ );
72
+
73
+ const oldEntity = { version: "1.0.0", age: "forty two" } as const;
74
+ const res = userV2.validate(oldEntity);
75
+
76
+ expect(Array.isArray(res.errors)).toBe(true);
77
+ expect(res.errors?.length).toBeGreaterThan(0);
78
+ // Should keep original invalid value when upgrade fails
79
+ expect(res.value?.age).toBe("forty two");
80
+ });
81
+ });
@@ -0,0 +1,213 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // --- Mock FieldBuilder so we can assert how field.ts wires it up ---
4
+ // We mock the *dependency module* that field.ts imports: "./field.builder.js"
5
+ vi.mock("../src/field.builder.js", () => {
6
+ class MockFieldBuilder<T = unknown> {
7
+ public type: string;
8
+ public options?: Record<string, unknown>;
9
+ public calls: { method: string; args: any[] }[] = [];
10
+
11
+ constructor(type: string, options?: Record<string, unknown>) {
12
+ this.type = type;
13
+ this.options = options;
14
+ }
15
+
16
+ validator(fn: (value: unknown) => boolean) {
17
+ this.calls.push({ method: "validator", args: [fn] });
18
+ return this;
19
+ }
20
+
21
+ PID(cfg: Record<string, unknown>) {
22
+ this.calls.push({ method: "PID", args: [cfg] });
23
+ return this;
24
+ }
25
+
26
+ min(n: number) {
27
+ this.calls.push({ method: "min", args: [n] });
28
+ return this;
29
+ }
30
+
31
+ max(n: number) {
32
+ this.calls.push({ method: "max", args: [n] });
33
+ return this;
34
+ }
35
+
36
+ description(text: string) {
37
+ this.calls.push({ method: "description", args: [text] });
38
+ return this;
39
+ }
40
+ }
41
+
42
+ return { FieldBuilder: MockFieldBuilder };
43
+ });
44
+
45
+ // Import after the mock so field.ts uses our mocked FieldBuilder
46
+ import { field } from "../src/field";
47
+
48
+ // Small helper to fetch a recorded call by method name
49
+ const getCall = (fb: any, method: string) =>
50
+ fb.calls.find((c: any) => c.method === method);
51
+
52
+ // Helper to run the first validator that was attached and return its boolean result
53
+ const runFirstValidator = (fb: any, value: unknown) => {
54
+ const call = getCall(fb, "validator");
55
+ if (!call) throw new Error("No validator attached");
56
+ const fn = call.args[0] as (v: unknown) => boolean;
57
+ return fn(value);
58
+ };
59
+
60
+ // --- Tests ---
61
+
62
+ describe("field factory basics", () => {
63
+ it("string/number/boolean create FieldBuilder of correct type", () => {
64
+ const s = field.string();
65
+ const n = field.number();
66
+ const b = field.boolean();
67
+
68
+ expect(s.type).toBe("string");
69
+ expect(n.type).toBe("number");
70
+ expect(b.type).toBe("boolean");
71
+ });
72
+ });
73
+
74
+ describe("validators: email/phone/url/uuid", () => {
75
+ it("email wires PID, validator, and description and validates", () => {
76
+ const fb = field.email() as any;
77
+ expect(fb.type).toBe("string");
78
+
79
+ // PID + description were set
80
+ expect(getCall(fb, "PID").args[0]).toMatchObject({
81
+ classification: "high",
82
+ action: "encrypt",
83
+ });
84
+ expect(getCall(fb, "description").args[0]).toMatch(/email address/i);
85
+
86
+ // Validator behaves sensibly
87
+ expect(runFirstValidator(fb, "user@example.com")).toBe(true);
88
+ expect(runFirstValidator(fb, "not-an-email")).toBe(false);
89
+ });
90
+
91
+ it("phone wires PID and validates", () => {
92
+ const fb = field.phone() as any;
93
+ expect(getCall(fb, "PID").args[0]).toMatchObject({
94
+ classification: "high",
95
+ action: "encrypt",
96
+ });
97
+ expect(getCall(fb, "description").args[0]).toMatch(/phone number/i);
98
+ expect(runFirstValidator(fb, "+442079460018")).toBe(true);
99
+ expect(runFirstValidator(fb, "123")).toBe(false);
100
+ expect(runFirstValidator(fb, "123 123 123")).toBe(false);
101
+ expect(runFirstValidator(fb, "+441234567890123456")).toBe(false);
102
+ });
103
+
104
+ it("url wires PID and validates", () => {
105
+ const fb = field.url() as any;
106
+ expect(getCall(fb, "PID").args[0]).toMatchObject({
107
+ classification: "low",
108
+ action: "hash",
109
+ });
110
+ expect(getCall(fb, "description").args[0]).toMatch(/url/i);
111
+ expect(runFirstValidator(fb, "https://example.com/path")).toBe(true);
112
+ expect(runFirstValidator(fb, "ht!tp://bad-url")).toBe(false);
113
+ });
114
+
115
+ it("uuid wires PID and validates", () => {
116
+ const fb = field.uuid() as any;
117
+ expect(getCall(fb, "PID").args[0]).toMatchObject({
118
+ classification: "low",
119
+ action: "hash",
120
+ });
121
+ expect(getCall(fb, "description").args[0]).toMatch(/uuid/i);
122
+ expect(runFirstValidator(fb, "550e8400-e29b-41d4-a716-446655440000")).toBe(
123
+ true
124
+ );
125
+ expect(runFirstValidator(fb, "not-a-uuid")).toBe(false);
126
+ });
127
+ });
128
+
129
+ describe("date/time validators", () => {
130
+ it("dateTimeISO validates full ISO 8601 date-time", () => {
131
+ const fb = field.dateTimeISO() as any;
132
+ expect(getCall(fb, "description").args[0]).toMatch(/ISO 8601/);
133
+ expect(runFirstValidator(fb, "2023-10-05T14:30:00Z")).toBe(true);
134
+ expect(runFirstValidator(fb, "2023-13-02T23:04:05Z")).toBe(false);
135
+ expect(runFirstValidator(fb, "2023-11-02T28:04:05Z")).toBe(false);
136
+ });
137
+
138
+ it("dateISO (date only) validates", () => {
139
+ const fb = field.dateISO() as any;
140
+ expect(getCall(fb, "description").args[0]).toMatch(/date only/i);
141
+ expect(runFirstValidator(fb, "2024-02-29")).toBe(true); // leap day valid
142
+ expect(runFirstValidator(fb, "2024-02-30")).toBe(false);
143
+ });
144
+
145
+ it("timeISO (time only) validates", () => {
146
+ const fb = field.timeISO() as any;
147
+ expect(getCall(fb, "description").args[0]).toMatch(/time only/i);
148
+ expect(runFirstValidator(fb, "23:59:59")).toBe(true);
149
+ expect(runFirstValidator(fb, "24:00:00")).toBe(false);
150
+ });
151
+ });
152
+
153
+ describe("text validators", () => {
154
+ it("richText uses validateRichText and marks lower sensitivity", () => {
155
+ const fb = field.richText() as any;
156
+ expect(getCall(fb, "PID").args[0]).toMatchObject({
157
+ classification: "low",
158
+ action: "clear",
159
+ });
160
+ expect(runFirstValidator(fb, "<p>Hello</p>")).toBe(true);
161
+ expect(runFirstValidator(fb, "<script>alert('x')</script>")).toBe(false);
162
+ });
163
+
164
+ it("generalText uses validateSafeText", () => {
165
+ const fb = field.generalText() as any;
166
+ expect(getCall(fb, "PID").args[0]).toMatchObject({
167
+ classification: "none",
168
+ action: "none",
169
+ });
170
+ expect(runFirstValidator(fb, "Just a sentence.")).toBe(true);
171
+ expect(runFirstValidator(fb, "<b>Not allowed</b>")).toBe(false);
172
+ });
173
+ });
174
+
175
+ describe("geo & versioning", () => {
176
+ it("latitude enforces -90..90", () => {
177
+ const fb = field.latitude() as any;
178
+ expect(getCall(fb, "min").args[0]).toBe(-90);
179
+ expect(getCall(fb, "max").args[0]).toBe(90);
180
+ expect(getCall(fb, "description").args[0]).toMatch(/WGS 84/);
181
+ });
182
+
183
+ it("longitude enforces -180..180", () => {
184
+ const fb = field.longitude() as any;
185
+ expect(getCall(fb, "min").args[0]).toBe(-180);
186
+ expect(getCall(fb, "max").args[0]).toBe(180);
187
+ expect(getCall(fb, "description").args[0]).toMatch(/WGS 84/);
188
+ });
189
+
190
+ it("version uses semver validator", () => {
191
+ const fb = field.version() as any;
192
+ expect(getCall(fb, "description").args[0]).toMatch(/semantic version/i);
193
+ expect(runFirstValidator(fb, "1.2.3")).toBe(true);
194
+ expect(runFirstValidator(fb, "1.2")).toBe(false);
195
+ });
196
+ });
197
+
198
+ describe("codes: country & language", () => {
199
+ it("countryCode uses ISO 3166 validator", () => {
200
+ const fb = field.countryCode() as any;
201
+ expect(getCall(fb, "description").args[0]).toMatch(/ISO 3166/i);
202
+ expect(runFirstValidator(fb, "GB")).toBe(true);
203
+ expect(runFirstValidator(fb, "ZZ")).toBe(false);
204
+ });
205
+
206
+ it("languageCode uses BCP 47 validator", () => {
207
+ const fb = field.languageCode() as any;
208
+ expect(getCall(fb, "description").args[0]).toMatch(/BCP 47/i);
209
+ expect(runFirstValidator(fb, "en")).toBe(true);
210
+ expect(runFirstValidator(fb, "en-GB")).toBe(true);
211
+ expect(runFirstValidator(fb, "english-UK")).toBe(false);
212
+ });
213
+ });
package/tests/pii.test.ts CHANGED
@@ -32,7 +32,7 @@ describe("PII log sanitization", () => {
32
32
  }),
33
33
  },
34
34
  "LogSchema",
35
- { version: "1.0", piiEnforcement: "strict" }
35
+ { version: "1.0.0", piiEnforcement: "strict" }
36
36
  );
37
37
 
38
38
  it("should redact, pseudonymize, pass-through, and omit according to logHandling", () => {
@@ -123,7 +123,7 @@ describe("PII log sanitization", () => {
123
123
  }),
124
124
  },
125
125
  "Alt",
126
- { version: "1.0", piiEnforcement: "none" }
126
+ { version: "1.0.0", piiEnforcement: "none" }
127
127
  );
128
128
  expect(AltSchema.getPiiAudit()).toEqual([
129
129
  {
@@ -17,7 +17,7 @@ describe("schema.ts – validator coverage", () => {
17
17
  bad: undefined as any,
18
18
  },
19
19
  "BadDef",
20
- { version: "1.0", piiEnforcement: "strict" }
20
+ { version: "1.0.0", piiEnforcement: "strict" }
21
21
  );
22
22
  const r = S.validate({ bad: "anything" });
23
23
  expectInvalid(r, "Field definition missing for: bad");
@@ -29,15 +29,15 @@ describe("schema.ts – validator coverage", () => {
29
29
  k: field.string().enum(["A", "B", "C"]),
30
30
  },
31
31
  "StringEnum",
32
- { version: "1.0", piiEnforcement: "strict" }
32
+ { version: "1.0.0", piiEnforcement: "strict" }
33
33
  );
34
34
 
35
35
  const NumberField = createSchema({ n: field.number() }, "NumberField", {
36
- version: "1.0",
36
+ version: "1.0.0",
37
37
  piiEnforcement: "strict",
38
38
  });
39
39
  const BooleanField = createSchema({ b: field.boolean() }, "BooleanField", {
40
- version: "1.0",
40
+ version: "1.0.0",
41
41
  piiEnforcement: "strict",
42
42
  });
43
43
 
@@ -55,23 +55,23 @@ describe("schema.ts – validator coverage", () => {
55
55
  }),
56
56
  },
57
57
  "SimpleObject",
58
- { version: "1.0", piiEnforcement: "strict" }
58
+ { version: "1.0.0", piiEnforcement: "strict" }
59
59
  );
60
60
 
61
61
  const ArrayOfStrings = createSchema(
62
62
  { tags: field.array(field.string().enum(["alpha", "beta"])) },
63
63
  "ArrayOfStrings",
64
- { version: "1.0", piiEnforcement: "strict" }
64
+ { version: "1.0.0", piiEnforcement: "strict" }
65
65
  );
66
66
  const ArrayOfNumbers = createSchema(
67
67
  { nums: field.array(field.number()) },
68
68
  "ArrayOfNumbers",
69
- { version: "1.0", piiEnforcement: "strict" }
69
+ { version: "1.0.0", piiEnforcement: "strict" }
70
70
  );
71
71
  const ArrayOfBooleans = createSchema(
72
72
  { flags: field.array(field.boolean()) },
73
73
  "ArrayOfBooleans",
74
- { version: "1.0", piiEnforcement: "strict" }
74
+ { version: "1.0.0", piiEnforcement: "strict" }
75
75
  );
76
76
 
77
77
  const ArrayOfObjects = createSchema(
@@ -85,7 +85,7 @@ describe("schema.ts – validator coverage", () => {
85
85
  ),
86
86
  },
87
87
  "ArrayOfObjects",
88
- { version: "1.0", piiEnforcement: "strict" }
88
+ { version: "1.0.0", piiEnforcement: "strict" }
89
89
  );
90
90
 
91
91
  // 2) Missing required field
@@ -98,7 +98,7 @@ describe("schema.ts – validator coverage", () => {
98
98
  it("should fail when immutable field is modified", () => {
99
99
  const fixed = field.string().immutable() as any;
100
100
  const S = createSchema({ fixed }, "Immutable", {
101
- version: "1.0",
101
+ version: "1.0.0",
102
102
  piiEnforcement: "strict",
103
103
  });
104
104
 
@@ -118,7 +118,7 @@ describe("schema.ts – validator coverage", () => {
118
118
  }) as any;
119
119
  // required by default (not optional)
120
120
  const S = createSchema({ secret }, "PII", {
121
- version: "1.0",
121
+ version: "1.0.0",
122
122
  piiEnforcement: "strict",
123
123
  });
124
124
  const r = S.validate({ secret: "" }); // empty string should trigger strict error
@@ -132,7 +132,7 @@ describe("schema.ts – validator coverage", () => {
132
132
  code: field.string().validator((v) => v === "ok"),
133
133
  },
134
134
  "CustomValidator",
135
- { version: "1.0", piiEnforcement: "strict" }
135
+ { version: "1.0.0", piiEnforcement: "strict" }
136
136
  );
137
137
  const result = S.validate({ code: "nope" });
138
138
  expectInvalid(result, "Invalid value for field: code");
@@ -187,7 +187,7 @@ describe("schema.ts – validator coverage", () => {
187
187
  }),
188
188
  },
189
189
  "ChildValidator",
190
- { version: "1.0", piiEnforcement: "strict" }
190
+ { version: "1.0.0", piiEnforcement: "strict" }
191
191
  );
192
192
  const result = S.validate({ o: { a: "x" } });
193
193
  expectInvalid(result, "Invalid value for field: o.a");
@@ -218,7 +218,7 @@ describe("schema.ts – validator coverage", () => {
218
218
  }),
219
219
  },
220
220
  "GrandChildValidator",
221
- { version: "1.0", piiEnforcement: "strict" }
221
+ { version: "1.0.0", piiEnforcement: "strict" }
222
222
  );
223
223
  const result = S.validate({ o: { a: "x", nested: { f: true } } });
224
224
  expectInvalid(result, "Invalid value for field: o.nested.f");
@@ -293,7 +293,7 @@ describe("schema.ts – validator coverage", () => {
293
293
  ),
294
294
  },
295
295
  "ArrayChildValidator",
296
- { version: "1.0", piiEnforcement: "strict" }
296
+ { version: "1.0.0", piiEnforcement: "strict" }
297
297
  );
298
298
  const result = S.validate({ items: [{ t: "ok" }] });
299
299
  expectInvalid(result, "Invalid value for field: items[0].t");
@@ -303,7 +303,7 @@ describe("schema.ts – validator coverage", () => {
303
303
  it("should fail when array item is not a valid ref object (or wrong ref type)", () => {
304
304
  const refItem = { type: "ref", refType: "asset" } as any;
305
305
  const S = createSchema({ items: field.array(refItem) }, "ArrayRefInvalid", {
306
- version: "1.0",
306
+ version: "1.0.0",
307
307
  piiEnforcement: "strict",
308
308
  });
309
309
  // bad id type (number) and missing/incorrect fields should trigger the message
@@ -322,7 +322,7 @@ describe("schema.ts – validator coverage", () => {
322
322
  shape: { region: field.string() },
323
323
  } as any;
324
324
  const S = createSchema({ items: field.array(refItem) }, "ArrayRefMissing", {
325
- version: "1.0",
325
+ version: "1.0.0",
326
326
  piiEnforcement: "strict",
327
327
  });
328
328
  // Valid ref, but missing required shape field 'region'
@@ -340,7 +340,7 @@ describe("schema.ts – validator coverage", () => {
340
340
  const S = createSchema(
341
341
  { items: field.array(refItem) },
342
342
  "ArrayRefChildValidator",
343
- { version: "1.0", piiEnforcement: "strict" }
343
+ { version: "1.0.0", piiEnforcement: "strict" }
344
344
  );
345
345
  const r = S.validate({ items: [{ type: "asset", id: "a1", code: "XYZ" }] });
346
346
  expectInvalid(r, "Invalid value for field: items[0].code");
@@ -350,7 +350,7 @@ describe("schema.ts – validator coverage", () => {
350
350
  it("should fail when array item type is unsupported", () => {
351
351
  const weirdItem = { type: "date" } as any;
352
352
  const S = createSchema({ xs: field.array(weirdItem) }, "ArrayUnsupported", {
353
- version: "1.0",
353
+ version: "1.0.0",
354
354
  piiEnforcement: "strict",
355
355
  });
356
356
  const r = S.validate({ xs: ["2025-01-01"] });
@@ -361,7 +361,7 @@ describe("schema.ts – validator coverage", () => {
361
361
  it("should fail when ref field is not a valid {type,id} object", () => {
362
362
  const refDef = { type: "ref" } as any;
363
363
  const S = createSchema({ r: refDef }, "SingleRef", {
364
- version: "1.0",
364
+ version: "1.0.0",
365
365
  piiEnforcement: "strict",
366
366
  });
367
367
  const r = S.validate({ r: { type: "asset", id: 42 } }); // id should be string
@@ -372,7 +372,7 @@ describe("schema.ts – validator coverage", () => {
372
372
  it("should fail when field type is unknown", () => {
373
373
  const unknownDef = { type: "wat" } as any;
374
374
  const S = createSchema({ a: unknownDef }, "UnknownType", {
375
- version: "1.0",
375
+ version: "1.0.0",
376
376
  piiEnforcement: "strict",
377
377
  });
378
378
  const r = S.validate({ a: 123 });
@@ -386,7 +386,7 @@ describe("schema.ts – validator coverage", () => {
386
386
  a: field.number(),
387
387
  },
388
388
  "SchemaLevel",
389
- { version: "1.0", piiEnforcement: "strict" }
389
+ { version: "1.0.0", piiEnforcement: "strict" }
390
390
  );
391
391
 
392
392
  // monkey-patch a validator on the schema (if supported by createSchema options)
@@ -399,7 +399,7 @@ describe("schema.ts – validator coverage", () => {
399
399
  x: field.string().validator(() => false),
400
400
  },
401
401
  "SchemaLevel2",
402
- { version: "1.0", piiEnforcement: "strict" }
402
+ { version: "1.0.0", piiEnforcement: "strict" }
403
403
  );
404
404
  const r2 = S2.validate({ x: "anything" });
405
405
  expectInvalid(r2, "Invalid value for field: x");
@@ -466,7 +466,7 @@ describe("schema.ts – validator coverage", () => {
466
466
  it("should pass when immutable field remains unchanged compared to existing value", () => {
467
467
  const fixed = field.string().immutable() as any;
468
468
  const S = createSchema({ fixed }, "ImmutableOK", {
469
- version: "1.0",
469
+ version: "1.0.0",
470
470
  piiEnforcement: "strict",
471
471
  });
472
472
  const existing = { fixed: "A" };
@@ -482,7 +482,7 @@ describe("schema.ts – validator coverage", () => {
482
482
  purpose: "pii security test",
483
483
  }) as any;
484
484
  const S = createSchema({ secret }, "PII_OK", {
485
- version: "1.0",
485
+ version: "1.0.0",
486
486
  piiEnforcement: "strict",
487
487
  });
488
488
  const r = S.validate({ secret: "non-empty" });
@@ -493,7 +493,7 @@ describe("schema.ts – validator coverage", () => {
493
493
  const S = createSchema(
494
494
  { ok: field.string().validator((v) => v === "ok") },
495
495
  "CustomValidatorOK",
496
- { version: "1.0", piiEnforcement: "strict" }
496
+ { version: "1.0.0", piiEnforcement: "strict" }
497
497
  );
498
498
  const r = S.validate({ ok: "ok" });
499
499
  expectValid(r);
@@ -9,7 +9,7 @@ import { SchemaShape, validateUUID } from "../src/index.js";
9
9
  // Mock "character" schema
10
10
  export const characterSchema = createSchema(
11
11
  {
12
- name: field.string().description("Character name").version("1.0"),
12
+ name: field.string().description("Character name").version("1.0.0"),
13
13
  },
14
14
  "character"
15
15
  );
@@ -17,20 +17,20 @@ export type Character = Infer<typeof characterSchema>;
17
17
 
18
18
  const userShape: SchemaShape = {
19
19
  id: field.string().system().validator(validateUUID).immutable(),
20
- name: field.string().description("User name").version("1.0"),
20
+ name: field.string().description("User name").version("1.0.0"),
21
21
 
22
22
  bestFriend: field
23
23
  .string()
24
24
  .optional()
25
25
  .description("User's best friend ID")
26
- .version("1.0")
26
+ .version("1.0.0")
27
27
  .validator(validateUUID),
28
28
 
29
29
  characters: field
30
30
  .array(field.ref("character").as<Character>())
31
31
  .optional()
32
32
  .description("Characters owned by this user")
33
- .version("1.0"),
33
+ .version("1.0.0"),
34
34
  };
35
35
  // Mock "user" schema
36
36
  export const userSchema = createSchema(userShape, "user");
@@ -49,21 +49,21 @@ export const mockDb: {
49
49
  } = {
50
50
  "user:user-1": {
51
51
  type: "user",
52
- version: "1.0",
52
+ version: "1.0.0",
53
53
  name: "Alice",
54
54
  bestFriend: { type: "user", id: "user-2" },
55
55
  characters: [{ type: "character", id: "char-1" }],
56
56
  },
57
57
  "user:user-2": {
58
58
  type: "user",
59
- version: "1.0",
59
+ version: "1.0.0",
60
60
  name: "Bob",
61
61
  bestFriend: { type: "user", id: "user-1" }, // cycle!
62
62
  characters: [],
63
63
  },
64
64
  "character:char-1": {
65
65
  type: "character",
66
- version: "1.0",
66
+ version: "1.0.0",
67
67
  name: "Hero",
68
68
  },
69
69
  };
@@ -16,7 +16,7 @@ const userSchema = createSchema(
16
16
  characters: field.array(field.ref("character")),
17
17
  },
18
18
  "user",
19
- { version: "1.0", piiEnforcement: "strict" }
19
+ { version: "1.0.0", piiEnforcement: "strict" }
20
20
  );
21
21
 
22
22
  // Example of a compiled/standalone validator function consumers might use
@@ -27,7 +27,7 @@ describe("validate() with field()-based schema", () => {
27
27
  it("validates a correct object", () => {
28
28
  const input = {
29
29
  type: "user",
30
- version: "1.0",
30
+ version: "1.0.0",
31
31
  name: "Alice",
32
32
  bestFriend: { type: "user", id: "user-123" },
33
33
  characters: [{ type: "character", id: "char-1" }],
@@ -41,7 +41,7 @@ describe("validate() with field()-based schema", () => {
41
41
  it("detects missing required field (name)", () => {
42
42
  const input = {
43
43
  type: "user",
44
- version: "1.0",
44
+ version: "1.0.0",
45
45
  bestFriend: { type: "user", id: "user-123" },
46
46
  characters: [],
47
47
  };
@@ -54,7 +54,7 @@ describe("validate() with field()-based schema", () => {
54
54
  it("detects invalid ref shape for bestFriend", () => {
55
55
  const input = {
56
56
  type: "user",
57
- version: "1.0",
57
+ version: "1.0.0",
58
58
  name: "Test",
59
59
  bestFriend: { id: "user-123" }, // missing type
60
60
  characters: [],
@@ -71,7 +71,7 @@ describe("validate() with field()-based schema", () => {
71
71
  it("rejects name that fails custom validator (too short)", () => {
72
72
  const input = {
73
73
  type: "user",
74
- version: "1.0",
74
+ version: "1.0.0",
75
75
  name: "A", // too short per custom validator
76
76
  bestFriend: { type: "user", id: "user-123" },
77
77
  characters: [],
@@ -87,7 +87,7 @@ describe("validate() with field()-based schema", () => {
87
87
  it("accepts name that passes custom validator (>=2 chars)", () => {
88
88
  const input = {
89
89
  type: "user",
90
- version: "1.0",
90
+ version: "1.0.0",
91
91
  name: "Al",
92
92
  bestFriend: { type: "user", id: "user-123" },
93
93
  characters: [],