@notionhq/workers 0.0.82 → 0.0.86
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/LICENSE.md +21 -0
- package/dist/builder.d.ts +9 -4
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +23 -18
- package/dist/builder.test.d.ts +2 -0
- package/dist/builder.test.d.ts.map +1 -0
- package/dist/capabilities/automation.js +2 -2
- package/dist/capabilities/sync.d.ts.map +1 -1
- package/dist/capabilities/sync.js +22 -6
- package/dist/capabilities/tool.d.ts +13 -6
- package/dist/capabilities/tool.d.ts.map +1 -1
- package/dist/capabilities/tool.js +28 -18
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/schema-builder.d.ts +79 -0
- package/dist/schema-builder.d.ts.map +1 -0
- package/dist/schema-builder.js +135 -0
- package/dist/schema-builder.test.d.ts +2 -0
- package/dist/schema-builder.test.d.ts.map +1 -0
- package/dist/worker.d.ts +6 -11
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +6 -11
- package/package.json +9 -3
- package/src/builder.test.ts +229 -0
- package/src/builder.ts +38 -25
- package/src/capabilities/automation.test.ts +8 -4
- package/src/capabilities/automation.ts +2 -2
- package/src/capabilities/sync.test.ts +70 -1
- package/src/capabilities/sync.ts +18 -6
- package/src/capabilities/tool.test.ts +32 -167
- package/src/capabilities/tool.ts +33 -26
- package/src/index.ts +2 -0
- package/src/schema-builder.test.ts +677 -0
- package/src/schema-builder.ts +203 -0
- package/src/worker.ts +3 -8
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { JSONSchema } from "./json-schema.js";
|
|
3
|
+
import {
|
|
4
|
+
getSchema,
|
|
5
|
+
type Infer,
|
|
6
|
+
j,
|
|
7
|
+
type SchemaBuilder,
|
|
8
|
+
} from "./schema-builder.js";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// 1. Primitives
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
describe("json-schema-builder — primitives", () => {
|
|
15
|
+
it("string()", () => {
|
|
16
|
+
expect(getSchema(j.string())).toEqual({ type: "string" });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("number()", () => {
|
|
20
|
+
expect(getSchema(j.number())).toEqual({ type: "number" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("integer()", () => {
|
|
24
|
+
expect(getSchema(j.integer())).toEqual({ type: "integer" });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("boolean()", () => {
|
|
28
|
+
expect(getSchema(j.boolean())).toEqual({ type: "boolean" });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// 2. Description
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
describe("json-schema-builder — description", () => {
|
|
37
|
+
it("adds description field", () => {
|
|
38
|
+
expect(getSchema(j.string().describe("A name"))).toEqual({
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "A name",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("is chainable — last wins", () => {
|
|
45
|
+
const s = j.number().describe("first").describe("second");
|
|
46
|
+
expect(getSchema(s)).toEqual({
|
|
47
|
+
type: "number",
|
|
48
|
+
description: "second",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("works on all primitive types", () => {
|
|
53
|
+
expect(getSchema(j.boolean().describe("flag"))).toEqual({
|
|
54
|
+
type: "boolean",
|
|
55
|
+
description: "flag",
|
|
56
|
+
});
|
|
57
|
+
expect(getSchema(j.integer().describe("count"))).toEqual({
|
|
58
|
+
type: "integer",
|
|
59
|
+
description: "count",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// 3. Enum
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
describe("json-schema-builder — enum", () => {
|
|
69
|
+
it("string enum", () => {
|
|
70
|
+
expect(getSchema(j.enum("a", "b", "c"))).toEqual({
|
|
71
|
+
type: "string",
|
|
72
|
+
enum: ["a", "b", "c"],
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("number enum", () => {
|
|
77
|
+
expect(getSchema(j.enum(1, 2, 3))).toEqual({
|
|
78
|
+
type: "number",
|
|
79
|
+
enum: [1, 2, 3],
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("string enum type inference", () => {
|
|
84
|
+
const s = j.enum("x", "y");
|
|
85
|
+
type T = Infer<typeof s>;
|
|
86
|
+
const _a: T = "x";
|
|
87
|
+
const _b: T = "y";
|
|
88
|
+
expect(getSchema(s)).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("number enum type inference", () => {
|
|
92
|
+
const s = j.enum(10, 20);
|
|
93
|
+
type T = Infer<typeof s>;
|
|
94
|
+
const _a: T = 10;
|
|
95
|
+
const _b: T = 20;
|
|
96
|
+
expect(getSchema(s)).toBeDefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("enum with description", () => {
|
|
100
|
+
expect(getSchema(j.enum("a", "b").describe("pick one"))).toEqual({
|
|
101
|
+
type: "string",
|
|
102
|
+
enum: ["a", "b"],
|
|
103
|
+
description: "pick one",
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// 4. Format shorthands
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
describe("json-schema-builder — format shorthands", () => {
|
|
113
|
+
it("datetime()", () => {
|
|
114
|
+
expect(getSchema(j.datetime())).toEqual({
|
|
115
|
+
type: "string",
|
|
116
|
+
format: "date-time",
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("date()", () => {
|
|
121
|
+
expect(getSchema(j.date())).toEqual({ type: "string", format: "date" });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("time()", () => {
|
|
125
|
+
expect(getSchema(j.time())).toEqual({ type: "string", format: "time" });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("duration()", () => {
|
|
129
|
+
expect(getSchema(j.duration())).toEqual({
|
|
130
|
+
type: "string",
|
|
131
|
+
format: "duration",
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("email()", () => {
|
|
136
|
+
expect(getSchema(j.email())).toEqual({
|
|
137
|
+
type: "string",
|
|
138
|
+
format: "email",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("hostname()", () => {
|
|
143
|
+
expect(getSchema(j.hostname())).toEqual({
|
|
144
|
+
type: "string",
|
|
145
|
+
format: "hostname",
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("ipv4()", () => {
|
|
150
|
+
expect(getSchema(j.ipv4())).toEqual({ type: "string", format: "ipv4" });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("ipv6()", () => {
|
|
154
|
+
expect(getSchema(j.ipv6())).toEqual({ type: "string", format: "ipv6" });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("uuid()", () => {
|
|
158
|
+
expect(getSchema(j.uuid())).toEqual({ type: "string", format: "uuid" });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// 5. Array
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
describe("json-schema-builder — array", () => {
|
|
167
|
+
it("basic array", () => {
|
|
168
|
+
expect(getSchema(j.array(j.string()))).toEqual({
|
|
169
|
+
type: "array",
|
|
170
|
+
items: { type: "string" },
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("array with minItems 1", () => {
|
|
175
|
+
expect(getSchema(j.array(j.string(), { minItems: 1 }))).toEqual({
|
|
176
|
+
type: "array",
|
|
177
|
+
items: { type: "string" },
|
|
178
|
+
minItems: 1,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("array with minItems 0", () => {
|
|
183
|
+
expect(getSchema(j.array(j.number(), { minItems: 0 }))).toEqual({
|
|
184
|
+
type: "array",
|
|
185
|
+
items: { type: "number" },
|
|
186
|
+
minItems: 0,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("array of objects", () => {
|
|
191
|
+
const schema = getSchema(
|
|
192
|
+
j.array(j.object({ name: j.string(), age: j.number() })),
|
|
193
|
+
);
|
|
194
|
+
expect(schema).toEqual({
|
|
195
|
+
type: "array",
|
|
196
|
+
items: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: {
|
|
199
|
+
name: { type: "string" },
|
|
200
|
+
age: { type: "number" },
|
|
201
|
+
},
|
|
202
|
+
required: ["name", "age"],
|
|
203
|
+
additionalProperties: false,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("rejects minItems > 1 at type level", () => {
|
|
209
|
+
// @ts-expect-error — minItems can only be 0 or 1
|
|
210
|
+
j.array(j.string(), { minItems: 2 });
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// 6. Object
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
describe("json-schema-builder — object", () => {
|
|
219
|
+
it("auto-generates required and additionalProperties: false", () => {
|
|
220
|
+
const schema = getSchema(
|
|
221
|
+
j.object({
|
|
222
|
+
name: j.string(),
|
|
223
|
+
age: j.number(),
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
expect(schema).toEqual({
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
name: { type: "string" },
|
|
230
|
+
age: { type: "number" },
|
|
231
|
+
},
|
|
232
|
+
required: ["name", "age"],
|
|
233
|
+
additionalProperties: false,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("nested objects", () => {
|
|
238
|
+
const schema = getSchema(
|
|
239
|
+
j.object({
|
|
240
|
+
address: j.object({
|
|
241
|
+
street: j.string(),
|
|
242
|
+
zip: j.string(),
|
|
243
|
+
}),
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
expect(schema).toEqual({
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
address: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
street: { type: "string" },
|
|
253
|
+
zip: { type: "string" },
|
|
254
|
+
},
|
|
255
|
+
required: ["street", "zip"],
|
|
256
|
+
additionalProperties: false,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
required: ["address"],
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("empty object", () => {
|
|
265
|
+
expect(getSchema(j.object({}))).toEqual({
|
|
266
|
+
type: "object",
|
|
267
|
+
properties: {},
|
|
268
|
+
required: [],
|
|
269
|
+
additionalProperties: false,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("object with description", () => {
|
|
274
|
+
const schema = getSchema(j.object({ x: j.number() }).describe("A point"));
|
|
275
|
+
expect(schema).toEqual({
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: { x: { type: "number" } },
|
|
278
|
+
required: ["x"],
|
|
279
|
+
additionalProperties: false,
|
|
280
|
+
description: "A point",
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// 7. Nullable
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
describe("json-schema-builder — nullable", () => {
|
|
290
|
+
it("wraps in anyOf with null", () => {
|
|
291
|
+
expect(getSchema(j.string().nullable())).toEqual({
|
|
292
|
+
anyOf: [{ type: "string" }, { type: "null" }],
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("hoists description to outer wrapper", () => {
|
|
297
|
+
expect(getSchema(j.string().describe("A name").nullable())).toEqual({
|
|
298
|
+
anyOf: [{ type: "string" }, { type: "null" }],
|
|
299
|
+
description: "A name",
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("description after nullable sets on wrapper", () => {
|
|
304
|
+
expect(getSchema(j.string().nullable().describe("A name"))).toEqual({
|
|
305
|
+
anyOf: [{ type: "string" }, { type: "null" }],
|
|
306
|
+
description: "A name",
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("nullable number", () => {
|
|
311
|
+
expect(getSchema(j.number().nullable())).toEqual({
|
|
312
|
+
anyOf: [{ type: "number" }, { type: "null" }],
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("nullable enum", () => {
|
|
317
|
+
expect(getSchema(j.enum("a", "b").nullable())).toEqual({
|
|
318
|
+
anyOf: [{ type: "string", enum: ["a", "b"] }, { type: "null" }],
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// 8. Ref
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
describe("json-schema-builder — ref", () => {
|
|
328
|
+
it("produces $ref schema", () => {
|
|
329
|
+
expect(getSchema(j.ref("#/$defs/Foo"))).toEqual({
|
|
330
|
+
$ref: "#/$defs/Foo",
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("ref with description", () => {
|
|
335
|
+
expect(getSchema(j.ref("#/$defs/Bar").describe("A bar ref"))).toEqual({
|
|
336
|
+
$ref: "#/$defs/Bar",
|
|
337
|
+
description: "A bar ref",
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// 9. AnyOf
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
describe("json-schema-builder — anyOf", () => {
|
|
347
|
+
it("produces anyOf schema", () => {
|
|
348
|
+
expect(getSchema(j.anyOf(j.string(), j.number()))).toEqual({
|
|
349
|
+
anyOf: [{ type: "string" }, { type: "number" }],
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("anyOf with description", () => {
|
|
354
|
+
expect(
|
|
355
|
+
getSchema(j.anyOf(j.string(), j.number()).describe("string or number")),
|
|
356
|
+
).toEqual({
|
|
357
|
+
anyOf: [{ type: "string" }, { type: "number" }],
|
|
358
|
+
description: "string or number",
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// 9b. Discriminated unions
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
describe("json-schema-builder — discriminated unions", () => {
|
|
368
|
+
it("two-variant discriminated union", () => {
|
|
369
|
+
const schema = j.anyOf(
|
|
370
|
+
j.object({ type: j.enum("text"), value: j.string() }),
|
|
371
|
+
j.object({ type: j.enum("number"), value: j.number() }),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(getSchema(schema)).toEqual({
|
|
375
|
+
anyOf: [
|
|
376
|
+
{
|
|
377
|
+
type: "object",
|
|
378
|
+
properties: {
|
|
379
|
+
type: { type: "string", enum: ["text"] },
|
|
380
|
+
value: { type: "string" },
|
|
381
|
+
},
|
|
382
|
+
required: ["type", "value"],
|
|
383
|
+
additionalProperties: false,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
type: "object",
|
|
387
|
+
properties: {
|
|
388
|
+
type: { type: "string", enum: ["number"] },
|
|
389
|
+
value: { type: "number" },
|
|
390
|
+
},
|
|
391
|
+
required: ["type", "value"],
|
|
392
|
+
additionalProperties: false,
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("three-variant discriminated union with different shapes", () => {
|
|
399
|
+
const schema = j.anyOf(
|
|
400
|
+
j.object({ kind: j.enum("circle"), radius: j.number() }),
|
|
401
|
+
j.object({
|
|
402
|
+
kind: j.enum("rectangle"),
|
|
403
|
+
width: j.number(),
|
|
404
|
+
height: j.number(),
|
|
405
|
+
}),
|
|
406
|
+
j.object({
|
|
407
|
+
kind: j.enum("polygon"),
|
|
408
|
+
sides: j.array(j.object({ x: j.number(), y: j.number() })),
|
|
409
|
+
}),
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const raw = getSchema(schema) as unknown as {
|
|
413
|
+
anyOf: Record<string, unknown>[];
|
|
414
|
+
};
|
|
415
|
+
expect(raw.anyOf).toHaveLength(3);
|
|
416
|
+
|
|
417
|
+
// Each variant is a valid object schema with required + additionalProperties
|
|
418
|
+
for (const variant of raw.anyOf) {
|
|
419
|
+
expect(variant.type).toBe("object");
|
|
420
|
+
expect(variant.additionalProperties).toBe(false);
|
|
421
|
+
expect(Array.isArray(variant.required)).toBe(true);
|
|
422
|
+
expect(variant.required).toContain("kind");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Verify discriminant values
|
|
426
|
+
const discriminants = raw.anyOf.map(
|
|
427
|
+
(v) =>
|
|
428
|
+
(
|
|
429
|
+
(v.properties as Record<string, Record<string, unknown>>).kind as {
|
|
430
|
+
enum: string[];
|
|
431
|
+
}
|
|
432
|
+
).enum[0],
|
|
433
|
+
);
|
|
434
|
+
expect(discriminants).toEqual(["circle", "rectangle", "polygon"]);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("discriminated union type inference", () => {
|
|
438
|
+
const schema = j.anyOf(
|
|
439
|
+
j.object({ status: j.enum("ok"), data: j.string() }),
|
|
440
|
+
j.object({ status: j.enum("error"), message: j.string() }),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
type T = Infer<typeof schema>;
|
|
444
|
+
|
|
445
|
+
// Both variants are assignable
|
|
446
|
+
const _ok: T = { status: "ok", data: "hello" };
|
|
447
|
+
const _err: T = { status: "error", message: "fail" };
|
|
448
|
+
expect(getSchema(schema)).toBeDefined();
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("discriminated union with description on wrapper", () => {
|
|
452
|
+
const schema = j
|
|
453
|
+
.anyOf(
|
|
454
|
+
j.object({ type: j.enum("a"), x: j.number() }),
|
|
455
|
+
j.object({ type: j.enum("b"), y: j.string() }),
|
|
456
|
+
)
|
|
457
|
+
.describe("A or B");
|
|
458
|
+
|
|
459
|
+
const raw = getSchema(schema) as unknown as Record<string, unknown>;
|
|
460
|
+
expect(raw.description).toBe("A or B");
|
|
461
|
+
expect(raw.anyOf).toHaveLength(2);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("discriminated union with descriptions on individual variants", () => {
|
|
465
|
+
const schema = j.anyOf(
|
|
466
|
+
j
|
|
467
|
+
.object({ type: j.enum("text"), value: j.string() })
|
|
468
|
+
.describe("A text node"),
|
|
469
|
+
j
|
|
470
|
+
.object({ type: j.enum("image"), url: j.string() })
|
|
471
|
+
.describe("An image node"),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const raw = getSchema(schema) as unknown as {
|
|
475
|
+
anyOf: Record<string, unknown>[];
|
|
476
|
+
};
|
|
477
|
+
expect(raw.anyOf[0]?.description).toBe("A text node");
|
|
478
|
+
expect(raw.anyOf[1]?.description).toBe("An image node");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("discriminated union nested inside object property", () => {
|
|
482
|
+
const schema = j.object({
|
|
483
|
+
id: j.string(),
|
|
484
|
+
content: j.anyOf(
|
|
485
|
+
j.object({ type: j.enum("text"), body: j.string() }),
|
|
486
|
+
j.object({
|
|
487
|
+
type: j.enum("code"),
|
|
488
|
+
language: j.string(),
|
|
489
|
+
source: j.string(),
|
|
490
|
+
}),
|
|
491
|
+
),
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const raw = getSchema(schema) as unknown as {
|
|
495
|
+
properties: { content: { anyOf: Record<string, unknown>[] } };
|
|
496
|
+
};
|
|
497
|
+
expect(raw.properties.content.anyOf).toHaveLength(2);
|
|
498
|
+
|
|
499
|
+
// The content field is in required
|
|
500
|
+
expect(
|
|
501
|
+
(getSchema(schema) as unknown as Record<string, unknown>).required,
|
|
502
|
+
).toContain("content");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("nullable discriminated union", () => {
|
|
506
|
+
const schema = j
|
|
507
|
+
.anyOf(
|
|
508
|
+
j.object({ type: j.enum("a"), value: j.number() }),
|
|
509
|
+
j.object({ type: j.enum("b"), value: j.string() }),
|
|
510
|
+
)
|
|
511
|
+
.nullable();
|
|
512
|
+
|
|
513
|
+
const raw = getSchema(schema) as unknown as {
|
|
514
|
+
anyOf: Record<string, unknown>[];
|
|
515
|
+
};
|
|
516
|
+
// Should have the original anyOf wrapper + null
|
|
517
|
+
expect(raw.anyOf).toHaveLength(2);
|
|
518
|
+
expect(raw.anyOf[0]).toHaveProperty("anyOf"); // inner anyOf with the two variants
|
|
519
|
+
expect(raw.anyOf[1]).toEqual({ type: "null" });
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("discriminated union validates with AJV", () => {
|
|
523
|
+
const { Ajv } = require("ajv") as typeof import("ajv");
|
|
524
|
+
const ajv = new Ajv();
|
|
525
|
+
|
|
526
|
+
const schema = j.anyOf(
|
|
527
|
+
j.object({ action: j.enum("create"), name: j.string() }),
|
|
528
|
+
j.object({ action: j.enum("delete"), id: j.number() }),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const validate = ajv.compile(getSchema(schema));
|
|
532
|
+
|
|
533
|
+
expect(validate({ action: "create", name: "foo" })).toBe(true);
|
|
534
|
+
expect(validate({ action: "delete", id: 42 })).toBe(true);
|
|
535
|
+
expect(validate({ action: "create", id: 42 })).toBe(false);
|
|
536
|
+
expect(validate({ action: "update", name: "foo" })).toBe(false);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
// 10. Type inference
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
describe("json-schema-builder — type inference", () => {
|
|
545
|
+
it("getSchema output satisfies JSONSchema<T>", () => {
|
|
546
|
+
const s = j.object({
|
|
547
|
+
name: j.string(),
|
|
548
|
+
tags: j.array(j.string()),
|
|
549
|
+
});
|
|
550
|
+
const schema: JSONSchema<{ name: string; tags: string[] }> = getSchema(s);
|
|
551
|
+
expect(schema).toBeDefined();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("Infer extracts T from SchemaBuilder<T>", () => {
|
|
555
|
+
type S = SchemaBuilder<{ name: string; age: number }>;
|
|
556
|
+
type T = Infer<S>;
|
|
557
|
+
const _: T = { name: "Alice", age: 30 };
|
|
558
|
+
expect(true).toBe(true);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("nullable type inference", () => {
|
|
562
|
+
const s = j.object({
|
|
563
|
+
name: j.string(),
|
|
564
|
+
nickname: j.string().nullable(),
|
|
565
|
+
});
|
|
566
|
+
type T = Infer<typeof s>;
|
|
567
|
+
const _: T = { name: "Alice", nickname: null };
|
|
568
|
+
const _2: T = { name: "Alice", nickname: "Bob" };
|
|
569
|
+
expect(getSchema(s)).toBeDefined();
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// 11. Integration — complex schema round-trip
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
describe("json-schema-builder — integration", () => {
|
|
578
|
+
it("complex schema produces correct output", () => {
|
|
579
|
+
const schema = j.object({
|
|
580
|
+
foo: j.enum("a", "b", "c").describe("The foo field"),
|
|
581
|
+
bar: j.string(),
|
|
582
|
+
count: j.number(),
|
|
583
|
+
tags: j.array(j.string()),
|
|
584
|
+
address: j.object({
|
|
585
|
+
street: j.string(),
|
|
586
|
+
zip: j.string(),
|
|
587
|
+
}),
|
|
588
|
+
nickname: j.string().nullable(),
|
|
589
|
+
createdAt: j.datetime(),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
expect(getSchema(schema)).toEqual({
|
|
593
|
+
type: "object",
|
|
594
|
+
properties: {
|
|
595
|
+
foo: {
|
|
596
|
+
type: "string",
|
|
597
|
+
enum: ["a", "b", "c"],
|
|
598
|
+
description: "The foo field",
|
|
599
|
+
},
|
|
600
|
+
bar: { type: "string" },
|
|
601
|
+
count: { type: "number" },
|
|
602
|
+
tags: { type: "array", items: { type: "string" } },
|
|
603
|
+
address: {
|
|
604
|
+
type: "object",
|
|
605
|
+
properties: {
|
|
606
|
+
street: { type: "string" },
|
|
607
|
+
zip: { type: "string" },
|
|
608
|
+
},
|
|
609
|
+
required: ["street", "zip"],
|
|
610
|
+
additionalProperties: false,
|
|
611
|
+
},
|
|
612
|
+
nickname: {
|
|
613
|
+
anyOf: [{ type: "string" }, { type: "null" }],
|
|
614
|
+
},
|
|
615
|
+
createdAt: { type: "string", format: "date-time" },
|
|
616
|
+
},
|
|
617
|
+
required: [
|
|
618
|
+
"foo",
|
|
619
|
+
"bar",
|
|
620
|
+
"count",
|
|
621
|
+
"tags",
|
|
622
|
+
"address",
|
|
623
|
+
"nickname",
|
|
624
|
+
"createdAt",
|
|
625
|
+
],
|
|
626
|
+
additionalProperties: false,
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
// 12. Constraint enforcement
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
|
|
635
|
+
describe("json-schema-builder — constraint enforcement", () => {
|
|
636
|
+
it("object always has required matching properties keys", () => {
|
|
637
|
+
const schema = getSchema(
|
|
638
|
+
j.object({ x: j.number(), y: j.string(), z: j.boolean() }),
|
|
639
|
+
);
|
|
640
|
+
const raw = schema as unknown as Record<string, unknown>;
|
|
641
|
+
expect(raw.required).toEqual(["x", "y", "z"]);
|
|
642
|
+
expect(raw.additionalProperties).toBe(false);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("no forbidden keywords in output", () => {
|
|
646
|
+
const schema = getSchema(
|
|
647
|
+
j.object({
|
|
648
|
+
name: j.string(),
|
|
649
|
+
items: j.array(j.number()),
|
|
650
|
+
nested: j.object({ a: j.boolean() }),
|
|
651
|
+
}),
|
|
652
|
+
);
|
|
653
|
+
const json = JSON.stringify(schema);
|
|
654
|
+
expect(json).not.toContain('"allOf"');
|
|
655
|
+
expect(json).not.toContain('"not"');
|
|
656
|
+
expect(json).not.toContain('"if"');
|
|
657
|
+
expect(json).not.toContain('"then"');
|
|
658
|
+
expect(json).not.toContain('"else"');
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it("nested objects also enforce required and additionalProperties", () => {
|
|
662
|
+
const schema = getSchema(
|
|
663
|
+
j.object({
|
|
664
|
+
outer: j.object({
|
|
665
|
+
inner: j.object({
|
|
666
|
+
value: j.string(),
|
|
667
|
+
}),
|
|
668
|
+
}),
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
const raw = schema as unknown as Record<string, unknown>;
|
|
672
|
+
const outer = (raw as { properties: { outer: Record<string, unknown> } })
|
|
673
|
+
.properties.outer;
|
|
674
|
+
expect(outer.required).toEqual(["inner"]);
|
|
675
|
+
expect(outer.additionalProperties).toBe(false);
|
|
676
|
+
});
|
|
677
|
+
});
|