@basictech/schema 0.5.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +27 -4
- package/dist/index.d.ts +27 -4
- package/dist/index.js +708 -66
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +706 -56
- package/dist/index.mjs.map +1 -1
- package/generated-validator.d.ts +5 -0
- package/generated-validator.js +4 -0
- package/index.test.ts +670 -0
- package/index.ts +129 -63
- package/package.json +11 -7
- package/scripts/generate-validator.js +160 -0
- package/vitest.config.ts +8 -0
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
|
+
})
|