A comprehensive SQL Server data provider implementation for the MemberJunction framework, serving as the primary bridge between MemberJunction applications and Microsoft SQL Server databases. This package implements all core data provider interfaces -- entity CRUD, metadata management, view/query/report execution, and transaction handling. SQL logging is inherited from @memberjunction/generic-database-provider.
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
graph TB
subgraph Application["Application Layer"]
MJS["MJServer / MJAPI"]
MS["MetadataSync"]
Custom["Custom Applications"]
end
subgraph Provider["SQLServerDataProvider"]
style Provider fill:#2d6a9f,stroke:#1a4971,color:#fff
SDP["SQLServerDataProvider"]
TXG["SQLServerTransactionGroup"]
UC["UserCache"]
QPP["QueryParameterProcessor"]
NFS["NodeFileSystemProvider"]
end
subgraph GenericDP["GenericDatabaseProvider (inherited)"]
style GenericDP fill:#b8762f,stroke:#8a5722,color:#fff
SL["SQL Logging Sessions"]
EH["Entity/AI Action Hooks"]
ENC["Field Encryption"]
end
subgraph Interfaces["MJ Core Interfaces"]
style Interfaces fill:#7c5295,stroke:#563a6b,color:#fff
IED["IEntityDataProvider"]
IMP["IMetadataProvider"]
IRV["IRunViewProvider"]
IRR["IRunReportProvider"]
end
subgraph Database["SQL Server"]
style Database fill:#2d8659,stroke:#1a5c3a,color:#fff
Pool["Connection Pool"]
SP["Stored Procedures"]
Views["Database Views"]
Tables["Entity Tables"]
end
MJS --> SDP
MS --> SDP
Custom --> SDP
SDP --> IED
SDP --> IMP
SDP --> IRV
SDP --> IRR
SDP --> TXG
SDP --> UC
SDP --> QPP
SDP --> NFS
SDP --> SL
SDP --> EH
SDP --> ENC
SDP --> Pool
Pool --> SP
Pool --> Views
Pool --> Tables
StartRow and keyset-based AfterKey — see KEYSET_PAGINATION_GUIDE.md), and aggregationGenericDatabaseProvider)GenericDatabaseProvider, with optional Redis support for shared, persistent caching across multiple server instancesnpm install @memberjunction/sqlserver-dataprovider
| Package | Purpose |
|---|---|
@memberjunction/core |
Core MJ framework: base entities, metadata, providers |
@memberjunction/core-entities |
Generated entity subclasses and type definitions |
@memberjunction/generic-database-provider |
Shared base class: entity hooks, encryption, SQL logging, view execution |
@memberjunction/global |
Shared utilities, global object store, SQL validation |
@memberjunction/actions |
Server-side entity action execution |
@memberjunction/actions-base |
Action result types |
@memberjunction/ai |
AI integration capabilities |
@memberjunction/ai-provider-bundle |
AI provider bundle |
@memberjunction/ai-vector-dupe |
AI-powered duplicate detection |
@memberjunction/aiengine |
AI engine for entity AI actions |
@memberjunction/encryption |
Field-level encryption engine |
@memberjunction/queue |
Queue management for async operations |
mssql |
SQL Server client for Node.js |
rxjs |
Reactive extensions for transaction queue processing |
The package exports the following public symbols from its entry point:
| Export | Type | Description |
|---|---|---|
SQLServerDataProvider |
Class | Main data provider implementing all MJ provider interfaces |
SQLServerProviderConfigData |
Class | Configuration data for provider initialization |
SQLServerTransactionGroup |
Class | Transaction group for atomic multi-entity operations |
UserCache |
Class | Singleton server-side user and role cache |
QueryParameterProcessor |
Class | Parameter validation and Nunjucks query template processor |
NodeFileSystemProvider |
Class | Node.js fs-based implementation of IFileSystemProvider |
SqlLoggingSessionImpl |
Class | Internal SQL logging session implementation |
SqlLoggingSession |
Interface | Public interface for a logging session |
SqlLoggingOptions |
Interface | Configuration options for SQL logging sessions |
ExecuteSQLOptions |
Interface | Options for SQL execution with logging support |
ExecuteSQLBatchOptions |
Interface | Options for batch SQL execution |
setupSQLServerClient |
Function | Helper to initialize provider, set global provider, start user cache |
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
sequenceDiagram
participant App as Application
participant SDP as SQLServerDataProvider
participant Queue as SQL Queue (RxJS)
participant Logger as SqlLoggingSessions
participant Pool as Connection Pool
participant DB as SQL Server
App->>SDP: Save(entity, user, options)
SDP->>SDP: Generate SP call SQL
SDP->>SDP: Encrypt sensitive fields
alt Transaction Active
SDP->>Queue: Enqueue (sequential)
Queue->>Pool: Execute via Transaction
else No Transaction
SDP->>Pool: Execute directly (parallel)
end
SDP-->>Logger: Log SQL (parallel, non-blocking)
Pool->>DB: Execute stored procedure
DB-->>Pool: Return result set
Pool-->>SDP: Raw result
SDP->>SDP: Process rows (decrypt, timezone adjust)
SDP-->>App: BaseEntityResult
The setupSQLServerClient helper handles full provider initialization: connecting to the pool, configuring the provider, loading the user cache, setting up the global MJ provider, and running the startup manager.
import { setupSQLServerClient } from '@memberjunction/sqlserver-dataprovider';
import { SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
import sql from 'mssql';
// Create and connect a connection pool
const pool = new sql.ConnectionPool({
server: 'your-server.database.windows.net',
port: 1433,
database: 'YourMJDatabase',
user: 'your-username',
password: 'your-password',
options: {
encrypt: true,
trustServerCertificate: false,
},
pool: {
max: 50,
min: 5,
idleTimeoutMillis: 30000,
},
});
await pool.connect();
// Initialize the provider (sets global MJ provider, loads user cache, runs startup)
const config = new SQLServerProviderConfigData(
pool,
'__mj', // MJ core schema name
60, // metadata refresh interval in seconds (0 to disable)
undefined, // includeSchemas (undefined = all)
undefined, // excludeSchemas
true // ignoreExistingMetadata (true for first instance)
);
const provider = await setupSQLServerClient(config);
import { Metadata, CompositeKey } from '@memberjunction/core';
import { UserEntity } from '@memberjunction/core-entities';
const md = new Metadata();
// Load an entity by primary key
const user = await md.GetEntityObject<UserEntity>('Users', contextUser);
const key = new CompositeKey([{ FieldName: 'ID', Value: userId }]);
await user.Load(key);
console.log(`Loaded: ${user.Name}`);
// Create a new entity
const newUser = await md.GetEntityObject<UserEntity>('Users', contextUser);
newUser.Name = 'John Doe';
newUser.Email = 'john@example.com';
const saved = await newUser.Save();
if (saved) {
console.log(`Created user with ID: ${newUser.ID}`);
}
// Delete an entity
await newUser.Delete();
Transaction groups execute multiple entity operations within a single database transaction, with automatic rollback on failure.
import { SQLServerTransactionGroup } from '@memberjunction/sqlserver-dataprovider';
const transaction = await provider.CreateTransactionGroup();
const order = await md.GetEntityObject('Orders', contextUser);
order.CustomerID = customerId;
order.Status = 'New';
order.TransactionGroup = transaction;
const item = await md.GetEntityObject('Order Items', contextUser);
item.ProductID = productId;
item.Quantity = 2;
item.TransactionGroup = transaction;
// Both saves are queued, then executed atomically on Submit
await order.Save();
await item.Save();
const results = await transaction.Submit();
For multi-user server environments, each provider instance supports isolated transaction state with nested savepoints.
try {
await provider.BeginTransaction();
await provider.Save(entity1, contextUser, {});
await provider.Save(entity2, contextUser, {});
await provider.CommitTransaction();
} catch (error) {
await provider.RollbackTransaction();
throw error;
}
import { RunView } from '@memberjunction/core';
const rv = new RunView();
const result = await rv.RunView({
EntityName: 'Users',
ExtraFilter: "Status = 'Active'",
OrderBy: 'Name',
MaxRows: 100,
ResultType: 'entity_object',
}, contextUser);
if (result.Success) {
console.log(`Found ${result.Results.length} active users`);
}
The QueryParameterProcessor validates parameters and processes Nunjucks templates for parameterized queries.
import { RunQuery } from '@memberjunction/core';
const rq = new RunQuery();
const result = await rq.RunQuery({
QueryName: 'ActiveUsersByDepartment',
CategoryPath: '/Reports/Users/',
Parameters: {
department: 'Engineering',
minHireDate: '2023-01-01',
},
}, contextUser);
if (result.Success) {
console.log('Query results:', result.Results);
}
// Ad-hoc SQL execution (SELECT/WITH only — validated via SQLExpressionValidator, read-only)
const adhocResult = await rq.RunQuery({
SQL: 'SELECT TOP 50 Name, Status FROM __mj.vwUsers WHERE IsActive = 1',
}, contextUser);
// Instance method
const rows = await provider.ExecuteSQL(
'SELECT * FROM Users WHERE Department = @dept',
{ dept: 'Engineering' }
);
// Static method (useful when you have a pool but not a provider)
const rows2 = await SQLServerDataProvider.ExecuteSQLWithPool(
pool,
'SELECT TOP 10 * FROM Users ORDER BY Name'
);
The provider includes a comprehensive SQL logging subsystem for capturing executed SQL statements to files. Logging sessions run in parallel with query execution and do not impact performance.
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
graph LR
subgraph Execution["SQL Execution"]
style Execution fill:#2d6a9f,stroke:#1a4971,color:#fff
EX["ExecuteSQL"]
end
subgraph Sessions["Active Logging Sessions"]
style Sessions fill:#b8762f,stroke:#8a5722,color:#fff
S1["Session 1: mutations only"]
S2["Session 2: migration format"]
S3["Session 3: user-filtered"]
end
subgraph Output["Output Files"]
style Output fill:#2d8659,stroke:#1a5c3a,color:#fff
F1["operations.sql"]
F2["migration.sql"]
F3["user-audit.sql"]
end
EX -->|parallel, non-blocking| S1
EX -->|parallel, non-blocking| S2
EX -->|parallel, non-blocking| S3
S1 -->|filtered + formatted| F1
S2 -->|schema placeholders| F2
S3 -->|user-scoped| F3
const session = await provider.CreateSqlLogger('./logs/operations.sql', {
sessionName: 'Debug session',
statementTypes: 'both', // 'queries', 'mutations', or 'both'
prettyPrint: true, // Format SQL with sql-formatter
formatAsMigration: false, // Replace schema names with Flyway placeholders
logRecordChangeMetadata: false, // Log only core SP calls, not change tracking wrapper
retainEmptyLogFiles: false, // Delete file if no statements were logged
filterByUserId: 'user@example.com', // Only capture this user's SQL
filterPatterns: [/spCreateAIPromptRun/i], // Exclude matching patterns
filterType: 'exclude', // 'exclude' or 'include'
});
try {
// All SQL operations are automatically captured
await provider.ExecuteSQL('INSERT INTO ...');
console.log(`Captured ${session.statementCount} statements`);
} finally {
await session.dispose(); // Stop logging, close file, clean up
}
When formatAsMigration: true, the logger automatically:
${flyway:defaultSchema} placeholders${...} patterns within SQL string literals to prevent Flyway interpretationCAST(... AS NVARCHAR(MAX))const session = await provider.CreateSqlLogger(
'./migrations/V20250207120000__entity_updates.sql',
{
formatAsMigration: true,
batchSeparator: 'GO',
description: 'Entity schema updates',
}
);
Filter which SQL statements are logged using simple wildcard patterns or full regular expressions.
Simple wildcards use * as a wildcard character:
*AIPrompt* -- matches anything containing "AIPrompt"spCreate* -- matches anything starting with "spCreate"Regex patterns provide full regular expression support:
/spCreate.*Run/i -- case-insensitive regex/^SELECT.*FROM.*vw/ -- queries from views// Exclude noisy patterns
const session = await provider.CreateSqlLogger('./logs/filtered.sql', {
filterPatterns: [
/spCreateAIPromptRun/i,
/^SELECT.*FROM.*vw.*Metadata/i,
'*EntityFieldValue*',
],
filterType: 'exclude', // Default: skip matching statements
});
// Include only specific patterns
const auditSession = await provider.CreateSqlLogger('./logs/audit.sql', {
filterPatterns: [/INSERT INTO Users/i, /UPDATE Users/i, /DELETE FROM Users/i],
filterType: 'include', // Only log matching statements
});
// List all active sessions
const active = provider.GetActiveSqlLoggingSessions();
console.log(`${active.length} sessions active`);
// Get a specific session
const session = provider.GetSqlLoggingSessionById(sessionId);
// Dispose all sessions (cleanup on shutdown)
await provider.DisposeAllSqlLoggingSessions();
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
graph TD
subgraph TransactionGroup["Transaction Group Flow"]
style TransactionGroup fill:#7c5295,stroke:#563a6b,color:#fff
TG1["AddTransaction(entity)"]
TG2["Submit()"]
TG3["Begin SQL Transaction"]
TG4["Execute items sequentially"]
TG5{"All succeeded?"}
TG6["Commit"]
TG7["Rollback"]
end
subgraph InstanceTx["Instance Transaction Flow"]
style InstanceTx fill:#2d6a9f,stroke:#1a4971,color:#fff
IT1["BeginTransaction()"]
IT2["Queue serializes queries"]
IT3["Save / Delete / ExecuteSQL"]
IT4["CommitTransaction()"]
IT5["RollbackTransaction()"]
end
TG1 --> TG2
TG2 --> TG3
TG3 --> TG4
TG4 --> TG5
TG5 -->|Yes| TG6
TG5 -->|No| TG7
IT1 --> IT2
IT2 --> IT3
IT3 --> IT4
IT3 -->|Error| IT5
The provider supports two transaction mechanisms:
Transaction Groups (SQLServerTransactionGroup) -- bundle multiple entity save/delete operations and execute them within a single SQL Server transaction. If any operation fails, the entire group is rolled back. Transaction groups also support inter-entity variable references, allowing a newly created entity's ID to be passed to dependent entities in the same batch.
Instance-Level Transactions -- each SQLServerDataProvider instance maintains its own transaction state. When a transaction is active, all SQL queries from that instance are serialized through an RxJS queue (concatMap) and executed against the same sql.Transaction object. Non-transactional queries bypass the queue for maximum parallelism. Nested transactions use SQL Server savepoints.
The UserCache is a singleton that loads all users and their role assignments from the database and keeps them in memory. It is used for user lookups during authentication and authorization.
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
// Access the singleton
const cache = UserCache.Instance;
// Look up users
const user = cache.UserByName('john@example.com');
const systemUser = cache.GetSystemUser();
const allUsers = cache.Users;
// Refresh from database (with optional auto-refresh interval in ms)
await cache.Refresh(pool, 60000);
Handles parameter validation and Nunjucks template rendering for parameterized queries.
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
graph LR
subgraph Input["Query Input"]
style Input fill:#2d6a9f,stroke:#1a4971,color:#fff
QI["QueryInfo with SQL template"]
P["Parameters"]
end
subgraph Processing["QueryParameterProcessor"]
style Processing fill:#b8762f,stroke:#8a5722,color:#fff
V["Validate parameters"]
T["Type conversion"]
R["Nunjucks render"]
end
subgraph Output["Result"]
style Output fill:#2d8659,stroke:#1a5c3a,color:#fff
SQL["Processed SQL"]
end
QI --> V
P --> V
V --> T
T --> R
R --> SQL
RunQuerySQLFilterManager| Parameter | Type | Default | Description |
|---|---|---|---|
connectionPool |
sql.ConnectionPool |
required | Connected mssql connection pool |
MJCoreSchemaName |
string |
'__mj' |
Database schema for MJ core tables |
checkRefreshIntervalSeconds |
number |
0 |
Interval for automatic metadata refresh (0 = disabled) |
includeSchemas |
string[] |
undefined |
Restrict metadata loading to these schemas |
excludeSchemas |
string[] |
undefined |
Exclude these schemas from metadata loading |
ignoreExistingMetadata |
boolean |
true |
Force full metadata reload; set false for per-request instances to reuse cache |
Configure via mj.config.cjs at the repository root:
module.exports = {
databaseSettings: {
connectionPool: {
max: 50, // Maximum connections
min: 5, // Minimum connections
idleTimeoutMillis: 30000, // Idle timeout in ms
acquireTimeoutMillis: 30000, // Acquire timeout in ms
},
},
};
Recommended pool sizes:
| Environment | max | min | Notes |
|---|---|---|---|
| Development | 10 | 2 | Low concurrency |
| Production Standard | 50 | 5 | 2-4x CPU cores of API server |
| Production High Load | 100 | 10 | Monitor SQL Server RESOURCE_SEMAPHORE and THREADPOOL wait types |
| Option | Type | Default | Description |
|---|---|---|---|
formatAsMigration |
boolean |
false |
Replace schema names with Flyway ${flyway:defaultSchema} placeholders |
defaultSchemaName |
string |
MJ core schema | Schema name to replace with Flyway placeholder |
description |
string |
undefined |
Comment written at the start of the log file |
statementTypes |
'queries' | 'mutations' | 'both' |
'both' |
Which statement types to log |
batchSeparator |
string |
undefined |
Separator emitted after each statement (e.g., 'GO') |
prettyPrint |
boolean |
false |
Format SQL using sql-formatter with T-SQL dialect |
logRecordChangeMetadata |
boolean |
false |
Log full change-tracking wrapper SQL vs. core SP calls only |
retainEmptyLogFiles |
boolean |
false |
Keep log files that contain zero statements |
filterByUserId |
string |
undefined |
Only log SQL executed by this user |
sessionName |
string |
undefined |
Friendly name for UI display |
verboseOutput |
boolean |
false |
Output debug information to console |
filterPatterns |
(string | RegExp)[] |
undefined |
Patterns for filtering SQL statements |
filterType |
'include' | 'exclude' |
'exclude' |
How filterPatterns are applied |
# Build the package
cd packages/SQLServerDataProvider && npm run build
# Run tests
cd packages/SQLServerDataProvider && npm test
# Run tests with coverage
cd packages/SQLServerDataProvider && npm run test:coverage
# Run tests in watch mode
cd packages/SQLServerDataProvider && npm run test:watch
The provider follows SQL Server connection pool best practices:
sql.ConnectionPool is created at server startup and reused for the application's lifetimeExecuteSQL call creates a new sql.Request from the pool, enabling safe parallel executionmax/min are tunable through mj.config.cjsThe provider automatically detects whether the SQL Server + mssql driver combination produces incorrect DATETIMEOFFSET values. On first query, it runs a diagnostic test and caches the result. If adjustment is needed, all DATETIMEOFFSET fields are corrected during row processing.
When checkRefreshIntervalSeconds > 0, the provider periodically checks whether database metadata has changed (new entities, field modifications, etc.) and reloads if needed. The RefreshIfNeeded() method can also be called on demand.
In server environments like MJAPI, a new SQLServerDataProvider instance is created per request. Setting ignoreExistingMetadata: false on subsequent instances allows them to reuse the metadata loaded by the first instance, avoiding redundant database queries.
| Symptom | Likely Cause | Solution |
|---|---|---|
| Connection timeout | Network or firewall issue | Increase connectionTimeout; verify SQL Server firewall rules |
| Authentication failure | Wrong credentials or permissions | Verify credentials; check encryption settings match server |
| Schema not found | Wrong MJCoreSchemaName |
Verify schema exists (default is __mj); check user schema access |
| Transaction rollback | Constraint violation in entity save | Check required fields, foreign key references, unique constraints |
| Pool exhausted | Too many concurrent connections | Increase pool.max; check for leaked connections or long-running queries |
| EREQINPROG error | Request reuse during transaction | This is handled automatically; the provider clears stale transaction references |
MemberJunction supports IS-A type relationships where child entities inherit from parent entities (e.g., MeetingEntity IS-A ProductEntity). The SQLServerDataProvider manages SQL transactions to ensure atomic save and delete operations across the entire entity hierarchy.
When you save or delete an entity that participates in an IS-A hierarchy, SQLServerDataProvider automatically:
BeginISATransaction() to create a new sql.Transaction on the connection poolBaseEntity.ProviderTransaction and shared across all entities in the parent chainCommitISATransaction() commits all changes, or RollbackISATransaction() reverts everything on failure// Example: Saving a MeetingEntity (which IS-A ProductEntity)
const meeting = await md.GetEntityObject<MeetingEntity>('Meetings');
meeting.Name = 'Project Planning';
meeting.MeetingDate = new Date();
// ... set other fields
// When you call Save(), the provider automatically:
// 1. Begins a SQL transaction
// 2. Saves the Product parent entity first
// 3. Uses the Product ID to save the Meeting child entity
// 4. Commits the transaction
const result = await meeting.Save();
// If any step fails, the entire transaction is rolled back
BeginISATransaction(): Creates a new sql.Transaction on the connection pool and stores it in BaseEntity.ProviderTransactionCommitISATransaction(): Commits the shared transaction across the entire IS-A chainRollbackISATransaction(): Rolls back all changes if any operation in the chain failssql.Transaction instance via BaseEntity.ProviderTransactionFor more details on IS-A relationships and how they work across MemberJunction, see MJCore IS-A Relationships Documentation.
MemberJunction supports virtual entities that are backed by SQL views instead of physical tables. Virtual entities provide read-only access to data and are commonly used for reporting, aggregations, and denormalized views.
Virtual entities work seamlessly with the SQLServerDataProvider for all read operations:
// Example: Querying a virtual entity backed by a view
const rv = new RunView();
const result = await rv.RunView<UserSummaryEntity>({
EntityName: 'User Summary', // Virtual entity backed by vwUserSummary
ExtraFilter: "Department = 'Engineering'",
OrderBy: 'LastLoginDate DESC',
ResultType: 'entity_object'
});
// result.Results contains fully-typed UserSummaryEntity objects
const users = result.Results;
Write operations (Save, Delete) are automatically blocked for virtual entities at the BaseEntity level before they reach the data provider:
For comprehensive documentation on virtual entities, their configuration, and advanced usage patterns, see MJCore Virtual Entities Documentation.
ISC