- Refactored delta creation in test files to use createDelta() pattern - Replaced direct Delta instantiations with fluent builder API - Updated relationship deltas to use setProperty with proper entity context - Ensured all tests pass with the new delta creation approach This is part of the ongoing effort to standardize on the DeltaBuilder API across the codebase for better consistency and maintainability.
295 lines
12 KiB
TypeScript
295 lines
12 KiB
TypeScript
/**
|
|
* Tests for nested object resolution with deltas containing 3+ pointers
|
|
* This tests the complex case where a single delta establishes relationships
|
|
* between multiple entities and includes scalar values.
|
|
*/
|
|
|
|
import { RhizomeNode } from '../src/node';
|
|
import { createDelta } from '../src/core/delta-builder';
|
|
import { DefaultSchemaRegistry } from '../src/schema';
|
|
import { SchemaBuilder, PrimitiveSchemas, ReferenceSchemas, SchemaAppliedViewWithNesting } from '../src/schema';
|
|
import { TypedCollectionImpl } from '../src/collections';
|
|
|
|
describe('Multi-Pointer Delta Resolution', () => {
|
|
let node: RhizomeNode;
|
|
let schemaRegistry: DefaultSchemaRegistry;
|
|
|
|
beforeEach(() => {
|
|
node = new RhizomeNode();
|
|
schemaRegistry = new DefaultSchemaRegistry();
|
|
});
|
|
|
|
describe('Three-Entity Relationship Deltas', () => {
|
|
it('should handle movie casting deltas with actor, movie, role, and scalars', async () => {
|
|
// Create schemas for a movie casting scenario
|
|
const actorSchema = SchemaBuilder
|
|
.create('actor')
|
|
.name('Actor')
|
|
.property('name', PrimitiveSchemas.requiredString())
|
|
.property('filmography', ReferenceSchemas.to('casting-summary', 3))
|
|
.required('name')
|
|
.build();
|
|
|
|
const movieSchema = SchemaBuilder
|
|
.create('movie')
|
|
.name('Movie')
|
|
.property('title', PrimitiveSchemas.requiredString())
|
|
.property('cast', ReferenceSchemas.to('casting-summary', 3))
|
|
.required('title')
|
|
.build();
|
|
|
|
const roleSchema = SchemaBuilder
|
|
.create('role')
|
|
.name('Role')
|
|
.property('name', PrimitiveSchemas.requiredString())
|
|
.property('portrayals', ReferenceSchemas.to('casting-summary', 3))
|
|
.required('name')
|
|
.build();
|
|
|
|
const castingSummarySchema = SchemaBuilder
|
|
.create('casting-summary')
|
|
.name('Casting Summary')
|
|
.property('name', PrimitiveSchemas.string())
|
|
.property('title', PrimitiveSchemas.string())
|
|
.property('salary', PrimitiveSchemas.number())
|
|
.property('contract_date', PrimitiveSchemas.string())
|
|
.additionalProperties(false)
|
|
.build();
|
|
|
|
schemaRegistry.register(actorSchema);
|
|
schemaRegistry.register(movieSchema);
|
|
schemaRegistry.register(roleSchema);
|
|
schemaRegistry.register(castingSummarySchema);
|
|
|
|
// Create collections
|
|
const actorCollection = new TypedCollectionImpl<{ name: string }>('actors', actorSchema, schemaRegistry);
|
|
const movieCollection = new TypedCollectionImpl<{ title: string }>('movies', movieSchema, schemaRegistry);
|
|
const roleCollection = new TypedCollectionImpl<{ name: string }>('roles', roleSchema, schemaRegistry);
|
|
|
|
actorCollection.rhizomeConnect(node);
|
|
movieCollection.rhizomeConnect(node);
|
|
roleCollection.rhizomeConnect(node);
|
|
|
|
// Create entities
|
|
await actorCollection.put('keanu', { name: 'Keanu Reeves' });
|
|
await movieCollection.put('matrix', { title: 'The Matrix' });
|
|
await roleCollection.put('neo', { name: 'Neo' });
|
|
|
|
// Create a complex casting delta with multiple entity references and scalar values
|
|
const castingDelta = createDelta(node.config.creator, node.config.peerId)
|
|
.addPointer('actors', 'keanu', 'filmography')
|
|
.addPointer('movies', 'matrix', 'cast')
|
|
.addPointer('roles', 'neo', 'portrayals')
|
|
.addPointer('salary', 15000000)
|
|
.addPointer('contract_date', '1999-03-31')
|
|
.buildV1();
|
|
node.lossless.ingestDelta(castingDelta);
|
|
|
|
// Test from Keanu's perspective
|
|
const keanuViews = node.lossless.view(['keanu']);
|
|
const keanuView = keanuViews['keanu'];
|
|
|
|
expect(keanuView.propertyDeltas.filmography).toBeDefined();
|
|
expect(keanuView.propertyDeltas.filmography.length).toBe(1);
|
|
|
|
const nestedKeanuView = schemaRegistry.applySchemaWithNesting(
|
|
keanuView,
|
|
'actor',
|
|
node.lossless,
|
|
{ maxDepth: 2 }
|
|
);
|
|
|
|
expect(nestedKeanuView.id).toBe('keanu');
|
|
|
|
// Should resolve references to matrix and neo, but not keanu (self)
|
|
expect(nestedKeanuView.nestedObjects.filmography).toBeDefined();
|
|
if (nestedKeanuView.nestedObjects.filmography) {
|
|
const castingEntry = nestedKeanuView.nestedObjects.filmography[0];
|
|
expect(castingEntry).toBeDefined();
|
|
|
|
// The casting entry should be resolved with casting-summary schema
|
|
expect(castingEntry.schemaId).toBe('casting-summary');
|
|
|
|
// Should not contain a reference to keanu (the parent)
|
|
expect(castingEntry.id).not.toBe('keanu');
|
|
}
|
|
|
|
// Test from Matrix's perspective
|
|
const matrixViews = node.lossless.view(['matrix']);
|
|
const matrixView = matrixViews['matrix'];
|
|
|
|
const nestedMatrixView = schemaRegistry.applySchemaWithNesting(
|
|
matrixView,
|
|
'movie',
|
|
node.lossless,
|
|
{ maxDepth: 2 }
|
|
);
|
|
|
|
expect(nestedMatrixView.id).toBe('matrix');
|
|
expect(nestedMatrixView.nestedObjects.cast).toBeDefined();
|
|
});
|
|
|
|
it('should handle deltas with mixed scalar and reference values correctly', async () => {
|
|
// Create a simpler schema for testing mixed values
|
|
const personSchema = SchemaBuilder
|
|
.create('person')
|
|
.name('Person')
|
|
.property('name', PrimitiveSchemas.requiredString())
|
|
.property('relationships', ReferenceSchemas.to('relationship-summary', 3))
|
|
.required('name')
|
|
.build();
|
|
|
|
const relationshipSummarySchema = SchemaBuilder
|
|
.create('relationship-summary')
|
|
.name('Relationship Summary')
|
|
.property('partner_name', PrimitiveSchemas.string())
|
|
.property('type', PrimitiveSchemas.string())
|
|
.property('since', PrimitiveSchemas.string())
|
|
.property('intensity', PrimitiveSchemas.number())
|
|
.additionalProperties(false)
|
|
.build();
|
|
|
|
schemaRegistry.register(personSchema);
|
|
schemaRegistry.register(relationshipSummarySchema);
|
|
|
|
const personCollection = new TypedCollectionImpl<{ name: string }>('people', personSchema, schemaRegistry);
|
|
personCollection.rhizomeConnect(node);
|
|
|
|
// Create people
|
|
await personCollection.put('alice', { name: 'Alice' });
|
|
await personCollection.put('bob', { name: 'Bob' });
|
|
|
|
// Create a relationship delta with one entity reference and multiple scalars
|
|
const relationshipDelta = createDelta(node.config.creator, node.config.peerId)
|
|
.addPointer('people', 'alice', 'relationships')
|
|
.addPointer('partner', 'bob')
|
|
.addPointer('type', 'friendship')
|
|
.addPointer('since', '2020-01-15')
|
|
.addPointer('intensity', 8)
|
|
.buildV1();
|
|
node.lossless.ingestDelta(relationshipDelta);
|
|
|
|
// Test from Alice's perspective
|
|
const aliceViews = node.lossless.view(['alice']);
|
|
const aliceView = aliceViews['alice'];
|
|
|
|
const nestedAliceView = schemaRegistry.applySchemaWithNesting(
|
|
aliceView,
|
|
'person',
|
|
node.lossless,
|
|
{ maxDepth: 2 }
|
|
);
|
|
|
|
expect(nestedAliceView.id).toBe('alice');
|
|
expect(nestedAliceView.nestedObjects.relationships).toBeDefined();
|
|
|
|
if (nestedAliceView.nestedObjects.relationships) {
|
|
expect(nestedAliceView.nestedObjects.relationships.length).toBe(1);
|
|
const relationshipEntry = nestedAliceView.nestedObjects.relationships[0];
|
|
|
|
// Should be resolved with relationship-summary schema
|
|
expect(relationshipEntry.schemaId).toBe('relationship-summary');
|
|
|
|
// Should contain scalar values and reference to bob but not alice
|
|
expect(relationshipEntry.id).not.toBe('alice');
|
|
|
|
// The relationship should contain the scalar values from the delta
|
|
// Note: The exact structure depends on how the resolution logic handles mixed values
|
|
}
|
|
});
|
|
|
|
it('should correctly identify multiple entity references within a single delta', async () => {
|
|
// Test a scenario with multiple entity references that should all be resolved
|
|
const projectSchema = SchemaBuilder
|
|
.create('project')
|
|
.name('Project')
|
|
.property('name', PrimitiveSchemas.requiredString())
|
|
.property('collaborations', ReferenceSchemas.to('collaboration-summary', 3))
|
|
.required('name')
|
|
.build();
|
|
|
|
const collaborationSummarySchema = SchemaBuilder
|
|
.create('collaboration-summary')
|
|
.name('Collaboration Summary')
|
|
.property('project_name', PrimitiveSchemas.string())
|
|
.property('developer_name', PrimitiveSchemas.string())
|
|
.property('designer_name', PrimitiveSchemas.string())
|
|
.property('budget', PrimitiveSchemas.number())
|
|
.additionalProperties(false)
|
|
.build();
|
|
|
|
schemaRegistry.register(projectSchema);
|
|
schemaRegistry.register(collaborationSummarySchema);
|
|
|
|
const projectCollection = new TypedCollectionImpl<{ name: string }>('projects', projectSchema, schemaRegistry);
|
|
const developerCollection = new TypedCollectionImpl<{ name: string }>('developers', projectSchema, schemaRegistry);
|
|
const designerCollection = new TypedCollectionImpl<{ name: string }>('designers', projectSchema, schemaRegistry);
|
|
|
|
projectCollection.rhizomeConnect(node);
|
|
developerCollection.rhizomeConnect(node);
|
|
designerCollection.rhizomeConnect(node);
|
|
|
|
// Create entities
|
|
await projectCollection.put('website', { name: 'Company Website' });
|
|
await developerCollection.put('alice', { name: 'Alice Developer' });
|
|
await designerCollection.put('bob', { name: 'Bob Designer' });
|
|
|
|
// Create a collaboration delta with multiple entity references
|
|
const collaborationDelta = createDelta(node.config.creator, node.config.peerId)
|
|
.addPointer('projects', 'website', 'collaborations')
|
|
.addPointer('developer', 'alice')
|
|
.addPointer('designer', 'bob')
|
|
.addPointer('budget', 50000)
|
|
.addPointer('deadline', '2024-06-01')
|
|
.buildV1();
|
|
node.lossless.ingestDelta(collaborationDelta);
|
|
|
|
// Test from project's perspective
|
|
const projectViews = node.lossless.view(['website']);
|
|
const projectView = projectViews['website'];
|
|
|
|
const nestedProjectView = schemaRegistry.applySchemaWithNesting(
|
|
projectView,
|
|
'project',
|
|
node.lossless,
|
|
{ maxDepth: 2 }
|
|
);
|
|
|
|
expect(nestedProjectView.id).toBe('website');
|
|
expect(nestedProjectView.nestedObjects.collaborations).toBeDefined();
|
|
|
|
if (nestedProjectView.nestedObjects.collaborations) {
|
|
|
|
// Verify we get exactly 1 composite object (not 2 separate objects)
|
|
expect(nestedProjectView.nestedObjects.collaborations.length).toBe(1);
|
|
const collaboration = nestedProjectView.nestedObjects.collaborations[0];
|
|
|
|
expect(collaboration.schemaId).toBe('collaboration-summary');
|
|
expect(collaboration.id).toMatch(/^composite-/); // Should be a synthetic composite ID
|
|
|
|
// Verify the composite object contains scalar properties
|
|
expect(collaboration.properties.budget).toBe(50000);
|
|
expect(collaboration.properties.deadline).toBe('2024-06-01');
|
|
|
|
// Verify the composite object contains nested entity references
|
|
expect(collaboration.nestedObjects.developer).toBeDefined();
|
|
expect(collaboration.nestedObjects.designer).toBeDefined();
|
|
|
|
// The nested entities should be resolved as arrays with single objects
|
|
const developers = collaboration.nestedObjects.developer as SchemaAppliedViewWithNesting[];
|
|
const designers = collaboration.nestedObjects.designer as SchemaAppliedViewWithNesting[];
|
|
|
|
// Each should have exactly one entity
|
|
expect(developers.length).toBe(1);
|
|
expect(designers.length).toBe(1);
|
|
|
|
// Verify each entity reference resolves to the correct entity
|
|
expect(developers[0].id).toBe('alice');
|
|
expect(developers[0].schemaId).toBe('collaboration-summary');
|
|
|
|
expect(designers[0].id).toBe('bob');
|
|
expect(designers[0].schemaId).toBe('collaboration-summary');
|
|
}
|
|
});
|
|
});
|
|
}); |