A comprehensive GraphQL client for MemberJunction that provides a complete data access layer for connecting applications to MemberJunction APIs.
The @memberjunction/graphql-dataprovider package is a full-featured GraphQL client implementation for MemberJunction applications. It provides a standardized way to interact with MemberJunction's GraphQL API, handling queries, mutations, subscriptions, and complex operations like transaction groups and entity relationships.
This data provider is designed for both frontend and backend applications that need to communicate with a MemberJunction API server, offering a consistent interface regardless of the underlying database technology.
npm install @memberjunction/graphql-dataprovider
import { setupGraphQLClient, GraphQLProviderConfigData } from '@memberjunction/graphql-dataprovider';
// Create configuration
const config = new GraphQLProviderConfigData(
'your-jwt-token',
'https://api.example.com/graphql',
'wss://api.example.com/graphql',
async () => {
// Refresh token function - called when JWT expires
const newToken = await refreshAuthToken();
return newToken;
},
'__mj', // Optional: MJ Core schema name (defaults to '__mj')
['schema1', 'schema2'], // Optional: Include only these schemas
['excluded_schema'], // Optional: Exclude these schemas
'mj-api-key' // Optional: For server-to-server communication
);
// Setup the client (returns configured instance)
const dataProvider = await setupGraphQLClient(config);
// Or create and configure manually
const dataProvider = new GraphQLDataProvider();
await dataProvider.Config(config);
import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
const dataProvider = new GraphQLDataProvider({
graphQLEndpoint: 'https://api.example.com/graphql',
});
// Load an entity
async function getUserById(userId: number) {
const result = await dataProvider.loadEntity('User', userId);
return result.success ? result.entity : null;
}
// Create a new entity
async function createUser(userData: any) {
const entityData = {
ID: 0, // 0 indicates a new entity
FirstName: userData.firstName,
LastName: userData.lastName,
Email: userData.email,
// other fields...
};
const options = {
IgnoreDirtyFields: false, // Save all fields
SkipValidation: false // Run validation before save
};
const result = await dataProvider.SaveEntity(
entityData,
'User',
options
);
return result;
}
// Update an existing entity
async function updateUser(userId: number, updatedData: any) {
// Load the entity
const entity = await dataProvider.GetEntityObject(
'User',
{ ID: userId }
);
if (entity) {
// Update fields
Object.assign(entity.GetData(), updatedData);
// Save changes
const result = await dataProvider.SaveEntity(
entity.GetData(),
'User'
);
return result;
}
return { Success: false, Message: 'User not found' };
}
// Delete an entity
async function deleteUser(userId: number) {
const options = {
IgnoreWarnings: false // Show warnings if any
};
const result = await dataProvider.DeleteEntity(
'User',
{ ID: userId },
options
);
return result;
}
import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
import { RunViewParams } from '@memberjunction/core';
const dataProvider = new GraphQLDataProvider();
// Execute a view
async function getActiveUsers() {
const params: RunViewParams = {
EntityName: 'Users',
ExtraFilter: "Status = 'Active'",
OrderBy: 'LastName, FirstName',
Fields: ['ID', 'FirstName', 'LastName', 'Email'], // Optional: specific fields
IgnoreMaxRows: false,
MaxRows: 50,
ResultType: 'entity_object', // or 'simple' for raw data
ForceAuditLog: true,
AuditLogDescription: 'Loading active users for report'
};
const result = await dataProvider.RunView(params);
return result.Success ? result.Results : [];
}
// Execute multiple views in parallel
async function getMultipleDatasets() {
const viewParams: RunViewParams[] = [
{ EntityName: 'Users', ExtraFilter: "Status = 'Active'" },
{ EntityName: 'Orders', ExtraFilter: "OrderDate >= '2024-01-01'" }
];
const results = await dataProvider.RunViews(viewParams);
return results;
}
// Iterate through a large entity using keyset (seek) pagination
// O(log N) per page, regardless of depth — ideal for background jobs and bulk processing
async function iterateAllTaxReturns() {
let lastSeenKey: CompositeKey | undefined; // undefined => first page
while (true) {
const result = await dataProvider.RunView({
EntityName: 'Tax Returns',
ExtraFilter: 'AddressLine1 IS NOT NULL',
AfterKey: lastSeenKey,
MaxRows: 500,
ResultType: 'entity_object',
});
if (!result.Success || result.Results.length === 0) break;
for (const r of result.Results) { /* process */ }
if (result.Results.length < 500) break;
lastSeenKey = CompositeKey.FromID(result.Results[result.Results.length - 1].ID);
}
}
// AfterKey is forwarded through the GraphQL wire via CompositeKeyInputType.
// See guides/KEYSET_PAGINATION_GUIDE.md for the full pattern, constraints,
// and reference implementations.
// Execute a report
async function getSalesReport(reportId: string) {
const params = {
ReportID: reportId
};
const result = await dataProvider.RunReport(params);
return result.Success ? result.Results : [];
}
// Execute a saved query
async function runCustomQuery(queryId: string, parameters: any) {
const params = {
QueryID: queryId,
Parameters: parameters
};
const result = await dataProvider.RunQuery(params);
return result;
}
// Execute an ad-hoc SQL query (SELECT/WITH only, validated server-side)
async function runAdhocQuery(sql: string) {
const result = await dataProvider.RunQuery({ SQL: sql });
return result;
}
import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
import { TransactionGroupBase } from '@memberjunction/core';
const dataProvider = new GraphQLDataProvider({
graphQLEndpoint: 'https://api.example.com/graphql',
});
// Define a transaction group
class OrderTransactionGroup extends TransactionGroupBase {
constructor() {
super('CreateOrderWithItems');
}
}
// Use the transaction group
async function createOrderWithItems(orderData: any, items: any[]) {
// Create transaction group
const transaction = await dataProvider.CreateTransactionGroup();
// Create order entity
const orderEntity = await dataProvider.GetEntityObject('Order');
orderEntity.NewRecord();
orderEntity.Set('CustomerID', orderData.customerId);
orderEntity.Set('OrderDate', new Date());
orderEntity.Set('Status', 'New');
// Add to transaction
const orderItem = transaction.AddTransaction(orderEntity, 'create');
// Add order items with references
for (const item of items) {
const itemEntity = await dataProvider.GetEntityObject('OrderItem');
itemEntity.NewRecord();
itemEntity.Set('ProductID', item.productId);
itemEntity.Set('Quantity', item.quantity);
itemEntity.Set('Price', item.price);
// Reference the order using a variable
const orderTransaction = transaction.AddTransaction(itemEntity, 'create');
transaction.AddVariable(
'orderID',
'ID',
'FieldValue',
orderItem.BaseEntity,
orderTransaction.BaseEntity,
'OrderID'
);
}
// Execute transaction
const results = await transaction.Submit();
return results;
}
import { GraphQLActionClient } from '@memberjunction/graphql-dataprovider';
import { ActionParam } from '@memberjunction/actions-base';
const actionClient = new GraphQLActionClient(dataProvider);
// Execute a standalone action
async function runAction(actionId: string) {
const params: ActionParam[] = [
{ Name: 'parameter1', Value: 'value1', Type: 'Input' },
{ Name: 'parameter2', Value: 123, Type: 'Input' }
];
const result = await actionClient.RunAction(
actionId,
params,
false // skipActionLog
);
if (result.Success) {
console.log('Action result:', result.ResultCode);
}
return result;
}
// Execute an entity action
async function runEntityAction() {
const params = {
EntityAction: entityAction, // EntityActionEntity instance
InvocationType: { Name: 'SingleRecord' },
EntityObject: userEntity, // BaseEntity instance
ContextUser: currentUser // UserInfo instance
};
const result = await actionClient.RunEntityAction(params);
return result;
}
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
// The GraphQL provider automatically handles field mapping for system fields
// __mj_CreatedAt <-> _mj__CreatedAt
// __mj_UpdatedAt <-> _mj__UpdatedAt
// __mj_DeletedAt <-> _mj__DeletedAt
// You can also use the FieldMapper directly
const mapper = new FieldMapper();
// Map fields in an object
const mappedData = mapper.MapFields({
__mj_CreatedAt: '2024-01-01',
Name: 'John Doe'
});
// Result: { _mj__CreatedAt: '2024-01-01', Name: 'John Doe' }
// Map individual field names
const mappedField = mapper.MapFieldName('__mj_CreatedAt');
// Result: '_mj__CreatedAt'
// Reverse mapping
const originalField = mapper.ReverseMapFieldName('_mj__CreatedAt');
// Result: '__mj_CreatedAt'
import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
import { Observable } from 'rxjs';
const dataProvider = new GraphQLDataProvider();
// Subscribe to record changes
function subscribeToRecordChanges() {
const observable: Observable<RecordChange[]> =
await dataProvider.GetRecordChanges(
'User',
{ ID: 123 },
['update', 'delete'], // Watch for these operations
true // Return initial values
);
const subscription = observable.subscribe(changes => {
console.log('Record changes:', changes);
// Handle changes
});
// Later, unsubscribe
subscription.unsubscribe();
}
The GraphQL provider includes a comprehensive AI client for executing prompts and generating embeddings.
import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
const dataProvider = new GraphQLDataProvider();
const aiClient = dataProvider.AI; // Access the AI client
// Execute a stored AI prompt
async function runAIPrompt() {
const result = await aiClient.RunAIPrompt({
promptId: 'prompt-123',
data: { context: 'user specific data' },
temperature: 0.7,
topP: 0.9,
responseFormat: 'json'
});
if (result.success) {
console.log('AI Response:', result.output);
console.log('Parsed Result:', result.parsedResult);
console.log('Tokens Used:', result.tokensUsed);
}
return result;
}
// Execute a simple prompt without stored configuration
async function runSimplePrompt() {
const result = await aiClient.ExecuteSimplePrompt({
systemPrompt: 'You are a helpful data analyst',
messages: [
{ message: 'What are the key trends?', role: 'user' },
{ message: 'Based on the data...', role: 'assistant' },
{ message: 'Can you elaborate?', role: 'user' }
],
preferredModels: ['gpt-4', 'claude-3'],
modelPower: 'medium', // 'lowest', 'medium', or 'highest'
responseFormat: 'json'
});
if (result.success) {
console.log('Response:', result.result);
console.log('Model Used:', result.modelName);
// If response contains JSON
if (result.resultObject) {
console.log('Parsed JSON:', result.resultObject);
}
}
return result;
}
// Generate text embeddings
async function generateEmbeddings() {
// Single text embedding
const single = await aiClient.EmbedText({
textToEmbed: 'This is a sample text',
modelSize: 'small' // 'small' or 'medium'
});
console.log('Embedding:', single.embeddings); // number[]
console.log('Dimensions:', single.vectorDimensions);
// Multiple text embeddings (batch)
const batch = await aiClient.EmbedText({
textToEmbed: [
'First text to embed',
'Second text to embed',
'Third text to embed'
],
modelSize: 'medium'
});
console.log('Embeddings:', batch.embeddings); // number[][]
console.log('Model:', batch.modelName);
return batch;
}
// Run an AI agent for conversational interactions
async function runAIAgent() {
const result = await aiClient.RunAIAgent({
agentId: 'agent-456',
messages: [
{ role: 'user', content: 'Hello, I need help with data analysis' },
{ role: 'assistant', content: 'I can help you analyze your data' },
{ role: 'user', content: 'What patterns do you see?' }
],
sessionId: 'session-789',
data: { contextData: 'relevant information' }
});
if (result.success) {
console.log('Agent Response:', result.payload);
console.log('Execution Time:', result.executionTimeMs, 'ms');
}
return result;
}
import { GraphQLSystemUserClient } from '@memberjunction/graphql-dataprovider';
// Create system user client for server-to-server communication
const systemClient = new GraphQLSystemUserClient(
'https://api.example.com/graphql',
'', // No JWT token needed
'session-id',
'mj-api-key' // Shared secret key
);
// Execute queries as system user
const queries = [
'SELECT * FROM Users WHERE Active = 1',
'SELECT COUNT(*) as Total FROM Orders'
];
const result = await systemClient.GetData(
queries,
'access-token' // Short-lived access token
);
if (result.Success) {
console.log('Query results:', result.Results);
}
// AI Operations with system privileges
async function systemAIOperations() {
// Run AI prompt as system user
const promptResult = await systemClient.RunAIPrompt({
promptId: 'system-prompt-123',
skipValidation: true,
data: { systemContext: 'internal data' }
});
// Execute simple prompt as system user
const simpleResult = await systemClient.ExecuteSimplePrompt({
systemPrompt: 'Analyze system performance',
modelPower: 'highest'
});
// Generate embeddings as system user
const embeddings = await systemClient.EmbedText({
textToEmbed: ['System log entry 1', 'System log entry 2'],
modelSize: 'medium'
});
return { promptResult, simpleResult, embeddings };
}
// Run AI agent as system user
async function systemAgentOperation() {
const result = await systemClient.RunAIAgent({
agentId: 'system-agent-123',
messages: [
{ role: 'system', content: 'Process batch data' }
],
sessionId: 'system-session-456'
});
return result;
}
// Sync roles and users
const syncResult = await systemClient.SyncRolesAndUsers({
Roles: [
{ ID: '1', Name: 'Admin', Description: 'Administrator role' }
],
Users: [
{
ID: '1',
Name: 'john.doe',
Email: 'john@example.com',
Type: 'User',
FirstName: 'John',
LastName: 'Doe',
Roles: [{ ID: '1', Name: 'Admin', Description: 'Administrator role' }]
}
]
});
The GraphQL data provider provides transparent handling of IS-A type relationships (entity inheritance) on the client side. When working with IS-A child entities, the provider seamlessly manages the complexity of parent-child field coordination.
For IS-A child entities:
// Example: Saving an AIPrompt (child of Template)
const aiPrompt = await md.GetEntityObject<AIPromptEntity>('AI Prompts');
aiPrompt.NewRecord();
// Set both parent fields (from Template)
aiPrompt.Name = 'Customer Analysis Prompt';
aiPrompt.Description = 'Analyzes customer behavior patterns';
// Set child fields (from AIPrompt)
aiPrompt.AIModelID = 'gpt-4-model-id';
aiPrompt.PromptRole = 'User';
// Save - client sends all fields in one mutation
await aiPrompt.Save();
// The server handles splitting and saving parent first, then child
Important aspects of transaction handling with IS-A relationships:
BaseEntity.ProviderTransaction remains null on the client sideWhen loading IS-A child entities:
// Loading returns merged parent + child view
const prompt = await md.GetEntityObject<AIPromptEntity>('AI Prompts', contextUser);
await prompt.Load('prompt-id');
// Access parent fields
console.log(prompt.Name); // From Template
// Access child fields
console.log(prompt.PromptRole); // From AIPrompt
// Both appear as regular properties with no distinction
For comprehensive information about IS-A relationships in MemberJunction:
| Class/Type | Description |
|---|---|
GraphQLDataProvider |
Main class implementing IEntityDataProvider, IMetadataProvider, IRunViewProvider, IRunReportProvider, and IRunQueryProvider interfaces |
GraphQLProviderConfigData |
Configuration class for setting up the GraphQL provider with authentication and connection details |
GraphQLActionClient |
Client for executing actions and entity actions through GraphQL |
GraphQLAIClient |
Client for AI operations including prompts, agents, and embeddings |
GraphQLSystemUserClient |
Specialized client for server-to-server communication using API keys |
GraphQLTransactionGroup |
Manages complex multi-entity transactions with variable support |
FieldMapper |
Handles automatic field name mapping between client and server |
setupGraphQLClient |
Helper function to quickly setup and configure the GraphQL client |
GetEntityObject(entityName: string, compositeKey?: CompositeKey) - Get an entity object instanceSaveEntity(entityData: any, entityName: string, options?: EntitySaveOptions) - Save entity dataDeleteEntity(entityName: string, compositeKey: CompositeKey, options?: EntityDeleteOptions) - Delete an entityGetRecordChanges(entityName: string, compositeKey: CompositeKey, operations: string[], includeInitial: boolean) - Subscribe to entity changesRunView(params: RunViewParams) - Execute a single viewRunViews(params: RunViewParams[]) - Execute multiple views in parallelRunReport(params: RunReportParams) - Execute a reportRunQuery(params: RunQueryParams) - Execute a custom queryCreateTransactionGroup() - Create a new transaction groupExecuteTransaction(transaction: TransactionGroupBase) - Execute a transaction groupGetRecordDuplicates(request: PotentialDuplicateRequest) - Find potential duplicate recordsMergeRecords(request: RecordMergeRequest, contextUser?: UserInfo, options?: EntityMergeOptions) - Merge duplicate records with optional transaction scope supportGetEntityRecordName(entityName: string, compositeKey: CompositeKey) - Get display name for a recordGetEntityRecordNames(info: EntityRecordNameInput[]) - Get display names for multiple recordsGetEntityDependencies(entityName: string, compositeKey: CompositeKey) - Get record dependenciesRunAIPrompt(params: RunAIPromptParams) - Execute a stored AI prompt with parametersRunAIAgent(params: RunAIAgentParams) - Run an AI agent for conversational interactionsExecuteSimplePrompt(params: ExecuteSimplePromptParams) - Execute ad-hoc prompts without stored configurationEmbedText(params: EmbedTextParams) - Generate text embeddings using local modelsThis package provides browser-side storage providers for LocalCacheManager and subscribes to CACHE_INVALIDATION GraphQL events to keep browser data fresh when other users or servers modify entities.
| Provider | Backing store | Key features |
|---|---|---|
BrowserIndexedDBStorageProvider |
IndexedDB | Native object storage via structured clone (no JSON parse/stringify on the hot path), single-transaction batched reads via GetItems, per-category object stores, version-bumped wipe on minor releases |
BrowserLocalStorageProvider |
localStorage |
Generic-typed SetItem<T> / GetItem<T> / GetItems<T> with internal JSON serialization, key-prefix-based category isolation |
Both implement the generic-typed ILocalStorageProvider interface defined in @memberjunction/core — see that package's README for interface details.
BrowserIndexedDBStorageProvider ties its IDB DB_VERSION to this package's package.json version (major * 1000 + minor):
503050316000Patch releases keep the same DB version, so frequent patch deploys don't force users into cold-cache loads. Each minor release triggers a one-time onupgradeneeded that wipes all object stores; caches repopulate on first use.
The version is generated at build time by scripts/generate-version.mjs (runs as a prebuild/pretest step) into src/version.generated.ts. To force an extra wipe within the same minor (rare — emergency hotfix scenario), bump the MANUAL_CACHE_REVISION constant at the top of storage-providers.ts.
BrowserIndexedDBStorageProvider.GetItems<T>(keys, category?) reads N keys inside a single read transaction. Per-call IDB transaction overhead is real (~3–10ms each in most browsers) and serialized on the same object store, so Promise.all([...N GetItem calls]) actually pays N × overhead even though it looks parallel.
For the warm-load smart-cache-check flow this matters a lot — the client reads cached fingerprint metadata for every view in the coalesced engine bundle, then reads the actual cached row data for entries the server marks as 'current'. Both passes used to do per-key reads; both now use GetItems to amortize the overhead into one transaction. For 85 keys this is the difference between ~425ms of pure IDB bookkeeping and ~10ms.
setupGraphQLClient orchestrates a deterministic warm-load path: after provider.Config(...) loads the cached AllMetadata blob from IndexedDB (gzip-compressed, three keys read in a single batched call), it calls provider.preValidateAndRefresh() to confirm the cache is current via a single batched timestamp round-trip. If current, engines trust their local caches and route per-view requests through RunViewsWithCacheCheck — a fingerprint-only GraphQL call that returns either a "current" marker (use local cache) or fresh data for stale entries.
For the full architecture — differential updates, Redis cross-server sync, session-based deduplication, and deployment topologies — see the Caching & Pub/Sub Guide.
@memberjunction/core - Core MemberJunction functionality@memberjunction/core-entities - Entity definitions@memberjunction/actions-base - Action system base classes@memberjunction/global - Global utilitiesgraphql - GraphQL language and executiongraphql-request - Minimal GraphQL clientgraphql-ws - GraphQL WebSocket client for subscriptions@tempfix/idb - IndexedDB wrapper for offline storagerxjs - Reactive extensions for subscriptionsuuid - UUID generation for session IDsISC