The @memberjunction/server library provides a comprehensive API server for MemberJunction, featuring both GraphQL and REST APIs. It includes all the functions required to start up the server, manage authentication, handle database connections, and provide a robust interface for accessing and managing metadata within MemberJunction.
npm install @memberjunction/server
This package depends on several core MemberJunction packages:
@memberjunction/core: Core functionality and metadata management@memberjunction/sqlserver-dataprovider: SQL Server data provider@memberjunction/graphql-dataprovider: GraphQL data provider@memberjunction/ai: AI engine integrationThe server uses configuration from its environment
| Env variable | Description |
|---|---|
| DB_HOST | The hostname for the common data store database |
| DB_PORT | The port for the common data store database (default 1433) |
| DB_USERNAME | The username used to authenticate with the common data store |
| DB_PASSWORD | The password used to authenticate with the common data store |
| DB_DATABASE | The common data store database name |
| PORT | The port used by the server (default 4000) |
| ROOT_PATH | The GraphQL root path (default /) |
| WEB_CLIENT_ID | The client ID used for MSAL authentication |
| TENANT_ID | The tenant ID used for MSAL authentication |
| ENABLE_INTROSPECTION | A flag to allow GraphQL introspection (default false) |
| WEBSITE_RUN_FROM_PACKAGE | An Azure flag to indicate a read-only file system |
| AUTH0_DOMAIN | The Auth0 domain |
| AUTH0_CLIENT_ID | The Auth0 Client ID |
| AUTH0_CLIENT_SECRET | The Auth0 Client secret |
| MJ_CORE_SCHEMA | The core schema to use for the data provider |
| CONFIG_FILE | An absolute path to the config file json |
| DB_READ_ONLY_USERNAME | Username for read-only database connection (optional) |
| DB_READ_ONLY_PASSWORD | Password for read-only database connection (optional) |
In addition to the GraphQL API, MemberJunction provides a REST API for applications that prefer RESTful architecture. By default, the REST API is enabled but can be disabled.
For comprehensive documentation on the REST API, including configuration options, security controls, and available endpoints, see REST_API.md.
The REST API supports:
Import the serve function from the package and run it as part of the server's main function. The function accepts an array of absolute paths to the resolver code.
import { serve } from '@memberjunction/server';
import { resolve } from 'node:path';
const localPath = (p: string) => resolve(__dirname, p);
const resolverPaths = [
'resolvers/**/*Resolver.{js,ts}',
'generic/*Resolver.{js,ts}',
'generated/generated.ts',
]
serve(resolverPaths.map(localPath));
The serve function accepts an optional MJServerOptions object:
import { serve, MJServerOptions } from '@memberjunction/server';
const options: MJServerOptions = {
onBeforeServe: async () => {
// Custom initialization logic
console.log('Server is about to start...');
},
restApiOptions: {
enabled: true,
includeEntities: ['User*', 'Entity*'],
excludeEntities: ['Password', 'APIKey*'],
includeSchemas: ['public'],
excludeSchemas: ['internal']
}
};
serve(resolverPaths.map(localPath), createApp(), options);
MJServer provides automatic transaction management for all GraphQL mutations with full support for multi-user environments through per-request provider instances.
Each GraphQL request receives its own SQLServerDataProvider instance, ensuring complete isolation between concurrent requests:
// Each request automatically:
// 1. Creates a new SQLServerDataProvider instance
// 2. Reuses cached metadata for fast initialization
// 3. Has isolated transaction state within the provider
// 4. Gets garbage collected after request completion
mutation {
CreateUser(input: { FirstName: "John", LastName: "Doe" }) {
ID
}
CreateUserRole(input: { UserID: "...", RoleID: "..." }) {
ID
}
}
// Both operations execute within the same provider's transaction
// Success: Both committed together
// Error: Both rolled back together
MJServer automatically creates per-request SQLServerDataProvider instances for optimal isolation and performance:
// In each GraphQL request context:
const provider = new SQLServerDataProvider();
await provider.Config({
connectionPool: pool,
MJCoreSchemaName: '__mj',
ignoreExistingMetadata: false // Reuse cached metadata
});
// Provider is included in AppContext
context.providers = [{
provider: provider,
type: 'Read-Write'
}];
The AppContext supports multiple providers with different access levels:
export type AppContext = {
dataSource: sql.ConnectionPool; // Legacy, for backward compatibility
userPayload: UserPayload;
dataSources: DataSourceInfo[];
providers: Array<ProviderInfo>; // Per-request provider instances
};
export class ProviderInfo {
provider: DatabaseProviderBase;
type: 'Admin' | 'Read-Write' | 'Read-Only' | 'Other';
}
The behavior to handle new users can be customized by subclassing the NewUserBase class. The subclass can pre-process, post-process or entirely override the base class behavior as needed. Import the class before calling serve to ensure the class is registered.
index.ts
import { serve } from '@memberjunction/server';
import { resolve } from 'node:path';
import './auth/exampleNewUserSubClass'; // make sure this new class gets registered
// ...
auth/exampleNewUserSubClass.ts
import { LogError, Metadata, RunView } from "@memberjunction/core";
import { RegisterClass } from "@memberjunction/global";
import { NewUserBase, configInfo } from '@memberjunction/server';
import { UserCache } from "@memberjunction/sqlserver-dataprovider";
/**
* This example class subclasses the @NewUserBase class and overrides the createNewUser method to create a new person record and then call the base class to create the user record. In this example there is an entity
* called "Persons" that is mapped to the User table in the core MemberJunction schema. You can sub-class the NewUserBase to do whatever behavior you want and pre-process, post-process or entirely override the base
* class behavior.
*/
@RegisterClass(NewUserBase, undefined, 1) /*by putting 1 into the priority setting, MJGlobal ClassFactory will use this instead of the base class as that registration had no priority*/
export class ExampleNewUserSubClass extends NewUserBase {
public override async createNewUser(firstName: string, lastName: string, email: string) {
try {
const md = new Metadata();
const contextUser = UserCache.Instance.Users.find(u => u.Email.trim().toLowerCase() === configInfo?.userHandling?.contextUserForNewUserCreation?.trim().toLowerCase())
if(!contextUser) {
LogError(`Failed to load context user ${configInfo?.userHandling?.contextUserForNewUserCreation}, if you've not specified this on your config.json you must do so. This is the user that is contextually used for creating a new user record dynamically.`);
return undefined;
}
const pEntity = md.Entities.find(e => e.Name === 'Persons'); // look up the entity info for the Persons entity
if (!pEntity) {
LogError('Failed to find Persons entity');
return undefined;
}
let personId;
// this block of code only executes if we have an entity called Persons
const rv = new RunView();
const viewResults = await rv.RunView({
EntityName: 'Persons',
ExtraFilter: `Email = '${email}'`
}, contextUser)
if (viewResults && viewResults.Success && Array.isArray(viewResults.Results) && viewResults.Results.length > 0) {
// we have a match so use it
const row = (viewResults.Results as { ID: number }[])[0]; // we know the rows will have an ID number
personId = row['ID'];
}
if (!personId) {
// we don't have a match so create a new person record
const p = await md.GetEntityObject('Persons', contextUser);
p.NewRecord(); // assumes we have an entity called Persons that has FirstName/LastName/Email fields
p.FirstName = firstName;
p.LastName = lastName;
p.Email = email;
p.Status = 'active';
if (await p.Save()) {
personId = p.ID;
}
else {
LogError(`Failed to create new person ${firstName} ${lastName} ${email}`)
}
}
// now call the base class to create the user, and pass in our LinkedRecordType and ID
return super.createNewUser(firstName, lastName, email, 'Other', pEntity?.ID, personId);
}
catch (e) {
LogError(`Error creating new user ${email} ${e}`);
return undefined;
}
}
}
The package exports numerous utilities and types:
serve(resolverPaths: string[], app?: Express, options?: MJServerOptions): Main server initializationcreateApp(): Creates an Express application instanceNewUserBase: Base class for custom new user handlingTokenExpiredError: Token expiration error classgetSystemUser(dataSource?: DataSource): Get system user for operationsResolverBase: Base class for custom resolversRunViewResolver: Base resolver for view operationsPushStatusResolver: Status update resolver baseAppContext: GraphQL context typeDataSourceInfo: Database connection informationKeyValuePairInput: Generic key-value input typeDeleteOptionsInput: Delete operation optionsGetReadOnlyDataSource(dataSources: DataSourceInfo[]): Get read-only data sourceGetReadWriteDataSource(dataSources: DataSourceInfo[]): Get read-write data sourceThe server includes specialized entity subclasses:
UserViewEntityServer: Server-side user view handlingEntityPermissionsEntityServer: Entity permission managementDuplicateRunEntityServer: Duplicate detection operationsReportEntityServer: Report generation and managementThe server includes built-in AI capabilities:
The server can automatically run AI learning cycles:
import { LearningCycleScheduler } from '@memberjunction/server/scheduler';
// In your server initialization
const scheduler = LearningCycleScheduler.Instance;
scheduler.setDataSources(dataSources);
scheduler.start(60); // Run every 60 minutes
The server includes comprehensive AI resolvers for various AI operations:
Handles AI prompt execution with multiple methods:
RunAIPrompt: Execute stored AI prompts with full parameter support
ExecuteSimplePrompt: Execute ad-hoc prompts without stored configuration
EmbedText: Generate text embeddings using local models
RunAIAgent: Execute AI agents for conversational interactionsAll AI operations have system user variants (queries) that use the @RequireSystemUser decorator:
RunAIPromptSystemUserExecuteSimplePromptSystemUserEmbedTextSystemUserRunAIAgentSystemUserThese allow server-to-server operations with elevated privileges.
AskSkipResolver: Handle Skip AI queriesSqlLoggingConfigResolver: Manage SQL logging configuration and sessionsThe server includes a comprehensive SQL logging management system that allows Owner-level users to control SQL statement capture in real-time.
Type = 'Owner' privilegesquery {
sqlLoggingConfig {
enabled
activeSessionCount
maxActiveSessions
allowedLogDirectory
sessionTimeout
defaultOptions {
prettyPrint
statementTypes
formatAsMigration
logRecordChangeMetadata
}
}
}
query {
activeSqlLoggingSessions {
id
sessionName
filePath
startTime
statementCount
filterByUserId
options {
prettyPrint
statementTypes
formatAsMigration
}
}
}
mutation {
startSqlLogging(input: {
fileName: "debug-session.sql"
filterToCurrentUser: true
options: {
sessionName: "Debug Session"
prettyPrint: true
statementTypes: "both"
formatAsMigration: false
}
}) {
id
filePath
sessionName
startTime
}
}
# Stop specific session
mutation {
stopSqlLogging(sessionId: "session-id-here")
}
# Stop all sessions
mutation {
stopAllSqlLogging
}
All SQL logging operations require:
Type = 'Owner' in the Users tableSQL logging is configured in mj.config.cjs:
sqlLogging: {
enabled: true, // Master switch
allowedLogDirectory: './logs/sql',
maxActiveSessions: 5,
sessionTimeout: 3600000, // 1 hour
autoCleanupEmptyFiles: true,
defaultOptions: {
formatAsMigration: false,
statementTypes: 'both',
prettyPrint: true,
logRecordChangeMetadata: false,
retainEmptyLogFiles: false
}
}
The server supports multiple authentication providers:
Azure AD (MSAL):
TENANT_ID and WEB_CLIENT_ID environment variablesAuth0:
AUTH0_DOMAIN, AUTH0_CLIENT_ID, and AUTH0_CLIENT_SECRETThe server also supports API key authentication via the x-mj-api-key header.
For REST API access control, see the comprehensive documentation in REST_API.md.
The server includes built-in compression middleware:
Database connections are managed with TypeORM's connection pooling. Configure pool size in your TypeORM configuration.
The server uses LRU caching for frequently accessed data. Configure cache settings in your mj.config.cjs:
databaseSettings: {
metadataCacheRefreshInterval: 300000 // 5 minutes
}
The server includes custom GraphQL directives:
@RequireSystemUser: Requires system user permissions@Public: Makes endpoints publicly accessibleThe server integrates with the SQLServerDataProvider's logging capabilities:
For real-time subscriptions:
const webSocketServer = new WebSocketServer({
server: httpServer,
path: graphqlRootPath
});
Enable detailed logging by setting environment variables:
DEBUG=mj:*
NODE_ENV=development
NewUserBase for organization-specific user creationWhen contributing to this package:
ISC