Skip to content

MemberJunction SQL Server Data Provider

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
  • Full CRUD Operations — Create, Read, Update, Delete for all MemberJunction entities via generated stored procedures
  • Transaction Support — Both transaction groups (multi-entity atomic operations) and instance-level transactions with nested savepoint support
  • View Execution — Run database views with filtering, sorting, pagination (both OFFSET-based StartRow and keyset-based AfterKey — see KEYSET_PAGINATION_GUIDE.md), and aggregation
  • Report and Query Execution — Execute reports and parameterized queries with Nunjucks template processing
  • Connection Pooling — Efficient shared connection pool management with configurable sizing
  • SQL Logging — Real-time SQL statement capture to files with session management, pattern filtering, and Flyway migration formatting (inherited from GenericDatabaseProvider)
  • User and Role Caching — Server-side singleton cache for user information and role assignments
  • Record Change Tracking — Integrated audit trail for entity modifications
  • Duplicate Detection — AI-powered duplicate record detection via vector similarity
  • Record Merging — Merge duplicate records with dependency resolution
  • Row-Level Security — Enforced data access controls at the database level
  • Metadata Refresh — Automatic and on-demand metadata refresh with configurable intervals
  • Field Encryption — Transparent encryption and decryption of sensitive entity fields
  • DateTime Offset Handling — Automatic detection and adjustment for SQL Server timezone behavior
  • Pluggable Cache Backend — Default in-memory caching inherited from GenericDatabaseProvider, with optional Redis support for shared, persistent caching across multiple server instances
Terminal window
npm install @memberjunction/sqlserver-dataprovider
PackagePurpose
@memberjunction/coreCore MJ framework: base entities, metadata, providers
@memberjunction/core-entitiesGenerated entity subclasses and type definitions
@memberjunction/generic-database-providerShared base class: entity hooks, encryption, SQL logging, view execution
@memberjunction/globalShared utilities, global object store, SQL validation
@memberjunction/actionsServer-side entity action execution
@memberjunction/actions-baseAction result types
@memberjunction/aiAI integration capabilities
@memberjunction/ai-provider-bundleAI provider bundle
@memberjunction/ai-vector-dupeAI-powered duplicate detection
@memberjunction/aiengineAI engine for entity AI actions
@memberjunction/encryptionField-level encryption engine
@memberjunction/queueQueue management for async operations
mssqlSQL Server client for Node.js
rxjsReactive extensions for transaction queue processing

The package exports the following public symbols from its entry point:

ExportTypeDescription
SQLServerDataProviderClassMain data provider implementing all MJ provider interfaces
SQLServerProviderConfigDataClassConfiguration data for provider initialization
SQLServerTransactionGroupClassTransaction group for atomic multi-entity operations
UserCacheClassSingleton server-side user and role cache
QueryParameterProcessorClassParameter validation and Nunjucks query template processor
NodeFileSystemProviderClassNode.js fs-based implementation of IFileSystemProvider
SqlLoggingSessionImplClassInternal SQL logging session implementation
SqlLoggingSessionInterfacePublic interface for a logging session
SqlLoggingOptionsInterfaceConfiguration options for SQL logging sessions
ExecuteSQLOptionsInterfaceOptions for SQL execution with logging support
ExecuteSQLBatchOptionsInterfaceOptions for batch SQL execution
setupSQLServerClientFunctionHelper 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:

  • Replaces schema names with ${flyway:defaultSchema} placeholders
  • Escapes ${...} patterns within SQL string literals to prevent Flyway interpretation
  • Splits string literals exceeding SQL Server’s 4000-character NVARCHAR limit into concatenated chunks with CAST(... 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
  • Validates required parameters, applies defaults, and performs type conversion (string, number, date, boolean, array)
  • Renders parameterized SQL using Nunjucks with custom SQL-safe filters registered through RunQuerySQLFilterManager
  • Rejects unknown parameters to prevent injection
ParameterTypeDefaultDescription
connectionPoolsql.ConnectionPoolrequiredConnected mssql connection pool
MJCoreSchemaNamestring'__mj'Database schema for MJ core tables
checkRefreshIntervalSecondsnumber0Interval for automatic metadata refresh (0 = disabled)
includeSchemasstring[]undefinedRestrict metadata loading to these schemas
excludeSchemasstring[]undefinedExclude these schemas from metadata loading
ignoreExistingMetadatabooleantrueForce 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:

EnvironmentmaxminNotes
Development102Low concurrency
Production Standard5052-4x CPU cores of API server
Production High Load10010Monitor SQL Server RESOURCE_SEMAPHORE and THREADPOOL wait types
OptionTypeDefaultDescription
formatAsMigrationbooleanfalseReplace schema names with Flyway ${flyway:defaultSchema} placeholders
defaultSchemaNamestringMJ core schemaSchema name to replace with Flyway placeholder
descriptionstringundefinedComment written at the start of the log file
statementTypes'queries' | 'mutations' | 'both''both'Which statement types to log
batchSeparatorstringundefinedSeparator emitted after each statement (e.g., 'GO')
prettyPrintbooleanfalseFormat SQL using sql-formatter with T-SQL dialect
logRecordChangeMetadatabooleanfalseLog full change-tracking wrapper SQL vs. core SP calls only
retainEmptyLogFilesbooleanfalseKeep log files that contain zero statements
filterByUserIdstringundefinedOnly log SQL executed by this user
sessionNamestringundefinedFriendly name for UI display
verboseOutputbooleanfalseOutput debug information to console
filterPatterns(string | RegExp)[]undefinedPatterns for filtering SQL statements
filterType'include' | 'exclude''exclude'How filterPatterns are applied
Terminal window
# 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:

  1. Single shared pool — one sql.ConnectionPool is created at server startup and reused for the application’s lifetime
  2. Fresh request per query — each ExecuteSQL call creates a new sql.Request from the pool, enabling safe parallel execution
  3. No pool close in handlers — the pool remains open; the caller is responsible for closing it on shutdown
  4. Configurable pool sizing — pool max/min are tunable through mj.config.cjs

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

SymptomLikely CauseSolution
Connection timeoutNetwork or firewall issueIncrease connectionTimeout; verify SQL Server firewall rules
Authentication failureWrong credentials or permissionsVerify credentials; check encryption settings match server
Schema not foundWrong MJCoreSchemaNameVerify schema exists (default is __mj); check user schema access
Transaction rollbackConstraint violation in entity saveCheck required fields, foreign key references, unique constraints
Pool exhaustedToo many concurrent connectionsIncrease pool.max; check for leaked connections or long-running queries
EREQINPROG errorRequest reuse during transactionThis is handled automatically; the provider clears stale transaction references

IS-A Type Relationship Transaction Support

Section titled “IS-A Type Relationship Transaction Support”

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:

  1. Creates a SQL Transaction: The initiating (leaf) entity calls BeginISATransaction() to create a new sql.Transaction on the connection pool
  2. Propagates the Transaction: The transaction is stored in BaseEntity.ProviderTransaction and shared across all entities in the parent chain
  3. Executes Operations in Order:
    • For saves: Parent entities are saved first, then the child entity uses the parent’s ID
    • For deletes: The child entity is deleted first, then parents are deleted in reverse order
  4. Commits or Rolls Back: CommitISATransaction() 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.ProviderTransaction
  • CommitISATransaction(): Commits the shared transaction across the entire IS-A chain
  • RollbackISATransaction(): Rolls back all changes if any operation in the chain fails
  • Atomicity: All saves/deletes in the hierarchy succeed or fail together
  • Consistency: No orphaned child records or missing parent data
  • Transparent: The transaction management is automatic - no manual transaction handling required
  • Shared State: All entities in the chain use the same sql.Transaction instance via BaseEntity.ProviderTransaction

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

  • RunView: Execute queries against the virtual entity’s underlying SQL view
  • Get: Load individual records by primary key (if the view supports it)
  • Filtering, Sorting, Pagination: All standard query operations work as expected
// 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:

  • BaseEntity.Save(): Returns an error if called on a virtual entity
  • BaseEntity.Delete(): Returns an error if called on a virtual entity
  • Why: Virtual entities represent read-only views and cannot be modified directly
  • Aggregated Data: Summary views that combine data from multiple tables
  • Denormalized Views: Flattened representations of complex relationships
  • Calculated Fields: Views that include computed columns or transformations
  • Security Views: Row-level filtering applied at the database view level
  • Reporting: Pre-joined data optimized for reporting queries

For comprehensive documentation on virtual entities, their configuration, and advanced usage patterns, see MJCore Virtual Entities Documentation.

ISC