@basictech/schema 0.6.0 → 0.7.0-beta.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/index.test.ts ADDED
@@ -0,0 +1,670 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ validateSchema,
4
+ validateData,
5
+ generateEmptySchema,
6
+ validateUpdateSchema,
7
+ compareSchemas,
8
+ getJsonSchema
9
+ } from './index'
10
+
11
+ // Test schema from basic.config.ts
12
+ const testSchema = {
13
+ project_id: "edf4539a-e2e6-403c-8dec-7267565ce46d",
14
+ version: 3,
15
+ tables: {
16
+ foo: {
17
+ fields: {
18
+ bar: {
19
+ indexed: true,
20
+ type: "string"
21
+ }
22
+ },
23
+ origin: {
24
+ project_id: "bd1e08c6-25d0-44eb-bf5a-53922874b5e8",
25
+ table: "foo",
26
+ type: "reference"
27
+ },
28
+ type: "collection"
29
+ },
30
+ hello: {
31
+ fields: {
32
+ hello: {
33
+ indexed: true,
34
+ type: "string"
35
+ }
36
+ },
37
+ type: "collection"
38
+ },
39
+ test: {
40
+ fields: {
41
+ boolone: {
42
+ indexed: true,
43
+ type: "boolean"
44
+ },
45
+ booltwo: {
46
+ indexed: true,
47
+ type: "boolean"
48
+ },
49
+ js: {
50
+ indexed: true,
51
+ type: "json"
52
+ },
53
+ num: {
54
+ indexed: true,
55
+ type: "number"
56
+ },
57
+ strone: {
58
+ indexed: true,
59
+ type: "string"
60
+ },
61
+ test: {
62
+ indexed: true,
63
+ type: "string"
64
+ }
65
+ },
66
+ type: "collection"
67
+ }
68
+ }
69
+ }
70
+
71
+ // Additional test schemas
72
+ const validMinimalSchema = {
73
+ project_id: "test-project-id",
74
+ version: 1,
75
+ tables: {
76
+ users: {
77
+ fields: {
78
+ name: {
79
+ type: "string",
80
+ required: true
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ const invalidSchema = {
88
+ project_id: "test-project-id",
89
+ // missing version
90
+ tables: {
91
+ users: {
92
+ fields: {
93
+ name: {
94
+ type: "invalid-type" // invalid field type
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ const caseInsensitiveDuplicateSchema = {
102
+ project_id: "test-project-id",
103
+ version: 1,
104
+ tables: {
105
+ users: {
106
+ fields: {
107
+ name: { type: "string" },
108
+ NAME: { type: "string" } // case-insensitive duplicate
109
+ }
110
+ },
111
+ Users: { // case-insensitive duplicate table
112
+ fields: {
113
+ email: { type: "string" }
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ describe('validateSchema', () => {
120
+ it('should validate a correct schema', () => {
121
+ const result = validateSchema(testSchema)
122
+ expect(result.valid).toBe(true)
123
+ expect(result.errors).toHaveLength(0)
124
+ })
125
+
126
+ it('should validate a minimal correct schema', () => {
127
+ const result = validateSchema(validMinimalSchema)
128
+ expect(result.valid).toBe(true)
129
+ expect(result.errors).toHaveLength(0)
130
+ })
131
+
132
+ it('should reject invalid schema with missing required fields', () => {
133
+ const result = validateSchema(invalidSchema)
134
+ expect(result.valid).toBe(false)
135
+ expect(result.errors.length).toBeGreaterThan(0)
136
+ })
137
+
138
+ it('should reject schema with invalid field types', () => {
139
+ const schema = {
140
+ project_id: "test-project-id",
141
+ version: 1,
142
+ tables: {
143
+ users: {
144
+ fields: {
145
+ name: {
146
+ type: "invalid-type"
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ const result = validateSchema(schema)
153
+ expect(result.valid).toBe(false)
154
+ expect(result.errors.length).toBeGreaterThan(0)
155
+ })
156
+
157
+ it('should reject schema with case-insensitive duplicate names', () => {
158
+ const result = validateSchema(caseInsensitiveDuplicateSchema)
159
+ expect(result.valid).toBe(false)
160
+ expect(result.errors.length).toBeGreaterThan(0)
161
+
162
+ // Check for case-insensitive duplicate errors
163
+ const duplicateErrors = result.errors.filter(err =>
164
+ err.keyword === 'caseInsensitiveDuplicate'
165
+ )
166
+ expect(duplicateErrors.length).toBeGreaterThan(0)
167
+ })
168
+
169
+ it('should reject schema with reserved field names (id variations)', () => {
170
+ const schema = {
171
+ project_id: "test-project-id",
172
+ version: 1,
173
+ tables: {
174
+ users: {
175
+ fields: {
176
+ id: { type: "string" }, // reserved name
177
+ name: { type: "string" }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ const result = validateSchema(schema)
183
+ expect(result.valid).toBe(false)
184
+ })
185
+
186
+ it('should reject schema with empty table names', () => {
187
+ const schema = {
188
+ project_id: "test-project-id",
189
+ version: 1,
190
+ tables: {
191
+ "": { // empty table name
192
+ fields: {
193
+ name: { type: "string" }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ const result = validateSchema(schema)
199
+ expect(result.valid).toBe(false)
200
+ })
201
+ })
202
+
203
+ describe('validateData', () => {
204
+ it('should validate correct data against schema', () => {
205
+ const data = {
206
+ boolone: true,
207
+ booltwo: false,
208
+ js: { key: "value" },
209
+ num: 42,
210
+ strone: "test string",
211
+ test: "another string"
212
+ }
213
+ const result = validateData(testSchema, 'test', data)
214
+ expect(result.valid).toBe(true)
215
+ expect(result.errors).toHaveLength(0)
216
+ })
217
+
218
+ it('should reject data with wrong types', () => {
219
+ const data = {
220
+ boolone: "not a boolean", // wrong type
221
+ num: 42,
222
+ strone: "test string"
223
+ }
224
+ const result = validateData(testSchema, 'test', data)
225
+ expect(result.valid).toBe(false)
226
+ expect(result.message).toBe("invalid type")
227
+ })
228
+
229
+ it('should reject data with fields not in schema', () => {
230
+ const data = {
231
+ boolone: true,
232
+ unknownField: "value" // field not in schema
233
+ }
234
+ const result = validateData(testSchema, 'test', data)
235
+ expect(result.valid).toBe(false)
236
+ expect(result.message).toBe("Invalid field")
237
+ })
238
+
239
+ it('should check required fields when checkRequired is true', () => {
240
+ const schema = {
241
+ project_id: "test-project-id",
242
+ version: 1,
243
+ tables: {
244
+ users: {
245
+ fields: {
246
+ name: {
247
+ type: "string",
248
+ required: true
249
+ },
250
+ email: {
251
+ type: "string",
252
+ required: false
253
+ }
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ const dataWithoutRequired = { email: "test@example.com" }
260
+ const result = validateData(schema, 'users', dataWithoutRequired, true)
261
+ expect(result.valid).toBe(false)
262
+ expect(result.message).toBe("Required field missing")
263
+ })
264
+
265
+ it('should skip required field check when checkRequired is false', () => {
266
+ const schema = {
267
+ project_id: "test-project-id",
268
+ version: 1,
269
+ tables: {
270
+ users: {
271
+ fields: {
272
+ name: {
273
+ type: "string",
274
+ required: true
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+
281
+ const dataWithoutRequired = {}
282
+ const result = validateData(schema, 'users', dataWithoutRequired, false)
283
+ expect(result.valid).toBe(true)
284
+ })
285
+
286
+ it('should reject data for non-existent table', () => {
287
+ const data = { name: "test" }
288
+ const result = validateData(testSchema, 'nonexistent', data)
289
+ expect(result.valid).toBe(false)
290
+ expect(result.message).toBe("Table not found")
291
+ })
292
+
293
+ it('should reject data when schema is invalid', () => {
294
+ const data = { name: "test" }
295
+ const result = validateData(invalidSchema, 'users', data)
296
+ expect(result.valid).toBe(false)
297
+ expect(result.message).toBe("Schema is invalid")
298
+ })
299
+
300
+ it('should validate different data types correctly', () => {
301
+ // String type
302
+ let result = validateData(testSchema, 'test', { strone: "valid string" })
303
+ expect(result.valid).toBe(true)
304
+
305
+ // Number type
306
+ result = validateData(testSchema, 'test', { num: 123.45 })
307
+ expect(result.valid).toBe(true)
308
+
309
+ // Boolean type
310
+ result = validateData(testSchema, 'test', { boolone: false })
311
+ expect(result.valid).toBe(true)
312
+
313
+ // JSON type
314
+ result = validateData(testSchema, 'test', { js: { nested: { object: true } } })
315
+ expect(result.valid).toBe(true)
316
+ })
317
+ })
318
+
319
+ describe('generateEmptySchema', () => {
320
+ it('should generate schema with default parameters', () => {
321
+ const schema = generateEmptySchema()
322
+ expect(schema.project_id).toBe("")
323
+ expect(schema.version).toBe(0)
324
+ expect(schema.tables).toBeDefined()
325
+ expect(schema.tables.foo).toBeDefined()
326
+ expect(schema.tables.foo.fields.bar.type).toBe("string")
327
+ })
328
+
329
+ it('should generate schema with custom project_id and version', () => {
330
+ const schema = generateEmptySchema("custom-project", 5)
331
+ expect(schema.project_id).toBe("custom-project")
332
+ expect(schema.version).toBe(5)
333
+ expect(schema.tables).toBeDefined()
334
+ })
335
+
336
+ it('should always include the foo table template', () => {
337
+ const schema = generateEmptySchema("test", 1)
338
+ expect(schema.tables.foo).toBeDefined()
339
+ expect(schema.tables.foo.name).toBe("foo")
340
+ expect(schema.tables.foo.type).toBe("collection")
341
+ expect(schema.tables.foo.fields.bar).toBeDefined()
342
+ expect(schema.tables.foo.fields.bar.type).toBe("string")
343
+ expect(schema.tables.foo.fields.bar.required).toBe(true)
344
+ })
345
+ })
346
+
347
+ describe('compareSchemas', () => {
348
+ it('should return valid for identical schemas', () => {
349
+ const result = compareSchemas(testSchema, testSchema)
350
+ expect(result.valid).toBe(true)
351
+ expect(result.changes).toHaveLength(0)
352
+ })
353
+
354
+ it('should detect property changes', () => {
355
+ const oldSchema = { ...testSchema, version: 3 }
356
+ const newSchema = { ...testSchema, version: 4 }
357
+
358
+ const result = compareSchemas(oldSchema, newSchema)
359
+ expect(result.valid).toBe(false)
360
+ expect(result.changes).toHaveLength(1)
361
+ expect(result.changes[0].type).toBe('property_changed')
362
+ expect(result.changes[0].property).toBe('version')
363
+ })
364
+
365
+ it('should detect added tables', () => {
366
+ const oldSchema = { ...validMinimalSchema }
367
+ const newSchema = {
368
+ ...validMinimalSchema,
369
+ tables: {
370
+ ...validMinimalSchema.tables,
371
+ newTable: {
372
+ fields: {
373
+ newField: { type: "string" }
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ const result = compareSchemas(oldSchema, newSchema)
380
+ expect(result.valid).toBe(false)
381
+ expect(result.changes.some(c => c.type === 'table_added')).toBe(true)
382
+ })
383
+
384
+ it('should detect removed tables', () => {
385
+ const oldSchema = { ...testSchema }
386
+ const newSchema = {
387
+ ...testSchema,
388
+ tables: {
389
+ foo: testSchema.tables.foo,
390
+ hello: testSchema.tables.hello
391
+ // test table removed
392
+ }
393
+ }
394
+
395
+ const result = compareSchemas(oldSchema, newSchema)
396
+ expect(result.valid).toBe(false)
397
+ expect(result.changes.some(c => c.type === 'table_removed')).toBe(true)
398
+ })
399
+
400
+ it('should detect field changes', () => {
401
+ const oldSchema = { ...validMinimalSchema }
402
+ const newSchema = {
403
+ ...validMinimalSchema,
404
+ tables: {
405
+ users: {
406
+ fields: {
407
+ name: { type: "string", required: true },
408
+ email: { type: "string" } // added field
409
+ }
410
+ }
411
+ }
412
+ }
413
+
414
+ const result = compareSchemas(oldSchema, newSchema)
415
+ expect(result.valid).toBe(false)
416
+ expect(result.changes.some(c => c.type === 'field_added')).toBe(true)
417
+ })
418
+
419
+ it('should detect field type changes', () => {
420
+ const oldSchema = {
421
+ project_id: "test",
422
+ version: 1,
423
+ tables: {
424
+ users: {
425
+ fields: {
426
+ age: { type: "string" }
427
+ }
428
+ }
429
+ }
430
+ }
431
+ const newSchema = {
432
+ project_id: "test",
433
+ version: 1,
434
+ tables: {
435
+ users: {
436
+ fields: {
437
+ age: { type: "number" } // type changed
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ const result = compareSchemas(oldSchema, newSchema)
444
+ expect(result.valid).toBe(false)
445
+ expect(result.changes.some(c => c.type === 'field_type_changed')).toBe(true)
446
+ })
447
+
448
+ it('should detect field property changes', () => {
449
+ const oldSchema = {
450
+ project_id: "test",
451
+ version: 1,
452
+ tables: {
453
+ users: {
454
+ fields: {
455
+ name: { type: "string", required: false }
456
+ }
457
+ }
458
+ }
459
+ }
460
+ const newSchema = {
461
+ project_id: "test",
462
+ version: 1,
463
+ tables: {
464
+ users: {
465
+ fields: {
466
+ name: { type: "string", required: true } // required changed
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ const result = compareSchemas(oldSchema, newSchema)
473
+ expect(result.valid).toBe(false)
474
+ expect(result.changes.some(c => c.type === 'field_required_changed')).toBe(true)
475
+ })
476
+ })
477
+
478
+ describe('validateUpdateSchema', () => {
479
+ it('should validate correct schema update', () => {
480
+ const oldSchema = { ...testSchema, version: 3 }
481
+ const newSchema = {
482
+ ...testSchema,
483
+ version: 4,
484
+ tables: {
485
+ ...testSchema.tables,
486
+ newTable: {
487
+ fields: {
488
+ newField: { type: "string" }
489
+ }
490
+ }
491
+ }
492
+ }
493
+
494
+ const result = validateUpdateSchema(oldSchema, newSchema)
495
+ expect(result.valid).toBe(true)
496
+ expect(result.changes).toBeDefined()
497
+ })
498
+
499
+ it('should reject update with incorrect version increment', () => {
500
+ const oldSchema = { ...testSchema, version: 3 }
501
+ const newSchema = { ...testSchema, version: 5 } // skipped version 4
502
+
503
+ const result = validateUpdateSchema(oldSchema, newSchema)
504
+ expect(result.valid).toBe(false)
505
+ expect(result.message).toBe("Version must be incremented by 1")
506
+ })
507
+
508
+ it('should reject project_id changes', () => {
509
+ const oldSchema = { ...testSchema, version: 3 }
510
+ const newSchema = {
511
+ ...testSchema,
512
+ version: 4,
513
+ project_id: "different-project-id"
514
+ }
515
+
516
+ const result = validateUpdateSchema(oldSchema, newSchema)
517
+ expect(result.valid).toBe(false)
518
+ expect(result.message).toBe("Invalid schema changes detected")
519
+ })
520
+
521
+ it('should reject field type changes', () => {
522
+ const oldSchema = {
523
+ project_id: "test",
524
+ version: 1,
525
+ tables: {
526
+ users: {
527
+ fields: {
528
+ age: { type: "string" }
529
+ }
530
+ }
531
+ }
532
+ }
533
+ const newSchema = {
534
+ project_id: "test",
535
+ version: 2,
536
+ tables: {
537
+ users: {
538
+ fields: {
539
+ age: { type: "number" } // type changed - not allowed
540
+ }
541
+ }
542
+ }
543
+ }
544
+
545
+ const result = validateUpdateSchema(oldSchema, newSchema)
546
+ expect(result.valid).toBe(false)
547
+ expect(result.errors.some(e => e.message.includes("Cannot change type of field"))).toBe(true)
548
+ })
549
+
550
+ it('should allow adding new fields', () => {
551
+ const oldSchema = { ...validMinimalSchema, version: 1 }
552
+ const newSchema = {
553
+ ...validMinimalSchema,
554
+ version: 2,
555
+ tables: {
556
+ users: {
557
+ fields: {
558
+ name: { type: "string", required: true },
559
+ email: { type: "string" } // new field
560
+ }
561
+ }
562
+ }
563
+ }
564
+
565
+ const result = validateUpdateSchema(oldSchema, newSchema)
566
+ expect(result.valid).toBe(true)
567
+ })
568
+
569
+ it('should reject invalid schemas', () => {
570
+ const result = validateUpdateSchema(invalidSchema, testSchema)
571
+ expect(result.valid).toBe(false)
572
+ expect(result.message).toBe("schemas are invalid")
573
+ })
574
+ })
575
+
576
+ describe('getJsonSchema', () => {
577
+ it('should return the JSON schema object', () => {
578
+ const jsonSchema = getJsonSchema()
579
+ expect(jsonSchema).toBeDefined()
580
+ expect(jsonSchema.$schema).toBe("http://json-schema.org/draft-07/schema#")
581
+ expect(jsonSchema.type).toBe("object")
582
+ expect(jsonSchema.properties).toBeDefined()
583
+ expect(jsonSchema.properties.project_id).toBeDefined()
584
+ expect(jsonSchema.properties.version).toBeDefined()
585
+ expect(jsonSchema.properties.tables).toBeDefined()
586
+ expect(jsonSchema.required).toContain("project_id")
587
+ expect(jsonSchema.required).toContain("version")
588
+ expect(jsonSchema.required).toContain("tables")
589
+ })
590
+
591
+ it('should be a valid JSON schema format', () => {
592
+ const jsonSchema = getJsonSchema()
593
+ expect(typeof jsonSchema).toBe('object')
594
+ expect(jsonSchema.$schema).toMatch(/json-schema\.org/)
595
+ })
596
+ })
597
+
598
+ describe('Edge Cases', () => {
599
+ it('should handle empty tables object', () => {
600
+ const schema = {
601
+ project_id: "test",
602
+ version: 1,
603
+ tables: {}
604
+ }
605
+ const result = validateSchema(schema)
606
+ expect(result.valid).toBe(true)
607
+ })
608
+
609
+ it('should handle table with no fields', () => {
610
+ const schema = {
611
+ project_id: "test",
612
+ version: 1,
613
+ tables: {
614
+ empty: {
615
+ fields: {}
616
+ }
617
+ }
618
+ }
619
+ const result = validateSchema(schema)
620
+ expect(result.valid).toBe(true)
621
+ })
622
+
623
+ it('should handle malformed schema objects', () => {
624
+ // Test with object that's missing required properties
625
+ const malformedSchema = { project_id: "test" } // missing version and tables
626
+ const result = validateSchema(malformedSchema as any)
627
+ expect(result.valid).toBe(false)
628
+ expect(result.errors.length).toBeGreaterThan(0)
629
+ })
630
+
631
+ it('should handle very long table and field names', () => {
632
+ const longName = 'a'.repeat(60) // exceeds 50 char limit
633
+ const schema = {
634
+ project_id: "test",
635
+ version: 1,
636
+ tables: {
637
+ [longName]: {
638
+ fields: {
639
+ [longName]: { type: "string" }
640
+ }
641
+ }
642
+ }
643
+ }
644
+ const result = validateSchema(schema)
645
+ expect(result.valid).toBe(false)
646
+ })
647
+
648
+ it('should validate origin reference properties', () => {
649
+ const schemaWithOrigin = {
650
+ project_id: "test",
651
+ version: 1,
652
+ tables: {
653
+ referenced: {
654
+ type: "collection",
655
+ fields: {
656
+ name: { type: "string" }
657
+ },
658
+ origin: {
659
+ type: "reference",
660
+ project_id: "other-project",
661
+ table: "source-table",
662
+ version: 1
663
+ }
664
+ }
665
+ }
666
+ }
667
+ const result = validateSchema(schemaWithOrigin)
668
+ expect(result.valid).toBe(true)
669
+ })
670
+ })
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Basic Schema Library
2
2
  // utils for validating and interacting with Basic schemas
3
- import Ajv from 'ajv'
3
+ import { validate as standaloneValidator } from './generated-validator'
4
+ import type { ErrorObject as AjvErrorObject } from 'ajv'
4
5
 
5
6
  const basicJsonSchema = {
6
7
  "$schema": "http://json-schema.org/draft-07/schema#",
@@ -103,8 +104,8 @@ const basicJsonSchema = {
103
104
  "required": ["project_id", "version", "tables"]
104
105
  }
105
106
 
106
- const ajv = new Ajv({ allErrors: true })
107
- const validator = ajv.compile(basicJsonSchema)
107
+ // Using standalone pre-compiled validator (no dynamic code generation)
108
+ const validator = standaloneValidator
108
109
 
109
110
  function generateEmptySchema(project_id: string = "", version: number = 0) {
110
111
  return {
@@ -152,7 +153,7 @@ function compareSchemas(oldSchema: any, newSchema: any) {
152
153
  * @param schema - The schema to validate
153
154
  * @returns {valid: boolean, errors: any[]} - The validation result
154
155
  */
155
- function validateSchema(schema: Schema): { valid: boolean, errors: ErrorObject[] } {
156
+ function validateSchema(schema: Schema): { valid: boolean, errors: AjvErrorObject[] } {
156
157
  const v = validator(schema)
157
158
  const ajvErrors = validator.errors || []
158
159