@schmock/schema 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,636 @@
1
+ import type { JSONSchema7 } from "json-schema";
2
+ import { describe, expect, it } from "vitest";
3
+ import { generateFromSchema, schemaPlugin } from "./index";
4
+ import { validators } from "./test-utils";
5
+
6
+ describe("Real-World Scenarios", () => {
7
+ describe("API Response Schemas", () => {
8
+ it("generates REST API list responses", () => {
9
+ const schema: JSONSchema7 = {
10
+ type: "object",
11
+ properties: {
12
+ data: {
13
+ type: "array",
14
+ items: {
15
+ type: "object",
16
+ properties: {
17
+ id: { type: "string", format: "uuid" },
18
+ title: { type: "string", minLength: 1, maxLength: 200 },
19
+ status: {
20
+ type: "string",
21
+ enum: ["draft", "published", "archived"],
22
+ },
23
+ author: {
24
+ type: "object",
25
+ properties: {
26
+ id: { type: "string", format: "uuid" },
27
+ name: { type: "string" },
28
+ email: { type: "string", format: "email" },
29
+ },
30
+ required: ["id", "name"],
31
+ },
32
+ createdAt: { type: "string", format: "date-time" },
33
+ tags: {
34
+ type: "array",
35
+ items: { type: "string" },
36
+ maxItems: 10,
37
+ },
38
+ },
39
+ required: ["id", "title", "status", "author", "createdAt"],
40
+ },
41
+ },
42
+ meta: {
43
+ type: "object",
44
+ properties: {
45
+ page: { type: "integer", minimum: 1 },
46
+ perPage: { type: "integer", minimum: 1, maximum: 100 },
47
+ total: { type: "integer", minimum: 0 },
48
+ totalPages: { type: "integer", minimum: 0 },
49
+ },
50
+ required: ["page", "perPage", "total", "totalPages"],
51
+ },
52
+ },
53
+ required: ["data", "meta"],
54
+ };
55
+
56
+ const result = generateFromSchema({ schema });
57
+
58
+ expect(result).toHaveProperty("data");
59
+ expect(result).toHaveProperty("meta");
60
+ expect(Array.isArray(result.data)).toBe(true);
61
+
62
+ // Verify structure
63
+ if (result.data.length > 0) {
64
+ const item = result.data[0];
65
+ expect(validators.appearsToBeFromCategory([item.id], "uuid")).toBe(
66
+ true,
67
+ );
68
+ expect(item.title.length).toBeGreaterThan(0);
69
+ expect(["draft", "published", "archived"]).toContain(item.status);
70
+ expect(
71
+ validators.appearsToBeFromCategory([item.author.email], "email"),
72
+ ).toBe(true);
73
+ }
74
+
75
+ // Meta should make sense
76
+ expect(result.meta.page).toBeGreaterThanOrEqual(1);
77
+ expect(result.meta.perPage).toBeGreaterThanOrEqual(1);
78
+ expect(result.meta.total).toBeGreaterThanOrEqual(0);
79
+ });
80
+
81
+ it("generates GraphQL-style responses", () => {
82
+ const schema: JSONSchema7 = {
83
+ type: "object",
84
+ properties: {
85
+ data: {
86
+ type: "object",
87
+ properties: {
88
+ user: {
89
+ type: "object",
90
+ properties: {
91
+ id: { type: "string" },
92
+ username: { type: "string" },
93
+ profile: {
94
+ type: "object",
95
+ properties: {
96
+ firstName: { type: "string" },
97
+ lastName: { type: "string" },
98
+ avatar: { type: "string", format: "uri" },
99
+ },
100
+ },
101
+ posts: {
102
+ type: "array",
103
+ items: {
104
+ type: "object",
105
+ properties: {
106
+ id: { type: "string" },
107
+ title: { type: "string" },
108
+ content: { type: "string" },
109
+ publishedAt: { type: "string", format: "date-time" },
110
+ },
111
+ },
112
+ },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ errors: {
118
+ type: "array",
119
+ items: {
120
+ type: "object",
121
+ properties: {
122
+ message: { type: "string" },
123
+ path: { type: "array", items: { type: "string" } },
124
+ extensions: { type: "object" },
125
+ },
126
+ },
127
+ },
128
+ },
129
+ };
130
+
131
+ const result = generateFromSchema({ schema });
132
+
133
+ if (result.data?.user) {
134
+ expect(result.data.user).toHaveProperty("id");
135
+ expect(result.data.user).toHaveProperty("username");
136
+
137
+ if (result.data.user.profile) {
138
+ expect(typeof result.data.user.profile.firstName).toBe("string");
139
+ expect(result.data.user.profile.firstName.length).toBeGreaterThan(0);
140
+ }
141
+
142
+ if (result.data.user.posts && result.data.user.posts.length > 0) {
143
+ expect(result.data.user.posts[0]).toHaveProperty("title");
144
+ }
145
+ }
146
+ });
147
+
148
+ it("generates webhook payloads", () => {
149
+ const schema: JSONSchema7 = {
150
+ type: "object",
151
+ properties: {
152
+ event: {
153
+ type: "string",
154
+ enum: ["order.created", "order.updated", "order.cancelled"],
155
+ },
156
+ timestamp: { type: "string", format: "date-time" },
157
+ data: {
158
+ type: "object",
159
+ properties: {
160
+ orderId: { type: "string", format: "uuid" },
161
+ customerId: { type: "string", format: "uuid" },
162
+ amount: { type: "number", minimum: 0 },
163
+ currency: { type: "string", enum: ["USD", "EUR", "GBP"] },
164
+ items: {
165
+ type: "array",
166
+ items: {
167
+ type: "object",
168
+ properties: {
169
+ productId: { type: "string" },
170
+ quantity: { type: "integer", minimum: 1 },
171
+ price: { type: "number", minimum: 0 },
172
+ },
173
+ },
174
+ },
175
+ },
176
+ },
177
+ signature: { type: "string", pattern: "^[a-f0-9]{64}$" },
178
+ },
179
+ required: ["event", "timestamp", "data", "signature"],
180
+ };
181
+
182
+ const result = generateFromSchema({ schema });
183
+
184
+ expect(["order.created", "order.updated", "order.cancelled"]).toContain(
185
+ result.event,
186
+ );
187
+ expect(new Date(result.timestamp).getTime()).not.toBeNaN();
188
+ expect(result.signature).toMatch(/^[a-f0-9]{64}$/);
189
+ expect(result.data.amount).toBeGreaterThanOrEqual(0);
190
+ });
191
+ });
192
+
193
+ describe("Database Model Schemas", () => {
194
+ it("generates user model data", () => {
195
+ const schema: JSONSchema7 = {
196
+ type: "object",
197
+ properties: {
198
+ id: { type: "integer", minimum: 1 },
199
+ username: { type: "string", pattern: "^[a-zA-Z0-9_]{3,20}$" },
200
+ email: { type: "string", format: "email" },
201
+ passwordHash: {
202
+ type: "string",
203
+ pattern: "^\\$2[aby]\\$[0-9]{2}\\$.{53}$",
204
+ },
205
+ profile: {
206
+ type: "object",
207
+ properties: {
208
+ firstName: { type: "string", maxLength: 50 },
209
+ lastName: { type: "string", maxLength: 50 },
210
+ bio: { type: "string", maxLength: 500 },
211
+ dateOfBirth: { type: "string", format: "date" },
212
+ phoneNumber: { type: "string", pattern: "^\\+?[1-9]\\d{1,14}$" },
213
+ },
214
+ },
215
+ settings: {
216
+ type: "object",
217
+ properties: {
218
+ theme: { type: "string", enum: ["light", "dark", "auto"] },
219
+ language: { type: "string", enum: ["en", "es", "fr", "de"] },
220
+ emailNotifications: { type: "boolean" },
221
+ twoFactorEnabled: { type: "boolean" },
222
+ },
223
+ },
224
+ createdAt: { type: "string", format: "date-time" },
225
+ updatedAt: { type: "string", format: "date-time" },
226
+ lastLoginAt: {
227
+ oneOf: [{ type: "string", format: "date-time" }, { type: "null" }],
228
+ },
229
+ },
230
+ required: [
231
+ "id",
232
+ "username",
233
+ "email",
234
+ "passwordHash",
235
+ "createdAt",
236
+ "updatedAt",
237
+ ],
238
+ };
239
+
240
+ const result = generateFromSchema({ schema });
241
+
242
+ expect(result.id).toBeGreaterThanOrEqual(1);
243
+ expect(result.username).toMatch(/^[a-zA-Z0-9_]{3,20}$/);
244
+ expect(validators.appearsToBeFromCategory([result.email], "email")).toBe(
245
+ true,
246
+ );
247
+ expect(result.passwordHash).toMatch(/^\$2[aby]\$[0-9]{2}\$.{53}$/);
248
+
249
+ // Dates should be valid
250
+ const created = new Date(result.createdAt);
251
+ const updated = new Date(result.updatedAt);
252
+ expect(created.getTime()).not.toBeNaN();
253
+ expect(updated.getTime()).not.toBeNaN();
254
+ });
255
+
256
+ it("generates product catalog data", () => {
257
+ const schema: JSONSchema7 = {
258
+ type: "object",
259
+ properties: {
260
+ sku: { type: "string", pattern: "^[A-Z]{3}-[0-9]{6}$" },
261
+ name: { type: "string", minLength: 1, maxLength: 200 },
262
+ description: { type: "string", maxLength: 2000 },
263
+ price: {
264
+ type: "object",
265
+ properties: {
266
+ amount: { type: "number", minimum: 0, multipleOf: 0.01 },
267
+ currency: { type: "string", enum: ["USD", "EUR", "GBP"] },
268
+ },
269
+ required: ["amount", "currency"],
270
+ },
271
+ inventory: {
272
+ type: "object",
273
+ properties: {
274
+ inStock: { type: "integer", minimum: 0 },
275
+ reserved: { type: "integer", minimum: 0 },
276
+ available: { type: "integer" },
277
+ },
278
+ },
279
+ categories: {
280
+ type: "array",
281
+ items: { type: "string" },
282
+ minItems: 1,
283
+ maxItems: 5,
284
+ },
285
+ attributes: {
286
+ type: "object",
287
+ additionalProperties: { type: "string" },
288
+ },
289
+ },
290
+ };
291
+
292
+ const result = generateFromSchema({ schema });
293
+
294
+ expect(result.sku).toMatch(/^[A-Z]{3}-[0-9]{6}$/);
295
+ expect(result.name.length).toBeGreaterThan(0);
296
+ expect(result.price.amount).toBeGreaterThanOrEqual(0);
297
+ expect(["USD", "EUR", "GBP"]).toContain(result.price.currency);
298
+
299
+ if (result.inventory) {
300
+ expect(result.inventory.inStock).toBeGreaterThanOrEqual(0);
301
+ expect(result.inventory.reserved).toBeGreaterThanOrEqual(0);
302
+ }
303
+ });
304
+ });
305
+
306
+ describe("Configuration Schemas", () => {
307
+ it("generates application config", () => {
308
+ const schema: JSONSchema7 = {
309
+ type: "object",
310
+ properties: {
311
+ app: {
312
+ type: "object",
313
+ properties: {
314
+ name: { type: "string" },
315
+ version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
316
+ environment: {
317
+ type: "string",
318
+ enum: ["development", "staging", "production"],
319
+ },
320
+ debug: { type: "boolean" },
321
+ },
322
+ required: ["name", "version", "environment"],
323
+ },
324
+ server: {
325
+ type: "object",
326
+ properties: {
327
+ host: { type: "string", format: "hostname" },
328
+ port: { type: "integer", minimum: 1, maximum: 65535 },
329
+ ssl: {
330
+ type: "object",
331
+ properties: {
332
+ enabled: { type: "boolean" },
333
+ cert: { type: "string" },
334
+ key: { type: "string" },
335
+ },
336
+ },
337
+ },
338
+ },
339
+ database: {
340
+ type: "object",
341
+ properties: {
342
+ type: { type: "string", enum: ["postgres", "mysql", "mongodb"] },
343
+ host: { type: "string" },
344
+ port: { type: "integer" },
345
+ name: { type: "string" },
346
+ pool: {
347
+ type: "object",
348
+ properties: {
349
+ min: { type: "integer", minimum: 0 },
350
+ max: { type: "integer", minimum: 1 },
351
+ },
352
+ },
353
+ },
354
+ },
355
+ },
356
+ };
357
+
358
+ const result = generateFromSchema({ schema });
359
+
360
+ if (result.app) {
361
+ expect(result.app.version).toMatch(/^\d+\.\d+\.\d+$/);
362
+ expect(["development", "staging", "production"]).toContain(
363
+ result.app.environment,
364
+ );
365
+ }
366
+
367
+ if (result.server?.port) {
368
+ expect(result.server.port).toBeGreaterThanOrEqual(1);
369
+ expect(result.server.port).toBeLessThanOrEqual(65535);
370
+ }
371
+ });
372
+
373
+ it("generates OpenAPI schema definitions", () => {
374
+ const schema: JSONSchema7 = {
375
+ type: "object",
376
+ properties: {
377
+ openapi: { type: "string", const: "3.0.0" },
378
+ info: {
379
+ type: "object",
380
+ properties: {
381
+ title: { type: "string" },
382
+ version: { type: "string" },
383
+ description: { type: "string" },
384
+ },
385
+ required: ["title", "version"],
386
+ },
387
+ servers: {
388
+ type: "array",
389
+ items: {
390
+ type: "object",
391
+ properties: {
392
+ url: { type: "string", format: "uri" },
393
+ description: { type: "string" },
394
+ },
395
+ required: ["url"],
396
+ },
397
+ },
398
+ paths: {
399
+ type: "object",
400
+ additionalProperties: {
401
+ type: "object",
402
+ properties: {
403
+ get: { type: "object" },
404
+ post: { type: "object" },
405
+ put: { type: "object" },
406
+ delete: { type: "object" },
407
+ },
408
+ },
409
+ },
410
+ },
411
+ required: ["openapi", "info", "paths"],
412
+ };
413
+
414
+ const result = generateFromSchema({ schema });
415
+
416
+ expect(result.openapi).toBe("3.0.0");
417
+ expect(result.info).toHaveProperty("title");
418
+ expect(result.info).toHaveProperty("version");
419
+ expect(result.paths).toBeDefined();
420
+ });
421
+ });
422
+
423
+ describe("Form Data Schemas", () => {
424
+ it("generates user registration form data", () => {
425
+ const schema: JSONSchema7 = {
426
+ type: "object",
427
+ properties: {
428
+ username: {
429
+ type: "string",
430
+ minLength: 3,
431
+ maxLength: 20,
432
+ pattern: "^[a-zA-Z0-9_]+$",
433
+ },
434
+ email: {
435
+ type: "string",
436
+ format: "email",
437
+ },
438
+ password: {
439
+ type: "string",
440
+ minLength: 8,
441
+ maxLength: 128,
442
+ },
443
+ confirmPassword: {
444
+ type: "string",
445
+ minLength: 8,
446
+ maxLength: 128,
447
+ },
448
+ profile: {
449
+ type: "object",
450
+ properties: {
451
+ firstName: { type: "string", maxLength: 50 },
452
+ lastName: { type: "string", maxLength: 50 },
453
+ dateOfBirth: { type: "string", format: "date" },
454
+ country: { type: "string" },
455
+ newsletter: { type: "boolean" },
456
+ },
457
+ },
458
+ termsAccepted: { type: "boolean", const: true },
459
+ },
460
+ required: [
461
+ "username",
462
+ "email",
463
+ "password",
464
+ "confirmPassword",
465
+ "termsAccepted",
466
+ ],
467
+ };
468
+
469
+ const result = generateFromSchema({ schema });
470
+
471
+ expect(result.username).toMatch(/^[a-zA-Z0-9_]+$/);
472
+ expect(result.username.length).toBeGreaterThanOrEqual(3);
473
+ expect(validators.appearsToBeFromCategory([result.email], "email")).toBe(
474
+ true,
475
+ );
476
+ expect(result.password.length).toBeGreaterThanOrEqual(8);
477
+ expect(result.termsAccepted).toBe(true);
478
+ });
479
+
480
+ it("generates complex survey responses", () => {
481
+ const schema: JSONSchema7 = {
482
+ type: "object",
483
+ properties: {
484
+ respondentId: { type: "string", format: "uuid" },
485
+ submittedAt: { type: "string", format: "date-time" },
486
+ responses: {
487
+ type: "array",
488
+ items: {
489
+ type: "object",
490
+ properties: {
491
+ questionId: { type: "string" },
492
+ type: {
493
+ type: "string",
494
+ enum: ["text", "choice", "scale", "multiselect"],
495
+ },
496
+ answer: {
497
+ oneOf: [
498
+ { type: "string" },
499
+ { type: "number" },
500
+ { type: "array", items: { type: "string" } },
501
+ ],
502
+ },
503
+ },
504
+ required: ["questionId", "type", "answer"],
505
+ },
506
+ },
507
+ metadata: {
508
+ type: "object",
509
+ properties: {
510
+ duration: { type: "integer", minimum: 0 },
511
+ device: { type: "string", enum: ["desktop", "mobile", "tablet"] },
512
+ browser: { type: "string" },
513
+ },
514
+ },
515
+ },
516
+ };
517
+
518
+ const result = generateFromSchema({ schema });
519
+
520
+ expect(
521
+ validators.appearsToBeFromCategory([result.respondentId], "uuid"),
522
+ ).toBe(true);
523
+ expect(new Date(result.submittedAt).getTime()).not.toBeNaN();
524
+
525
+ if (result.responses && result.responses.length > 0) {
526
+ result.responses.forEach((response) => {
527
+ expect(["text", "choice", "scale", "multiselect"]).toContain(
528
+ response.type,
529
+ );
530
+ expect(response).toHaveProperty("answer");
531
+ });
532
+ }
533
+ });
534
+ });
535
+
536
+ describe("Integration with Schmock", () => {
537
+ it("works with schmock route handlers", () => {
538
+ const plugin = schemaPlugin({
539
+ schema: {
540
+ type: "object",
541
+ properties: {
542
+ users: {
543
+ type: "array",
544
+ items: {
545
+ type: "object",
546
+ properties: {
547
+ id: { type: "string", format: "uuid" },
548
+ name: { type: "string" },
549
+ email: { type: "string", format: "email" },
550
+ },
551
+ },
552
+ },
553
+ _links: {
554
+ type: "object",
555
+ properties: {
556
+ self: { type: "string", format: "uri" },
557
+ next: { type: "string", format: "uri" },
558
+ prev: { type: "string", format: "uri" },
559
+ },
560
+ },
561
+ },
562
+ },
563
+ count: 10,
564
+ });
565
+
566
+ const context = {
567
+ method: "GET",
568
+ path: "/api/users",
569
+ params: {},
570
+ query: { page: "2", limit: "10" },
571
+ state: {},
572
+ headers: {},
573
+ body: null,
574
+ route: {},
575
+ };
576
+
577
+ const result = plugin.process(context);
578
+
579
+ expect(result.response).toHaveProperty("users");
580
+ expect(Array.isArray(result.response.users)).toBe(true);
581
+
582
+ if (result.response.users.length > 0) {
583
+ const user = result.response.users[0];
584
+ expect(validators.appearsToBeFromCategory([user.id], "uuid")).toBe(
585
+ true,
586
+ );
587
+ expect(validators.appearsToBeFromCategory([user.email], "email")).toBe(
588
+ true,
589
+ );
590
+ }
591
+ });
592
+
593
+ it("integrates with template overrides", () => {
594
+ const plugin = schemaPlugin({
595
+ schema: {
596
+ type: "object",
597
+ properties: {
598
+ orderId: { type: "string" },
599
+ customerId: { type: "string" },
600
+ total: { type: "number" },
601
+ status: { type: "string" },
602
+ createdAt: { type: "string" },
603
+ },
604
+ },
605
+ overrides: {
606
+ orderId: "{{params.orderId}}",
607
+ customerId: "{{state.user.id}}",
608
+ status: "pending",
609
+ createdAt: "{{state.timestamp}}",
610
+ },
611
+ });
612
+
613
+ const context = {
614
+ method: "GET",
615
+ path: "/api/orders/order-123",
616
+ params: { orderId: "order-123" },
617
+ query: {},
618
+ state: new Map(),
619
+ routeState: {
620
+ user: { id: "customer-456" },
621
+ timestamp: "2024-01-01T12:00:00Z",
622
+ },
623
+ headers: {},
624
+ body: null,
625
+ route: {},
626
+ };
627
+
628
+ const result = plugin.process(context);
629
+
630
+ expect(result.response.orderId).toBe("order-123");
631
+ expect(result.response.customerId).toBe("customer-456");
632
+ expect(result.response.status).toBe("pending");
633
+ expect(result.response.createdAt).toBe("2024-01-01T12:00:00Z");
634
+ });
635
+ });
636
+ });