@memberjunction/server 5.8.0 → 5.10.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 +1 -0
- package/dist/agents/skip-agent.d.ts.map +1 -1
- package/dist/agents/skip-agent.js +1 -0
- package/dist/agents/skip-agent.js.map +1 -1
- package/dist/agents/skip-sdk.d.ts +6 -0
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +5 -3
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/apolloServer/index.d.ts +10 -2
- package/dist/apolloServer/index.d.ts.map +1 -1
- package/dist/apolloServer/index.js +22 -8
- package/dist/apolloServer/index.js.map +1 -1
- package/dist/config.d.ts +125 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +17 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +144 -62
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +210 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1049 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +32 -0
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -0
- package/dist/generic/CacheInvalidationResolver.js +80 -0
- package/dist/generic/CacheInvalidationResolver.js.map +1 -0
- package/dist/generic/PubSubManager.d.ts +27 -0
- package/dist/generic/PubSubManager.d.ts.map +1 -0
- package/dist/generic/PubSubManager.js +41 -0
- package/dist/generic/PubSubManager.js.map +1 -0
- package/dist/generic/ResolverBase.d.ts +14 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +66 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/hooks.d.ts +65 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +14 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +173 -45
- package/dist/index.js.map +1 -1
- package/dist/multiTenancy/index.d.ts +47 -0
- package/dist/multiTenancy/index.d.ts.map +1 -0
- package/dist/multiTenancy/index.js +152 -0
- package/dist/multiTenancy/index.js.map +1 -0
- package/dist/resolvers/CurrentUserContextResolver.d.ts +18 -0
- package/dist/resolvers/CurrentUserContextResolver.d.ts.map +1 -0
- package/dist/resolvers/CurrentUserContextResolver.js +54 -0
- package/dist/resolvers/CurrentUserContextResolver.js.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +123 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.js +624 -0
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -0
- package/dist/rest/RESTEndpointHandler.d.ts +3 -1
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +14 -33
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/test-dynamic-plugin.d.ts +6 -0
- package/dist/test-dynamic-plugin.d.ts.map +1 -0
- package/dist/test-dynamic-plugin.js +18 -0
- package/dist/test-dynamic-plugin.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +61 -57
- package/src/__tests__/bcsaas-integration.test.ts +455 -0
- package/src/__tests__/middleware-integration.test.ts +877 -0
- package/src/__tests__/mjapi-bootstrap.test.ts +29 -0
- package/src/__tests__/multiTenancy.security.test.ts +334 -0
- package/src/__tests__/multiTenancy.test.ts +225 -0
- package/src/__tests__/unifiedAuth.test.ts +416 -0
- package/src/agents/skip-agent.ts +1 -0
- package/src/agents/skip-sdk.ts +13 -3
- package/src/apolloServer/index.ts +32 -16
- package/src/config.ts +25 -0
- package/src/context.ts +205 -98
- package/src/generated/generated.ts +746 -1
- package/src/generic/CacheInvalidationResolver.ts +66 -0
- package/src/generic/PubSubManager.ts +46 -0
- package/src/generic/ResolverBase.ts +70 -4
- package/src/hooks.ts +77 -0
- package/src/index.ts +199 -49
- package/src/multiTenancy/index.ts +183 -0
- package/src/resolvers/CurrentUserContextResolver.ts +39 -0
- package/src/resolvers/IntegrationDiscoveryResolver.ts +584 -0
- package/src/rest/RESTEndpointHandler.ts +23 -42
- package/src/test-dynamic-plugin.ts +36 -0
- package/src/types.ts +10 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Field, ObjectType, Resolver, Root, Subscription } from 'type-graphql';
|
|
2
|
+
|
|
3
|
+
export const CACHE_INVALIDATION_TOPIC = 'CACHE_INVALIDATION';
|
|
4
|
+
|
|
5
|
+
@ObjectType()
|
|
6
|
+
export class CacheInvalidationNotification {
|
|
7
|
+
@Field(() => String)
|
|
8
|
+
EntityName!: string;
|
|
9
|
+
|
|
10
|
+
@Field(() => String, { nullable: true })
|
|
11
|
+
PrimaryKeyValues?: string;
|
|
12
|
+
|
|
13
|
+
@Field(() => String)
|
|
14
|
+
Action!: string;
|
|
15
|
+
|
|
16
|
+
@Field(() => String)
|
|
17
|
+
SourceServerID!: string;
|
|
18
|
+
|
|
19
|
+
@Field(() => Date)
|
|
20
|
+
Timestamp!: Date;
|
|
21
|
+
|
|
22
|
+
@Field(() => String, { nullable: true })
|
|
23
|
+
OriginSessionID?: string;
|
|
24
|
+
|
|
25
|
+
@Field(() => String, { nullable: true })
|
|
26
|
+
RecordData?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Payload interface for publishing cache invalidation events.
|
|
31
|
+
* Used by PubSubManager.Publish() to send events from Redis callbacks.
|
|
32
|
+
*/
|
|
33
|
+
export interface CacheInvalidationPayload {
|
|
34
|
+
entityName: string;
|
|
35
|
+
primaryKeyValues: string | null;
|
|
36
|
+
action: string;
|
|
37
|
+
sourceServerId: string;
|
|
38
|
+
timestamp: Date;
|
|
39
|
+
originSessionId?: string;
|
|
40
|
+
recordData?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Resolver()
|
|
44
|
+
export class CacheInvalidationResolver {
|
|
45
|
+
/**
|
|
46
|
+
* Subscription that broadcasts cache invalidation events to ALL connected clients.
|
|
47
|
+
* No session filter — every browser connected via WebSocket receives every event.
|
|
48
|
+
* This enables cross-server cache invalidation to propagate to browser clients.
|
|
49
|
+
*/
|
|
50
|
+
@Subscription(() => CacheInvalidationNotification, {
|
|
51
|
+
topics: CACHE_INVALIDATION_TOPIC,
|
|
52
|
+
})
|
|
53
|
+
cacheInvalidation(
|
|
54
|
+
@Root() payload: CacheInvalidationPayload
|
|
55
|
+
): CacheInvalidationNotification {
|
|
56
|
+
return {
|
|
57
|
+
EntityName: payload.entityName,
|
|
58
|
+
PrimaryKeyValues: payload.primaryKeyValues ?? undefined,
|
|
59
|
+
Action: payload.action,
|
|
60
|
+
SourceServerID: payload.sourceServerId,
|
|
61
|
+
Timestamp: payload.timestamp,
|
|
62
|
+
OriginSessionID: payload.originSessionId ?? undefined,
|
|
63
|
+
RecordData: payload.recordData ?? undefined,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { PubSubEngine } from 'type-graphql';
|
|
2
|
+
import { BaseSingleton } from '@memberjunction/global';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Singleton manager that holds a reference to the type-graphql PubSubEngine,
|
|
6
|
+
* allowing any server-side code (not just resolvers with @PubSub() injection)
|
|
7
|
+
* to publish events to GraphQL subscriptions.
|
|
8
|
+
*/
|
|
9
|
+
export class PubSubManager extends BaseSingleton<PubSubManager> {
|
|
10
|
+
private _pubSub: PubSubEngine | null = null;
|
|
11
|
+
|
|
12
|
+
protected constructor() {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static get Instance(): PubSubManager {
|
|
17
|
+
return super.getInstance<PubSubManager>();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sets the PubSubEngine instance. Should be called once during server startup
|
|
22
|
+
* after buildSchemaSync creates the PubSub instance.
|
|
23
|
+
*/
|
|
24
|
+
public SetPubSubEngine(pubSub: PubSubEngine): void {
|
|
25
|
+
this._pubSub = pubSub;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Gets the current PubSubEngine instance, or null if not yet configured.
|
|
30
|
+
*/
|
|
31
|
+
public get PubSubEngine(): PubSubEngine | null {
|
|
32
|
+
return this._pubSub;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Publishes a payload to the specified topic via the PubSubEngine.
|
|
37
|
+
* No-op if the PubSubEngine has not been configured yet.
|
|
38
|
+
*/
|
|
39
|
+
public Publish(topic: string, payload: Record<string, unknown>): void {
|
|
40
|
+
if (this._pubSub) {
|
|
41
|
+
this._pubSub.publish(topic, payload);
|
|
42
|
+
} else {
|
|
43
|
+
console.warn(`[PubSubManager] Cannot publish to "${topic}" — PubSubEngine not set`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -31,6 +31,8 @@ import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
|
31
31
|
import { MJEvent, MJEventType, MJGlobal, ENCRYPTED_SENTINEL, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
32
32
|
import { EncryptionEngine } from '@memberjunction/encryption';
|
|
33
33
|
import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver.js';
|
|
34
|
+
import { CACHE_INVALIDATION_TOPIC } from './CacheInvalidationResolver.js';
|
|
35
|
+
import { PubSubManager } from './PubSubManager.js';
|
|
34
36
|
import { FieldMapper } from '@memberjunction/graphql-dataprovider';
|
|
35
37
|
import { Subscription } from 'rxjs';
|
|
36
38
|
|
|
@@ -64,12 +66,17 @@ export class ResolverBase {
|
|
|
64
66
|
* @returns The processed data object
|
|
65
67
|
*/
|
|
66
68
|
protected async MapFieldNamesToCodeNames(entityName: string, dataObject: any, contextUser?: UserInfo): Promise<any> {
|
|
69
|
+
// Return null for empty objects (e.g. when no rows found due to RLS filtering)
|
|
70
|
+
if (!dataObject || Object.keys(dataObject).length === 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
// for the given entity name provided, check to see if there are any fields
|
|
68
75
|
// where the code name is different from the field name, and for just those
|
|
69
76
|
// fields, iterate through the dataObject and REPLACE the property that has the field name
|
|
70
77
|
// with the CodeName, because we can't transfer those via GraphQL as they are not
|
|
71
78
|
// valid property names in GraphQL
|
|
72
|
-
|
|
79
|
+
{
|
|
73
80
|
const md = new Metadata();
|
|
74
81
|
const entityInfo = md.Entities.find((e) => e.Name === entityName);
|
|
75
82
|
if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
|
|
@@ -128,6 +135,31 @@ export class ResolverBase {
|
|
|
128
135
|
return dataObject;
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Reverse-maps GraphQL-safe field names back to entity CodeNames in a mutation input object.
|
|
140
|
+
* For example, `_mj__integration_SyncStatus` is mapped back to `__mj_integration_SyncStatus`.
|
|
141
|
+
* Also reverse-maps keys inside the `OldValues___` array if present.
|
|
142
|
+
* This is the inverse of MapFieldNamesToCodeNames and must be called before passing
|
|
143
|
+
* GraphQL input to entity SetMany() or field lookups.
|
|
144
|
+
*/
|
|
145
|
+
protected ReverseMapInputFieldNames(input: Record<string, unknown>): Record<string, unknown> {
|
|
146
|
+
const mapper = new FieldMapper();
|
|
147
|
+
const mapped: Record<string, unknown> = {};
|
|
148
|
+
for (const key of Object.keys(input)) {
|
|
149
|
+
if (key === 'OldValues___') {
|
|
150
|
+
// Reverse-map the Key property inside each OldValues entry
|
|
151
|
+
const oldValues = input[key] as Array<{ Key: string; Value: unknown }>;
|
|
152
|
+
mapped[key] = oldValues.map((item) => ({
|
|
153
|
+
Key: mapper.ReverseMapFieldName(item.Key),
|
|
154
|
+
Value: item.Value,
|
|
155
|
+
}));
|
|
156
|
+
} else {
|
|
157
|
+
mapped[mapper.ReverseMapFieldName(key)] = input[key];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return mapped;
|
|
161
|
+
}
|
|
162
|
+
|
|
131
163
|
protected async ArrayMapFieldNamesToCodeNames(entityName: string, dataObjectArray: any[], contextUser?: UserInfo): Promise<any[]> {
|
|
132
164
|
// iterate through the array and call MapFieldNamesToCodeNames for each element
|
|
133
165
|
if (dataObjectArray && dataObjectArray.length > 0) {
|
|
@@ -947,6 +979,23 @@ export class ResolverBase {
|
|
|
947
979
|
return Metadata.Provider.ConfigData.MJCoreSchemaName;
|
|
948
980
|
}
|
|
949
981
|
|
|
982
|
+
/**
|
|
983
|
+
* Publishes a CACHE_INVALIDATION event to connected browser clients after a successful
|
|
984
|
+
* entity save or delete. Includes the originSessionId so the originating browser can
|
|
985
|
+
* skip redundant re-fetches (it already handled the event locally).
|
|
986
|
+
*/
|
|
987
|
+
protected PublishCacheInvalidation(entityObject: BaseEntity, action: 'save' | 'delete', userPayload: UserPayload): void {
|
|
988
|
+
PubSubManager.Instance.Publish(CACHE_INVALIDATION_TOPIC, {
|
|
989
|
+
entityName: entityObject.EntityInfo.Name,
|
|
990
|
+
primaryKeyValues: JSON.stringify(entityObject.PrimaryKey.KeyValuePairs),
|
|
991
|
+
action,
|
|
992
|
+
sourceServerId: MJGlobal.Instance.ProcessUUID,
|
|
993
|
+
timestamp: new Date(),
|
|
994
|
+
originSessionId: userPayload?.sessionId || null,
|
|
995
|
+
recordData: action === 'save' ? JSON.stringify(entityObject.GetAll()) : undefined,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
950
999
|
protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
|
|
951
1000
|
// The unique key is set up for each entity object via it's primary key to ensure that we only have one listener at most for each unique
|
|
952
1001
|
// entity in the system. This is important because we don't want to have multiple listeners for the same entity as it could
|
|
@@ -997,6 +1046,9 @@ export class ResolverBase {
|
|
|
997
1046
|
// Check API key scope authorization for entity create operations
|
|
998
1047
|
await this.CheckAPIKeyScopeAuthorization('entity:create', entityName, userPayload);
|
|
999
1048
|
|
|
1049
|
+
// Reverse-map GraphQL field names (e.g. _mj__*) back to entity CodeNames (e.g. __mj_*)
|
|
1050
|
+
input = this.ReverseMapInputFieldNames(input);
|
|
1051
|
+
|
|
1000
1052
|
if (await this.BeforeCreate(provider, input)) {
|
|
1001
1053
|
// fire event and proceed if it wasn't cancelled
|
|
1002
1054
|
const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
|
|
@@ -1009,6 +1061,7 @@ export class ResolverBase {
|
|
|
1009
1061
|
if (await entityObject.Save()) {
|
|
1010
1062
|
// save worked, fire the AfterCreate event and then return all the data
|
|
1011
1063
|
await this.AfterCreate(provider, input); // fire event
|
|
1064
|
+
this.PublishCacheInvalidation(entityObject, 'save', userPayload);
|
|
1012
1065
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
1013
1066
|
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
1014
1067
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), contextUser);
|
|
@@ -1032,6 +1085,9 @@ export class ResolverBase {
|
|
|
1032
1085
|
// Check API key scope authorization for entity update operations
|
|
1033
1086
|
await this.CheckAPIKeyScopeAuthorization('entity:update', entityName, userPayload);
|
|
1034
1087
|
|
|
1088
|
+
// Reverse-map GraphQL field names (e.g. _mj__*) back to entity CodeNames (e.g. __mj_*)
|
|
1089
|
+
input = this.ReverseMapInputFieldNames(input);
|
|
1090
|
+
|
|
1035
1091
|
if (await this.BeforeUpdate(provider, input)) {
|
|
1036
1092
|
// fire event and proceed if it wasn't cancelled
|
|
1037
1093
|
const userInfo = this.GetUserFromPayload(userPayload);
|
|
@@ -1065,8 +1121,9 @@ export class ResolverBase {
|
|
|
1065
1121
|
entityObject.SetMany(input);
|
|
1066
1122
|
}
|
|
1067
1123
|
} else {
|
|
1068
|
-
//
|
|
1069
|
-
|
|
1124
|
+
// Use a generic message to avoid leaking whether a record exists — distinguishing
|
|
1125
|
+
// "not found" from "access denied" would let an attacker enumerate valid record IDs.
|
|
1126
|
+
throw new GraphQLError(`Record not found or access denied`, {
|
|
1070
1127
|
extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
|
|
1071
1128
|
});
|
|
1072
1129
|
}
|
|
@@ -1088,6 +1145,7 @@ export class ResolverBase {
|
|
|
1088
1145
|
if (await entityObject.Save()) {
|
|
1089
1146
|
// save worked, fire afterevent and return all the data
|
|
1090
1147
|
await this.AfterUpdate(provider, input); // fire event
|
|
1148
|
+
this.PublishCacheInvalidation(entityObject, 'save', userPayload);
|
|
1091
1149
|
|
|
1092
1150
|
// MapFieldNamesToCodeNames now handles encryption filtering as well
|
|
1093
1151
|
return await this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll(), userInfo);
|
|
@@ -1300,13 +1358,21 @@ export class ResolverBase {
|
|
|
1300
1358
|
if (await this.BeforeDelete(provider, key)) {
|
|
1301
1359
|
// fire event and proceed if it wasn't cancelled
|
|
1302
1360
|
const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
|
|
1303
|
-
await entityObject.InnerLoad(key);
|
|
1361
|
+
const loadSuccess = await entityObject.InnerLoad(key);
|
|
1362
|
+
if (!loadSuccess) {
|
|
1363
|
+
// Use a generic message to avoid leaking whether a record exists — distinguishing
|
|
1364
|
+
// "not found" from "access denied" would let an attacker enumerate valid record IDs.
|
|
1365
|
+
throw new GraphQLError(`Record not found or access denied`, {
|
|
1366
|
+
extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1304
1369
|
const returnValue = entityObject.GetAll(); // grab the values before we delete so we can return last state before delete if we are successful.
|
|
1305
1370
|
|
|
1306
1371
|
this.ListenForEntityMessages(entityObject, pubSub, userPayload);
|
|
1307
1372
|
|
|
1308
1373
|
if (await entityObject.Delete(options)) {
|
|
1309
1374
|
await this.AfterDelete(provider, key); // fire event
|
|
1375
|
+
this.PublishCacheInvalidation(entityObject, 'delete', userPayload);
|
|
1310
1376
|
return returnValue;
|
|
1311
1377
|
} else {
|
|
1312
1378
|
throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-level extensibility types for MJServer.
|
|
3
|
+
*
|
|
4
|
+
* Provider-level hook types (PreRunViewHook, PostRunViewHook, PreSaveHook)
|
|
5
|
+
* are defined and exported from @memberjunction/core (hookRegistry.ts) so
|
|
6
|
+
* that ProviderBase and BaseEntity can reference them without depending
|
|
7
|
+
* on @memberjunction/server.
|
|
8
|
+
*
|
|
9
|
+
* This file re-exports those types for convenience and adds the
|
|
10
|
+
* server-specific extensibility options (Express middleware, Apollo plugins,
|
|
11
|
+
* schema transformers).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type { PreRunViewHook, PostRunViewHook, PreSaveHook, HookName, HookRegistrationOptions } from '@memberjunction/core';
|
|
15
|
+
|
|
16
|
+
import type { PreRunViewHook, PostRunViewHook, PreSaveHook, HookRegistrationOptions } from '@memberjunction/core';
|
|
17
|
+
import type { RequestHandler, ErrorRequestHandler, Application } from 'express';
|
|
18
|
+
import type { ApolloServerPlugin } from '@apollo/server';
|
|
19
|
+
import type { GraphQLSchema } from 'graphql';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A hook entry that includes the hook function plus optional registration metadata.
|
|
23
|
+
* Dynamic packages use this format to declare hooks with priority and namespace.
|
|
24
|
+
*/
|
|
25
|
+
export interface HookWithOptions<T> {
|
|
26
|
+
hook: T;
|
|
27
|
+
Priority?: number;
|
|
28
|
+
Namespace?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A hook value is either a plain function or a function with registration options */
|
|
32
|
+
export type HookOrEntry<T> = T | HookWithOptions<T>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extensibility options that can be passed to `serve()` (via MJServerOptions)
|
|
36
|
+
* or to `createMJServer()` (via MJServerConfig).
|
|
37
|
+
*
|
|
38
|
+
* All properties are optional — when omitted, zero behavior change (backward compatible).
|
|
39
|
+
*/
|
|
40
|
+
export interface ServerExtensibilityOptions {
|
|
41
|
+
/** Express middleware applied after compression but before OAuth/REST/GraphQL routes */
|
|
42
|
+
ExpressMiddlewareBefore?: RequestHandler[];
|
|
43
|
+
|
|
44
|
+
/** Express middleware/error handlers applied after all routes (catch-alls, error handlers) */
|
|
45
|
+
ExpressMiddlewareAfter?: (RequestHandler | ErrorRequestHandler)[];
|
|
46
|
+
|
|
47
|
+
/** Escape hatch for advanced Express app configuration (CORS, trust proxy, etc.) */
|
|
48
|
+
ConfigureExpressApp?: (app: Application) => void | Promise<void>;
|
|
49
|
+
|
|
50
|
+
/** Additional Apollo Server plugins merged with the built-in drain/cleanup plugins */
|
|
51
|
+
ApolloPlugins?: ApolloServerPlugin[];
|
|
52
|
+
|
|
53
|
+
/** Schema transformers applied after built-in directive transformers */
|
|
54
|
+
SchemaTransformers?: ((schema: GraphQLSchema) => GraphQLSchema)[];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Express middleware that runs AFTER authentication has resolved UserInfo
|
|
58
|
+
* onto the request. Use this for middleware that needs the authenticated
|
|
59
|
+
* user (e.g., tenant context resolution, org membership loading).
|
|
60
|
+
*
|
|
61
|
+
* The authenticated user payload is available at `req.userPayload`.
|
|
62
|
+
* Middleware in this array runs in registration order.
|
|
63
|
+
*/
|
|
64
|
+
ExpressMiddlewarePostAuth?: RequestHandler[];
|
|
65
|
+
|
|
66
|
+
/** Hooks that modify RunViewParams before query execution (e.g., tenant filter injection).
|
|
67
|
+
* Each entry can be a plain hook function or a `{ hook, Priority, Namespace }` object. */
|
|
68
|
+
PreRunViewHooks?: HookOrEntry<PreRunViewHook>[];
|
|
69
|
+
|
|
70
|
+
/** Hooks that modify RunViewResult after query execution (e.g., data masking).
|
|
71
|
+
* Each entry can be a plain hook function or a `{ hook, Priority, Namespace }` object. */
|
|
72
|
+
PostRunViewHooks?: HookOrEntry<PostRunViewHook>[];
|
|
73
|
+
|
|
74
|
+
/** Hooks that validate/reject Save operations before they hit the database.
|
|
75
|
+
* Each entry can be a plain hook function or a `{ hook, Priority, Namespace }` object. */
|
|
76
|
+
PreSaveHooks?: HookOrEntry<PreSaveHook>[];
|
|
77
|
+
}
|