@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.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Basic Schema Library
|
|
2
2
|
// utils for validating and interacting with Basic schemas
|
|
3
|
-
import
|
|
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#",
|
|
@@ -19,16 +20,16 @@ const basicJsonSchema = {
|
|
|
19
20
|
"tables": {
|
|
20
21
|
"type": "object",
|
|
21
22
|
"propertyNames": {
|
|
22
|
-
"pattern": "^[a-zA-Z0-9_]
|
|
23
|
+
"pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
|
|
23
24
|
"minLength": 1,
|
|
24
25
|
"maxLength": 50,
|
|
25
26
|
"type": "string"
|
|
26
27
|
},
|
|
27
28
|
"patternProperties": {
|
|
28
|
-
"^[a-zA-Z0-9_]
|
|
29
|
+
"^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$": {
|
|
29
30
|
"type": "object",
|
|
30
31
|
"propertyNames": {
|
|
31
|
-
"pattern": "^[a-zA-Z0-9_]
|
|
32
|
+
"pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
|
|
32
33
|
"minLength": 1,
|
|
33
34
|
"maxLength": 50,
|
|
34
35
|
"type": "string"
|
|
@@ -68,13 +69,13 @@ const basicJsonSchema = {
|
|
|
68
69
|
"fields": {
|
|
69
70
|
"type": "object",
|
|
70
71
|
"propertyNames": {
|
|
71
|
-
"pattern": "^[a-zA-Z0-9_]
|
|
72
|
+
"pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
|
|
72
73
|
"minLength": 1,
|
|
73
74
|
"maxLength": 50,
|
|
74
75
|
"type": "string"
|
|
75
76
|
},
|
|
76
77
|
"patternProperties": {
|
|
77
|
-
"^[a-zA-Z0-9_]
|
|
78
|
+
"^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$": {
|
|
78
79
|
"type": "object",
|
|
79
80
|
"properties": {
|
|
80
81
|
"type": {
|
|
@@ -103,9 +104,8 @@ const basicJsonSchema = {
|
|
|
103
104
|
"required": ["project_id", "version", "tables"]
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
const validator =
|
|
108
|
-
|
|
107
|
+
// Using standalone pre-compiled validator (no dynamic code generation)
|
|
108
|
+
const validator = standaloneValidator
|
|
109
109
|
|
|
110
110
|
function generateEmptySchema(project_id: string = "", version: number = 0) {
|
|
111
111
|
return {
|
|
@@ -153,12 +153,69 @@ function compareSchemas(oldSchema: any, newSchema: any) {
|
|
|
153
153
|
* @param schema - The schema to validate
|
|
154
154
|
* @returns {valid: boolean, errors: any[]} - The validation result
|
|
155
155
|
*/
|
|
156
|
-
function validateSchema(schema: Schema): { valid: boolean, errors:
|
|
156
|
+
function validateSchema(schema: Schema): { valid: boolean, errors: AjvErrorObject[] } {
|
|
157
157
|
const v = validator(schema)
|
|
158
|
+
const ajvErrors = validator.errors || []
|
|
159
|
+
|
|
160
|
+
// Add custom validation for case-insensitive duplicates
|
|
161
|
+
const customErrors = validateCaseInsensitiveNames(schema)
|
|
162
|
+
|
|
158
163
|
return {
|
|
159
|
-
valid: v,
|
|
160
|
-
errors:
|
|
164
|
+
valid: v && customErrors.length === 0,
|
|
165
|
+
errors: [...ajvErrors, ...customErrors]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate that table and field names are case-insensitive unique
|
|
171
|
+
* @param schema - The schema to validate
|
|
172
|
+
* @returns Array of validation errors
|
|
173
|
+
*/
|
|
174
|
+
function validateCaseInsensitiveNames(schema: Schema): ErrorObject[] {
|
|
175
|
+
const errors: ErrorObject[] = []
|
|
176
|
+
|
|
177
|
+
if (!schema.tables) return errors
|
|
178
|
+
|
|
179
|
+
// Check for case-insensitive duplicate table names
|
|
180
|
+
const tableNames = new Set<string>()
|
|
181
|
+
for (const tableName in schema.tables) {
|
|
182
|
+
const lowerTableName = tableName.toLowerCase()
|
|
183
|
+
if (tableNames.has(lowerTableName)) {
|
|
184
|
+
errors.push({
|
|
185
|
+
keyword: 'caseInsensitiveDuplicate',
|
|
186
|
+
instancePath: `/tables/${tableName}`,
|
|
187
|
+
schemaPath: '#/properties/tables/propertyNames',
|
|
188
|
+
params: { propertyName: tableName },
|
|
189
|
+
message: `Table name "${tableName}" conflicts with another table name (case-insensitive)`
|
|
190
|
+
})
|
|
191
|
+
} else {
|
|
192
|
+
tableNames.add(lowerTableName)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check for case-insensitive duplicate field names within each table
|
|
197
|
+
for (const tableName in schema.tables) {
|
|
198
|
+
const table = schema.tables[tableName]
|
|
199
|
+
if (!table.fields) continue
|
|
200
|
+
|
|
201
|
+
const fieldNames = new Set<string>()
|
|
202
|
+
for (const fieldName in table.fields) {
|
|
203
|
+
const lowerFieldName = fieldName.toLowerCase()
|
|
204
|
+
if (fieldNames.has(lowerFieldName)) {
|
|
205
|
+
errors.push({
|
|
206
|
+
keyword: 'caseInsensitiveDuplicate',
|
|
207
|
+
instancePath: `/tables/${tableName}/fields/${fieldName}`,
|
|
208
|
+
schemaPath: '#/properties/tables/patternProperties/^%5E%28%3F%21id%24%7CID%24%7CId%24%7CiD%24%29%5Ba-zA-Z0-9%5D%5Ba-zA-Z0-9_%5D*%24/properties/fields/propertyNames',
|
|
209
|
+
params: { propertyName: fieldName },
|
|
210
|
+
message: `Field name "${fieldName}" conflicts with another field name in table "${tableName}" (case-insensitive)`
|
|
211
|
+
})
|
|
212
|
+
} else {
|
|
213
|
+
fieldNames.add(lowerFieldName)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
161
216
|
}
|
|
217
|
+
|
|
218
|
+
return errors
|
|
162
219
|
}
|
|
163
220
|
|
|
164
221
|
type ErrorObject = {
|
|
@@ -298,51 +355,53 @@ function _getSchemaChanges(oldSchema: any, newSchema: any): SchemaChange[] {
|
|
|
298
355
|
continue
|
|
299
356
|
}
|
|
300
357
|
|
|
301
|
-
// Compare fields
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
358
|
+
// Compare fields - only if both tables have fields
|
|
359
|
+
if (newTable.fields && oldTable.fields) {
|
|
360
|
+
for (const fieldName in newTable.fields) {
|
|
361
|
+
const newField = newTable.fields[fieldName]
|
|
362
|
+
const oldField = oldTable.fields[fieldName]
|
|
305
363
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
364
|
+
if (!oldField) {
|
|
365
|
+
changes.push({
|
|
366
|
+
type: 'field_added',
|
|
367
|
+
table: tableName,
|
|
368
|
+
field: fieldName
|
|
369
|
+
})
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
314
372
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
373
|
+
// Check for field type changes
|
|
374
|
+
if (newField.type !== oldField.type) {
|
|
375
|
+
changes.push({
|
|
376
|
+
type: 'field_type_changed',
|
|
377
|
+
table: tableName,
|
|
378
|
+
field: fieldName,
|
|
379
|
+
old: oldField.type,
|
|
380
|
+
new: newField.type
|
|
381
|
+
})
|
|
382
|
+
}
|
|
325
383
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
384
|
+
// Check for required flag changes
|
|
385
|
+
if (newField.required !== oldField.required) {
|
|
386
|
+
changes.push({
|
|
387
|
+
type: 'field_required_changed',
|
|
388
|
+
table: tableName,
|
|
389
|
+
field: fieldName,
|
|
390
|
+
old: oldField.required,
|
|
391
|
+
new: newField.required
|
|
392
|
+
})
|
|
393
|
+
}
|
|
335
394
|
}
|
|
336
|
-
}
|
|
337
395
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
396
|
+
// Check for removed fields
|
|
397
|
+
for (const fieldName in oldTable.fields) {
|
|
398
|
+
if (!newTable.fields[fieldName]) {
|
|
399
|
+
changes.push({
|
|
400
|
+
type: 'field_removed',
|
|
401
|
+
table: tableName,
|
|
402
|
+
field: fieldName
|
|
403
|
+
})
|
|
404
|
+
}
|
|
346
405
|
}
|
|
347
406
|
}
|
|
348
407
|
}
|
|
@@ -352,7 +411,7 @@ function _getSchemaChanges(oldSchema: any, newSchema: any): SchemaChange[] {
|
|
|
352
411
|
const newTable = newSchema.tables[tableName]
|
|
353
412
|
const oldTable = oldSchema.tables[tableName]
|
|
354
413
|
|
|
355
|
-
if (!oldTable) continue
|
|
414
|
+
if (!oldTable || !newTable.fields || !oldTable.fields) continue
|
|
356
415
|
|
|
357
416
|
for (const fieldName in newTable.fields) {
|
|
358
417
|
const newField = newTable.fields[fieldName]
|
|
@@ -409,9 +468,25 @@ function validateUpdateSchema(oldSchema: any, newSchema: any) {
|
|
|
409
468
|
const newValid = validateSchema(newSchema)
|
|
410
469
|
|
|
411
470
|
if (!oldValid.valid || !newValid.valid) {
|
|
412
|
-
return { valid: false, errors: oldValid.errors.concat(newValid.errors), message: "schemas are
|
|
471
|
+
return { valid: false, errors: oldValid.errors.concat(newValid.errors), message: "schemas are invalid" }
|
|
413
472
|
}
|
|
414
473
|
|
|
474
|
+
// Always check that version is incremented by 1
|
|
475
|
+
if (newSchema.version !== oldSchema.version + 1) {
|
|
476
|
+
return {
|
|
477
|
+
valid: false,
|
|
478
|
+
errors: [{
|
|
479
|
+
change: {
|
|
480
|
+
type: 'property_changed',
|
|
481
|
+
property: 'version',
|
|
482
|
+
old: oldSchema.version,
|
|
483
|
+
new: newSchema.version
|
|
484
|
+
},
|
|
485
|
+
message: `Version must be incremented by 1. Expected version:${oldSchema.version + 1}, got version:${newSchema.version}`
|
|
486
|
+
}],
|
|
487
|
+
message: "Version must be incremented by 1"
|
|
488
|
+
}
|
|
489
|
+
}
|
|
415
490
|
|
|
416
491
|
const changes = _getSchemaChanges(oldSchema, newSchema)
|
|
417
492
|
|
|
@@ -424,15 +499,6 @@ function validateUpdateSchema(oldSchema: any, newSchema: any) {
|
|
|
424
499
|
})
|
|
425
500
|
}
|
|
426
501
|
|
|
427
|
-
if (change.type === 'property_changed' && change.property === 'version') {
|
|
428
|
-
if (change.new !== change.old + 1) {
|
|
429
|
-
changeErrors.push({
|
|
430
|
-
change: change,
|
|
431
|
-
message: `Version must be incremented by 1. Expected version:${change.old + 1}, got version:${change.new}`
|
|
432
|
-
})
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
502
|
if (change.type === 'field_type_changed') {
|
|
437
503
|
changeErrors.push({
|
|
438
504
|
change: change,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basictech/schema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-beta.0",
|
|
4
4
|
"description": "utils for Basic Schema",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -10,19 +10,23 @@
|
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
+
"prebuild": "node scripts/generate-validator.js",
|
|
13
14
|
"build": "tsup",
|
|
14
15
|
"dev": "tsup --watch",
|
|
15
|
-
"test": "
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"generate-validator": "node scripts/generate-validator.js"
|
|
16
19
|
},
|
|
17
20
|
"author": "",
|
|
18
21
|
"license": "ISC",
|
|
19
22
|
"dependencies": {
|
|
20
|
-
"ajv": "^8.17.1"
|
|
23
|
+
"ajv": "^8.17.1",
|
|
24
|
+
"ajv-errors": "^3.0.0"
|
|
21
25
|
},
|
|
22
26
|
"devDependencies": {
|
|
23
27
|
"@repo/typescript-config": "*",
|
|
24
28
|
"tsup": "^7.2.0",
|
|
25
|
-
"typescript": "^5.0.0"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
+
"typescript": "^5.0.0",
|
|
30
|
+
"vitest": "^1.6.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const Ajv = require('ajv')
|
|
4
|
+
const standaloneCode = require('ajv/dist/standalone')
|
|
5
|
+
const { writeFileSync, mkdirSync } = require('fs')
|
|
6
|
+
const { dirname } = require('path')
|
|
7
|
+
|
|
8
|
+
// Define the same schema as in index.ts
|
|
9
|
+
const basicJsonSchema = {
|
|
10
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"project_id": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"namespace": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
},
|
|
19
|
+
"version": {
|
|
20
|
+
"type": "integer",
|
|
21
|
+
"minimum": 0
|
|
22
|
+
},
|
|
23
|
+
"tables": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"propertyNames": {
|
|
26
|
+
"pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
|
|
27
|
+
"minLength": 1,
|
|
28
|
+
"maxLength": 50,
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"patternProperties": {
|
|
32
|
+
"^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"propertyNames": {
|
|
35
|
+
"pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
|
|
36
|
+
"minLength": 1,
|
|
37
|
+
"maxLength": 50,
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"properties": {
|
|
41
|
+
"name": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
},
|
|
44
|
+
"type": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"enum": ["collection"]
|
|
47
|
+
},
|
|
48
|
+
"origin": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"properties": {
|
|
51
|
+
"type": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"enum": ["reference"]
|
|
54
|
+
},
|
|
55
|
+
"project_id": {
|
|
56
|
+
"type": "string"
|
|
57
|
+
},
|
|
58
|
+
"table": {
|
|
59
|
+
"type": "string"
|
|
60
|
+
},
|
|
61
|
+
"version": {
|
|
62
|
+
"type": "integer"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"if": {
|
|
66
|
+
"properties": { "type": { "const": "reference" } }
|
|
67
|
+
},
|
|
68
|
+
"then": {
|
|
69
|
+
"required": ["project_id", "table"]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"fields": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"propertyNames": {
|
|
75
|
+
"pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
|
|
76
|
+
"minLength": 1,
|
|
77
|
+
"maxLength": 50,
|
|
78
|
+
"type": "string"
|
|
79
|
+
},
|
|
80
|
+
"patternProperties": {
|
|
81
|
+
"^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"type": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"enum": ["string", "boolean", "number", "json"]
|
|
87
|
+
},
|
|
88
|
+
"indexed": {
|
|
89
|
+
"type": "boolean"
|
|
90
|
+
},
|
|
91
|
+
"required": {
|
|
92
|
+
"type": "boolean"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"required": ["type"]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"additionalProperties": true
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"required": ["fields"]
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"additionalProperties": true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"required": ["project_id", "version", "tables"]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log('Generating standalone AJV validator...')
|
|
111
|
+
|
|
112
|
+
// Create AJV instance with source code generation enabled
|
|
113
|
+
const ajv = new Ajv({
|
|
114
|
+
code: {
|
|
115
|
+
source: true,
|
|
116
|
+
esm: true,
|
|
117
|
+
optimize: false // Disable optimizations that might require runtime deps
|
|
118
|
+
},
|
|
119
|
+
allErrors: true,
|
|
120
|
+
strict: false, // Be more lenient to avoid runtime dependencies
|
|
121
|
+
validateFormats: false, // Disable format validation to avoid dependencies
|
|
122
|
+
addUsedSchema: false // Don't add used schemas to avoid dependencies
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Compile the schema
|
|
126
|
+
const validate = ajv.compile(basicJsonSchema)
|
|
127
|
+
|
|
128
|
+
// Generate standalone code
|
|
129
|
+
let moduleCode = standaloneCode(ajv, validate)
|
|
130
|
+
|
|
131
|
+
// Post-process to remove runtime dependencies
|
|
132
|
+
// Replace the ucs2length require with a native implementation
|
|
133
|
+
const ucs2LengthFunc = `
|
|
134
|
+
// Modern JavaScript Unicode string length (replaces ajv/dist/runtime/ucs2length)
|
|
135
|
+
const ucs2length = (str) => [...str].length;
|
|
136
|
+
`
|
|
137
|
+
|
|
138
|
+
// Replace the require statement with our function
|
|
139
|
+
moduleCode = moduleCode.replace(
|
|
140
|
+
/const func2 = require\("ajv\/dist\/runtime\/ucs2length"\)\.default;/g,
|
|
141
|
+
ucs2LengthFunc + 'const func2 = ucs2length;'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Ensure the directory exists
|
|
145
|
+
const outputPath = 'generated-validator.js'
|
|
146
|
+
const typesPath = 'generated-validator.d.ts'
|
|
147
|
+
|
|
148
|
+
// Write the generated validator
|
|
149
|
+
writeFileSync(outputPath, moduleCode)
|
|
150
|
+
|
|
151
|
+
// Create TypeScript declaration file
|
|
152
|
+
const typeDeclaration = `export declare const validate: {
|
|
153
|
+
(data: any, options?: any): boolean;
|
|
154
|
+
errors?: any[] | null;
|
|
155
|
+
};
|
|
156
|
+
export default validate;
|
|
157
|
+
`
|
|
158
|
+
writeFileSync(typesPath, typeDeclaration)
|
|
159
|
+
|
|
160
|
+
console.log(`✅ Standalone validator generated at ${outputPath}`)
|