@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/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#",
@@ -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
- const ajv = new Ajv()
107
- const validator = ajv.compile(basicJsonSchema)
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: ErrorObject[] } {
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: validator.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
- for (const fieldName in newTable.fields) {
303
- const newField = newTable.fields[fieldName]
304
- const oldField = oldTable.fields[fieldName]
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
- if (!oldField) {
307
- changes.push({
308
- type: 'field_added',
309
- table: tableName,
310
- field: fieldName
311
- })
312
- continue
313
- }
364
+ if (!oldField) {
365
+ changes.push({
366
+ type: 'field_added',
367
+ table: tableName,
368
+ field: fieldName
369
+ })
370
+ continue
371
+ }
314
372
 
315
- // Check for field type changes
316
- if (newField.type !== oldField.type) {
317
- changes.push({
318
- type: 'field_type_changed',
319
- table: tableName,
320
- field: fieldName,
321
- old: oldField.type,
322
- new: newField.type
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
- // Check for required flag changes
327
- if (newField.required !== oldField.required) {
328
- changes.push({
329
- type: 'field_required_changed',
330
- table: tableName,
331
- field: fieldName,
332
- old: oldField.required,
333
- new: newField.required
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
- // Check for removed fields
339
- for (const fieldName in oldTable.fields) {
340
- if (!newTable.fields[fieldName]) {
341
- changes.push({
342
- type: 'field_removed',
343
- table: tableName,
344
- field: fieldName
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 is invalid" }
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.5.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": "echo \"Error: no test specified\" && exit 1"
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
- "peerDependencies": {}
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}`)
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ globals: true,
7
+ },
8
+ })