@memberjunction/version-history 0.0.1 → 4.0.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 CHANGED
@@ -1,45 +1,229 @@
1
1
  # @memberjunction/version-history
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ Version labeling, snapshot capture, diff, and restore for MemberJunction records.
4
+
5
+ ## Architecture Overview
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
14
+ ```
15
+
16
+ ## Dependency Graph Walker
17
+
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).
21
+
22
+ ### Two Key Mechanisms
23
+
24
+ #### 1. EntityRelationship-Driven Reverse Walking
25
+
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.
29
+
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.
34
+
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
45
+ ```
46
+
47
+ #### 2. Ancestor Stack — Prevents Backtracking
48
+
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**.
52
+
53
+ This surgically prevents graph explosion without arbitrary depth limits:
54
+
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
+ ```
66
+
67
+ ### Walk Algorithm — Step by Step
68
+
69
+ ```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
96
+
97
+ SkipR --> ReverseLoop
98
+ SkipF --> ForwardLoop
99
+ SkipF2 --> ForwardLoop
100
+
101
+ style SkipR fill:#fee,stroke:#c00
102
+ style SkipF fill:#fee,stroke:#c00
103
+ style SkipF2 fill:#fee,stroke:#c00
104
+ ```
105
+
106
+ ### Concrete Example — Labeling an AI Agent
107
+
108
+ Given an AI Agent with 2 prompts, 1 action, and 1 sub-agent (via AI Agent
109
+ Relationships), the walker produces:
110
+
111
+ ```mermaid
112
+ 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]"]
119
+
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]"]
124
+
125
+ P1 --> M1["AI Model<br/>(forward via DefaultModelID)"]
126
+ P2 --> M1
127
+
128
+ SubAgent --> SAP["Sub-Agent's Prompt<br/>(reverse)"]
129
+ SAP --> SP["AI Prompt #3<br/>(forward)"]
130
+
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)"]
134
+
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
138
+ ```
139
+
140
+ **Result**: ~15-25 targeted records instead of 1,363 from the naive approach.
141
+
142
+ ### Why This Design
143
+
144
+ | 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 |
151
+
152
+ ### Forward FK Skip Patterns
153
+
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:
157
+
158
+ - `CreatedByUserID`, `UpdatedByUserID`, `UserID`
159
+ - `ContextUserID`, `ModifiedByUserID`
160
+ - `CreatedBy`, `UpdatedBy`
161
+ - `OwnerID`, `OwnerUserID`
162
+ - `AssignedToID`, `AssignedToUserID`
163
+ - `EntityID` (polymorphic reference)
164
+
165
+ ## Snapshot Builder — Batched Capture
166
+
167
+ When capturing records into a version label, the SnapshotBuilder uses **batched
168
+ queries** to minimize database round trips:
169
+
170
+ ```mermaid
171
+ sequenceDiagram
172
+ participant SB as SnapshotBuilder
173
+ participant DB as Database
174
+
175
+ Note over SB: Receive flat list of N nodes
176
+
177
+ SB->>SB: Group nodes by EntityID
178
+ loop For each entity group
179
+ SB->>DB: ONE RunView: RecordChanges<br/>WHERE EntityID = X<br/>AND RecordID IN (a, b, c, ...)
180
+ DB-->>SB: Latest changes for all records in group
181
+ end
182
+
183
+ Note over SB: Build lookup map:<br/>entityId::recordId → RecordChange
184
+
185
+ loop For each node without a RecordChange
186
+ SB->>DB: Create synthetic snapshot (Save)
187
+ end
188
+
189
+ loop For each node
190
+ SB->>DB: Create VersionLabelItem (Save)
191
+ end
192
+ ```
4
193
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
194
+ **Before batching**: N individual RunView calls (946 for a 1363-record label).
195
+ **After batching**: ~5-10 RunView calls (one per unique entity type in the graph).
6
196
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
197
+ ## API
8
198
 
9
- ## Purpose
199
+ ### VersionHistoryEngine (main facade)
10
200
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `@memberjunction/version-history`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
201
+ ```typescript
202
+ const engine = new VersionHistoryEngine();
15
203
 
16
- ## What is OIDC Trusted Publishing?
204
+ // Create a label with dependency walking
205
+ const { Label, CaptureResult } = await engine.CreateLabel({
206
+ Name: 'Before Refactor v2',
207
+ Scope: 'Record',
208
+ EntityName: 'AI Agents',
209
+ RecordKey: agentKey,
210
+ IncludeDependencies: true,
211
+ MaxDepth: 10,
212
+ ExcludeEntities: ['AI Agent Runs'], // skip run history
213
+ }, contextUser);
17
214
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
215
+ // Diff against current state
216
+ const diff = await engine.DiffLabelToCurrentState(Label.ID, contextUser);
19
217
 
20
- ## Setup Instructions
218
+ // Restore if needed
219
+ const result = await engine.RestoreToLabel(Label.ID, { DryRun: true }, contextUser);
220
+ ```
21
221
 
22
- To properly configure OIDC trusted publishing for this package:
222
+ ### WalkOptions
23
223
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
28
-
29
- ## DO NOT USE THIS PACKAGE
30
-
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
36
-
37
- ## More Information
38
-
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
42
-
43
- ---
44
-
45
- **Maintained for OIDC setup purposes only**
224
+ | Option | Default | Description |
225
+ |---|---|---|
226
+ | `MaxDepth` | `10` | Maximum recursion depth |
227
+ | `EntityFilter` | `[]` | Only include these entities (empty = all) |
228
+ | `ExcludeEntities` | `[]` | Skip these entities entirely |
229
+ | `IncludeDeleted` | `false` | Include soft-deleted records |
@@ -0,0 +1,165 @@
1
+ import { CompositeKey, UserInfo } from '@memberjunction/core';
2
+ import { DependencyNode, WalkOptions } from './types.js';
3
+ /**
4
+ * Walks entity relationship graphs to discover records for version labeling.
5
+ *
6
+ * ## Two-direction traversal
7
+ *
8
+ * **Reverse**: Uses the entity's `RelatedEntities` (EntityRelationship metadata)
9
+ * to find child records. This is curated by CodeGen and admins — only explicitly
10
+ * registered One-To-Many relationships are walked.
11
+ *
12
+ * **Forward**: Scans the current entity's own FK fields to find records it
13
+ * references (e.g., AI Agent Prompt → AI Prompt via PromptID). Skips
14
+ * system/infrastructure fields (UserID, CreatedBy, etc.).
15
+ *
16
+ * ## Discovery mode — prevents explosion from forward references
17
+ *
18
+ * Records found via **reverse walk** are "owned children" and get full treatment
19
+ * (both reverse + forward walks). Records found via **forward walk** are
20
+ * "referenced records" and only get forward-only treatment (their own FK refs
21
+ * are followed, but their EntityRelationship children are NOT discovered).
22
+ *
23
+ * This prevents the classic explosion pattern:
24
+ * ```
25
+ * Agent → AgentPrompt → Prompt → [reverse to ALL PromptModels] → Model
26
+ * → [reverse to ALL AgentModels across ALL agents] → explosion!
27
+ * ```
28
+ *
29
+ * With discovery mode:
30
+ * ```
31
+ * Agent → AgentPrompt(reverse,full) → Prompt(forward,forward-only)
32
+ * → Model(forward,forward-only) → STOP (no reverse walk on Model)
33
+ * ```
34
+ *
35
+ * ## Ancestor stack — prevents backtracking
36
+ *
37
+ * A set of entity names on the current path from root to the current node is
38
+ * maintained. When evaluating any relationship, if the target entity type is
39
+ * already on the ancestor stack, it is skipped. This is a secondary safety net
40
+ * that prevents cycles even within full-mode walks.
41
+ *
42
+ * ## Filters
43
+ *
44
+ * - Only walks entities with `TrackRecordChanges === true`
45
+ * - Skips system FK fields via regex patterns (UserID, CreatedBy, etc.)
46
+ * - Respects `ExcludeEntities` and `EntityFilter` options
47
+ * - Uses a global `visited` set for record-level cycle detection
48
+ */
49
+ export declare class DependencyGraphWalker {
50
+ /** Cache: entityID → reverse relationships from EntityRelationship metadata */
51
+ private reverseRelCache;
52
+ /** Cache: entityID → forward FK references from field metadata */
53
+ private forwardRefCache;
54
+ /**
55
+ * Walk from a root record through its relationships, building a tree of
56
+ * all records that should be included in a version label.
57
+ *
58
+ * @param entityName - The starting entity name
59
+ * @param recordKey - The starting record's primary key
60
+ * @param options - Controls depth, filtering, etc.
61
+ * @param contextUser - Server-side user context
62
+ * @returns The root DependencyNode with all descendants populated
63
+ */
64
+ WalkDependents(entityName: string, recordKey: CompositeKey, options: WalkOptions, contextUser: UserInfo): Promise<DependencyNode>;
65
+ /**
66
+ * Flatten a dependency tree into a topologically sorted list.
67
+ * Parents appear before their children, ensuring safe restore ordering.
68
+ */
69
+ FlattenTopological(root: DependencyNode): DependencyNode[];
70
+ /**
71
+ * Recursively discover and attach child nodes for a given parent node.
72
+ *
73
+ * The `discoveryMode` controls which directions are walked:
74
+ * - `'full'`: both reverse (EntityRelationship) and forward (FK refs)
75
+ * - `'forward-only'`: only forward FK refs — no reverse children discovered
76
+ *
77
+ * Records found via reverse walk are recursed with `'full'` mode.
78
+ * Records found via forward walk are recursed with `'forward-only'` mode.
79
+ */
80
+ private walkChildren;
81
+ /**
82
+ * Walk reverse relationships: for each EntityRelationship on the parent
83
+ * entity, load matching child records and recurse into them.
84
+ *
85
+ * Skips any child entity type that is already on the ancestor stack,
86
+ * preventing backtracking (e.g., from AI Prompt back to AI Agent Prompts).
87
+ *
88
+ * Children found via reverse walk are recursed with 'full' discovery mode,
89
+ * since they are "owned children" that may have their own children.
90
+ */
91
+ private walkReverseRelationships;
92
+ /**
93
+ * Walk forward FK references: for each FK field on the parent entity that
94
+ * points to another tracked entity, load the referenced record and recurse.
95
+ *
96
+ * Skips system/infrastructure FKs (UserID, CreatedBy, etc.) and any target
97
+ * entity type already on the ancestor stack.
98
+ *
99
+ * Targets found via forward walk are recursed with 'forward-only' discovery
100
+ * mode — they won't discover their own reverse children, preventing graph
101
+ * explosion from "hub" entities like AI Models.
102
+ */
103
+ private walkForwardReferences;
104
+ /**
105
+ * Discover reverse relationships for an entity using EntityRelationship
106
+ * metadata. Only includes One-To-Many relationships to entities that have
107
+ * TrackRecordChanges enabled.
108
+ *
109
+ * Uses EntityRelationship (already loaded in memory on EntityInfo) rather
110
+ * than scanning all entities' FK fields. This means only relationships
111
+ * that CodeGen or admins have explicitly registered are walked.
112
+ */
113
+ private discoverReverseRelationships;
114
+ /**
115
+ * Discover forward FK references on an entity — fields that point TO other
116
+ * entities. Only includes references to entities with TrackRecordChanges
117
+ * enabled, and skips system/infrastructure FK fields.
118
+ */
119
+ private discoverForwardReferences;
120
+ /**
121
+ * Load child records for a reverse relationship.
122
+ * Queries the child entity where the join field matches the parent's key value.
123
+ */
124
+ private loadChildRecords;
125
+ /**
126
+ * Load a single record's data by primary key.
127
+ */
128
+ private loadRecordData;
129
+ /**
130
+ * Build a DependencyNode from record data, register it in the visited set,
131
+ * and attach it as a child of the parent. Returns null if already visited.
132
+ */
133
+ private registerNode;
134
+ /** Create an empty stats accumulator. */
135
+ private createEmptyStats;
136
+ /** Increment the per-entity record counter. */
137
+ private incrementEntityCount;
138
+ /**
139
+ * Log a detailed summary of the walk: total records, per-entity counts,
140
+ * skips, and suppression stats.
141
+ */
142
+ private logWalkSummary;
143
+ /** Build a unique key for the visited set: "EntityName::recordID" */
144
+ private visitKey;
145
+ /**
146
+ * Determine the parent key field from an EntityRelationship record.
147
+ * Uses EntityKeyField if specified, otherwise falls back to the first PK.
148
+ */
149
+ private resolveParentKeyFieldFromRelationship;
150
+ /**
151
+ * Determine the target key field from a FK field definition.
152
+ * Uses RelatedEntityFieldName if specified, otherwise falls back to the
153
+ * target entity's first PK.
154
+ */
155
+ private resolveParentKeyFieldFromFK;
156
+ /** Check if a field name matches a system/infrastructure FK pattern. */
157
+ private isSystemFKField;
158
+ /** Check if an entity should be skipped based on filter options. */
159
+ private shouldSkipEntity;
160
+ /** Check if an entity has a soft delete field. */
161
+ private entityHasSoftDelete;
162
+ /** Apply defaults to walk options. */
163
+ private resolveDefaults;
164
+ }
165
+ //# sourceMappingURL=DependencyGraphWalker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DependencyGraphWalker.d.ts","sourceRoot":"","sources":["../src/DependencyGraphWalker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA0E,QAAQ,EAAuB,MAAM,sBAAsB,CAAC;AAC3J,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AA6FtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,qBAAa,qBAAqB;IAC9B,+EAA+E;IAC/E,OAAO,CAAC,eAAe,CAA4C;IACnE,kEAAkE;IAClE,OAAO,CAAC,eAAe,CAAyC;IAMhE;;;;;;;;;OASG;IACU,cAAc,CACvB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,WAAW,EACpB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,cAAc,CAAC;IA2C1B;;;OAGG;IACI,kBAAkB,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,EAAE;IAiBjE;;;;;;;;;OASG;YACW,YAAY;IA4B1B;;;;;;;;;OASG;YACW,wBAAwB;IAyCtC;;;;;;;;;;OAUG;YACW,qBAAqB;IAwDnC;;;;;;;;OAQG;IACH,OAAO,CAAC,4BAA4B;IA6BpC;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IA+BjC;;;OAGG;YACW,gBAAgB;IAsC9B;;OAEG;YACW,cAAc;IA0B5B;;;OAGG;IACH,OAAO,CAAC,YAAY;IAqCpB,yCAAyC;IACzC,OAAO,CAAC,gBAAgB;IAUxB,+CAA+C;IAC/C,OAAO,CAAC,oBAAoB;IAK5B;;;OAGG;IACH,OAAO,CAAC,cAAc;IA0BtB,qEAAqE;IACrE,OAAO,CAAC,QAAQ;IAIhB;;;OAGG;IACH,OAAO,CAAC,qCAAqC;IAU7C;;;;OAIG;IACH,OAAO,CAAC,2BAA2B;IAUnC,wEAAwE;IACxE,OAAO,CAAC,eAAe;IAIvB,oEAAoE;IACpE,OAAO,CAAC,gBAAgB;IAUxB,kDAAkD;IAClD,OAAO,CAAC,mBAAmB;IAI3B,sCAAsC;IACtC,OAAO,CAAC,eAAe;CAQ1B"}