The @memberjunction/data-context library provides a metadata-driven framework for managing collections of related data items in MemberJunction applications. It enables developers to define, load, persist, and manipulate data from multiple sources -- views, queries, entities, single records, and raw SQL -- through a unified, type-safe API that works across both client and server tiers.
graph TD
subgraph Consumer["Consumer Code"]
style Consumer fill:#2d6a9f,stroke:#1a4971,color:#fff
A["Application / Agent / Workflow"]
end
subgraph DataContextPkg["@memberjunction/data-context"]
style DataContextPkg fill:#7c5295,stroke:#563a6b,color:#fff
DC["DataContext"]
DCI["DataContextItem"]
DCFI["DataContextFieldInfo"]
end
subgraph Sources["Data Sources"]
style Sources fill:#2d8659,stroke:#1a5c3a,color:#fff
V["Views (RunView)"]
Q["Queries (RunQuery)"]
E["Full Entities"]
SR["Single Records"]
SQL["SQL Statements"]
end
subgraph Persistence["Persistence Layer"]
style Persistence fill:#b8762f,stroke:#8a5722,color:#fff
DCE["DataContextEntity"]
DCIE["DataContextItemEntity"]
end
A --> DC
DC -->|"manages"| DCI
DCI -->|"field metadata"| DCFI
DCI -->|"loads from"| V
DCI -->|"loads from"| Q
DCI -->|"loads from"| E
DCI -->|"loads from"| SR
DCI -->|"loads from"| SQL
DC -->|"persists via"| DCE
DC -->|"persists items via"| DCIE
npm install @memberjunction/data-context
A Data Context is a named collection of Data Context Items, each of which represents a distinct data source. An item can be one of five types:
| Type | Description | Required Fields |
|---|---|---|
view |
A saved MemberJunction User View | ViewID, EntityID |
query |
A registered MemberJunction Query | QueryID |
full_entity |
All records from an entity | EntityID |
single_record |
One record, optionally with related entity data | EntityID, RecordID |
sql |
A raw SQL statement (server-side only) | SQL |
classDiagram
class DataContext {
+ID: string
+DataContextEntity: DataContextEntity
+Items: DataContextItem[]
+LoadMetadata(id, contextUser, provider) Promise~boolean~
+LoadData(dataSource, forceRefresh, ...) Promise~boolean~
+Load(id, dataSource, ...) Promise~boolean~
+SaveItems(contextUser, persistItemData) Promise~boolean~
+AddDataContextItem() DataContextItem
+ValidateDataExists(ignoreFailedLoadItems) boolean
+ConvertToSimpleObject(itemPrefix, includeFailedLoadItems) object
+CreateSimpleObjectTypeDefinition(itemPrefix, includeFailedLoadItems) string
+LoadDataFromObject(data) boolean
+Clone(context, includeData, contextUser)$ Promise~DataContext~
+FromRawData(rawData)$ Promise~DataContext~
+CreateDataContextItem()$ DataContextItem
+MapEntityFieldsToDataContextFields(entity)$ DataContextFieldInfo[]
}
class DataContextItem {
+Type: ItemType
+RecordID: string
+EntityID: string
+ViewID: string
+QueryID: string
+RecordName: string
+SQL: string
+CodeName: string
+EntityName: string
+Fields: DataContextFieldInfo[]
+DataContextItemID: string
+Data: object[]
+DataLoaded: boolean
+DataLoadingError: string
+Description: string
+AdditionalDescription: string
+LoadData(dataSource, ...) Promise~boolean~
+LoadDataFromObject(data) boolean
+ValidateDataExists(ignoreFailedLoad) boolean
+FromViewEntity(viewEntity)$ DataContextItem
+FromSingleRecord(record)$ DataContextItem
+FromQuery(query)$ DataContextItem
+FromFullEntity(entity)$ DataContextItem
+FromRawItem(rawItem)$ DataContextItem
#LoadFromView(contextUser) Promise~boolean~
#LoadFromFullEntity(contextUser) Promise~boolean~
#LoadFromSingleRecord(contextUser, ...) Promise~boolean~
#LoadFromQuery(contextUser) Promise~boolean~
#LoadFromSQL(dataSource, contextUser) Promise~boolean~
}
class DataContextFieldInfo {
+Name: string
+Type: string
+Description: string
}
class DataContextItemServer {
#LoadFromSQL(dataSource, contextUser) Promise~boolean~
}
DataContext "1" --> "*" DataContextItem : contains
DataContextItem "1" --> "*" DataContextFieldInfo : describes fields
DataContextItemServer --|> DataContextItem : extends
flowchart LR
subgraph Step1["1. Create"]
style Step1 fill:#2d6a9f,stroke:#1a4971,color:#fff
C1["new DataContext()"]
end
subgraph Step2["2. Load Metadata"]
style Step2 fill:#7c5295,stroke:#563a6b,color:#fff
C2["LoadMetadata(id)"]
end
subgraph Step3["3. Load Data"]
style Step3 fill:#2d8659,stroke:#1a5c3a,color:#fff
C3["LoadData(dataSource)"]
end
subgraph Step4["4. Process"]
style Step4 fill:#b8762f,stroke:#8a5722,color:#fff
C4["ValidateDataExists()\nConvertToSimpleObject()"]
end
subgraph Step5["5. Persist"]
style Step5 fill:#2d6a9f,stroke:#1a4971,color:#fff
C5["SaveItems(contextUser)"]
end
Step1 --> Step2 --> Step3 --> Step4 --> Step5
Use Load() to fetch both metadata and data in a single call:
import { DataContext } from '@memberjunction/data-context';
const context = new DataContext();
const loaded = await context.Load(
dataContextID, // ID of the data context record
dataSource, // Required only for SQL-type items (server-side)
false, // forceRefresh -- reload even if data is cached
true, // loadRelatedDataOnSingleRecords
10, // maxRecordsPerRelationship
contextUser // required on server-side
);
if (loaded) {
context.Items.forEach(item => {
console.log(`${item.Description}: ${item.Data?.length ?? 0} rows`);
});
}
Alternatively, call LoadMetadata() and LoadData() separately for finer control:
const context = new DataContext();
await context.LoadMetadata(dataContextID, contextUser);
// Inspect or modify items before loading data
console.log(`Items to load: ${context.Items.length}`);
await context.LoadData(dataSource, false, true, 10, contextUser);
Each item type has a dedicated static factory method.
import { UserViewEntityExtended } from '@memberjunction/core-entities';
const viewEntity: UserViewEntityExtended = await md.GetEntityObject<UserViewEntityExtended>('User Views');
await viewEntity.Load(viewID);
const viewItem = DataContextItem.FromViewEntity(viewEntity);
context.Items.push(viewItem);
import { BaseEntity } from '@memberjunction/core';
const record: BaseEntity = await md.GetEntityObject('Customers');
await record.Load(recordID);
const recordItem = DataContextItem.FromSingleRecord(record);
context.Items.push(recordItem);
import { QueryInfo } from '@memberjunction/core';
const queryInfo = md.Queries.find(q => q.Name === 'Monthly Revenue');
if (queryInfo) {
const queryItem = DataContextItem.FromQuery(queryInfo);
context.Items.push(queryItem);
}
import { EntityInfo } from '@memberjunction/core';
const entityInfo = md.Entities.find(e => e.Name === 'Products');
if (entityInfo) {
const entityItem = DataContextItem.FromFullEntity(entityInfo);
context.Items.push(entityItem);
}
// Load data for all items in the context
const allLoaded = await context.LoadData(
dataSource, // required for SQL-type items
false, // forceRefresh
true, // loadRelatedDataOnSingleRecords
10 // maxRecordsPerRelationship
);
// Or load data for a single item
const itemLoaded = await context.Items[0].LoadData(
dataSource, false, true, 10, contextUser
);
If you already have the data available (for example, from a cache or external API), load it directly:
// Load into a single item
const item = context.Items[0];
item.LoadDataFromObject(myDataArray);
// Load into all items at once (2D array, one sub-array per item in order)
context.LoadDataFromObject([
rowsForItem0,
rowsForItem1,
rowsForItem2
]);
const saved = await context.SaveItems(
contextUser, // context user for server-side operations
true // persistItemData -- also save loaded data as JSON
);
if (saved) {
// Each item now has a DataContextItemID populated
context.Items.forEach(item => {
console.log(`Saved item ${item.DataContextItemID}`);
});
}
Items are saved in a single transaction group -- either all succeed or none are committed.
// Validate all items have data
if (context.ValidateDataExists()) {
// Convert to a flat keyed object
const simpleObj = context.ConvertToSimpleObject('item_', false);
// Result: { item_0: [...], item_1: [...], ... }
// Generate a TypeScript-style type definition string
const typeDef = context.CreateSimpleObjectTypeDefinition('item_');
// Output: {item_0: []; // View: Customer List, From Entity: Customers\n...}
}
// Check individual items for errors
context.Items.forEach(item => {
if (item.DataLoaded && item.Data) {
console.log(`${item.Description}: ${item.Data.length} rows`);
} else if (item.DataLoadingError) {
console.error(`Failed: ${item.DataLoadingError}`);
}
});
const clonedContext = await DataContext.Clone(
originalContext,
true, // includeData -- copy loaded data into the clone
contextUser
);
if (clonedContext) {
console.log(`Cloned context ID: ${clonedContext.ID}`);
}
const context = await DataContext.FromRawData(rawObject);
// rawObject should have { ID, Items: [ { Type, RecordID, ... }, ... ] }
The base DataContextItem class throws an error when LoadFromSQL() is called because raw SQL execution is inherently a server-side operation. The companion package @memberjunction/data-context-server provides DataContextItemServer, which overrides this method using an mssql connection pool:
flowchart TD
subgraph Client["Client Tier"]
style Client fill:#2d6a9f,stroke:#1a4971,color:#fff
DCI_C["DataContextItem\n(base class)"]
end
subgraph Server["Server Tier"]
style Server fill:#2d8659,stroke:#1a5c3a,color:#fff
DCI_S["DataContextItemServer\n(@RegisterClass override)"]
MSSQL["mssql ConnectionPool"]
end
DCI_C -->|"LoadFromSQL() throws error"| X["Not supported client-side"]
DCI_S -->|"LoadFromSQL() executes via"| MSSQL
DCI_S -.->|"extends"| DCI_C
DataContextItemServer is registered via @RegisterClass with a higher priority, so the MemberJunction class factory automatically returns the server variant when the server package is included. No code changes are needed -- just include @memberjunction/data-context-server in your server project's dependencies.
| Method | Returns | Description |
|---|---|---|
LoadMetadata(id, contextUser?, provider?) |
Promise<boolean> |
Loads metadata for the context and all its items from the database |
LoadData(dataSource, forceRefresh?, loadRelated?, maxRecords?, contextUser?) |
Promise<boolean> |
Loads data for all items; must call LoadMetadata() first |
Load(id, dataSource, forceRefresh?, loadRelated?, maxRecords?, contextUser?) |
Promise<boolean> |
Combined metadata + data load in one call |
SaveItems(contextUser?, persistItemData?) |
Promise<boolean> |
Persists all items to the database in a single transaction |
AddDataContextItem() |
DataContextItem |
Creates a new item and adds it to Items |
ValidateDataExists(ignoreFailedLoadItems?) |
boolean |
Returns true if all items have their Data property set |
ConvertToSimpleObject(itemPrefix?, includeFailedLoadItems?) |
object |
Converts context to a flat keyed object |
CreateSimpleObjectTypeDefinition(itemPrefix?, includeFailedLoadItems?) |
string |
Generates a type definition string for the simple object |
LoadDataFromObject(data) |
boolean |
Loads pre-fetched data as a 2D array mapped by item index |
Clone(context, includeData?, contextUser?) (static) |
Promise<DataContext> |
Deep-clones a context and its items into new database records |
FromRawData(rawData) (static) |
Promise<DataContext> |
Reconstructs a DataContext from a plain object |
CreateDataContextItem() (static) |
DataContextItem |
Creates a new item via the class factory (does not add to context) |
MapEntityFieldsToDataContextFields(entity) (static) |
DataContextFieldInfo[] |
Maps EntityInfo fields to simplified field info objects |
| Property | Type | Description |
|---|---|---|
Type |
'view' | 'query' | 'full_entity' | 'sql' | 'single_record' |
The data source type |
RecordID |
string |
Primary key for single_record items (comma-separated for composite keys) |
EntityID |
string |
Entity identifier (not used for query or sql types) |
ViewID |
string |
View identifier (only for view type) |
QueryID |
string |
Query identifier (only for query type) |
RecordName |
string |
Display name of the view, query, or entity |
SQL |
string |
SQL statement (only for sql type) |
CodeName |
string |
System-generated unique code name within the context |
EntityName |
string |
Entity name (not used for query or sql types) |
Fields |
DataContextFieldInfo[] |
Field metadata for the item |
DataContextItemID |
string |
Database record ID (populated after save) |
Data |
object[] |
The loaded data rows |
DataLoaded |
boolean |
Whether data has been successfully loaded |
DataLoadingError |
string |
Error message if loading failed |
Description |
string (readonly) |
Auto-generated description based on type |
AdditionalDescription |
string |
Optional supplementary description |
| Static Factory Method | Description |
|---|---|
FromViewEntity(viewEntity) |
Creates an item from a UserViewEntityExtended |
FromSingleRecord(record) |
Creates an item from a BaseEntity instance |
FromQuery(query) |
Creates an item from a QueryInfo |
FromFullEntity(entity) |
Creates an item from an EntityInfo |
FromRawItem(rawItem) |
Creates an item from a plain object |
| Property | Type | Description |
|---|---|---|
Name |
string |
Field name |
Type |
string |
Field data type |
Description |
string (optional) |
Field description |
| Package | Purpose |
|---|---|
@memberjunction/global |
Class factory and @RegisterClass decorator |
@memberjunction/core |
Core framework: Metadata, RunView, RunQuery, BaseEntity, EntityInfo, UserInfo |
@memberjunction/core-entities |
Generated entity classes: DataContextEntity, DataContextItemEntity, UserViewEntityExtended |
LoadMetadata() then LoadData(), or use the combined Load() method.DataLoaded and DataLoadingError on each item after loading. LoadData() does not throw exceptions for individual item failures -- it returns false and populates the error properties.contextUser on the server side. Server code serves multiple users concurrently and must include the context user for proper security and audit tracking.maxRecordsPerRelationship to limit the amount of related data loaded for single_record items, especially in production environments.ValidateDataExists() before working with data to ensure all items are ready.SaveItems() automatically wraps all item saves in a transaction group for atomicity.@memberjunction/data-context-server in server projects if any items use the sql type. The class factory handles the override automatically.ISC