Member Junction
    Preparing search index...

    Module @memberjunction/version-history

    @memberjunction/version-history

    Version labeling, snapshot capture, diff, and restore for MemberJunction records. Provides point-in-time versioning with full entity dependency graph awareness.

    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.

    graph TD
        A["VersionHistoryEngine
    (Facade)"] --> B["LabelManager"] A --> C["SnapshotBuilder"] A --> D["DiffEngine"] A --> E["RestoreEngine"] A --> F["DependencyGraphWalker"] B --> G["Version Labels"] C --> H["Version Label Items"] D --> I["DiffResult"] E --> J["RestoreResult"] F --> K["DependencyNode Tree"] style A fill:#2d6a9f,stroke:#1a4971,color:#fff style B fill:#7c5295,stroke:#563a6b,color:#fff style C fill:#7c5295,stroke:#563a6b,color:#fff style D fill:#7c5295,stroke:#563a6b,color:#fff style E fill:#7c5295,stroke:#563a6b,color:#fff style F fill:#7c5295,stroke:#563a6b,color:#fff style G fill:#2d8659,stroke:#1a5c3a,color:#fff style H fill:#2d8659,stroke:#1a5c3a,color:#fff style I fill:#b8762f,stroke:#8a5722,color:#fff style J fill:#b8762f,stroke:#8a5722,color:#fff style K fill:#b8762f,stroke:#8a5722,color:#fff
    npm install @memberjunction/version-history
    
    import { VersionHistoryEngine } from '@memberjunction/version-history';

    const engine = new VersionHistoryEngine();

    // Create a label capturing a record and its dependencies
    const { Label, CaptureResult } = await engine.CreateLabel({
    Name: 'Before Refactor',
    Scope: 'Record',
    EntityName: 'AI Prompts',
    RecordKey: promptKey,
    IncludeDependencies: true,
    }, contextUser);

    // Later: see what changed since the label
    const diff = await engine.DiffLabelToCurrentState(Label.ID, contextUser);

    // Restore if needed
    const result = await engine.RestoreToLabel(Label.ID, {}, contextUser);
    Scope Description Use Case
    Record Single record and its dependencies Safe point before editing a specific record
    Entity All records of a specific entity Checkpoint before bulk updates
    System All tracked entities Full system snapshot before a release
    Sub-Engine Responsibility
    LabelManager Label CRUD and lifecycle management
    SnapshotBuilder Captures record state into label items with batched queries
    DependencyGraphWalker Traverses entity relationships to discover dependent records
    DiffEngine Compares snapshots between labels or between a label and current state
    RestoreEngine Applies labeled state back to records in dependency order
    sequenceDiagram
        participant User
        participant VHE as VersionHistoryEngine
        participant SB as SnapshotBuilder
        participant DGW as DependencyGraphWalker
        participant DB as Database
    
        User->>VHE: CreateLabel(params)
        VHE->>SB: CaptureRecord(labelId, entity, key)
        SB->>DGW: WalkDependents(entity, key)
        DGW->>DB: Query related records
        DGW-->>SB: DependencyNode tree
        SB->>DB: Save VersionLabelItem for each record
        SB-->>VHE: CaptureResult
    
        Note over User,DB: Time passes, records are modified
    
        User->>VHE: RestoreToLabel(labelId, options)
        VHE->>VHE: Create safety Pre-Restore label
        VHE->>DB: Load snapshots, apply in dependency order
        VHE-->>User: RestoreResult
    

    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).

    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.

    graph TD
        A["AI Agents"] -->|"EntityRelationship"| B["AI Agent Prompts"]
        A -->|"EntityRelationship"| C["AI Agent Actions"]
        A -->|"EntityRelationship"| D["AI Agent Relationships"]
        A -->|"EntityRelationship"| E["AI Agent Models"]
        A -.-x|"NOT walked"| G["Random Table with FK"]
    
        style A fill:#2d6a9f,stroke:#1a4971,color:#fff
        style B fill:#2d8659,stroke:#1a5c3a,color:#fff
        style C fill:#2d8659,stroke:#1a5c3a,color:#fff
        style D fill:#2d8659,stroke:#1a5c3a,color:#fff
        style E fill:#2d8659,stroke:#1a5c3a,color:#fff
        style G fill:#b8762f,stroke:#8a5722,color:#fff
    

    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.

    flowchart TD
        Start(["walkChildren called"]) --> DepthCheck{"Depth >= MaxDepth?"}
        DepthCheck -->|Yes| Stop(["Return"])
        DepthCheck -->|No| Reverse["Walk Reverse Relationships"]
    
        Reverse --> ReverseLoop{"For each EntityRelationship"}
        ReverseLoop -->|Next| AncestorR{"On ancestor stack?"}
        AncestorR -->|Yes| SkipR(["Skip"])
        AncestorR -->|No| LoadChildren["Load child records"]
        LoadChildren --> RecurseR["Recurse walkChildren"]
        RecurseR --> ReverseLoop
    
        ReverseLoop -->|Done| Forward["Walk Forward References"]
        Forward --> ForwardLoop{"For each FK field"}
        ForwardLoop -->|Next| SystemCheck{"System FK?"}
        SystemCheck -->|Yes| SkipF(["Skip"])
        SystemCheck -->|No| AncestorF{"On ancestor stack?"}
        AncestorF -->|Yes| SkipF2(["Skip"])
        AncestorF -->|No| LoadTarget["Load referenced record"]
        LoadTarget --> RecurseF["Recurse walkChildren"]
        RecurseF --> ForwardLoop
        ForwardLoop -->|Done| Stop
    
        style SkipR fill:#b8762f,stroke:#8a5722,color:#fff
        style SkipF fill:#b8762f,stroke:#8a5722,color:#fff
        style SkipF2 fill:#b8762f,stroke:#8a5722,color:#fff
        style Start fill:#2d6a9f,stroke:#1a4971,color:#fff
        style Stop fill:#2d8659,stroke:#1a5c3a,color:#fff
    
    Concern Solution
    Which children to walk? EntityRelationship -- admin-controlled, CodeGen-maintained
    Preventing graph explosion? Ancestor stack -- blocks backtracking to any entity type on current path
    Infrastructure FKs (UserID, etc.)? System FK skip list -- regex patterns for known infrastructure fields
    Cycle detection? Visited set -- entityName::recordID prevents revisiting any record
    Sub-agent recursion? Ancestor stack is path-based -- pops on backtrack, allowing re-entry from a different branch

    The following FK field name patterns are never followed during forward walking, as they reference system infrastructure rather than business data:

    • CreatedByUserID, UpdatedByUserID, UserID
    • ContextUserID, ModifiedByUserID
    • CreatedBy, UpdatedBy
    • OwnerID, OwnerUserID
    • AssignedToID, AssignedToUserID
    • EntityID (polymorphic reference)

    When capturing records into a version label, the SnapshotBuilder uses batched queries to minimize database round trips:

    sequenceDiagram
        participant SB as SnapshotBuilder
        participant DB as Database
    
        Note over SB: Receive flat list of N nodes
    
        SB->>SB: Group nodes by EntityID
        loop For each entity group
            SB->>DB: ONE RunView: RecordChanges
    WHERE EntityID = X
    AND RecordID IN (a, b, c, ...) DB-->>SB: Latest changes for all records in group end Note over SB: Build lookup map loop For each node without a RecordChange SB->>DB: Create synthetic snapshot end loop For each node SB->>DB: Create VersionLabelItem end

    Before batching: N individual RunView calls. After batching: ~5-10 RunView calls (one per unique entity type in the graph).

    const engine = new VersionHistoryEngine();

    // Create a label with dependency walking
    const { Label, CaptureResult } = await engine.CreateLabel({
    Name: 'Before Refactor v2',
    Scope: 'Record',
    EntityName: 'AI Agents',
    RecordKey: agentKey,
    IncludeDependencies: true,
    MaxDepth: 10,
    ExcludeEntities: ['AI Agent Runs'],
    }, contextUser);

    // Diff against current state
    const diff = await engine.DiffLabelToCurrentState(Label.ID, contextUser);

    // Restore if needed
    const result = await engine.RestoreToLabel(Label.ID, { DryRun: true }, contextUser);
    Option Default Description
    MaxDepth 10 Maximum recursion depth
    EntityFilter [] Only include these entities (empty = all)
    ExcludeEntities [] Skip these entities entirely
    IncludeDeleted false Include soft-deleted records
    Package Purpose
    @memberjunction/core Entity system, metadata, and CompositeKey
    @memberjunction/core-entities VersionLabel entity types
    @memberjunction/global Global state management

    ISC

    Classes

    DependencyGraphWalker
    DiffEngine
    LabelManager
    RestoreEngine
    SnapshotBuilder
    VersionHistoryEngine

    Interfaces

    CaptureError
    CaptureResult
    CreateLabelParams
    CreateLabelProgressUpdate
    DependencyNode
    DiffResult
    DiffSummary
    EntityDiffGroup
    FieldChange
    LabelFilter
    RecordDiff
    RecordSnapshot
    RestoreItemResult
    RestoreOptions
    RestoreResult
    WalkOptions

    Type Aliases

    CreateLabelProgressCallback
    CreateLabelStep
    RecordDiffType
    RestoreStatus
    VersionLabelScope
    VersionLabelStatus

    Variables

    ENTITY_RECORD_CHANGES
    ENTITY_VERSION_LABEL_ITEMS
    ENTITY_VERSION_LABEL_RESTORES
    ENTITY_VERSION_LABELS

    Functions

    buildCompositeKeyFromRecord
    buildIdKey
    buildPrimaryKeyForLoad
    escapeSqlString
    loadEntityById
    loadRecordChangeSnapshot
    sqlContains
    sqlEquals
    sqlIn
    sqlNotIn