npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@memberjunction/version-history

v5.7.0

Published

Server-side version history engine providing label-based versioning, dependency-graph snapshots, cross-entity diffs, and point-in-time restore for MemberJunction record changes

Readme

@memberjunction/version-history

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

Overview

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<br/>(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

Installation

npm install @memberjunction/version-history

Quick Start

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

Label Scopes

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

Architecture

Sub-Engines

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

Snapshot and Restore Flow

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

Dependency Graph Walker

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

Two Key Mechanisms

1. EntityRelationship-Driven Reverse Walking

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

2. Ancestor Stack -- Prevents Backtracking

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.

Walk Algorithm

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

Design Rationale

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

Forward FK Skip Patterns

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)

Snapshot Builder -- Batched Capture

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<br/>WHERE EntityID = X<br/>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).

API Reference

VersionHistoryEngine

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

WalkOptions

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

Dependencies

| Package | Purpose | |---------|---------| | @memberjunction/core | Entity system, metadata, and CompositeKey | | @memberjunction/core-entities | VersionLabel entity types | | @memberjunction/global | Global state management |

License

ISC