@basictech/schema 0.6.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,536 +0,0 @@
1
- // Basic Schema Library
2
- // utils for validating and interacting with Basic schemas
3
- import Ajv from 'ajv'
4
-
5
- const basicJsonSchema = {
6
- "$schema": "http://json-schema.org/draft-07/schema#",
7
- "type": "object",
8
- "properties": {
9
- "project_id": {
10
- "type": "string"
11
- },
12
- "namespace": {
13
- "type": "string",
14
- },
15
- "version": {
16
- "type": "integer",
17
- "minimum": 0
18
- },
19
- "tables": {
20
- "type": "object",
21
- "propertyNames": {
22
- "pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
23
- "minLength": 1,
24
- "maxLength": 50,
25
- "type": "string"
26
- },
27
- "patternProperties": {
28
- "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$": {
29
- "type": "object",
30
- "propertyNames": {
31
- "pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
32
- "minLength": 1,
33
- "maxLength": 50,
34
- "type": "string"
35
- },
36
- "properties": {
37
- "name": {
38
- "type": "string"
39
- },
40
- "type": {
41
- "type": "string",
42
- "enum": ["collection"]
43
- },
44
- "origin": {
45
- "type": "object",
46
- "properties": {
47
- "type": {
48
- "type": "string",
49
- "enum": ["reference"]
50
- },
51
- "project_id": {
52
- "type": "string"
53
- },
54
- "table": {
55
- "type": "string"
56
- },
57
- "version": {
58
- "type": "integer"
59
- }
60
- },
61
- "if": {
62
- "properties": { "type": { "const": "reference" } }
63
- },
64
- "then": {
65
- "required": ["project_id", "table"]
66
- }
67
- },
68
- "fields": {
69
- "type": "object",
70
- "propertyNames": {
71
- "pattern": "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$",
72
- "minLength": 1,
73
- "maxLength": 50,
74
- "type": "string"
75
- },
76
- "patternProperties": {
77
- "^(?!id$|ID$|Id$|iD$)[a-zA-Z0-9][a-zA-Z0-9_]*$": {
78
- "type": "object",
79
- "properties": {
80
- "type": {
81
- "type": "string",
82
- "enum": ["string", "boolean", "number", "json"]
83
- },
84
- "indexed": {
85
- "type": "boolean"
86
- },
87
- "required": {
88
- "type": "boolean"
89
- }
90
- },
91
- "required": ["type"]
92
- }
93
- },
94
- "additionalProperties": true
95
- }
96
- },
97
- "required": ["fields"]
98
- }
99
- },
100
- "additionalProperties": true
101
- }
102
- },
103
- "required": ["project_id", "version", "tables"]
104
- }
105
-
106
- const ajv = new Ajv({ allErrors: true })
107
- const validator = ajv.compile(basicJsonSchema)
108
-
109
- function generateEmptySchema(project_id: string = "", version: number = 0) {
110
- return {
111
- project_id: project_id,
112
- version: version,
113
- tables: {
114
- foo: {
115
- name: "foo",
116
- type: "collection",
117
- fields: {
118
- bar: {
119
- type: "string",
120
- required: true,
121
- }
122
- }
123
- }
124
- }
125
- }
126
- }
127
-
128
- type Schema = {
129
- project_id: string,
130
- version: number,
131
- tables: any
132
- }
133
-
134
-
135
- /**
136
- * Compare two schemas and detect any differences between them
137
- * @param oldSchema - The original schema to compare against
138
- * @param newSchema - The new schema to compare with the original
139
- * @returns {Object} Comparison result containing:
140
- * - valid: boolean indicating if schemas are identical
141
- * - changes: Array of detected changes between schemas
142
- */
143
- function compareSchemas(oldSchema: any, newSchema: any) {
144
- const changes = _getSchemaChanges(oldSchema, newSchema)
145
- const valid = changes.length === 0 ? true : false
146
- return { valid, changes }
147
- }
148
-
149
- /**
150
- * Validate a schema
151
- * only checks if the schema is formatted correctly, not if can be published
152
- * @param schema - The schema to validate
153
- * @returns {valid: boolean, errors: any[]} - The validation result
154
- */
155
- function validateSchema(schema: Schema): { valid: boolean, errors: ErrorObject[] } {
156
- const v = validator(schema)
157
- const ajvErrors = validator.errors || []
158
-
159
- // Add custom validation for case-insensitive duplicates
160
- const customErrors = validateCaseInsensitiveNames(schema)
161
-
162
- return {
163
- valid: v && customErrors.length === 0,
164
- errors: [...ajvErrors, ...customErrors]
165
- }
166
- }
167
-
168
- /**
169
- * Validate that table and field names are case-insensitive unique
170
- * @param schema - The schema to validate
171
- * @returns Array of validation errors
172
- */
173
- function validateCaseInsensitiveNames(schema: Schema): ErrorObject[] {
174
- const errors: ErrorObject[] = []
175
-
176
- if (!schema.tables) return errors
177
-
178
- // Check for case-insensitive duplicate table names
179
- const tableNames = new Set<string>()
180
- for (const tableName in schema.tables) {
181
- const lowerTableName = tableName.toLowerCase()
182
- if (tableNames.has(lowerTableName)) {
183
- errors.push({
184
- keyword: 'caseInsensitiveDuplicate',
185
- instancePath: `/tables/${tableName}`,
186
- schemaPath: '#/properties/tables/propertyNames',
187
- params: { propertyName: tableName },
188
- message: `Table name "${tableName}" conflicts with another table name (case-insensitive)`
189
- })
190
- } else {
191
- tableNames.add(lowerTableName)
192
- }
193
- }
194
-
195
- // Check for case-insensitive duplicate field names within each table
196
- for (const tableName in schema.tables) {
197
- const table = schema.tables[tableName]
198
- if (!table.fields) continue
199
-
200
- const fieldNames = new Set<string>()
201
- for (const fieldName in table.fields) {
202
- const lowerFieldName = fieldName.toLowerCase()
203
- if (fieldNames.has(lowerFieldName)) {
204
- errors.push({
205
- keyword: 'caseInsensitiveDuplicate',
206
- instancePath: `/tables/${tableName}/fields/${fieldName}`,
207
- 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',
208
- params: { propertyName: fieldName },
209
- message: `Field name "${fieldName}" conflicts with another field name in table "${tableName}" (case-insensitive)`
210
- })
211
- } else {
212
- fieldNames.add(lowerFieldName)
213
- }
214
- }
215
- }
216
-
217
- return errors
218
- }
219
-
220
- type ErrorObject = {
221
- keyword?: string;
222
- instancePath?: string;
223
- schemaPath?: string;
224
- params?: Record<string, any>;
225
- propertyName?: string;
226
- message?: string;
227
- schema?: any;
228
- parentSchema?: any;
229
- data?: any;
230
- }
231
-
232
-
233
- /**
234
- * Validate data against a schema's table definition. Only checks against provided schema.
235
- * @param schema - The schema to validate against
236
- * @param table - The table name in the schema to validate against
237
- * @param data - The data object to validate
238
- * @param checkRequired - Whether to check if required fields are present (default: true)
239
- * @returns {Object} Validation result containing:
240
- * - valid: boolean indicating if validation passed
241
- * - errors: Array of validation error objects
242
- * - message: Error message if validation failed
243
- */
244
- function validateData(schema: Schema, table: string, data: Record<string, any>, checkRequired: boolean = true) : { valid: boolean, errors?: ErrorObject[], message?: string } {
245
- const valid = validateSchema(schema)
246
- if (!valid.valid) {
247
- return { valid: false, errors: valid.errors, message: "Schema is invalid" }
248
- }
249
-
250
- const tableSchema = schema.tables[table]
251
-
252
- if (!tableSchema) {
253
- return { valid: false, errors: [{ message: `Table ${table} not found in schema` }], message: "Table not found" }
254
- }
255
-
256
- for (const [fieldName, fieldValue] of Object.entries(data)) {
257
- const fieldSchema = tableSchema.fields[fieldName]
258
-
259
- if (!fieldSchema) {
260
- return {
261
- valid: false,
262
- errors: [{ message: `Field ${fieldName} not found in schema` }],
263
- message: "Invalid field"
264
- }
265
- }
266
-
267
- const schemaType = fieldSchema.type
268
- const valueType = typeof fieldValue
269
-
270
- if (
271
- (schemaType === 'string' && valueType !== 'string') ||
272
- (schemaType === 'number' && valueType !== 'number') ||
273
- (schemaType === 'boolean' && valueType !== 'boolean') ||
274
- (schemaType === 'json' && valueType !== 'object')
275
- ) {
276
- return {
277
- valid: false,
278
- errors: [{
279
- message: `Field ${fieldName} should be type ${schemaType}, got ${valueType}`
280
- }],
281
- message: "invalid type"
282
- }
283
- }
284
- }
285
-
286
- if (checkRequired) {
287
- for (const [fieldName, fieldSchema] of Object.entries(tableSchema.fields)) {
288
- if ((fieldSchema as { required?: boolean }).required && !data[fieldName]) {
289
- return { valid: false, errors: [{ message: `Field ${fieldName} is required` }], message: "Required field missing" }
290
- }
291
- }
292
- }
293
-
294
- return { valid: true, errors: [] }
295
- }
296
-
297
- 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"
298
-
299
- type SchemaChange = {
300
- type: SchemaChangeType,
301
- property?: string,
302
- table?: string,
303
- field?: string,
304
- old?: any,
305
- new?: any
306
- }
307
-
308
- function _getSchemaChanges(oldSchema: any, newSchema: any): SchemaChange[] {
309
- // Compare tables between schemas
310
- const changes: SchemaChange[] = []
311
-
312
- // Check for top level property changes
313
- for (const key in newSchema) {
314
- if (key !== 'tables' && newSchema[key] !== oldSchema[key]) {
315
- changes.push({
316
- type: 'property_changed',
317
- property: key,
318
- old: oldSchema[key],
319
- new: newSchema[key]
320
- })
321
- }
322
- }
323
-
324
- for (const key in oldSchema) {
325
- if (key !== 'tables' && !newSchema.hasOwnProperty(key)) {
326
- changes.push({
327
- type: 'property_removed',
328
- property: key,
329
- old: oldSchema[key]
330
- })
331
- }
332
- }
333
-
334
- // Check for removed tables
335
- for (const tableName in oldSchema.tables) {
336
- if (!newSchema.tables[tableName]) {
337
- changes.push({
338
- type: 'table_removed',
339
- table: tableName
340
- })
341
- }
342
- }
343
-
344
- // Check for added tables and field changes
345
- for (const tableName in newSchema.tables) {
346
- const newTable = newSchema.tables[tableName]
347
- const oldTable = oldSchema.tables[tableName]
348
-
349
- if (!oldTable) {
350
- changes.push({
351
- type: 'table_added',
352
- table: tableName
353
- })
354
- continue
355
- }
356
-
357
- // Compare fields - only if both tables have fields
358
- if (newTable.fields && oldTable.fields) {
359
- for (const fieldName in newTable.fields) {
360
- const newField = newTable.fields[fieldName]
361
- const oldField = oldTable.fields[fieldName]
362
-
363
- if (!oldField) {
364
- changes.push({
365
- type: 'field_added',
366
- table: tableName,
367
- field: fieldName
368
- })
369
- continue
370
- }
371
-
372
- // Check for field type changes
373
- if (newField.type !== oldField.type) {
374
- changes.push({
375
- type: 'field_type_changed',
376
- table: tableName,
377
- field: fieldName,
378
- old: oldField.type,
379
- new: newField.type
380
- })
381
- }
382
-
383
- // Check for required flag changes
384
- if (newField.required !== oldField.required) {
385
- changes.push({
386
- type: 'field_required_changed',
387
- table: tableName,
388
- field: fieldName,
389
- old: oldField.required,
390
- new: newField.required
391
- })
392
- }
393
- }
394
-
395
- // Check for removed fields
396
- for (const fieldName in oldTable.fields) {
397
- if (!newTable.fields[fieldName]) {
398
- changes.push({
399
- type: 'field_removed',
400
- table: tableName,
401
- field: fieldName
402
- })
403
- }
404
- }
405
- }
406
- }
407
-
408
- // Check for field property changes (excluding type which is already checked)
409
- for (const tableName in newSchema.tables) {
410
- const newTable = newSchema.tables[tableName]
411
- const oldTable = oldSchema.tables[tableName]
412
-
413
- if (!oldTable || !newTable.fields || !oldTable.fields) continue
414
-
415
- for (const fieldName in newTable.fields) {
416
- const newField = newTable.fields[fieldName]
417
- const oldField = oldTable.fields[fieldName]
418
-
419
- if (!oldField) continue
420
-
421
- // Compare all properties except type
422
- for (const prop in newField) {
423
- if (prop === 'type') continue
424
-
425
- if (!(prop in oldField)) {
426
- changes.push({
427
- type: 'field_property_added',
428
- table: tableName,
429
- field: fieldName,
430
- property: prop,
431
- new: newField[prop]
432
- })
433
- } else if (JSON.stringify(newField[prop]) !== JSON.stringify(oldField[prop])) {
434
- changes.push({
435
- type: 'field_property_changed',
436
- table: tableName,
437
- field: fieldName,
438
- property: prop,
439
- old: oldField[prop],
440
- new: newField[prop]
441
- })
442
- }
443
- }
444
-
445
- // Check for removed properties
446
- for (const prop in oldField) {
447
- if (prop === 'type') continue
448
- if (!(prop in newField)) {
449
- changes.push({
450
- type: 'field_property_removed',
451
- table: tableName,
452
- field: fieldName,
453
- property: prop,
454
- old: oldField[prop]
455
- })
456
- }
457
- }
458
- }
459
- }
460
-
461
- return changes
462
- }
463
-
464
-
465
- function validateUpdateSchema(oldSchema: any, newSchema: any) {
466
- const oldValid = validateSchema(oldSchema)
467
- const newValid = validateSchema(newSchema)
468
-
469
- if (!oldValid.valid || !newValid.valid) {
470
- return { valid: false, errors: oldValid.errors.concat(newValid.errors), message: "schemas are invalid" }
471
- }
472
-
473
- // Always check that version is incremented by 1
474
- if (newSchema.version !== oldSchema.version + 1) {
475
- return {
476
- valid: false,
477
- errors: [{
478
- change: {
479
- type: 'property_changed',
480
- property: 'version',
481
- old: oldSchema.version,
482
- new: newSchema.version
483
- },
484
- message: `Version must be incremented by 1. Expected version:${oldSchema.version + 1}, got version:${newSchema.version}`
485
- }],
486
- message: "Version must be incremented by 1"
487
- }
488
- }
489
-
490
- const changes = _getSchemaChanges(oldSchema, newSchema)
491
-
492
- const changeErrors = []
493
- for (const change of changes) {
494
- if (change.type === 'property_changed' && change.property === 'project_id') {
495
- changeErrors.push({
496
- change: change,
497
- message: "Cannot modify project_id property"
498
- })
499
- }
500
-
501
- if (change.type === 'field_type_changed') {
502
- changeErrors.push({
503
- change: change,
504
- message: `Cannot change type of field "${change.field}" from "${change.old}" to "${change.new}"`
505
- })
506
- }
507
- }
508
-
509
- if (changeErrors.length > 0) {
510
- return {
511
- valid: false,
512
- errors: changeErrors,
513
- message: "Invalid schema changes detected"
514
- }
515
- }
516
-
517
- return { valid: true, changes: changes, errors: [] }
518
- }
519
-
520
-
521
- /**
522
- * Get the JSON schema definition for the Basic Schema
523
- * @returns {Object} The JSON schema
524
- */
525
- function getJsonSchema() {
526
- return basicJsonSchema
527
- }
528
-
529
- export {
530
- validateSchema,
531
- validateData,
532
- generateEmptySchema,
533
- validateUpdateSchema,
534
- compareSchemas,
535
- getJsonSchema
536
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "@repo/typescript-config/base.json",
3
- "compilerOptions": {
4
- "outDir": "dist"
5
- },
6
- "include": ["index.ts"],
7
- "exclude": ["node_modules", "dist"]
8
- }
9
-
package/tsup.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from 'tsup'
2
-
3
- export default defineConfig({
4
- entry: ['./index.ts'],
5
- format: ['cjs', 'esm'],
6
- dts: true,
7
- splitting: false,
8
- sourcemap: true,
9
- clean: true,
10
- })