@basictech/schema 0.7.0-beta.0 → 0.7.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 DELETED
@@ -1,537 +0,0 @@
1
- // Basic Schema Library
2
- // utils for validating and interacting with Basic schemas
3
- import { validate as standaloneValidator } from './generated-validator'
4
- import type { ErrorObject as AjvErrorObject } from 'ajv'
5
-
6
- const basicJsonSchema = {
7
- "$schema": "http://json-schema.org/draft-07/schema#",
8
- "type": "object",
9
- "properties": {
10
- "project_id": {
11
- "type": "string"
12
- },
13
- "namespace": {
14
- "type": "string",
15
- },
16
- "version": {
17
- "type": "integer",
18
- "minimum": 0
19
- },
20
- "tables": {
21
- "type": "object",
22
- "propertyNames": {
23
- "pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
24
- "minLength": 1,
25
- "maxLength": 50,
26
- "type": "string"
27
- },
28
- "patternProperties": {
29
- "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$": {
30
- "type": "object",
31
- "propertyNames": {
32
- "pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
33
- "minLength": 1,
34
- "maxLength": 50,
35
- "type": "string"
36
- },
37
- "properties": {
38
- "name": {
39
- "type": "string"
40
- },
41
- "type": {
42
- "type": "string",
43
- "enum": ["collection"]
44
- },
45
- "origin": {
46
- "type": "object",
47
- "properties": {
48
- "type": {
49
- "type": "string",
50
- "enum": ["reference"]
51
- },
52
- "project_id": {
53
- "type": "string"
54
- },
55
- "table": {
56
- "type": "string"
57
- },
58
- "version": {
59
- "type": "integer"
60
- }
61
- },
62
- "if": {
63
- "properties": { "type": { "const": "reference" } }
64
- },
65
- "then": {
66
- "required": ["project_id", "table"]
67
- }
68
- },
69
- "fields": {
70
- "type": "object",
71
- "propertyNames": {
72
- "pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
73
- "minLength": 1,
74
- "maxLength": 50,
75
- "type": "string"
76
- },
77
- "patternProperties": {
78
- "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$": {
79
- "type": "object",
80
- "properties": {
81
- "type": {
82
- "type": "string",
83
- "enum": ["string", "boolean", "number", "json"]
84
- },
85
- "indexed": {
86
- "type": "boolean"
87
- },
88
- "required": {
89
- "type": "boolean"
90
- }
91
- },
92
- "required": ["type"]
93
- }
94
- },
95
- "additionalProperties": true
96
- }
97
- },
98
- "required": ["fields"]
99
- }
100
- },
101
- "additionalProperties": true
102
- }
103
- },
104
- "required": ["project_id", "version", "tables"]
105
- }
106
-
107
- // Using standalone pre-compiled validator (no dynamic code generation)
108
- const validator = standaloneValidator
109
-
110
- function generateEmptySchema(project_id: string = "", version: number = 0) {
111
- return {
112
- project_id: project_id,
113
- version: version,
114
- tables: {
115
- foo: {
116
- name: "foo",
117
- type: "collection",
118
- fields: {
119
- bar: {
120
- type: "string",
121
- required: true,
122
- }
123
- }
124
- }
125
- }
126
- }
127
- }
128
-
129
- type Schema = {
130
- project_id: string,
131
- version: number,
132
- tables: any
133
- }
134
-
135
-
136
- /**
137
- * Compare two schemas and detect any differences between them
138
- * @param oldSchema - The original schema to compare against
139
- * @param newSchema - The new schema to compare with the original
140
- * @returns {Object} Comparison result containing:
141
- * - valid: boolean indicating if schemas are identical
142
- * - changes: Array of detected changes between schemas
143
- */
144
- function compareSchemas(oldSchema: any, newSchema: any) {
145
- const changes = _getSchemaChanges(oldSchema, newSchema)
146
- const valid = changes.length === 0 ? true : false
147
- return { valid, changes }
148
- }
149
-
150
- /**
151
- * Validate a schema
152
- * only checks if the schema is formatted correctly, not if can be published
153
- * @param schema - The schema to validate
154
- * @returns {valid: boolean, errors: any[]} - The validation result
155
- */
156
- function validateSchema(schema: Schema): { valid: boolean, errors: AjvErrorObject[] } {
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
-
163
- return {
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
- }
216
- }
217
-
218
- return errors
219
- }
220
-
221
- type ErrorObject = {
222
- keyword?: string;
223
- instancePath?: string;
224
- schemaPath?: string;
225
- params?: Record<string, any>;
226
- propertyName?: string;
227
- message?: string;
228
- schema?: any;
229
- parentSchema?: any;
230
- data?: any;
231
- }
232
-
233
-
234
- /**
235
- * Validate data against a schema's table definition. Only checks against provided schema.
236
- * @param schema - The schema to validate against
237
- * @param table - The table name in the schema to validate against
238
- * @param data - The data object to validate
239
- * @param checkRequired - Whether to check if required fields are present (default: true)
240
- * @returns {Object} Validation result containing:
241
- * - valid: boolean indicating if validation passed
242
- * - errors: Array of validation error objects
243
- * - message: Error message if validation failed
244
- */
245
- function validateData(schema: Schema, table: string, data: Record<string, any>, checkRequired: boolean = true) : { valid: boolean, errors?: ErrorObject[], message?: string } {
246
- const valid = validateSchema(schema)
247
- if (!valid.valid) {
248
- return { valid: false, errors: valid.errors, message: "Schema is invalid" }
249
- }
250
-
251
- const tableSchema = schema.tables[table]
252
-
253
- if (!tableSchema) {
254
- return { valid: false, errors: [{ message: `Table ${table} not found in schema` }], message: "Table not found" }
255
- }
256
-
257
- for (const [fieldName, fieldValue] of Object.entries(data)) {
258
- const fieldSchema = tableSchema.fields[fieldName]
259
-
260
- if (!fieldSchema) {
261
- return {
262
- valid: false,
263
- errors: [{ message: `Field ${fieldName} not found in schema` }],
264
- message: "Invalid field"
265
- }
266
- }
267
-
268
- const schemaType = fieldSchema.type
269
- const valueType = typeof fieldValue
270
-
271
- if (
272
- (schemaType === 'string' && valueType !== 'string') ||
273
- (schemaType === 'number' && valueType !== 'number') ||
274
- (schemaType === 'boolean' && valueType !== 'boolean') ||
275
- (schemaType === 'json' && valueType !== 'object')
276
- ) {
277
- return {
278
- valid: false,
279
- errors: [{
280
- message: `Field ${fieldName} should be type ${schemaType}, got ${valueType}`
281
- }],
282
- message: "invalid type"
283
- }
284
- }
285
- }
286
-
287
- if (checkRequired) {
288
- for (const [fieldName, fieldSchema] of Object.entries(tableSchema.fields)) {
289
- if ((fieldSchema as { required?: boolean }).required && !data[fieldName]) {
290
- return { valid: false, errors: [{ message: `Field ${fieldName} is required` }], message: "Required field missing" }
291
- }
292
- }
293
- }
294
-
295
- return { valid: true, errors: [] }
296
- }
297
-
298
- type SchemaChangeType = "property_changed" | "property_removed" | "table_added" | "table_removed" | "field_added" | "field_removed" | "field_type_changed" | "field_required_changed" | "field_property_added" | "field_property_changed" | "field_property_removed"
299
-
300
- type SchemaChange = {
301
- type: SchemaChangeType,
302
- property?: string,
303
- table?: string,
304
- field?: string,
305
- old?: any,
306
- new?: any
307
- }
308
-
309
- function _getSchemaChanges(oldSchema: any, newSchema: any): SchemaChange[] {
310
- // Compare tables between schemas
311
- const changes: SchemaChange[] = []
312
-
313
- // Check for top level property changes
314
- for (const key in newSchema) {
315
- if (key !== 'tables' && newSchema[key] !== oldSchema[key]) {
316
- changes.push({
317
- type: 'property_changed',
318
- property: key,
319
- old: oldSchema[key],
320
- new: newSchema[key]
321
- })
322
- }
323
- }
324
-
325
- for (const key in oldSchema) {
326
- if (key !== 'tables' && !newSchema.hasOwnProperty(key)) {
327
- changes.push({
328
- type: 'property_removed',
329
- property: key,
330
- old: oldSchema[key]
331
- })
332
- }
333
- }
334
-
335
- // Check for removed tables
336
- for (const tableName in oldSchema.tables) {
337
- if (!newSchema.tables[tableName]) {
338
- changes.push({
339
- type: 'table_removed',
340
- table: tableName
341
- })
342
- }
343
- }
344
-
345
- // Check for added tables and field changes
346
- for (const tableName in newSchema.tables) {
347
- const newTable = newSchema.tables[tableName]
348
- const oldTable = oldSchema.tables[tableName]
349
-
350
- if (!oldTable) {
351
- changes.push({
352
- type: 'table_added',
353
- table: tableName
354
- })
355
- continue
356
- }
357
-
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]
363
-
364
- if (!oldField) {
365
- changes.push({
366
- type: 'field_added',
367
- table: tableName,
368
- field: fieldName
369
- })
370
- continue
371
- }
372
-
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
- }
383
-
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
- }
394
- }
395
-
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
- }
405
- }
406
- }
407
- }
408
-
409
- // Check for field property changes (excluding type which is already checked)
410
- for (const tableName in newSchema.tables) {
411
- const newTable = newSchema.tables[tableName]
412
- const oldTable = oldSchema.tables[tableName]
413
-
414
- if (!oldTable || !newTable.fields || !oldTable.fields) continue
415
-
416
- for (const fieldName in newTable.fields) {
417
- const newField = newTable.fields[fieldName]
418
- const oldField = oldTable.fields[fieldName]
419
-
420
- if (!oldField) continue
421
-
422
- // Compare all properties except type
423
- for (const prop in newField) {
424
- if (prop === 'type') continue
425
-
426
- if (!(prop in oldField)) {
427
- changes.push({
428
- type: 'field_property_added',
429
- table: tableName,
430
- field: fieldName,
431
- property: prop,
432
- new: newField[prop]
433
- })
434
- } else if (JSON.stringify(newField[prop]) !== JSON.stringify(oldField[prop])) {
435
- changes.push({
436
- type: 'field_property_changed',
437
- table: tableName,
438
- field: fieldName,
439
- property: prop,
440
- old: oldField[prop],
441
- new: newField[prop]
442
- })
443
- }
444
- }
445
-
446
- // Check for removed properties
447
- for (const prop in oldField) {
448
- if (prop === 'type') continue
449
- if (!(prop in newField)) {
450
- changes.push({
451
- type: 'field_property_removed',
452
- table: tableName,
453
- field: fieldName,
454
- property: prop,
455
- old: oldField[prop]
456
- })
457
- }
458
- }
459
- }
460
- }
461
-
462
- return changes
463
- }
464
-
465
-
466
- function validateUpdateSchema(oldSchema: any, newSchema: any) {
467
- const oldValid = validateSchema(oldSchema)
468
- const newValid = validateSchema(newSchema)
469
-
470
- if (!oldValid.valid || !newValid.valid) {
471
- return { valid: false, errors: oldValid.errors.concat(newValid.errors), message: "schemas are invalid" }
472
- }
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
- }
490
-
491
- const changes = _getSchemaChanges(oldSchema, newSchema)
492
-
493
- const changeErrors = []
494
- for (const change of changes) {
495
- if (change.type === 'property_changed' && change.property === 'project_id') {
496
- changeErrors.push({
497
- change: change,
498
- message: "Cannot modify project_id property"
499
- })
500
- }
501
-
502
- if (change.type === 'field_type_changed') {
503
- changeErrors.push({
504
- change: change,
505
- message: `Cannot change type of field "${change.field}" from "${change.old}" to "${change.new}"`
506
- })
507
- }
508
- }
509
-
510
- if (changeErrors.length > 0) {
511
- return {
512
- valid: false,
513
- errors: changeErrors,
514
- message: "Invalid schema changes detected"
515
- }
516
- }
517
-
518
- return { valid: true, changes: changes, errors: [] }
519
- }
520
-
521
-
522
- /**
523
- * Get the JSON schema definition for the Basic Schema
524
- * @returns {Object} The JSON schema
525
- */
526
- function getJsonSchema() {
527
- return basicJsonSchema
528
- }
529
-
530
- export {
531
- validateSchema,
532
- validateData,
533
- generateEmptySchema,
534
- validateUpdateSchema,
535
- compareSchemas,
536
- getJsonSchema
537
- }
@@ -1,160 +0,0 @@
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}`)