import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, ArraySchemas, CommonSchemas, ObjectSchema } from '../src/schema'; import { DefaultSchemaRegistry } from '../src/schema'; import { TypedCollectionImpl, SchemaValidationError } from '../src/collections'; import { RhizomeNode } from '../src/node'; import { Delta } from '../src/core'; describe('Schema System', () => { let schemaRegistry: DefaultSchemaRegistry; let node: RhizomeNode; beforeEach(() => { schemaRegistry = new DefaultSchemaRegistry(); node = new RhizomeNode(); }); describe('Schema Builder', () => { it('should create a basic schema', () => { const schema = SchemaBuilder .create('user') .name('User') .description('A user entity') .property('name', PrimitiveSchemas.requiredString()) .property('age', PrimitiveSchemas.number()) .property('active', PrimitiveSchemas.boolean()) .required('name') .build(); expect(schema.id).toBe('user'); expect(schema.name).toBe('User'); expect(schema.description).toBe('A user entity'); expect(schema.properties.name).toEqual({ type: 'primitive', primitiveType: 'string', required: true }); expect(schema.requiredProperties).toContain('name'); }); it('should create schema with references', () => { const schema = SchemaBuilder .create('post') .name('Post') .property('title', PrimitiveSchemas.requiredString()) .property('author', ReferenceSchemas.required('user')) .property('tags', ArraySchemas.of(PrimitiveSchemas.string())) .build(); expect(schema.properties.author).toEqual({ type: 'reference', targetSchema: 'user', maxDepth: 3, required: true }); expect(schema.properties.tags).toEqual({ type: 'array', itemSchema: { type: 'primitive', primitiveType: 'string' } }); }); it('should enforce required fields', () => { expect(() => { SchemaBuilder.create('').build(); }).toThrow('Schema must have id and name'); expect(() => { SchemaBuilder.create('test').build(); }).toThrow('Schema must have id and name'); }); }); describe('Schema Registry', () => { it('should register and retrieve schemas', () => { const schema = CommonSchemas.User(); schemaRegistry.register(schema); const retrieved = schemaRegistry.get('user'); expect(retrieved).toEqual(schema); const all = schemaRegistry.list(); expect(all).toContain(schema); }); it('should validate schema structure', () => { const invalidSchema = { id: 'invalid', name: 'Invalid', properties: { badProperty: { type: 'unknown' } as never } }; expect(() => { schemaRegistry.register(invalidSchema as ObjectSchema); }).toThrow('Unknown schema type'); }); it('should validate required properties exist', () => { const schema = SchemaBuilder .create('test') .name('Test') .property('name', PrimitiveSchemas.string()) .required('name', 'nonexistent') .build(); expect(() => { schemaRegistry.register(schema); }).toThrow("Required property 'nonexistent' not found"); }); it('should detect circular dependencies', () => { // Create schemas with circular references const userSchema = SchemaBuilder .create('user') .name('User') .property('name', PrimitiveSchemas.string()) .property('bestFriend', ReferenceSchemas.to('user')) .build(); schemaRegistry.register(userSchema); // This is circular (self-reference) expect(schemaRegistry.hasCircularDependencies()).toBe(true); // Create actual circular dependency const groupSchema = SchemaBuilder .create('group') .name('Group') .property('owner', ReferenceSchemas.to('user')) .build(); const userWithGroupSchema = SchemaBuilder .create('user-with-group') .name('User With Group') .property('group', ReferenceSchemas.to('group')) .build(); schemaRegistry.register(groupSchema); schemaRegistry.register(userWithGroupSchema); // Still circular due to the self-referencing user schema expect(schemaRegistry.hasCircularDependencies()).toBe(true); }); it('should validate lossless views against schemas', () => { const userSchema = CommonSchemas.User(); schemaRegistry.register(userSchema); // Create a valid lossless view const validView = { id: 'user123', referencedAs: ['user'], propertyDeltas: { name: [{ id: 'delta1', timeCreated: 123, host: 'host1', creator: 'creator1', pointers: [{ name: 'Alice' }] }], age: [{ id: 'delta2', timeCreated: 124, host: 'host1', creator: 'creator1', pointers: [{ age: 25 }] }] } }; const result = schemaRegistry.validate('user123', 'user', validView); expect(result.valid).toBe(true); expect(result.errors).toHaveLength(0); // Test invalid view (missing required property) const invalidView = { id: 'user456', referencedAs: ['user'], propertyDeltas: { age: [{ id: 'delta3', timeCreated: 125, host: 'host1', creator: 'creator1', pointers: [{ age: 30 }] }] } }; const invalidResult = schemaRegistry.validate('user456', 'user', invalidView); expect(invalidResult.valid).toBe(false); expect(invalidResult.errors).toContainEqual( expect.objectContaining({ property: 'name', message: expect.stringContaining('Required property') }) ); }); it('should validate primitive types', () => { const schema = SchemaBuilder .create('test') .name('Test') .property('stringProp', PrimitiveSchemas.string()) .property('numberProp', PrimitiveSchemas.number()) .property('booleanProp', PrimitiveSchemas.boolean()) .build(); schemaRegistry.register(schema); // Valid types const validView = { id: 'test1', referencedAs: [], propertyDeltas: { stringProp: [{ id: 'd1', timeCreated: 1, host: 'h', creator: 'c', pointers: [{ stringProp: 'hello' }] }], numberProp: [{ id: 'd2', timeCreated: 1, host: 'h', creator: 'c', pointers: [{ numberProp: 42 }] }], booleanProp: [{ id: 'd3', timeCreated: 1, host: 'h', creator: 'c', pointers: [{ booleanProp: true }] }] } }; const validResult = schemaRegistry.validate('test1', 'test', validView); expect(validResult.valid).toBe(true); // Invalid types const invalidView = { id: 'test2', referencedAs: [], propertyDeltas: { stringProp: [{ id: 'd4', timeCreated: 1, host: 'h', creator: 'c', pointers: [{ stringProp: 123 as never }] }], numberProp: [{ id: 'd5', timeCreated: 1, host: 'h', creator: 'c', pointers: [{ numberProp: 'not-number' as never }] }] } }; const invalidResult = schemaRegistry.validate('test2', 'test', invalidView); expect(invalidResult.valid).toBe(false); expect(invalidResult.errors).toHaveLength(2); }); }); describe('Typed Collection', () => { it('should create typed collection with schema validation', () => { const userSchema = CommonSchemas.User(); const collection = new TypedCollectionImpl<{ name: string; email?: string; age?: number; active?: boolean; }>('users', userSchema, schemaRegistry); expect(collection.schema).toEqual(userSchema); expect(collection.name).toBe('users'); }); it('should validate entities against schema', () => { const userSchema = CommonSchemas.User(); const collection = new TypedCollectionImpl<{ name: string; email?: string; age?: number; }>('users', userSchema, schemaRegistry); // Valid entity const validUser = { name: 'Alice', email: 'alice@test.com', age: 25 }; const validResult = collection.validate(validUser); expect(validResult.valid).toBe(true); // Invalid entity (missing required name) const invalidUser = { email: 'bob@test.com', age: 30 } as never; const invalidResult = collection.validate(invalidUser); expect(invalidResult.valid).toBe(false); }); it('should enforce strict validation on put operations', async () => { const userSchema = CommonSchemas.User(); const collection = new TypedCollectionImpl<{ name: string; email?: string; }>('users', userSchema, schemaRegistry, { strictValidation: true }); collection.rhizomeConnect(node); // Valid put should succeed await expect(collection.put('user1', { name: 'Alice' })).resolves.toBeDefined(); // Invalid put should fail await expect(collection.put('user2', { email: 'invalid@test.com' })).rejects.toThrow(SchemaValidationError); }); it('should provide validation statistics', async () => { const userSchema = CommonSchemas.User(); const collection = new TypedCollectionImpl<{ name: string; email?: string; }>('users', userSchema, schemaRegistry); collection.rhizomeConnect(node); // Add some entities await collection.put('user1', { name: 'Alice', email: 'alice@test.com' }); await collection.put('user2', { name: 'Bob' }); // Manually create an invalid entity by bypassing validation const invalidDelta = new Delta({ creator: node.config.creator, host: node.config.peerId, pointers: [ { localContext: 'users', target: 'user3', targetContext: 'email' }, { localContext: 'email', target: 'invalid@test.com' } ] }); node.lossless.ingestDelta(invalidDelta); const stats = collection.getValidationStats(); expect(stats.totalEntities).toBe(3); expect(stats.validEntities).toBe(2); expect(stats.invalidEntities).toBe(1); }); it('should filter valid and invalid entities', async () => { const userSchema = CommonSchemas.User(); const collection = new TypedCollectionImpl<{ name: string; email?: string; }>('users', userSchema, schemaRegistry); collection.rhizomeConnect(node); await collection.put('user1', { name: 'Alice' }); await collection.put('user2', { name: 'Bob' }); // Create invalid entity manually const invalidDelta = new Delta({ creator: node.config.creator, host: node.config.peerId, pointers: [ { localContext: 'users', target: 'user3', targetContext: 'age' }, { localContext: 'age', target: 'not-a-number' } ] }); node.lossless.ingestDelta(invalidDelta); const validIds = collection.getValidEntities(); expect(validIds).toContain('user1'); expect(validIds).toContain('user2'); expect(validIds).not.toContain('user3'); const invalidEntities = collection.getInvalidEntities(); expect(invalidEntities).toHaveLength(1); expect(invalidEntities[0].entityId).toBe('user3'); }); it('should apply schema to lossless views', async () => { const userSchema = CommonSchemas.User(); const collection = new TypedCollectionImpl<{ name: string; age?: number; }>('users', userSchema, schemaRegistry); collection.rhizomeConnect(node); await collection.put('user1', { name: 'Alice', age: 25 }); const validatedView = collection.getValidatedView('user1'); expect(validatedView).toBeDefined(); expect(validatedView!.schemaId).toBe('user'); expect(validatedView!.properties.name).toBeDefined(); expect(validatedView!.properties.age).toBeDefined(); expect(validatedView!.metadata?.appliedAt).toBeDefined(); }); it('should provide schema introspection', () => { const userSchema = CommonSchemas.User(); schemaRegistry.register(CommonSchemas.UserSummary()); const collection = new TypedCollectionImpl<{ name: string; }>('users', userSchema, schemaRegistry); const schemaInfo = collection.getSchemaInfo(); expect(schemaInfo.schema).toEqual(userSchema); expect(schemaInfo.dependencies).toContain('user-summary'); expect(schemaInfo.hasCircularDependencies).toBe(false); }); }); describe('Common Schemas', () => { it('should provide working User schema', () => { const userSchema = CommonSchemas.User(); expect(userSchema.id).toBe('user'); expect(userSchema.name).toBe('User'); expect(userSchema.properties.name).toBeDefined(); expect(userSchema.properties.friends).toBeDefined(); expect(userSchema.requiredProperties).toContain('name'); }); it('should provide working Document schema', () => { const docSchema = CommonSchemas.Document(); expect(docSchema.id).toBe('document'); expect(docSchema.properties.title).toBeDefined(); expect(docSchema.properties.author).toBeDefined(); expect(docSchema.requiredProperties).toContain('title'); expect(docSchema.requiredProperties).toContain('author'); }); it('should work together in a registry', () => { schemaRegistry.register(CommonSchemas.User()); schemaRegistry.register(CommonSchemas.UserSummary()); schemaRegistry.register(CommonSchemas.Document()); expect(schemaRegistry.list()).toHaveLength(3); expect(schemaRegistry.hasCircularDependencies()).toBe(false); // No circular deps in CommonSchemas }); }); });