relationships as domain entities

This commit is contained in:
Lentil Hoffman 2025-06-22 02:44:31 -05:00
parent 54a50a9c22
commit c04439713c
Signed by: lentil
GPG Key ID: 0F5B99F3F4D0C087
4 changed files with 289 additions and 11 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ coverage/
data/
test-data/
*.code-workspace
local-notes/

View File

@ -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', () => {

222
plans/ent-rel-graph.md Normal file
View File

@ -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<PropertyID, Set<Delta>>();
// Track relationships where this entity is the source
outboundRelationships = new Map<string, Set<string>>(); // relationshipType -> Set<relationshipId>
// Track relationships where this entity is the target
inboundRelationships = new Map<string, Set<string>>(); // relationshipType -> Set<relationshipId>
// ... 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<string, any>; // 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

View File

@ -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<string, any>): 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;
}