@plasius/schema 1.0.18 → 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.
- package/.github/workflows/cd.yml +51 -1
- package/CHANGELOG.md +35 -1
- package/README.md +140 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/src/field.builder.ts +121 -1
- package/src/field.ts +139 -0
- package/src/schema.ts +78 -11
- package/src/validation/dateTime.ISO8601.ts +56 -5
- package/src/validation/languageCode.BCP47.ts +299 -0
- package/src/validation/version.SEMVER2.0.0.ts +3 -1
- package/tests/field.builder.test.ts +81 -0
- package/tests/fields.test.ts +213 -0
- package/tests/pii.test.ts +2 -2
- package/tests/schema.test.ts +26 -26
- package/tests/test-utils.ts +7 -7
- package/tests/validate.test.ts +6 -6
|
@@ -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
|
{
|
package/tests/schema.test.ts
CHANGED
|
@@ -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);
|
package/tests/test-utils.ts
CHANGED
|
@@ -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
|
};
|
package/tests/validate.test.ts
CHANGED
|
@@ -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: [],
|