@memberjunction/version-history 4.0.0 → 4.1.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/README.md +168 -127
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,159 +1,189 @@
|
|
|
1
1
|
# @memberjunction/version-history
|
|
2
2
|
|
|
3
|
-
Version labeling, snapshot capture, diff, and restore for MemberJunction records.
|
|
3
|
+
Version labeling, snapshot capture, diff, and restore for MemberJunction records. Provides point-in-time versioning with full entity dependency graph awareness.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
The `@memberjunction/version-history` package enables developers to create named version labels that capture record state at specific points in time, compare changes between labels, and restore records to previous states while respecting entity dependency ordering.
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
graph TD
|
|
11
|
+
A["VersionHistoryEngine<br/>(Facade)"] --> B["LabelManager"]
|
|
12
|
+
A --> C["SnapshotBuilder"]
|
|
13
|
+
A --> D["DiffEngine"]
|
|
14
|
+
A --> E["RestoreEngine"]
|
|
15
|
+
A --> F["DependencyGraphWalker"]
|
|
16
|
+
|
|
17
|
+
B --> G["Version Labels"]
|
|
18
|
+
C --> H["Version Label Items"]
|
|
19
|
+
D --> I["DiffResult"]
|
|
20
|
+
E --> J["RestoreResult"]
|
|
21
|
+
F --> K["DependencyNode Tree"]
|
|
22
|
+
|
|
23
|
+
style A fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
24
|
+
style B fill:#7c5295,stroke:#563a6b,color:#fff
|
|
25
|
+
style C fill:#7c5295,stroke:#563a6b,color:#fff
|
|
26
|
+
style D fill:#7c5295,stroke:#563a6b,color:#fff
|
|
27
|
+
style E fill:#7c5295,stroke:#563a6b,color:#fff
|
|
28
|
+
style F fill:#7c5295,stroke:#563a6b,color:#fff
|
|
29
|
+
style G fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
30
|
+
style H fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
31
|
+
style I fill:#b8762f,stroke:#8a5722,color:#fff
|
|
32
|
+
style J fill:#b8762f,stroke:#8a5722,color:#fff
|
|
33
|
+
style K fill:#b8762f,stroke:#8a5722,color:#fff
|
|
14
34
|
```
|
|
15
35
|
|
|
16
|
-
##
|
|
36
|
+
## Installation
|
|
17
37
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
```bash
|
|
39
|
+
npm install @memberjunction/version-history
|
|
40
|
+
```
|
|
21
41
|
|
|
22
|
-
|
|
42
|
+
## Quick Start
|
|
23
43
|
|
|
24
|
-
|
|
44
|
+
```typescript
|
|
45
|
+
import { VersionHistoryEngine } from '@memberjunction/version-history';
|
|
46
|
+
|
|
47
|
+
const engine = new VersionHistoryEngine();
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
// Create a label capturing a record and its dependencies
|
|
50
|
+
const { Label, CaptureResult } = await engine.CreateLabel({
|
|
51
|
+
Name: 'Before Refactor',
|
|
52
|
+
Scope: 'Record',
|
|
53
|
+
EntityName: 'AI Prompts',
|
|
54
|
+
RecordKey: promptKey,
|
|
55
|
+
IncludeDependencies: true,
|
|
56
|
+
}, contextUser);
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
detected, and admins can add/remove them. This means only explicitly registered
|
|
33
|
-
children are walked — not every table that happens to have a matching FK.
|
|
58
|
+
// Later: see what changed since the label
|
|
59
|
+
const diff = await engine.DiffLabelToCurrentState(Label.ID, contextUser);
|
|
34
60
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
A[AI Agents] -->|"EntityRelationship<br/>RelatedEntities"| B[AI Agent Prompts]
|
|
38
|
-
A -->|EntityRelationship| C[AI Agent Actions]
|
|
39
|
-
A -->|EntityRelationship| D[AI Agent Relationships]
|
|
40
|
-
A -->|EntityRelationship| E[AI Agent Models]
|
|
41
|
-
A -->|EntityRelationship| F[AI Agent Configurations]
|
|
42
|
-
A -.-x|"NOT walked<br/>(no EntityRelationship)"| G[Some Random Table<br/>with AgentID FK]
|
|
43
|
-
|
|
44
|
-
style G fill:#fee,stroke:#c00,stroke-dasharray: 5 5
|
|
61
|
+
// Restore if needed
|
|
62
|
+
const result = await engine.RestoreToLabel(Label.ID, {}, contextUser);
|
|
45
63
|
```
|
|
46
64
|
|
|
47
|
-
|
|
65
|
+
## Label Scopes
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
| Scope | Description | Use Case |
|
|
68
|
+
|-------|-------------|----------|
|
|
69
|
+
| `Record` | Single record and its dependencies | Safe point before editing a specific record |
|
|
70
|
+
| `Entity` | All records of a specific entity | Checkpoint before bulk updates |
|
|
71
|
+
| `System` | All tracked entities | Full system snapshot before a release |
|
|
52
72
|
|
|
53
|
-
|
|
73
|
+
## Architecture
|
|
54
74
|
|
|
55
|
-
|
|
56
|
-
graph TD
|
|
57
|
-
Agent["AI Agents<br/>(root)"] --> AP["AI Agent Prompts<br/>(reverse)"]
|
|
58
|
-
AP --> Prompt["AI Prompts<br/>(forward via PromptID)"]
|
|
59
|
-
Prompt -.->|"BLOCKED<br/>AI Agent Prompts<br/>is on ancestor stack"| AP2["Other AI Agent Prompts<br/>referencing same prompt"]
|
|
60
|
-
Prompt --> Model["AI Models<br/>(forward via DefaultModelID)"]
|
|
61
|
-
Model -.->|"BLOCKED<br/>AI Prompts<br/>is on ancestor stack"| Prompt2["Other AI Prompts<br/>using same model"]
|
|
62
|
-
|
|
63
|
-
style AP2 fill:#fee,stroke:#c00,stroke-dasharray: 5 5
|
|
64
|
-
style Prompt2 fill:#fee,stroke:#c00,stroke-dasharray: 5 5
|
|
65
|
-
```
|
|
75
|
+
### Sub-Engines
|
|
66
76
|
|
|
67
|
-
|
|
77
|
+
| Sub-Engine | Responsibility |
|
|
78
|
+
|-----------|----------------|
|
|
79
|
+
| **LabelManager** | Label CRUD and lifecycle management |
|
|
80
|
+
| **SnapshotBuilder** | Captures record state into label items with batched queries |
|
|
81
|
+
| **DependencyGraphWalker** | Traverses entity relationships to discover dependent records |
|
|
82
|
+
| **DiffEngine** | Compares snapshots between labels or between a label and current state |
|
|
83
|
+
| **RestoreEngine** | Applies labeled state back to records in dependency order |
|
|
84
|
+
|
|
85
|
+
### Snapshot and Restore Flow
|
|
68
86
|
|
|
69
87
|
```mermaid
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
ReverseLoop -->|Done| Forward[Walk Forward References]
|
|
85
|
-
Forward --> ForwardLoop{For each FK field<br/>on current entity}
|
|
86
|
-
ForwardLoop -->|Next FK| SystemCheck{System FK?<br/>UserID, CreatedBy, etc.}
|
|
87
|
-
SystemCheck -->|Yes| SkipF([Skip — infrastructure field])
|
|
88
|
-
SystemCheck -->|No| AncestorCheckF{Target entity<br/>on ancestor stack?}
|
|
89
|
-
AncestorCheckF -->|Yes| SkipF2([Skip — would backtrack])
|
|
90
|
-
AncestorCheckF -->|No| LoadTarget[Load referenced record<br/>via RunView]
|
|
91
|
-
LoadTarget --> RegisterF[Register node + push<br/>target entity onto ancestors]
|
|
92
|
-
RegisterF --> RecurseF[Recurse walkChildren<br/>for referenced record]
|
|
93
|
-
RecurseF --> PopF[Pop target entity<br/>from ancestors]
|
|
94
|
-
PopF --> ForwardLoop
|
|
95
|
-
ForwardLoop -->|Done| Stop
|
|
88
|
+
sequenceDiagram
|
|
89
|
+
participant User
|
|
90
|
+
participant VHE as VersionHistoryEngine
|
|
91
|
+
participant SB as SnapshotBuilder
|
|
92
|
+
participant DGW as DependencyGraphWalker
|
|
93
|
+
participant DB as Database
|
|
94
|
+
|
|
95
|
+
User->>VHE: CreateLabel(params)
|
|
96
|
+
VHE->>SB: CaptureRecord(labelId, entity, key)
|
|
97
|
+
SB->>DGW: WalkDependents(entity, key)
|
|
98
|
+
DGW->>DB: Query related records
|
|
99
|
+
DGW-->>SB: DependencyNode tree
|
|
100
|
+
SB->>DB: Save VersionLabelItem for each record
|
|
101
|
+
SB-->>VHE: CaptureResult
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
SkipF --> ForwardLoop
|
|
99
|
-
SkipF2 --> ForwardLoop
|
|
103
|
+
Note over User,DB: Time passes, records are modified
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
User->>VHE: RestoreToLabel(labelId, options)
|
|
106
|
+
VHE->>VHE: Create safety Pre-Restore label
|
|
107
|
+
VHE->>DB: Load snapshots, apply in dependency order
|
|
108
|
+
VHE-->>User: RestoreResult
|
|
104
109
|
```
|
|
105
110
|
|
|
106
|
-
|
|
111
|
+
## Dependency Graph Walker
|
|
112
|
+
|
|
113
|
+
The walker discovers all records that should be included in a version label. It traverses both **reverse relationships** (child records that belong to the root) and **forward references** (records the root or its children point to).
|
|
114
|
+
|
|
115
|
+
### Two Key Mechanisms
|
|
116
|
+
|
|
117
|
+
#### 1. EntityRelationship-Driven Reverse Walking
|
|
107
118
|
|
|
108
|
-
|
|
109
|
-
Relationships), the walker produces:
|
|
119
|
+
Instead of scanning every entity for foreign keys that point to the current entity, the walker uses **EntityRelationship metadata** that MemberJunction already maintains. Only explicitly registered children are walked.
|
|
110
120
|
|
|
111
121
|
```mermaid
|
|
112
122
|
graph TD
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
A["AI Agents"] -->|"EntityRelationship"| B["AI Agent Prompts"]
|
|
124
|
+
A -->|"EntityRelationship"| C["AI Agent Actions"]
|
|
125
|
+
A -->|"EntityRelationship"| D["AI Agent Relationships"]
|
|
126
|
+
A -->|"EntityRelationship"| E["AI Agent Models"]
|
|
127
|
+
A -.-x|"NOT walked"| G["Random Table with FK"]
|
|
128
|
+
|
|
129
|
+
style A fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
130
|
+
style B fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
131
|
+
style C fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
132
|
+
style D fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
133
|
+
style E fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
134
|
+
style G fill:#b8762f,stroke:#8a5722,color:#fff
|
|
135
|
+
```
|
|
119
136
|
|
|
120
|
-
|
|
121
|
-
AP2 --> P2["AI Prompt #2<br/>(forward)"]
|
|
122
|
-
AA --> Act["Action<br/>(forward via ActionID)<br/>ancestors: [..., Actions]"]
|
|
123
|
-
AR --> SubAgent["Sub-Agent<br/>(forward via SubAgentID)<br/>ancestors: [..., AI Agents]"]
|
|
137
|
+
#### 2. Ancestor Stack -- Prevents Backtracking
|
|
124
138
|
|
|
125
|
-
|
|
126
|
-
P2 --> M1
|
|
139
|
+
The walker maintains a stack of entity type names representing the path from root to the current node. When evaluating any relationship, if the target entity type is already on the ancestor stack, it is skipped. This surgically prevents graph explosion without arbitrary depth limits.
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
SAP --> SP["AI Prompt #3<br/>(forward)"]
|
|
141
|
+
### Walk Algorithm
|
|
130
142
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
```mermaid
|
|
144
|
+
flowchart TD
|
|
145
|
+
Start(["walkChildren called"]) --> DepthCheck{"Depth >= MaxDepth?"}
|
|
146
|
+
DepthCheck -->|Yes| Stop(["Return"])
|
|
147
|
+
DepthCheck -->|No| Reverse["Walk Reverse Relationships"]
|
|
148
|
+
|
|
149
|
+
Reverse --> ReverseLoop{"For each EntityRelationship"}
|
|
150
|
+
ReverseLoop -->|Next| AncestorR{"On ancestor stack?"}
|
|
151
|
+
AncestorR -->|Yes| SkipR(["Skip"])
|
|
152
|
+
AncestorR -->|No| LoadChildren["Load child records"]
|
|
153
|
+
LoadChildren --> RecurseR["Recurse walkChildren"]
|
|
154
|
+
RecurseR --> ReverseLoop
|
|
155
|
+
|
|
156
|
+
ReverseLoop -->|Done| Forward["Walk Forward References"]
|
|
157
|
+
Forward --> ForwardLoop{"For each FK field"}
|
|
158
|
+
ForwardLoop -->|Next| SystemCheck{"System FK?"}
|
|
159
|
+
SystemCheck -->|Yes| SkipF(["Skip"])
|
|
160
|
+
SystemCheck -->|No| AncestorF{"On ancestor stack?"}
|
|
161
|
+
AncestorF -->|Yes| SkipF2(["Skip"])
|
|
162
|
+
AncestorF -->|No| LoadTarget["Load referenced record"]
|
|
163
|
+
LoadTarget --> RecurseF["Recurse walkChildren"]
|
|
164
|
+
RecurseF --> ForwardLoop
|
|
165
|
+
ForwardLoop -->|Done| Stop
|
|
134
166
|
|
|
135
|
-
style
|
|
136
|
-
style
|
|
137
|
-
style
|
|
167
|
+
style SkipR fill:#b8762f,stroke:#8a5722,color:#fff
|
|
168
|
+
style SkipF fill:#b8762f,stroke:#8a5722,color:#fff
|
|
169
|
+
style SkipF2 fill:#b8762f,stroke:#8a5722,color:#fff
|
|
170
|
+
style Start fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
171
|
+
style Stop fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
138
172
|
```
|
|
139
173
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
### Why This Design
|
|
174
|
+
### Design Rationale
|
|
143
175
|
|
|
144
176
|
| Concern | Solution |
|
|
145
|
-
|
|
146
|
-
| Which children to walk? | **EntityRelationship**
|
|
147
|
-
| Preventing graph explosion? | **Ancestor stack**
|
|
148
|
-
| Infrastructure FKs (UserID, etc.)? | **System FK skip list**
|
|
149
|
-
| Cycle detection? | **Visited set**
|
|
150
|
-
| Sub-agent recursion? | Ancestor stack is **path-based**
|
|
177
|
+
|---------|----------|
|
|
178
|
+
| Which children to walk? | **EntityRelationship** -- admin-controlled, CodeGen-maintained |
|
|
179
|
+
| Preventing graph explosion? | **Ancestor stack** -- blocks backtracking to any entity type on current path |
|
|
180
|
+
| Infrastructure FKs (UserID, etc.)? | **System FK skip list** -- regex patterns for known infrastructure fields |
|
|
181
|
+
| Cycle detection? | **Visited set** -- `entityName::recordID` prevents revisiting any record |
|
|
182
|
+
| Sub-agent recursion? | Ancestor stack is **path-based** -- pops on backtrack, allowing re-entry from a different branch |
|
|
151
183
|
|
|
152
184
|
### Forward FK Skip Patterns
|
|
153
185
|
|
|
154
|
-
The following FK field name patterns are never followed during forward walking,
|
|
155
|
-
as they reference system infrastructure (Users, audit fields) rather than
|
|
156
|
-
business data:
|
|
186
|
+
The following FK field name patterns are never followed during forward walking, as they reference system infrastructure rather than business data:
|
|
157
187
|
|
|
158
188
|
- `CreatedByUserID`, `UpdatedByUserID`, `UserID`
|
|
159
189
|
- `ContextUserID`, `ModifiedByUserID`
|
|
@@ -162,10 +192,9 @@ business data:
|
|
|
162
192
|
- `AssignedToID`, `AssignedToUserID`
|
|
163
193
|
- `EntityID` (polymorphic reference)
|
|
164
194
|
|
|
165
|
-
## Snapshot Builder
|
|
195
|
+
## Snapshot Builder -- Batched Capture
|
|
166
196
|
|
|
167
|
-
When capturing records into a version label, the SnapshotBuilder uses **batched
|
|
168
|
-
queries** to minimize database round trips:
|
|
197
|
+
When capturing records into a version label, the SnapshotBuilder uses **batched queries** to minimize database round trips:
|
|
169
198
|
|
|
170
199
|
```mermaid
|
|
171
200
|
sequenceDiagram
|
|
@@ -180,23 +209,23 @@ sequenceDiagram
|
|
|
180
209
|
DB-->>SB: Latest changes for all records in group
|
|
181
210
|
end
|
|
182
211
|
|
|
183
|
-
Note over SB: Build lookup map
|
|
212
|
+
Note over SB: Build lookup map
|
|
184
213
|
|
|
185
214
|
loop For each node without a RecordChange
|
|
186
|
-
SB->>DB: Create synthetic snapshot
|
|
215
|
+
SB->>DB: Create synthetic snapshot
|
|
187
216
|
end
|
|
188
217
|
|
|
189
218
|
loop For each node
|
|
190
|
-
SB->>DB: Create VersionLabelItem
|
|
219
|
+
SB->>DB: Create VersionLabelItem
|
|
191
220
|
end
|
|
192
221
|
```
|
|
193
222
|
|
|
194
|
-
**Before batching**: N individual RunView calls
|
|
223
|
+
**Before batching**: N individual RunView calls.
|
|
195
224
|
**After batching**: ~5-10 RunView calls (one per unique entity type in the graph).
|
|
196
225
|
|
|
197
|
-
## API
|
|
226
|
+
## API Reference
|
|
198
227
|
|
|
199
|
-
### VersionHistoryEngine
|
|
228
|
+
### VersionHistoryEngine
|
|
200
229
|
|
|
201
230
|
```typescript
|
|
202
231
|
const engine = new VersionHistoryEngine();
|
|
@@ -209,7 +238,7 @@ const { Label, CaptureResult } = await engine.CreateLabel({
|
|
|
209
238
|
RecordKey: agentKey,
|
|
210
239
|
IncludeDependencies: true,
|
|
211
240
|
MaxDepth: 10,
|
|
212
|
-
ExcludeEntities: ['AI Agent Runs'],
|
|
241
|
+
ExcludeEntities: ['AI Agent Runs'],
|
|
213
242
|
}, contextUser);
|
|
214
243
|
|
|
215
244
|
// Diff against current state
|
|
@@ -222,8 +251,20 @@ const result = await engine.RestoreToLabel(Label.ID, { DryRun: true }, contextUs
|
|
|
222
251
|
### WalkOptions
|
|
223
252
|
|
|
224
253
|
| Option | Default | Description |
|
|
225
|
-
|
|
254
|
+
|--------|---------|-------------|
|
|
226
255
|
| `MaxDepth` | `10` | Maximum recursion depth |
|
|
227
256
|
| `EntityFilter` | `[]` | Only include these entities (empty = all) |
|
|
228
257
|
| `ExcludeEntities` | `[]` | Skip these entities entirely |
|
|
229
258
|
| `IncludeDeleted` | `false` | Include soft-deleted records |
|
|
259
|
+
|
|
260
|
+
## Dependencies
|
|
261
|
+
|
|
262
|
+
| Package | Purpose |
|
|
263
|
+
|---------|---------|
|
|
264
|
+
| `@memberjunction/core` | Entity system, metadata, and CompositeKey |
|
|
265
|
+
| `@memberjunction/core-entities` | VersionLabel entity types |
|
|
266
|
+
| `@memberjunction/global` | Global state management |
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
ISC
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/version-history",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"description": "Server-side version history engine providing label-based versioning, dependency-graph snapshots, cross-entity diffs, and point-in-time restore for MemberJunction record changes",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"typescript": "^5.9.3"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@memberjunction/core": "4.
|
|
22
|
-
"@memberjunction/core-entities": "4.
|
|
23
|
-
"@memberjunction/global": "4.
|
|
21
|
+
"@memberjunction/core": "4.1.0",
|
|
22
|
+
"@memberjunction/core-entities": "4.1.0",
|
|
23
|
+
"@memberjunction/global": "4.1.0"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|