@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.
Files changed (2) hide show
  1. package/README.md +168 -127
  2. 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
- ## Architecture Overview
5
+ ## Overview
6
6
 
7
- ```
8
- VersionHistoryEngine (facade)
9
- ├── LabelManager — label CRUD and lifecycle
10
- ├── SnapshotBuilder — captures record state into label items (batched)
11
- ├── DiffEngine — compares snapshots between labels
12
- ├── RestoreEngine — applies labeled state back to records
13
- └── DependencyGraphWalker — traverses entity relationships
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
- ## Dependency Graph Walker
36
+ ## Installation
17
37
 
18
- The walker discovers all records that should be included in a version label. It
19
- traverses both **reverse relationships** (child records that belong to the root)
20
- and **forward references** (records the root or its children point to).
38
+ ```bash
39
+ npm install @memberjunction/version-history
40
+ ```
21
41
 
22
- ### Two Key Mechanisms
42
+ ## Quick Start
23
43
 
24
- #### 1. EntityRelationship-Driven Reverse Walking
44
+ ```typescript
45
+ import { VersionHistoryEngine } from '@memberjunction/version-history';
46
+
47
+ const engine = new VersionHistoryEngine();
25
48
 
26
- Instead of scanning every entity in the system for foreign keys that point to the
27
- current entity (expensive O(N*M) scan), the walker uses the
28
- **EntityRelationship metadata** that MemberJunction already maintains.
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
- Each entity's `RelatedEntities` array defines which child entities are
31
- meaningful. These are auto-generated by CodeGen when FK relationships are
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
- ```mermaid
36
- graph TD
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
- #### 2. Ancestor Stack — Prevents Backtracking
65
+ ## Label Scopes
48
66
 
49
- The walker maintains a **stack of entity type names** representing the path from
50
- root to the current node. When evaluating any relationship (reverse or forward),
51
- if the target entity type is already on the ancestor stack, it is **skipped**.
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
- This surgically prevents graph explosion without arbitrary depth limits:
73
+ ## Architecture
54
74
 
55
- ```mermaid
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
- ### Walk Algorithm Step by Step
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
- flowchart TD
71
- Start([walkChildren called]) --> DepthCheck{Depth >= MaxDepth?}
72
- DepthCheck -->|Yes| Stop([Return])
73
- DepthCheck -->|No| Reverse[Walk Reverse Relationships]
74
-
75
- Reverse --> ReverseLoop{For each EntityRelationship<br/>on current entity}
76
- ReverseLoop -->|Next rel| AncestorCheckR{Child entity<br/>on ancestor stack?}
77
- AncestorCheckR -->|Yes| SkipR([Skip — would backtrack])
78
- AncestorCheckR -->|No| LoadChildren[Load child records<br/>via RunView]
79
- LoadChildren --> RegisterR[Register nodes + push<br/>child entity onto ancestors]
80
- RegisterR --> RecurseR[Recurse walkChildren<br/>for each child]
81
- RecurseR --> PopR[Pop child entity<br/>from ancestors]
82
- PopR --> ReverseLoop
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
- SkipR --> ReverseLoop
98
- SkipF --> ForwardLoop
99
- SkipF2 --> ForwardLoop
103
+ Note over User,DB: Time passes, records are modified
100
104
 
101
- style SkipR fill:#fee,stroke:#c00
102
- style SkipF fill:#fee,stroke:#c00
103
- style SkipF2 fill:#fee,stroke:#c00
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
- ### Concrete Example — Labeling an AI Agent
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
- Given an AI Agent with 2 prompts, 1 action, and 1 sub-agent (via AI Agent
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
- Root["AI Agent (root)<br/>ancestors: [AI Agents]"]
114
-
115
- Root --> AP1["AI Agent Prompt #1<br/>(reverse)<br/>ancestors: [..., AI Agent Prompts]"]
116
- Root --> AP2["AI Agent Prompt #2<br/>(reverse)"]
117
- Root --> AA["AI Agent Action<br/>(reverse)<br/>ancestors: [..., AI Agent Actions]"]
118
- Root --> AR["AI Agent Relationship<br/>(reverse)<br/>ancestors: [..., AI Agent Relationships]"]
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
- AP1 --> P1["AI Prompt #1<br/>(forward via PromptID)<br/>ancestors: [..., AI Prompts]"]
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
- P1 --> M1["AI Model<br/>(forward via DefaultModelID)"]
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
- SubAgent --> SAP["Sub-Agent's Prompt<br/>(reverse)"]
129
- SAP --> SP["AI Prompt #3<br/>(forward)"]
141
+ ### Walk Algorithm
130
142
 
131
- P1 -.->|"BLOCKED: AI Agent Prompts on stack"| X1["Other Agent Prompts"]
132
- M1 -.->|"BLOCKED: AI Prompts on stack"| X2["Other Prompts"]
133
- Act -.->|"BLOCKED: skipped via EntityRel filter"| X3["Action Params (403 rows)"]
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 X1 fill:#fee,stroke:#c00,stroke-dasharray: 5 5
136
- style X2 fill:#fee,stroke:#c00,stroke-dasharray: 5 5
137
- style X3 fill:#fee,stroke:#c00,stroke-dasharray: 5 5
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
- **Result**: ~15-25 targeted records instead of 1,363 from the naive approach.
141
-
142
- ### Why This Design
174
+ ### Design Rationale
143
175
 
144
176
  | Concern | Solution |
145
- |---|---|
146
- | Which children to walk? | **EntityRelationship** admin-controlled, CodeGen-maintained |
147
- | Preventing graph explosion? | **Ancestor stack** blocks backtracking to any entity type on current path |
148
- | Infrastructure FKs (UserID, etc.)? | **System FK skip list** regex patterns for known infrastructure fields |
149
- | Cycle detection? | **Visited set** `entityName::recordID` prevents revisiting any record |
150
- | Sub-agent recursion? | Ancestor stack is **path-based** pops on backtrack, allowing re-entry from a different branch |
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 Batched Capture
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:<br/>entityId::recordId → RecordChange
212
+ Note over SB: Build lookup map
184
213
 
185
214
  loop For each node without a RecordChange
186
- SB->>DB: Create synthetic snapshot (Save)
215
+ SB->>DB: Create synthetic snapshot
187
216
  end
188
217
 
189
218
  loop For each node
190
- SB->>DB: Create VersionLabelItem (Save)
219
+ SB->>DB: Create VersionLabelItem
191
220
  end
192
221
  ```
193
222
 
194
- **Before batching**: N individual RunView calls (946 for a 1363-record label).
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 (main facade)
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'], // skip run history
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.0.0",
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.0.0",
22
- "@memberjunction/core-entities": "4.0.0",
23
- "@memberjunction/global": "4.0.0"
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",