From c04439713c50628096e2c17324579673602c4403 Mon Sep 17 00:00:00 2001 From: Lentil Hoffman Date: Sun, 22 Jun 2025 02:44:31 -0500 Subject: [PATCH] relationships as domain entities --- .gitignore | 1 + __tests__/delta-builder.test.ts | 63 +++++++-- plans/ent-rel-graph.md | 222 ++++++++++++++++++++++++++++++++ src/core/delta-builder.ts | 14 +- 4 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 plans/ent-rel-graph.md diff --git a/.gitignore b/.gitignore index 2849e74..0d4fde3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ coverage/ data/ test-data/ *.code-workspace +local-notes/ \ No newline at end of file diff --git a/__tests__/delta-builder.test.ts b/__tests__/delta-builder.test.ts index c8aa404..36061c3 100644 --- a/__tests__/delta-builder.test.ts +++ b/__tests__/delta-builder.test.ts @@ -80,18 +80,55 @@ describe('DeltaBuilder', () => { it('should create a V1 delta with relationships', () => { const delta = createDelta(creator, host) - .relate('user-1', 'follows', 'user-2') + .relate('user-1', 'user-2', 'follows') .buildV1(); + // This delta sets values on a new relationship entity + expect(delta.pointers).toContainEqual({ - localContext: 'follows', - target: 'user-2', - targetContext: 'follows' + localContext: '_target', + target: expect.any(String), + targetContext: 'target' + }); + const relId = delta.pointers.find(p => p.localContext === '_target')?.target; + expect(delta.pointers).toContainEqual({ + localContext: '_source', + target: relId, + targetContext: 'source' }); expect(delta.pointers).toContainEqual({ - localContext: 'source', - target: 'user-1', - targetContext: 'follows' + localContext: '_type', + target: relId, + targetContext: 'type' + }); + }); + + it('should create a V1 delta with relationships and properties', () => { + const delta = createDelta(creator, host) + .relate('user-1', 'user-2', 'follows', { version: 1}) + .buildV1(); + + // This delta sets values on a new relationship entity + expect(delta.pointers).toContainEqual({ + localContext: '_target', + target: expect.any(String), + targetContext: 'target' + }); + const relId = delta.pointers.find(p => p.localContext === '_target')?.target; + expect(delta.pointers).toContainEqual({ + localContext: '_source', + target: relId, + targetContext: 'source' + }); + expect(delta.pointers).toContainEqual({ + localContext: '_type', + target: relId, + targetContext: 'type' + }); + expect(delta.pointers).toContainEqual({ + localContext: '_version', + target: relId, + targetContext: 'version' }); }); }); @@ -121,12 +158,22 @@ describe('DeltaBuilder', () => { it('should create a V2 delta with relationships', () => { const delta = createDelta(creator, host) - .relate('user-1', 'follows', 'user-2') + .relate('user-1', 'user-2', 'follows') .buildV2(); expect(delta.pointers).toHaveProperty('follows', { 'user-2': 'follows' }); expect(delta.pointers).toHaveProperty('source', { 'user-1': 'follows' }); }); + + it('should create a V2 delta with relationships and properties', () => { + const delta = createDelta(creator, host) + .relate('user-1', 'user-2', 'follows', { version: 1}) + .buildV2(); + + expect(delta.pointers).toHaveProperty('follows', { 'user-2': 'follows' }); + expect(delta.pointers).toHaveProperty('source', { 'user-1': 'follows' }); + expect(delta.pointers).toHaveProperty('version', { 1: 'follows' }); + }); }); describe('Common functionality', () => { diff --git a/plans/ent-rel-graph.md b/plans/ent-rel-graph.md new file mode 100644 index 0000000..2413ce2 --- /dev/null +++ b/plans/ent-rel-graph.md @@ -0,0 +1,222 @@ +# Entity Relationship Graph Implementation Plan + +## Overview +This document outlines the plan to implement entity relationship tracking in the rhizome-node system. The implementation treats relationships as first-class entities, each with their own identity and properties. + +## Core Design + +### Relationship as First-Class Entities +- Each relationship is a domain entity with its own unique ID +- Relationships have standard properties: `source`, `target`, and `type` +- Additional properties can be added to relationships +- Relationships are created using the `relate()` method in `DeltaBuilder` + +### Delta Structure for Relationships +```typescript +// Creating a relationship +createDelta(creator, host) + .relate( + sourceId, // ID of the source entity + targetId, // ID of the target entity + 'REL_TYPE', // Relationship type + { // Optional properties + prop1: 'value1', + prop2: 'value2' + } + ) + .build(); +``` + +### Data Structures + +#### `LosslessEntity` Updates +```typescript +class LosslessEntity { + // Existing properties + properties = new Map>(); + + // Track relationships where this entity is the source + outboundRelationships = new Map>(); // relationshipType -> Set + + // Track relationships where this entity is the target + inboundRelationships = new Map>(); // relationshipType -> Set + + // ... rest of the class +} +``` + +#### `LosslessViewOne` Extension +```typescript +type RelationshipView = { + id: string; // Relationship ID + type: string; // Relationship type + direction: 'inbound' | 'outbound'; + target: string; // Target entity ID + properties: Record; // Relationship properties +}; + +type LosslessViewOne = { + id: DomainEntityID; + // ... existing fields ... + relationships?: { + outbound: RelationshipView[]; + inbound: RelationshipView[]; + }; +}; +``` + +## Implementation Steps + +### Phase 1: Core Data Structures +1. [x] Update `DeltaBuilder.relate()` to create relationship entities +2. [ ] Update `LosslessEntity` to track relationship IDs +3. [ ] Extend `LosslessViewOne` type to include relationships + +### Phase 2: Relationship Management +1. [ ] Implement relationship tracking in `Lossless` class + - Track all relationships by ID + - Maintain source/target indexes +2. [ ] Implement methods for querying relationships + - Get relationships for an entity + - Filter by type and direction + - Support pagination + +### Phase 3: Delta Processing +1. [ ] Update `ingestDelta` to handle relationship deltas + - Extract relationship information from deltas + - Update relationship indexes + - Handle relationship updates and deletions +2. [ ] Add conflict resolution for concurrent relationship updates + +### Phase 4: View Generation +1. [ ] Update `view` method to include relationships + - Option to include/exclude relationships + - Support for filtering relationships + - Handle circular references + +### Phase 5: Performance Optimization +1. [ ] Add indexing for relationship lookups +2. [ ] Implement lazy loading for large relationship sets +3. [ ] Add caching for frequently accessed relationships + +## API Extensions + +### Get Entity with Relationships +```typescript +// Get an entity with its relationships +GET /entities/{id}?include=relationships + +// Response +{ + "id": "entity1", + "properties": { /* ... */ }, + "relationships": { + "outbound": [ + { + "id": "rel-123", + "type": "OWNS", + "target": "entity2", + "direction": "outbound", + "properties": { + "since": "2023-01-01" + } + } + ], + "inbound": [] + } +} +``` + +### Query Relationships +```typescript +// Get relationships for an entity +GET /entities/{id}/relationships?type=OWNS&direction=outbound + +// Response +{ + "relationships": [ + { + "id": "rel-123", + "type": "OWNS", + "source": "entity1", + "target": "entity2", + "properties": { + "since": "2023-01-01" + } + } + ] +} +``` + +### Create Relationship +```typescript +// Create a new relationship +POST /relationships +{ + "source": "entity1", + "target": "entity2", + "type": "OWNS", + "properties": { + "since": "2023-01-01" + } +} + +// Response +{ + "id": "rel-123", + "source": "entity1", + "target": "entity2", + "type": "OWNS", + "properties": { + "since": "2023-01-01" + } +} +``` + +## Performance Considerations + +1. **Memory Usage**: + - Store only relationship IDs in entity maps + - Use lazy loading for relationship properties + - Consider weak references if memory becomes an issue + +2. **Query Performance**: + - Add indexes for common relationship queries + - Cache frequently accessed relationships + - Support pagination for large relationship sets + +3. **Delta Processing**: + - Batch process relationship updates + - Optimize delta application for relationship-heavy workloads + +## Future Enhancements + +1. **Advanced Querying**: + - GraphQL support for complex relationship queries + - Support for recursive relationship traversal + +2. **Schema Validation**: + - Define relationship schemas with property validation + - Support for required/optional properties + - Default values for relationship properties + +3. **Indexing**: + - Add support for indexing relationship properties + - Implement efficient querying of relationships by property values + +## Testing Strategy + +1. **Unit Tests**: + - Test relationship creation and deletion + - Verify relationship queries with various filters + - Test delta processing for relationships + +2. **Integration Tests**: + - Test relationship persistence across restarts + - Verify concurrent relationship updates + - Test with large numbers of relationships + +3. **Performance Tests**: + - Measure memory usage with large relationship graphs + - Test query performance with complex relationship patterns + - Benchmark delta processing speed for relationship operations diff --git a/src/core/delta-builder.ts b/src/core/delta-builder.ts index c4ebce3..d332829 100644 --- a/src/core/delta-builder.ts +++ b/src/core/delta-builder.ts @@ -87,6 +87,7 @@ export class DeltaBuilder { * Set a property on an entity */ setProperty(entityId: string, property: string, value: string | number | boolean | null, entityLabel = "entity"): this { + // Note that entityLabe and property each need to be unique within a given delta this.addPointer(entityLabel, entityId, property) this.addPointer(property, value); return this; @@ -95,9 +96,16 @@ export class DeltaBuilder { /** * Create a relationship between two entities */ - relate(sourceId: string, relationship: string, targetId: string): this { - this.pointers[relationship] = { [targetId]: relationship }; - this.pointers.source = { [sourceId]: relationship }; + relate(sourceId: string, targetId: string, relationship: string, properties?: Record): this { + const relId = randomUUID(); + this.setProperty(relId, 'source', sourceId, '_source'); + this.setProperty(relId, 'target', targetId, '_target'); + this.setProperty(relId, 'type', relationship, '_type'); + if (properties) { + for (const [key, value] of Object.entries(properties)) { + this.setProperty(relId, key, value, `_${key}`); + } + } return this; }