@memberjunction/server 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +689 -513
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +4 -2
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/generated/generated.d.ts +20 -6
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +107 -51
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +2 -2
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/InfoResolver.d.ts +2 -2
- package/dist/resolvers/InfoResolver.d.ts.map +1 -1
- package/dist/resolvers/InfoResolver.js +13 -13
- package/dist/resolvers/InfoResolver.js.map +1 -1
- package/package.json +52 -48
- package/src/auth/newUsers.ts +4 -2
- package/src/generated/generated.ts +82 -40
- package/src/index.ts +12 -0
- package/src/resolvers/FileCategoryResolver.ts +1 -1
- package/src/resolvers/InfoResolver.ts +5 -5
- package/dist/apolloServer/TransactionPlugin.d.ts +0 -4
- package/dist/apolloServer/TransactionPlugin.d.ts.map +0 -1
- package/dist/apolloServer/TransactionPlugin.js +0 -46
- package/dist/apolloServer/TransactionPlugin.js.map +0 -1
- package/dist/auth/__tests__/backward-compatibility.test.d.ts +0 -2
- package/dist/auth/__tests__/backward-compatibility.test.d.ts.map +0 -1
- package/dist/auth/__tests__/backward-compatibility.test.js +0 -135
- package/dist/auth/__tests__/backward-compatibility.test.js.map +0 -1
- package/dist/resolvers/AskSkipResolver.d.ts +0 -123
- package/dist/resolvers/AskSkipResolver.d.ts.map +0 -1
- package/dist/resolvers/AskSkipResolver.js +0 -1788
- package/dist/resolvers/AskSkipResolver.js.map +0 -1
- package/dist/scheduler/LearningCycleScheduler.d.ts +0 -4
- package/dist/scheduler/LearningCycleScheduler.d.ts.map +0 -1
- package/dist/scheduler/LearningCycleScheduler.js +0 -4
- package/dist/scheduler/LearningCycleScheduler.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,84 +1,180 @@
|
|
|
1
1
|
# @memberjunction/server
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
## Key Features
|
|
6
|
-
|
|
7
|
-
- **Dual API Support**: Both GraphQL and REST APIs with consistent authentication
|
|
8
|
-
- **Multi-Database Support**: Read-write and optional read-only database connections
|
|
9
|
-
- **Advanced Authentication**: Support for MSAL (Azure AD) and Auth0 authentication providers
|
|
10
|
-
- **Transaction Management**: Automatic transaction wrapper for GraphQL mutations
|
|
11
|
-
- **Type-Safe Resolvers**: Full TypeScript support with type-graphql integration
|
|
12
|
-
- **Entity Management**: Comprehensive CRUD operations with change tracking
|
|
13
|
-
- **Real-time Support**: WebSocket subscriptions for GraphQL
|
|
14
|
-
- **Compression**: Built-in response compression for better performance
|
|
15
|
-
- **Extensible Architecture**: Support for custom resolvers and entity subclasses
|
|
16
|
-
- **AI Integration**: Built-in support for AI operations, prompts, agents, and embeddings
|
|
17
|
-
- **Security Features**: Entity-level and schema-level access control for REST API
|
|
18
|
-
- **SQL Logging**: Runtime SQL logging configuration and session management for debugging
|
|
3
|
+
The MemberJunction server package provides the complete API server infrastructure for MemberJunction applications. It delivers both GraphQL and REST APIs with a unified authentication layer, per-request transaction isolation, pluggable authentication providers, scope-based API key authorization, scheduled job orchestration, SQL logging, and built-in AI operation endpoints. This package is the primary integration point between client applications and the MemberJunction data layer.
|
|
19
4
|
|
|
20
5
|
## Installation
|
|
21
6
|
|
|
22
|
-
```
|
|
7
|
+
```bash
|
|
23
8
|
npm install @memberjunction/server
|
|
24
9
|
```
|
|
25
10
|
|
|
26
|
-
##
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
MJServer acts as the central API gateway for MemberJunction, sitting between client applications (MJExplorer, external integrations, AI agents) and the SQL Server database via the `@memberjunction/sqlserver-dataprovider`. It initializes database connections, loads entity metadata, builds a GraphQL schema from dynamically discovered resolver modules, optionally exposes a REST API, and manages the full request lifecycle including authentication, per-request provider isolation, and transaction management.
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart TD
|
|
17
|
+
subgraph Clients["Client Applications"]
|
|
18
|
+
EX[MJ Explorer]
|
|
19
|
+
EXT[External Apps]
|
|
20
|
+
AI[AI Agents / MCP]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subgraph MJServer["@memberjunction/server"]
|
|
24
|
+
direction TB
|
|
25
|
+
AUTH[Authentication Layer]
|
|
26
|
+
GQL[GraphQL API - Apollo Server]
|
|
27
|
+
REST[REST API - Express Routes]
|
|
28
|
+
CTX[Request Context & Provider Factory]
|
|
29
|
+
RES[Resolver Layer]
|
|
30
|
+
SCHED[Scheduled Jobs Service]
|
|
31
|
+
SQLLOG[SQL Logging Manager]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
subgraph Data["Data Layer"]
|
|
35
|
+
SQLP[SQL Server Data Provider]
|
|
36
|
+
DB[(SQL Server Database)]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Clients --> AUTH
|
|
40
|
+
AUTH --> GQL
|
|
41
|
+
AUTH --> REST
|
|
42
|
+
GQL --> CTX
|
|
43
|
+
REST --> CTX
|
|
44
|
+
CTX --> RES
|
|
45
|
+
RES --> SQLP
|
|
46
|
+
SQLP --> DB
|
|
47
|
+
SCHED --> SQLP
|
|
48
|
+
|
|
49
|
+
style Clients fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
50
|
+
style MJServer fill:#7c5295,stroke:#563a6b,color:#fff
|
|
51
|
+
style Data fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
52
|
+
```
|
|
27
53
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
54
|
+
### Key Features
|
|
55
|
+
|
|
56
|
+
- **Dual API Support**: GraphQL (Apollo Server) and REST APIs with consistent authentication and authorization
|
|
57
|
+
- **Pluggable Authentication**: Support for Azure AD/Entra ID (MSAL), Auth0, Okta, AWS Cognito, and Google via the `IAuthProvider` interface
|
|
58
|
+
- **API Key Authorization**: User-level (`X-API-Key`) and system-level (`x-mj-api-key`) API keys with scope-based access control
|
|
59
|
+
- **Per-Request Provider Isolation**: Each GraphQL request receives its own `SQLServerDataProvider` instance for transaction safety
|
|
60
|
+
- **Multi-Database Support**: Separate read-write and read-only database connection pools
|
|
61
|
+
- **Transaction Management**: Automatic transaction wrapping for GraphQL mutations with savepoint support
|
|
62
|
+
- **Scheduled Jobs**: Built-in job scheduler with configurable polling, concurrency limits, and lock management
|
|
63
|
+
- **SQL Logging**: Real-time SQL statement capture with session management, user filtering, and migration-format output
|
|
64
|
+
- **AI Integration**: Resolvers for AI prompt execution, agent orchestration, text embeddings, and Skip AI
|
|
65
|
+
- **Real-time Support**: WebSocket subscriptions via `graphql-ws`
|
|
66
|
+
- **Response Compression**: Built-in gzip compression with configurable thresholds
|
|
67
|
+
- **Encryption Handling**: Transparent field-level encryption/decryption with configurable API exposure policies
|
|
68
|
+
- **CloudEvents**: Optional CloudEvent emission for entity lifecycle events
|
|
69
|
+
- **Telemetry**: Configurable server-side telemetry with multiple verbosity levels
|
|
70
|
+
- **Extensible Architecture**: Custom resolvers, entity subclasses, and new user handling via `@RegisterClass`
|
|
34
71
|
|
|
35
72
|
## Configuration
|
|
36
73
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
|
46
|
-
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
MJServer uses a layered configuration system with the following priority (highest to lowest):
|
|
75
|
+
|
|
76
|
+
1. Environment variables
|
|
77
|
+
2. `mj.config.cjs` file (discovered via [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig))
|
|
78
|
+
3. `DEFAULT_SERVER_CONFIG` hardcoded defaults
|
|
79
|
+
|
|
80
|
+
### Environment Variables
|
|
81
|
+
|
|
82
|
+
| Variable | Description | Default |
|
|
83
|
+
|----------|-------------|---------|
|
|
84
|
+
| `DB_HOST` | Database server hostname | `localhost` |
|
|
85
|
+
| `DB_PORT` | Database server port | `1433` |
|
|
86
|
+
| `DB_DATABASE` | Database name | (required) |
|
|
87
|
+
| `DB_USERNAME` | Database username | (required) |
|
|
88
|
+
| `DB_PASSWORD` | Database password | (required) |
|
|
89
|
+
| `DB_READ_ONLY_USERNAME` | Read-only connection username | (optional) |
|
|
90
|
+
| `DB_READ_ONLY_PASSWORD` | Read-only connection password | (optional) |
|
|
91
|
+
| `DB_TRUST_SERVER_CERTIFICATE` | Trust self-signed certs | `false` |
|
|
92
|
+
| `DB_INSTANCE_NAME` | Named SQL Server instance | (optional) |
|
|
93
|
+
| `MJ_CORE_SCHEMA` | MJ metadata schema name | `__mj` |
|
|
94
|
+
| `GRAPHQL_PORT` | Server listen port | `4000` |
|
|
95
|
+
| `GRAPHQL_ROOT_PATH` | GraphQL endpoint path | `/` |
|
|
96
|
+
| `GRAPHQL_BASE_URL` | Server base URL | `http://localhost` |
|
|
97
|
+
| `MJAPI_PUBLIC_URL` | Public callback URL (e.g. ngrok) | (optional) |
|
|
98
|
+
| `ENABLE_INTROSPECTION` | Allow GraphQL introspection | `false` |
|
|
99
|
+
| `MJ_API_KEY` | System-level API key | (optional) |
|
|
100
|
+
| `TENANT_ID` | Azure AD tenant ID | (optional) |
|
|
101
|
+
| `WEB_CLIENT_ID` | Azure AD client ID | (optional) |
|
|
102
|
+
| `AUTH0_DOMAIN` | Auth0 domain | (optional) |
|
|
103
|
+
| `AUTH0_CLIENT_ID` | Auth0 client ID | (optional) |
|
|
104
|
+
| `AUTH0_CLIENT_SECRET` | Auth0 client secret | (optional) |
|
|
105
|
+
| `WEBSITE_RUN_FROM_PACKAGE` | Azure read-only filesystem flag | (optional) |
|
|
106
|
+
| `MJ_REST_API_ENABLED` | Enable/disable REST API | (from config) |
|
|
107
|
+
| `MJ_REST_API_INCLUDE_ENTITIES` | Comma-separated entity include list | (optional) |
|
|
108
|
+
| `MJ_REST_API_EXCLUDE_ENTITIES` | Comma-separated entity exclude list | (optional) |
|
|
109
|
+
| `MJ_TELEMETRY_ENABLED` | Enable server telemetry | `true` |
|
|
110
|
+
| `METADATA_CACHE_REFRESH_INTERVAL` | Metadata refresh interval (ms) | `180000` |
|
|
111
|
+
|
|
112
|
+
### Configuration File (`mj.config.cjs`)
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
module.exports = {
|
|
116
|
+
dbHost: 'myserver.database.windows.net',
|
|
117
|
+
dbDatabase: 'MemberJunction',
|
|
118
|
+
dbUsername: 'mj_user',
|
|
119
|
+
dbPassword: 'secret',
|
|
120
|
+
mjCoreSchema: '__mj',
|
|
121
|
+
graphqlPort: 4000,
|
|
122
|
+
|
|
123
|
+
userHandling: {
|
|
124
|
+
autoCreateNewUsers: true,
|
|
125
|
+
newUserLimitedToAuthorizedDomains: false,
|
|
126
|
+
newUserRoles: ['UI', 'Developer'],
|
|
127
|
+
contextUserForNewUserCreation: 'admin@example.com',
|
|
128
|
+
CreateUserApplicationRecords: true,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
databaseSettings: {
|
|
132
|
+
connectionTimeout: 45000,
|
|
133
|
+
requestTimeout: 30000,
|
|
134
|
+
metadataCacheRefreshInterval: 180000,
|
|
135
|
+
connectionPool: {
|
|
136
|
+
max: 50,
|
|
137
|
+
min: 5,
|
|
138
|
+
idleTimeoutMillis: 30000,
|
|
139
|
+
acquireTimeoutMillis: 30000,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
restApiOptions: {
|
|
144
|
+
enabled: true,
|
|
145
|
+
includeEntities: ['User*', 'Entity*'],
|
|
146
|
+
excludeEntities: ['Password', 'APIKey*'],
|
|
147
|
+
includeSchemas: ['public'],
|
|
148
|
+
excludeSchemas: ['internal'],
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
scheduledJobs: {
|
|
152
|
+
enabled: true,
|
|
153
|
+
systemUserEmail: 'system@example.com',
|
|
154
|
+
maxConcurrentJobs: 5,
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
sqlLogging: {
|
|
158
|
+
enabled: true,
|
|
159
|
+
allowedLogDirectory: './logs/sql',
|
|
160
|
+
maxActiveSessions: 5,
|
|
161
|
+
sessionTimeout: 3600000,
|
|
162
|
+
},
|
|
73
163
|
|
|
164
|
+
telemetry: {
|
|
165
|
+
enabled: true,
|
|
166
|
+
level: 'standard', // 'minimal' | 'standard' | 'verbose' | 'debug'
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
```
|
|
74
170
|
|
|
75
171
|
## Usage
|
|
76
172
|
|
|
77
173
|
### Basic Server Setup
|
|
78
174
|
|
|
79
|
-
Import the `serve` function
|
|
175
|
+
Import the `serve` function and provide paths to your custom resolver modules. The server automatically includes its own built-in resolvers.
|
|
80
176
|
|
|
81
|
-
```
|
|
177
|
+
```typescript
|
|
82
178
|
import { serve } from '@memberjunction/server';
|
|
83
179
|
import { resolve } from 'node:path';
|
|
84
180
|
|
|
@@ -88,357 +184,338 @@ const resolverPaths = [
|
|
|
88
184
|
'resolvers/**/*Resolver.{js,ts}',
|
|
89
185
|
'generic/*Resolver.{js,ts}',
|
|
90
186
|
'generated/generated.ts',
|
|
91
|
-
]
|
|
187
|
+
];
|
|
92
188
|
|
|
93
189
|
serve(resolverPaths.map(localPath));
|
|
94
190
|
```
|
|
95
191
|
|
|
96
192
|
### Advanced Server Options
|
|
97
193
|
|
|
98
|
-
The `serve` function accepts an optional `MJServerOptions` object
|
|
194
|
+
The `serve` function accepts an optional `MJServerOptions` object for lifecycle hooks and REST API overrides.
|
|
99
195
|
|
|
100
196
|
```typescript
|
|
101
|
-
import { serve, MJServerOptions } from '@memberjunction/server';
|
|
197
|
+
import { serve, createApp, MJServerOptions } from '@memberjunction/server';
|
|
102
198
|
|
|
103
199
|
const options: MJServerOptions = {
|
|
104
200
|
onBeforeServe: async () => {
|
|
105
|
-
// Custom initialization
|
|
201
|
+
// Custom initialization after schema is built, before HTTP listen
|
|
106
202
|
console.log('Server is about to start...');
|
|
107
203
|
},
|
|
108
204
|
restApiOptions: {
|
|
109
205
|
enabled: true,
|
|
110
206
|
includeEntities: ['User*', 'Entity*'],
|
|
111
207
|
excludeEntities: ['Password', 'APIKey*'],
|
|
112
|
-
|
|
113
|
-
excludeSchemas: ['internal']
|
|
114
|
-
}
|
|
208
|
+
},
|
|
115
209
|
};
|
|
116
210
|
|
|
117
211
|
serve(resolverPaths.map(localPath), createApp(), options);
|
|
118
212
|
```
|
|
119
213
|
|
|
120
|
-
###
|
|
214
|
+
### Custom New User Handling
|
|
121
215
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
#### Per-Request Provider Architecture
|
|
125
|
-
|
|
126
|
-
Each GraphQL request receives its own SQLServerDataProvider instance, ensuring complete isolation between concurrent requests:
|
|
216
|
+
Override the default new user creation behavior by subclassing `NewUserBase` and registering it with a higher priority.
|
|
127
217
|
|
|
128
218
|
```typescript
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
219
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
220
|
+
import { NewUserBase } from '@memberjunction/server';
|
|
221
|
+
|
|
222
|
+
@RegisterClass(NewUserBase, undefined, 1)
|
|
223
|
+
export class CustomNewUserHandler extends NewUserBase {
|
|
224
|
+
public override async createNewUser(
|
|
225
|
+
firstName: string,
|
|
226
|
+
lastName: string,
|
|
227
|
+
email: string
|
|
228
|
+
) {
|
|
229
|
+
// Custom logic: create linked records, assign roles, etc.
|
|
230
|
+
return super.createNewUser(firstName, lastName, email, 'Other');
|
|
141
231
|
}
|
|
142
232
|
}
|
|
143
|
-
// Both operations execute within the same provider's transaction
|
|
144
|
-
// Success: Both committed together
|
|
145
|
-
// Error: Both rolled back together
|
|
146
233
|
```
|
|
147
234
|
|
|
148
|
-
|
|
235
|
+
Import the file before calling `serve` to ensure registration:
|
|
149
236
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
237
|
+
```typescript
|
|
238
|
+
import './auth/customNewUserHandler';
|
|
239
|
+
import { serve } from '@memberjunction/server';
|
|
240
|
+
// ...
|
|
241
|
+
serve(resolverPaths);
|
|
242
|
+
```
|
|
156
243
|
|
|
157
|
-
|
|
244
|
+
## Architecture
|
|
245
|
+
|
|
246
|
+
### Request Lifecycle
|
|
247
|
+
|
|
248
|
+
```mermaid
|
|
249
|
+
sequenceDiagram
|
|
250
|
+
participant C as Client
|
|
251
|
+
participant A as Auth Layer
|
|
252
|
+
participant CF as Context Factory
|
|
253
|
+
participant R as Resolver
|
|
254
|
+
participant P as SQLServerDataProvider
|
|
255
|
+
participant DB as SQL Server
|
|
256
|
+
|
|
257
|
+
C->>A: Request (Bearer token / API key)
|
|
258
|
+
A->>A: Validate JWT or API key
|
|
259
|
+
A->>A: Resolve UserInfo from UserCache
|
|
260
|
+
A->>CF: Create request context
|
|
261
|
+
CF->>P: New per-request provider instance
|
|
262
|
+
P->>P: Reuse cached metadata
|
|
263
|
+
CF->>R: Pass AppContext with providers
|
|
264
|
+
R->>P: Execute operations
|
|
265
|
+
P->>DB: SQL queries within transaction
|
|
266
|
+
DB-->>P: Results
|
|
267
|
+
P-->>R: Entity objects / data
|
|
268
|
+
R-->>C: GraphQL / REST response
|
|
269
|
+
Note over P,DB: Transaction auto-committed on success, rolled back on error
|
|
270
|
+
```
|
|
158
271
|
|
|
159
|
-
|
|
272
|
+
### Per-Request Provider Isolation
|
|
273
|
+
|
|
274
|
+
Each GraphQL request receives its own `SQLServerDataProvider` instance, ensuring complete transaction isolation between concurrent requests. Metadata is cached and reused across providers for efficiency.
|
|
160
275
|
|
|
161
276
|
```typescript
|
|
162
|
-
//
|
|
277
|
+
// Automatically created per request in context.ts:
|
|
163
278
|
const provider = new SQLServerDataProvider();
|
|
164
279
|
await provider.Config({
|
|
165
280
|
connectionPool: pool,
|
|
166
281
|
MJCoreSchemaName: '__mj',
|
|
167
|
-
ignoreExistingMetadata: false // Reuse cached metadata
|
|
282
|
+
ignoreExistingMetadata: false, // Reuse cached metadata
|
|
168
283
|
});
|
|
169
284
|
|
|
170
|
-
//
|
|
171
|
-
context.providers = [
|
|
172
|
-
provider:
|
|
173
|
-
type: 'Read-
|
|
174
|
-
|
|
285
|
+
// Included in AppContext for resolvers:
|
|
286
|
+
context.providers = [
|
|
287
|
+
{ provider: readWriteProvider, type: 'Read-Write' },
|
|
288
|
+
{ provider: readOnlyProvider, type: 'Read-Only' }, // if configured
|
|
289
|
+
];
|
|
175
290
|
```
|
|
176
291
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
292
|
+
### Authentication Architecture
|
|
293
|
+
|
|
294
|
+
MJServer uses a pluggable authentication provider system built on the `IAuthProvider` interface and `AuthProviderFactory`.
|
|
295
|
+
|
|
296
|
+
```mermaid
|
|
297
|
+
flowchart LR
|
|
298
|
+
subgraph Providers["Auth Providers"]
|
|
299
|
+
MSAL[MSAL / Azure AD]
|
|
300
|
+
A0[Auth0]
|
|
301
|
+
OK[Okta]
|
|
302
|
+
COG[AWS Cognito]
|
|
303
|
+
GOOG[Google]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
subgraph Factory["AuthProviderFactory"]
|
|
307
|
+
REG[Provider Registry]
|
|
308
|
+
CACHE[Issuer Cache]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
subgraph Auth["Authentication Flow"]
|
|
312
|
+
JWT[JWT Token]
|
|
313
|
+
APIKEY[API Key]
|
|
314
|
+
SYS[System API Key]
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
JWT --> REG
|
|
318
|
+
REG --> Providers
|
|
319
|
+
APIKEY --> VAL[API Key Engine]
|
|
320
|
+
SYS --> SYSUSER[System User Lookup]
|
|
321
|
+
|
|
322
|
+
style Providers fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
323
|
+
style Factory fill:#7c5295,stroke:#563a6b,color:#fff
|
|
324
|
+
style Auth fill:#b8762f,stroke:#8a5722,color:#fff
|
|
193
325
|
```
|
|
194
326
|
|
|
195
|
-
|
|
327
|
+
Providers are registered via `@RegisterClass(BaseAuthProvider, 'type-key')` and automatically discovered at startup. Each provider implements:
|
|
196
328
|
|
|
197
|
-
|
|
329
|
+
- `validateConfig()` -- Validates required configuration
|
|
330
|
+
- `getSigningKey()` -- Retrieves JWKS signing keys with retry logic
|
|
331
|
+
- `extractUserInfo()` -- Extracts email, name from provider-specific JWT claims
|
|
332
|
+
- `matchesIssuer()` -- Matches JWT `iss` claim to the provider
|
|
198
333
|
|
|
199
|
-
`
|
|
334
|
+
Configure providers in `mj.config.cjs` under `authProviders` or via environment variables (see Configuration section).
|
|
200
335
|
|
|
201
|
-
|
|
202
|
-
import { serve } from '@memberjunction/server';
|
|
203
|
-
import { resolve } from 'node:path';
|
|
204
|
-
|
|
205
|
-
import './auth/exampleNewUserSubClass'; // make sure this new class gets registered
|
|
206
|
-
|
|
207
|
-
// ...
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`auth/exampleNewUserSubClass.ts`
|
|
212
|
-
|
|
213
|
-
```ts
|
|
214
|
-
import { LogError, Metadata, RunView } from "@memberjunction/core";
|
|
215
|
-
import { RegisterClass } from "@memberjunction/global";
|
|
216
|
-
import { NewUserBase, configInfo } from '@memberjunction/server';
|
|
217
|
-
import { UserCache } from "@memberjunction/sqlserver-dataprovider";
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* 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
|
|
221
|
-
* 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
|
|
222
|
-
* class behavior.
|
|
223
|
-
*/
|
|
224
|
-
@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*/
|
|
225
|
-
export class ExampleNewUserSubClass extends NewUserBase {
|
|
226
|
-
public override async createNewUser(firstName: string, lastName: string, email: string) {
|
|
227
|
-
try {
|
|
228
|
-
const md = new Metadata();
|
|
229
|
-
const contextUser = UserCache.Instance.Users.find(u => u.Email.trim().toLowerCase() === configInfo?.userHandling?.contextUserForNewUserCreation?.trim().toLowerCase())
|
|
230
|
-
if(!contextUser) {
|
|
231
|
-
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.`);
|
|
232
|
-
return undefined;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const pEntity = md.Entities.find(e => e.Name === 'Persons'); // look up the entity info for the Persons entity
|
|
236
|
-
if (!pEntity) {
|
|
237
|
-
LogError('Failed to find Persons entity');
|
|
238
|
-
return undefined;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
let personId;
|
|
242
|
-
// this block of code only executes if we have an entity called Persons
|
|
243
|
-
const rv = new RunView();
|
|
244
|
-
const viewResults = await rv.RunView({
|
|
245
|
-
EntityName: 'Persons',
|
|
246
|
-
ExtraFilter: `Email = '${email}'`
|
|
247
|
-
}, contextUser)
|
|
248
|
-
|
|
249
|
-
if (viewResults && viewResults.Success && Array.isArray(viewResults.Results) && viewResults.Results.length > 0) {
|
|
250
|
-
// we have a match so use it
|
|
251
|
-
const row = (viewResults.Results as { ID: number }[])[0]; // we know the rows will have an ID number
|
|
252
|
-
personId = row['ID'];
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (!personId) {
|
|
256
|
-
// we don't have a match so create a new person record
|
|
257
|
-
const p = await md.GetEntityObject('Persons', contextUser);
|
|
258
|
-
p.NewRecord(); // assumes we have an entity called Persons that has FirstName/LastName/Email fields
|
|
259
|
-
p.FirstName = firstName;
|
|
260
|
-
p.LastName = lastName;
|
|
261
|
-
p.Email = email;
|
|
262
|
-
p.Status = 'active';
|
|
263
|
-
if (await p.Save()) {
|
|
264
|
-
personId = p.ID;
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
LogError(`Failed to create new person ${firstName} ${lastName} ${email}`)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// now call the base class to create the user, and pass in our LinkedRecordType and ID
|
|
272
|
-
return super.createNewUser(firstName, lastName, email, 'Other', pEntity?.ID, personId);
|
|
273
|
-
}
|
|
274
|
-
catch (e) {
|
|
275
|
-
LogError(`Error creating new user ${email} ${e}`);
|
|
276
|
-
return undefined;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
## API Documentation
|
|
283
|
-
|
|
284
|
-
### Core Exports
|
|
285
|
-
|
|
286
|
-
The package exports numerous utilities and types:
|
|
287
|
-
|
|
288
|
-
#### Server Functions
|
|
289
|
-
- `serve(resolverPaths: string[], app?: Express, options?: MJServerOptions)`: Main server initialization
|
|
290
|
-
- `createApp()`: Creates an Express application instance
|
|
291
|
-
|
|
292
|
-
#### Authentication
|
|
293
|
-
- `NewUserBase`: Base class for custom new user handling
|
|
294
|
-
- `TokenExpiredError`: Token expiration error class
|
|
295
|
-
- `getSystemUser(dataSource?: DataSource)`: Get system user for operations
|
|
296
|
-
|
|
297
|
-
#### Resolvers and Base Classes
|
|
298
|
-
- `ResolverBase`: Base class for custom resolvers
|
|
299
|
-
- `RunViewResolver`: Base resolver for view operations
|
|
300
|
-
- `PushStatusResolver`: Status update resolver base
|
|
301
|
-
|
|
302
|
-
#### Type Definitions
|
|
303
|
-
- `AppContext`: GraphQL context type
|
|
304
|
-
- `DataSourceInfo`: Database connection information
|
|
305
|
-
- `KeyValuePairInput`: Generic key-value input type
|
|
306
|
-
- `DeleteOptionsInput`: Delete operation options
|
|
336
|
+
### API Key Authentication
|
|
307
337
|
|
|
308
|
-
|
|
309
|
-
- `GetReadOnlyDataSource(dataSources: DataSourceInfo[])`: Get read-only data source
|
|
310
|
-
- `GetReadWriteDataSource(dataSources: DataSourceInfo[])`: Get read-write data source
|
|
338
|
+
The server supports two types of API key authentication:
|
|
311
339
|
|
|
312
|
-
|
|
340
|
+
**User API Keys** (`X-API-Key` header, `mj_sk_*` format):
|
|
341
|
+
- Authenticate as a specific user
|
|
342
|
+
- Support expiration dates and individual revocation
|
|
343
|
+
- Subject to scope-based authorization
|
|
344
|
+
- Usage is logged for audit
|
|
313
345
|
|
|
314
|
-
|
|
315
|
-
-
|
|
316
|
-
-
|
|
317
|
-
-
|
|
318
|
-
- `ReportEntityServer`: Report generation and management
|
|
346
|
+
**System API Key** (`x-mj-api-key` header):
|
|
347
|
+
- Single shared key set via `MJ_API_KEY` environment variable
|
|
348
|
+
- Authenticates as the system user with elevated privileges
|
|
349
|
+
- Used for server-to-server communication
|
|
319
350
|
|
|
320
|
-
|
|
351
|
+
### Scope-Based Authorization
|
|
321
352
|
|
|
322
|
-
|
|
353
|
+
API keys are subject to a two-level scope evaluation: the application ceiling (maximum allowed scopes for MJAPI, MCP Server, etc.) and the individual key's assigned scopes.
|
|
323
354
|
|
|
324
|
-
|
|
355
|
+
| Scope | Description |
|
|
356
|
+
|-------|-------------|
|
|
357
|
+
| `full_access` | Bypass all scope checks |
|
|
358
|
+
| `entity:read` | Read entity records |
|
|
359
|
+
| `entity:create` | Create records |
|
|
360
|
+
| `entity:update` | Update records |
|
|
361
|
+
| `entity:delete` | Delete records |
|
|
362
|
+
| `view:run` | Execute RunView queries |
|
|
363
|
+
| `agent:execute` | Execute AI agents |
|
|
364
|
+
| `agent:monitor` | Check agent run status |
|
|
365
|
+
| `action:execute` | Execute MJ Actions |
|
|
366
|
+
| `prompt:execute` | Execute AI prompts |
|
|
367
|
+
| `query:run` | Execute queries |
|
|
368
|
+
| `metadata:entities:read` | Read entity metadata |
|
|
369
|
+
| `metadata:agents:read` | Read agent metadata |
|
|
370
|
+
| `communication:send` | Send emails/messages |
|
|
325
371
|
|
|
326
|
-
|
|
372
|
+
Add scope checks to custom resolvers:
|
|
327
373
|
|
|
328
374
|
```typescript
|
|
329
|
-
import {
|
|
375
|
+
import { ResolverBase } from '@memberjunction/server';
|
|
330
376
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
377
|
+
@Resolver()
|
|
378
|
+
export class MyResolver extends ResolverBase {
|
|
379
|
+
@Mutation(() => MyResult)
|
|
380
|
+
async myOperation(@Ctx() ctx: AppContext): Promise<MyResult> {
|
|
381
|
+
// Checks scope for API key auth; no-op for JWT auth
|
|
382
|
+
await this.CheckAPIKeyScopeAuthorization('my:scope', 'resource-name', ctx.userPayload);
|
|
383
|
+
// Proceed with operation...
|
|
384
|
+
}
|
|
385
|
+
}
|
|
335
386
|
```
|
|
336
387
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
The server includes comprehensive AI resolvers for various AI operations:
|
|
340
|
-
|
|
341
|
-
#### RunAIPromptResolver
|
|
342
|
-
Handles AI prompt execution with multiple methods:
|
|
388
|
+
Alternatively, use the standalone utility functions:
|
|
343
389
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
- Handles conversation messages and system prompt overrides
|
|
347
|
-
- Returns parsed results, token usage, and execution metadata
|
|
348
|
-
|
|
349
|
-
- `ExecuteSimplePrompt`: Execute ad-hoc prompts without stored configuration
|
|
350
|
-
- Direct system prompt and message execution
|
|
351
|
-
- Smart model selection based on preferences or power level
|
|
352
|
-
- Automatic JSON extraction from responses
|
|
353
|
-
- Available for both regular users and system users
|
|
354
|
-
|
|
355
|
-
- `EmbedText`: Generate text embeddings using local models
|
|
356
|
-
- Batch processing support for multiple texts
|
|
357
|
-
- Model size selection (small/medium)
|
|
358
|
-
- Returns vector dimensions and model information
|
|
359
|
-
- Available for both regular users and system users
|
|
360
|
-
|
|
361
|
-
#### RunAIAgentResolver
|
|
362
|
-
- `RunAIAgent`: Execute AI agents for conversational interactions
|
|
363
|
-
- Session management for conversation context
|
|
364
|
-
- Streaming support for real-time responses
|
|
365
|
-
- Progress tracking and partial results
|
|
366
|
-
- Available for both regular users and system users
|
|
367
|
-
|
|
368
|
-
#### System User Variants
|
|
369
|
-
All AI operations have system user variants (queries) that use the `@RequireSystemUser` decorator:
|
|
370
|
-
- `RunAIPromptSystemUser`
|
|
371
|
-
- `ExecuteSimplePromptSystemUser`
|
|
372
|
-
- `EmbedTextSystemUser`
|
|
373
|
-
- `RunAIAgentSystemUser`
|
|
374
|
-
|
|
375
|
-
These allow server-to-server operations with elevated privileges.
|
|
376
|
-
|
|
377
|
-
#### AskSkipResolver
|
|
378
|
-
- `AskSkipResolver`: Handle Skip AI queries
|
|
379
|
-
|
|
380
|
-
### SQL Logging Resolver
|
|
390
|
+
```typescript
|
|
391
|
+
import { CheckAPIKeyScope, RequireScope } from '@memberjunction/server';
|
|
381
392
|
|
|
382
|
-
|
|
393
|
+
// Standalone function
|
|
394
|
+
await CheckAPIKeyScope(ctx.userPayload.apiKeyId, 'view:run', ctx.userPayload.userRecord, {
|
|
395
|
+
resource: 'Users',
|
|
396
|
+
});
|
|
383
397
|
|
|
384
|
-
|
|
398
|
+
// Pre-built scope checker
|
|
399
|
+
const requireViewRun = RequireScope('view:run');
|
|
400
|
+
await requireViewRun(ctx);
|
|
401
|
+
```
|
|
385
402
|
|
|
386
|
-
|
|
403
|
+
## GraphQL API
|
|
387
404
|
|
|
388
|
-
###
|
|
405
|
+
### Custom Directives
|
|
389
406
|
|
|
390
|
-
-
|
|
391
|
-
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
407
|
+
- **`@RequireSystemUser`**: Restricts a field or mutation to system-user-only access. Applied at the schema level; non-system users receive an `AuthorizationError`.
|
|
408
|
+
- **`@Public`**: Marks a field as publicly accessible without authentication. All other fields require an active, authenticated user by default.
|
|
409
|
+
|
|
410
|
+
### Built-in Resolvers
|
|
411
|
+
|
|
412
|
+
The server includes resolvers for the following domains:
|
|
413
|
+
|
|
414
|
+
| Resolver | Operations |
|
|
415
|
+
|----------|------------|
|
|
416
|
+
| `EntityResolver` | CRUD operations for all entities |
|
|
417
|
+
| `RunViewResolver` | RunView by ID, name, or dynamic entity |
|
|
418
|
+
| `RunAIPromptResolver` | Execute AI prompts, simple prompts, text embeddings |
|
|
419
|
+
| `RunAIAgentResolver` | Execute AI agents with session and streaming support |
|
|
420
|
+
| `ActionResolver` | Execute MJ Actions |
|
|
421
|
+
| `QueryResolver` | Execute and create queries |
|
|
422
|
+
| `ReportResolver` | Run and manage reports |
|
|
423
|
+
| `DatasetResolver` | Dataset operations |
|
|
424
|
+
| `UserViewResolver` | User view management |
|
|
425
|
+
| `UserResolver` | User profile operations |
|
|
426
|
+
| `UserFavoriteResolver` | Favorite record management |
|
|
427
|
+
| `MergeRecordsResolver` | Record merge operations |
|
|
428
|
+
| `SyncDataResolver` / `SyncRolesUsersResolver` | Data synchronization |
|
|
429
|
+
| `FileResolver` / `FileCategoryResolver` | File and category management |
|
|
430
|
+
| `EntityCommunicationsResolver` | Entity-level communications |
|
|
431
|
+
| `EntityRecordNameResolver` | Record name resolution |
|
|
432
|
+
| `TransactionGroupResolver` | Transaction group management |
|
|
433
|
+
| `ComponentRegistryResolver` | Component registry queries |
|
|
434
|
+
| `MCPResolver` | MCP server operations |
|
|
435
|
+
| `APIKeyResolver` | API key management |
|
|
436
|
+
| `SqlLoggingConfigResolver` | SQL logging session management |
|
|
437
|
+
| `TelemetryResolver` | Server telemetry queries |
|
|
438
|
+
| `ColorResolver` | Color palette operations |
|
|
439
|
+
| `InfoResolver` | Server info queries |
|
|
440
|
+
| `PotentialDuplicateRecordResolver` | Duplicate detection |
|
|
441
|
+
| `RunTestResolver` | Test execution |
|
|
442
|
+
| `RunTemplateResolver` | Template execution |
|
|
443
|
+
| `TaskResolver` | Task orchestration |
|
|
396
444
|
|
|
397
|
-
###
|
|
445
|
+
### Transaction Management
|
|
398
446
|
|
|
399
|
-
|
|
447
|
+
GraphQL mutations are automatically wrapped in transactions through the per-request provider:
|
|
400
448
|
|
|
401
449
|
```graphql
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
activeSessionCount
|
|
406
|
-
maxActiveSessions
|
|
407
|
-
allowedLogDirectory
|
|
408
|
-
sessionTimeout
|
|
409
|
-
defaultOptions {
|
|
410
|
-
prettyPrint
|
|
411
|
-
statementTypes
|
|
412
|
-
formatAsMigration
|
|
413
|
-
logRecordChangeMetadata
|
|
414
|
-
}
|
|
415
|
-
}
|
|
450
|
+
mutation {
|
|
451
|
+
CreateUser(input: { FirstName: "John", LastName: "Doe" }) { ID }
|
|
452
|
+
CreateUserRole(input: { UserID: "...", RoleID: "..." }) { ID }
|
|
416
453
|
}
|
|
454
|
+
# Both operations execute within the same provider's transaction scope.
|
|
455
|
+
# Success: both committed together. Error: both rolled back.
|
|
417
456
|
```
|
|
418
457
|
|
|
419
|
-
|
|
458
|
+
### WebSocket Subscriptions
|
|
420
459
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
filterByUserId
|
|
430
|
-
options {
|
|
431
|
-
prettyPrint
|
|
432
|
-
statementTypes
|
|
433
|
-
formatAsMigration
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
460
|
+
Real-time updates are supported via WebSocket at the same path as the GraphQL endpoint:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// Server-side: already configured
|
|
464
|
+
const webSocketServer = new WebSocketServer({
|
|
465
|
+
server: httpServer,
|
|
466
|
+
path: graphqlRootPath,
|
|
467
|
+
});
|
|
437
468
|
```
|
|
438
469
|
|
|
439
|
-
|
|
470
|
+
### ResolverBase
|
|
471
|
+
|
|
472
|
+
All built-in resolvers extend `ResolverBase`, which provides:
|
|
473
|
+
|
|
474
|
+
| Method | Description |
|
|
475
|
+
|--------|-------------|
|
|
476
|
+
| `CreateRecord()` | Create entity with before/after hooks |
|
|
477
|
+
| `UpdateRecord()` | Update with optimistic concurrency detection |
|
|
478
|
+
| `DeleteRecord()` | Delete with before/after hooks |
|
|
479
|
+
| `RunViewByIDGeneric()` | Execute a saved view by ID |
|
|
480
|
+
| `RunViewByNameGeneric()` | Execute a saved view by name |
|
|
481
|
+
| `RunDynamicViewGeneric()` | Execute an ad-hoc view on an entity |
|
|
482
|
+
| `RunViewsGeneric()` | Batch-execute multiple views |
|
|
483
|
+
| `CheckUserReadPermissions()` | Validate entity-level read access |
|
|
484
|
+
| `CheckAPIKeyScopeAuthorization()` | Validate API key scope |
|
|
485
|
+
| `MapFieldNamesToCodeNames()` | Map field names for GraphQL transport |
|
|
486
|
+
| `FilterEncryptedFieldsForAPI()` | Handle encryption policy for API responses |
|
|
487
|
+
| `EmitCloudEvent()` | Emit CloudEvents for entity lifecycle |
|
|
488
|
+
| `ListenForEntityMessages()` | Subscribe to entity event messages |
|
|
489
|
+
| `BeforeCreate()` / `AfterCreate()` | Lifecycle hooks for create |
|
|
490
|
+
| `BeforeUpdate()` / `AfterUpdate()` | Lifecycle hooks for update |
|
|
491
|
+
| `BeforeDelete()` / `AfterDelete()` | Lifecycle hooks for delete |
|
|
492
|
+
|
|
493
|
+
## REST API
|
|
494
|
+
|
|
495
|
+
In addition to GraphQL, MJServer provides a REST API at `/api/v1/`. By default it is disabled and can be enabled via configuration.
|
|
496
|
+
|
|
497
|
+
For comprehensive REST API documentation including endpoints, security configuration, wildcard filtering, and examples, see [REST_API.md](./REST_API.md).
|
|
498
|
+
|
|
499
|
+
Key endpoints:
|
|
500
|
+
|
|
501
|
+
| Endpoint | Method | Description |
|
|
502
|
+
|----------|--------|-------------|
|
|
503
|
+
| `/api/v1/entities/:entityName` | GET | List entity records |
|
|
504
|
+
| `/api/v1/entities/:entityName` | POST | Create a record |
|
|
505
|
+
| `/api/v1/entities/:entityName/:id` | GET | Get a record by ID |
|
|
506
|
+
| `/api/v1/entities/:entityName/:id` | PUT | Update a record |
|
|
507
|
+
| `/api/v1/entities/:entityName/:id` | DELETE | Delete a record |
|
|
508
|
+
| `/api/v1/views/:entityName` | POST | Run a view |
|
|
509
|
+
| `/api/v1/views/batch` | POST | Batch view execution |
|
|
510
|
+
| `/api/v1/metadata/entities` | GET | List entity metadata |
|
|
511
|
+
| `/api/v1/users/current` | GET | Get current user |
|
|
512
|
+
|
|
513
|
+
## SQL Logging
|
|
514
|
+
|
|
515
|
+
The server includes a runtime SQL logging system for debugging and migration generation. Requires `Owner`-level user privileges.
|
|
440
516
|
|
|
441
517
|
```graphql
|
|
518
|
+
# Start a logging session
|
|
442
519
|
mutation {
|
|
443
520
|
startSqlLogging(input: {
|
|
444
521
|
fileName: "debug-session.sql"
|
|
@@ -453,264 +530,363 @@ mutation {
|
|
|
453
530
|
id
|
|
454
531
|
filePath
|
|
455
532
|
sessionName
|
|
456
|
-
startTime
|
|
457
533
|
}
|
|
458
534
|
}
|
|
459
|
-
```
|
|
460
535
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
536
|
+
# Query active sessions
|
|
537
|
+
query {
|
|
538
|
+
activeSqlLoggingSessions {
|
|
539
|
+
id
|
|
540
|
+
sessionName
|
|
541
|
+
statementCount
|
|
542
|
+
}
|
|
467
543
|
}
|
|
468
544
|
|
|
469
|
-
# Stop
|
|
545
|
+
# Stop a session
|
|
470
546
|
mutation {
|
|
471
|
-
|
|
547
|
+
stopSqlLogging(sessionId: "session-id")
|
|
472
548
|
}
|
|
473
549
|
```
|
|
474
550
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
All SQL logging operations require:
|
|
478
|
-
1. **Authentication**: Valid user session or API key
|
|
479
|
-
2. **Authorization**: User must have `Type = 'Owner'` in the Users table
|
|
480
|
-
3. **Configuration**: SQL logging must be enabled in server config
|
|
481
|
-
|
|
482
|
-
### Configuration
|
|
483
|
-
|
|
484
|
-
SQL logging is configured in `mj.config.cjs`:
|
|
551
|
+
Configuration in `mj.config.cjs`:
|
|
485
552
|
|
|
486
553
|
```javascript
|
|
487
554
|
sqlLogging: {
|
|
488
|
-
enabled: true,
|
|
555
|
+
enabled: true,
|
|
489
556
|
allowedLogDirectory: './logs/sql',
|
|
490
557
|
maxActiveSessions: 5,
|
|
491
|
-
sessionTimeout: 3600000,
|
|
558
|
+
sessionTimeout: 3600000,
|
|
492
559
|
autoCleanupEmptyFiles: true,
|
|
493
560
|
defaultOptions: {
|
|
494
561
|
formatAsMigration: false,
|
|
495
|
-
statementTypes: 'both',
|
|
562
|
+
statementTypes: 'both', // 'queries' | 'mutations' | 'both'
|
|
496
563
|
prettyPrint: true,
|
|
497
564
|
logRecordChangeMetadata: false,
|
|
498
|
-
|
|
499
|
-
}
|
|
565
|
+
},
|
|
500
566
|
}
|
|
501
567
|
```
|
|
502
568
|
|
|
503
|
-
##
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
569
|
+
## Scheduled Jobs
|
|
570
|
+
|
|
571
|
+
MJServer integrates with the `@memberjunction/scheduling-engine` to execute scheduled jobs defined in MemberJunction metadata.
|
|
572
|
+
|
|
573
|
+
```mermaid
|
|
574
|
+
flowchart LR
|
|
575
|
+
subgraph Config["Configuration"]
|
|
576
|
+
CFG[mj.config.cjs]
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
subgraph Service["ScheduledJobsService"]
|
|
580
|
+
INIT[Initialize]
|
|
581
|
+
POLL[Start Polling]
|
|
582
|
+
STOP[Stop Polling]
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
subgraph Engine["SchedulingEngine"]
|
|
586
|
+
JOBS[Active Jobs]
|
|
587
|
+
EXEC[Job Execution]
|
|
588
|
+
LOCK[Lock Management]
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
CFG --> INIT
|
|
592
|
+
INIT --> POLL
|
|
593
|
+
POLL --> JOBS
|
|
594
|
+
JOBS --> EXEC
|
|
595
|
+
EXEC --> LOCK
|
|
596
|
+
|
|
597
|
+
style Config fill:#b8762f,stroke:#8a5722,color:#fff
|
|
598
|
+
style Service fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
599
|
+
style Engine fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
600
|
+
```
|
|
512
601
|
|
|
513
|
-
|
|
514
|
-
- Set `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET`
|
|
515
|
-
- Supports Auth0 authentication
|
|
602
|
+
Configuration:
|
|
516
603
|
|
|
517
|
-
|
|
604
|
+
```javascript
|
|
605
|
+
scheduledJobs: {
|
|
606
|
+
enabled: true,
|
|
607
|
+
systemUserEmail: 'system@example.com',
|
|
608
|
+
maxConcurrentJobs: 5,
|
|
609
|
+
defaultLockTimeout: 600000, // 10 minutes
|
|
610
|
+
staleLockCleanupInterval: 300000, // 5 minutes
|
|
611
|
+
}
|
|
612
|
+
```
|
|
518
613
|
|
|
519
|
-
The server
|
|
614
|
+
The service starts automatically during server initialization and shuts down gracefully on SIGTERM/SIGINT.
|
|
520
615
|
|
|
521
|
-
|
|
616
|
+
## AI Integration
|
|
522
617
|
|
|
523
|
-
|
|
618
|
+
### AI Prompt Execution
|
|
524
619
|
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
console.log('API Key:', result.rawKey); // Save this - cannot be recovered!
|
|
620
|
+
```graphql
|
|
621
|
+
mutation {
|
|
622
|
+
RunAIPrompt(input: {
|
|
623
|
+
PromptName: "Summarize Content"
|
|
624
|
+
ModelID: "model-guid"
|
|
625
|
+
Temperature: 0.7
|
|
626
|
+
Messages: [{ role: "user", content: "Summarize this article..." }]
|
|
627
|
+
}) {
|
|
628
|
+
Success
|
|
629
|
+
Result
|
|
630
|
+
TokenUsage { InputTokens OutputTokens }
|
|
631
|
+
}
|
|
538
632
|
}
|
|
539
633
|
```
|
|
540
634
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
635
|
+
### Simple Prompt Execution
|
|
636
|
+
|
|
637
|
+
```graphql
|
|
638
|
+
mutation {
|
|
639
|
+
ExecuteSimplePrompt(input: {
|
|
640
|
+
SystemPrompt: "You are a helpful assistant."
|
|
641
|
+
UserMessage: "What is MemberJunction?"
|
|
642
|
+
ModelPowerLevel: "Standard"
|
|
643
|
+
}) {
|
|
644
|
+
Success
|
|
645
|
+
Result
|
|
646
|
+
}
|
|
647
|
+
}
|
|
544
648
|
```
|
|
545
649
|
|
|
546
|
-
|
|
547
|
-
- Authenticates as the specific user who owns the key
|
|
548
|
-
- Supports expiration dates
|
|
549
|
-
- Can be revoked individually
|
|
550
|
-
- Usage is logged for audit purposes
|
|
551
|
-
- `apiKeyId` is included in the request context
|
|
650
|
+
### Text Embeddings
|
|
552
651
|
|
|
553
|
-
|
|
652
|
+
```graphql
|
|
653
|
+
mutation {
|
|
654
|
+
EmbedText(input: {
|
|
655
|
+
Texts: ["Hello world", "MemberJunction framework"]
|
|
656
|
+
ModelSize: "small"
|
|
657
|
+
}) {
|
|
658
|
+
Success
|
|
659
|
+
Embeddings
|
|
660
|
+
Dimensions
|
|
661
|
+
Model
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
```
|
|
554
665
|
|
|
555
|
-
|
|
666
|
+
### AI Agent Execution
|
|
556
667
|
|
|
557
|
-
```
|
|
558
|
-
|
|
668
|
+
```graphql
|
|
669
|
+
mutation {
|
|
670
|
+
RunAIAgent(input: {
|
|
671
|
+
AgentID: "agent-guid"
|
|
672
|
+
SessionID: "session-guid"
|
|
673
|
+
UserMessage: "Find all active users"
|
|
674
|
+
}) {
|
|
675
|
+
Success
|
|
676
|
+
Result
|
|
677
|
+
SessionID
|
|
678
|
+
}
|
|
679
|
+
}
|
|
559
680
|
```
|
|
560
681
|
|
|
561
|
-
|
|
562
|
-
- Authenticates as the system user
|
|
563
|
-
- Used for server-to-server communication
|
|
564
|
-
- Has elevated privileges for system operations
|
|
565
|
-
- `isSystemUser: true` is set in the request context
|
|
566
|
-
|
|
567
|
-
### Scope-Based Authorization
|
|
682
|
+
All AI operations have system-user variants (e.g., `RunAIPromptSystemUser`) that use the `@RequireSystemUser` directive for server-to-server operations.
|
|
568
683
|
|
|
569
|
-
API
|
|
684
|
+
## API Reference
|
|
570
685
|
|
|
571
|
-
|
|
686
|
+
### Core Exports
|
|
572
687
|
|
|
573
|
-
|
|
574
|
-
2. **API Key Scopes**: Each API key has assigned scopes from the `MJ: API Scopes` entity
|
|
575
|
-
3. **Two-Level Evaluation**: Authorization succeeds only if both application ceiling AND API key scopes permit the operation
|
|
688
|
+
#### Server Functions
|
|
576
689
|
|
|
577
|
-
|
|
690
|
+
| Export | Description |
|
|
691
|
+
|--------|-------------|
|
|
692
|
+
| `serve(resolverPaths, app?, options?)` | Main server initialization function |
|
|
693
|
+
| `createApp()` | Creates a new Express application instance |
|
|
578
694
|
|
|
579
|
-
|
|
580
|
-
|-------|-------------|
|
|
581
|
-
| `full_access` | Bypass all scope checks ("god mode") |
|
|
582
|
-
| `entity:read` | Read entity records |
|
|
583
|
-
| `entity:create` | Create new records |
|
|
584
|
-
| `entity:update` | Update existing records |
|
|
585
|
-
| `entity:delete` | Delete records |
|
|
586
|
-
| `view:run` | Execute RunView queries |
|
|
587
|
-
| `agent:execute` | Execute AI agents |
|
|
588
|
-
| `agent:monitor` | Check agent run status |
|
|
589
|
-
| `action:execute` | Execute MJ Actions |
|
|
590
|
-
| `prompt:execute` | Execute AI prompts |
|
|
591
|
-
| `query:run` | Execute queries |
|
|
592
|
-
| `metadata:entities:read` | Read entity metadata |
|
|
593
|
-
| `metadata:agents:read` | Read agent metadata |
|
|
594
|
-
| `communication:send` | Send emails/messages |
|
|
695
|
+
#### Types
|
|
595
696
|
|
|
596
|
-
|
|
697
|
+
| Export | Description |
|
|
698
|
+
|--------|-------------|
|
|
699
|
+
| `AppContext` | GraphQL resolver context type |
|
|
700
|
+
| `UserPayload` | Authenticated user payload |
|
|
701
|
+
| `DataSourceInfo` | Database connection descriptor |
|
|
702
|
+
| `ProviderInfo` | Per-request provider descriptor |
|
|
703
|
+
| `MJServerOptions` | Options for `serve()` |
|
|
704
|
+
| `ConfigInfo` | Full server configuration type |
|
|
705
|
+
| `MJServerEvent` | Server lifecycle event type |
|
|
597
706
|
|
|
598
|
-
|
|
707
|
+
#### Authentication
|
|
599
708
|
|
|
600
|
-
|
|
709
|
+
| Export | Description |
|
|
710
|
+
|--------|-------------|
|
|
711
|
+
| `IAuthProvider` | Authentication provider interface |
|
|
712
|
+
| `AuthProviderFactory` | Provider registry and factory |
|
|
713
|
+
| `NewUserBase` | Base class for custom new user handling |
|
|
714
|
+
| `TokenExpiredError` | Token expiration error class |
|
|
715
|
+
| `getSystemUser(dataSource?)` | Retrieve the system user |
|
|
716
|
+
| `getSigningKeys(issuer)` | Get JWT signing keys for an issuer |
|
|
717
|
+
| `extractUserInfoFromPayload(payload)` | Extract user info from JWT claims |
|
|
718
|
+
| `verifyUserRecord(email, ...)` | Verify and optionally create a user record |
|
|
719
|
+
|
|
720
|
+
#### Scope Authorization
|
|
721
|
+
|
|
722
|
+
| Export | Description |
|
|
723
|
+
|--------|-------------|
|
|
724
|
+
| `CheckAPIKeyScope(apiKeyId, scopePath, contextUser, options?)` | Check API key scope |
|
|
725
|
+
| `CheckAPIKeyScopeAndLog(apiKeyId, scopePath, contextUser, usageDetails, options?)` | Check scope with usage logging |
|
|
726
|
+
| `RequireScope(scopePath, options?)` | Create a reusable scope checker |
|
|
727
|
+
| `RequireViewRun` | Pre-built scope checker for `view:run` |
|
|
728
|
+
| `RequireQueryRun` | Pre-built scope checker for `query:run` |
|
|
729
|
+
| `RequireAgentExecute` | Pre-built scope checker for `agent:execute` |
|
|
730
|
+
|
|
731
|
+
#### Resolver Base Classes
|
|
732
|
+
|
|
733
|
+
| Export | Description |
|
|
734
|
+
|--------|-------------|
|
|
735
|
+
| `ResolverBase` | Base class for all resolvers |
|
|
736
|
+
| `RunViewResolver` | Base resolver for view operations |
|
|
737
|
+
| `PushStatusResolver` | Status update resolver with pub/sub |
|
|
601
738
|
|
|
602
|
-
|
|
739
|
+
#### Utility Functions
|
|
603
740
|
|
|
604
|
-
|
|
605
|
-
|
|
741
|
+
| Export | Description |
|
|
742
|
+
|--------|-------------|
|
|
743
|
+
| `GetReadOnlyDataSource(dataSources, options?)` | Get read-only connection pool |
|
|
744
|
+
| `GetReadWriteDataSource(dataSources)` | Get read-write connection pool |
|
|
745
|
+
| `GetReadOnlyProvider(providers, options?)` | Get read-only provider instance |
|
|
746
|
+
| `GetReadWriteProvider(providers, options?)` | Get read-write provider instance |
|
|
606
747
|
|
|
607
|
-
|
|
608
|
-
export class MyResolver extends ResolverBase {
|
|
609
|
-
@Mutation(() => MyResult)
|
|
610
|
-
async myOperation(
|
|
611
|
-
@Ctx() ctx: AppContext
|
|
612
|
-
): Promise<MyResult> {
|
|
613
|
-
// Check scope authorization for API keys
|
|
614
|
-
await this.CheckAPIKeyScopeAuthorization('my:scope', 'resource-name', ctx.userPayload);
|
|
615
|
-
|
|
616
|
-
// Proceed with operation...
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
```
|
|
748
|
+
#### GraphQL Inputs
|
|
620
749
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
750
|
+
| Export | Description |
|
|
751
|
+
|--------|-------------|
|
|
752
|
+
| `KeyValuePairInput` | Generic key-value input type |
|
|
753
|
+
| `DeleteOptionsInput` | Delete operation options |
|
|
625
754
|
|
|
626
|
-
|
|
755
|
+
#### Directives
|
|
627
756
|
|
|
628
|
-
|
|
757
|
+
| Export | Description |
|
|
758
|
+
|--------|-------------|
|
|
759
|
+
| `RequireSystemUser` | Decorator restricting access to system users |
|
|
760
|
+
| `Public` | Decorator marking endpoints as publicly accessible |
|
|
761
|
+
| `configInfo` | Parsed server configuration singleton |
|
|
762
|
+
| `DEFAULT_SERVER_CONFIG` | Default configuration values |
|
|
629
763
|
|
|
630
|
-
## Performance
|
|
764
|
+
## Performance
|
|
631
765
|
|
|
632
|
-
### Compression
|
|
766
|
+
### Response Compression
|
|
633
767
|
|
|
634
|
-
|
|
635
|
-
- Responses larger than 1KB are compressed
|
|
636
|
-
- Binary files are excluded from compression
|
|
637
|
-
- Uses compression level 6 for optimal balance
|
|
768
|
+
Responses larger than 1KB are automatically compressed using gzip at compression level 6. Binary content types (images, video, audio) are excluded.
|
|
638
769
|
|
|
639
770
|
### Connection Pooling
|
|
640
771
|
|
|
641
|
-
Database connections are managed
|
|
772
|
+
Database connections are managed via `mssql` connection pools. Configure pool size in `databaseSettings.connectionPool`:
|
|
642
773
|
|
|
643
|
-
|
|
774
|
+
```javascript
|
|
775
|
+
connectionPool: {
|
|
776
|
+
max: 50, // Maximum connections
|
|
777
|
+
min: 5, // Minimum connections
|
|
778
|
+
idleTimeoutMillis: 30000,
|
|
779
|
+
acquireTimeoutMillis: 30000,
|
|
780
|
+
}
|
|
781
|
+
```
|
|
644
782
|
|
|
645
|
-
|
|
783
|
+
**Recommended settings:**
|
|
784
|
+
- Development: `max: 10, min: 2`
|
|
785
|
+
- Production: `max: 50, min: 5`
|
|
786
|
+
- High load: `max: 100, min: 10`
|
|
787
|
+
|
|
788
|
+
### Metadata Caching
|
|
789
|
+
|
|
790
|
+
Entity metadata is loaded once at startup and shared across all per-request provider instances. The cache refresh interval is configurable:
|
|
646
791
|
|
|
647
792
|
```javascript
|
|
648
793
|
databaseSettings: {
|
|
649
|
-
metadataCacheRefreshInterval:
|
|
794
|
+
metadataCacheRefreshInterval: 180000, // 3 minutes
|
|
650
795
|
}
|
|
651
796
|
```
|
|
652
797
|
|
|
653
|
-
|
|
798
|
+
### Authentication Token Caching
|
|
654
799
|
|
|
655
|
-
|
|
800
|
+
Validated JWT tokens are cached using an LRU cache, avoiding repeated cryptographic verification for the same token within its lifetime.
|
|
656
801
|
|
|
657
|
-
|
|
658
|
-
- `@RequireSystemUser`: Requires system user permissions
|
|
659
|
-
- `@Public`: Makes endpoints publicly accessible
|
|
802
|
+
## Graceful Shutdown
|
|
660
803
|
|
|
661
|
-
|
|
804
|
+
The server registers handlers for `SIGTERM` and `SIGINT` to:
|
|
662
805
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
-
|
|
666
|
-
- Real-time session monitoring and control
|
|
667
|
-
- Integration with MemberJunction Explorer UI
|
|
806
|
+
1. Stop the scheduled jobs service
|
|
807
|
+
2. Close the HTTP server
|
|
808
|
+
3. Force-exit after a 10-second timeout if graceful shutdown stalls
|
|
668
809
|
|
|
669
|
-
|
|
810
|
+
Unhandled promise rejections are caught and logged without crashing the server.
|
|
670
811
|
|
|
671
|
-
|
|
812
|
+
## Dependencies
|
|
672
813
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
814
|
+
### Core MemberJunction Packages
|
|
815
|
+
|
|
816
|
+
| Package | Purpose |
|
|
817
|
+
|---------|---------|
|
|
818
|
+
| [@memberjunction/core](../MJCore/README.md) | Core metadata, entities, RunView |
|
|
819
|
+
| [@memberjunction/core-entities](../MJCoreEntities/README.md) | Generated entity classes |
|
|
820
|
+
| [@memberjunction/global](../MJGlobal/README.md) | ClassFactory, event system |
|
|
821
|
+
| [@memberjunction/sqlserver-dataprovider](../SQLServerDataProvider/README.md) | SQL Server data provider |
|
|
822
|
+
| [@memberjunction/graphql-dataprovider](../GraphQLDataProvider/README.md) | GraphQL field mapping |
|
|
823
|
+
| [@memberjunction/config](../Config/README.md) | Configuration utilities |
|
|
824
|
+
| [@memberjunction/api-keys](../APIKeys/README.md) | API key engine and scope evaluation |
|
|
825
|
+
| [@memberjunction/encryption](../Encryption/README.md) | Field-level encryption engine |
|
|
826
|
+
|
|
827
|
+
### AI Packages
|
|
828
|
+
|
|
829
|
+
| Package | Purpose |
|
|
830
|
+
|---------|---------|
|
|
831
|
+
| [@memberjunction/ai](../AI/README.md) | AI engine abstraction |
|
|
832
|
+
| [@memberjunction/ai-prompts](../AI/Prompts/README.md) | AI prompt execution |
|
|
833
|
+
| [@memberjunction/ai-agents](../AI/Agents/README.md) | AI agent framework |
|
|
834
|
+
| [@memberjunction/ai-core-plus](../AI/CorePlus/README.md) | AI prompt parameters |
|
|
835
|
+
| [@memberjunction/aiengine](../AIEngine/README.md) | AI engine orchestration |
|
|
836
|
+
| [@memberjunction/ai-provider-bundle](../AI/ProviderBundle/README.md) | Bundled AI providers |
|
|
837
|
+
|
|
838
|
+
### Infrastructure Packages
|
|
839
|
+
|
|
840
|
+
| Package | Purpose |
|
|
841
|
+
|---------|---------|
|
|
842
|
+
| [@memberjunction/scheduling-engine](../SchedulingEngine/README.md) | Scheduled job execution |
|
|
843
|
+
| [@memberjunction/actions](../Actions/README.md) | Action framework |
|
|
844
|
+
| [@memberjunction/templates](../Templates/README.md) | Template engine |
|
|
845
|
+
| [@memberjunction/notifications](../Notifications/README.md) | Notification system |
|
|
846
|
+
| [@memberjunction/storage](../Storage/README.md) | File storage |
|
|
847
|
+
| [@memberjunction/communication-ms-graph](../Communication/providers/MSGraphProvider/README.md) | MS Graph communications |
|
|
848
|
+
| [@memberjunction/communication-sendgrid](../Communication/providers/SendGridProvider/README.md) | SendGrid communications |
|
|
849
|
+
|
|
850
|
+
### Third-Party Dependencies
|
|
851
|
+
|
|
852
|
+
| Package | Purpose |
|
|
853
|
+
|---------|---------|
|
|
854
|
+
| `@apollo/server` | GraphQL server |
|
|
855
|
+
| `express` | HTTP framework |
|
|
856
|
+
| `type-graphql` | TypeScript GraphQL decorators |
|
|
857
|
+
| `mssql` | SQL Server client |
|
|
858
|
+
| `jsonwebtoken` / `jwks-rsa` | JWT verification |
|
|
859
|
+
| `graphql-ws` / `ws` | WebSocket subscriptions |
|
|
860
|
+
| `compression` | Response compression |
|
|
861
|
+
| `cosmiconfig` | Configuration file discovery |
|
|
862
|
+
| `cloudevents` | CloudEvent emission |
|
|
863
|
+
| `zod` | Configuration schema validation |
|
|
864
|
+
|
|
865
|
+
## Related Packages
|
|
866
|
+
|
|
867
|
+
- [@memberjunction/server-bootstrap](../ServerBootstrap/README.md) -- Pre-built class registration manifest for tree-shaking prevention
|
|
868
|
+
- [MJAPI](../../MJAPI/README.md) -- Reference server application that consumes this package
|
|
869
|
+
- [@memberjunction/core](../MJCore/README.md) -- Core framework that MJServer exposes via API
|
|
679
870
|
|
|
680
871
|
## Troubleshooting
|
|
681
872
|
|
|
682
|
-
|
|
873
|
+
| Issue | Solution |
|
|
874
|
+
|-------|----------|
|
|
875
|
+
| Authentication errors | Verify environment variables for your auth provider are set |
|
|
876
|
+
| Database connection failures | Check `DB_HOST`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` |
|
|
877
|
+
| No resolvers found | Verify resolver paths passed to `serve()` are absolute and use correct glob patterns |
|
|
878
|
+
| Transaction errors | Review mutation logic; check that entity operations are within the per-request provider |
|
|
879
|
+
| SQL logging access denied | Ensure user has `Type = 'Owner'` in the Users table |
|
|
880
|
+
| Metadata not loading | Check `MJ_CORE_SCHEMA` matches your database schema name |
|
|
881
|
+
| Token expired errors | Expected behavior for long-lived sessions; client should refresh tokens |
|
|
683
882
|
|
|
684
|
-
|
|
685
|
-
2. **Database Connection**: Verify connection string and credentials
|
|
686
|
-
3. **Module Loading**: Check resolver paths are correct and accessible
|
|
687
|
-
4. **Transaction Errors**: Review mutation logic for proper error handling
|
|
688
|
-
5. **SQL Logging Access**: Ensure user has Owner privileges and logging is enabled in config
|
|
883
|
+
Enable verbose logging:
|
|
689
884
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
Enable detailed logging by setting environment variables:
|
|
693
|
-
```shell
|
|
885
|
+
```bash
|
|
694
886
|
DEBUG=mj:*
|
|
695
887
|
NODE_ENV=development
|
|
696
888
|
```
|
|
697
889
|
|
|
698
|
-
## Best Practices
|
|
699
|
-
|
|
700
|
-
1. **Use Read-Only Connections**: Configure read-only database connections for query operations
|
|
701
|
-
2. **Implement Custom User Handling**: Extend `NewUserBase` for organization-specific user creation
|
|
702
|
-
3. **Monitor Performance**: Use the built-in timing logs for transaction monitoring
|
|
703
|
-
4. **Secure Your REST API**: Always configure entity and schema filters for production
|
|
704
|
-
5. **Handle Errors Gracefully**: Implement proper error handling in custom resolvers
|
|
705
|
-
|
|
706
890
|
## Contributing
|
|
707
891
|
|
|
708
|
-
|
|
709
|
-
1. Follow the existing code style and patterns
|
|
710
|
-
2. Add appropriate TypeScript types
|
|
711
|
-
3. Include tests for new functionality
|
|
712
|
-
4. Update documentation as needed
|
|
713
|
-
|
|
714
|
-
## License
|
|
715
|
-
|
|
716
|
-
ISC
|
|
892
|
+
See the [MemberJunction Contributing Guide](../../CONTRIBUTING.md) for development setup and guidelines.
|