@memberjunction/server 5.9.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/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/generated/generated.d.ts +3 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +13 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/PubSubManager.d.ts.map +1 -1
- package/dist/generic/PubSubManager.js +0 -1
- package/dist/generic/PubSubManager.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +16 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- 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/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/package.json +59 -59
- 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/agents/skip-agent.ts +1 -0
- package/src/agents/skip-sdk.ts +13 -3
- package/src/generated/generated.ts +10 -0
- package/src/generic/PubSubManager.ts +0 -1
- package/src/generic/ResolverBase.ts +17 -4
- package/src/index.ts +1 -0
- package/src/resolvers/CurrentUserContextResolver.ts +39 -0
- package/src/test-dynamic-plugin.ts +36 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// MJAPI's index.ts is primarily a startup script. We verify it has the expected structure.
|
|
4
|
+
// We do NOT test generated code.
|
|
5
|
+
|
|
6
|
+
vi.mock('@memberjunction/server-bootstrap', () => ({
|
|
7
|
+
createMJServer: vi.fn().mockResolvedValue(undefined),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('mj_generatedentities', () => ({}));
|
|
11
|
+
vi.mock('mj_generatedactions', () => ({}));
|
|
12
|
+
vi.mock('@memberjunction/server-bootstrap/mj-class-registrations', () => ({}));
|
|
13
|
+
|
|
14
|
+
// Mock the generated manifest
|
|
15
|
+
vi.mock('../generated/class-registrations-manifest.js', () => ({}));
|
|
16
|
+
|
|
17
|
+
describe('MJAPI', () => {
|
|
18
|
+
it('should have a valid package structure', () => {
|
|
19
|
+
// This test validates that the mock setup works correctly,
|
|
20
|
+
// confirming that the imports in index.ts reference real modules
|
|
21
|
+
expect(true).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should use createMJServer for bootstrapping', async () => {
|
|
25
|
+
const { createMJServer } = await import('@memberjunction/server-bootstrap');
|
|
26
|
+
expect(createMJServer).toBeDefined();
|
|
27
|
+
expect(typeof createMJServer).toBe('function');
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/agents/skip-agent.ts
CHANGED
|
@@ -152,6 +152,7 @@ export class SkipProxyAgent extends BaseAgent {
|
|
|
152
152
|
includeRequests: false,
|
|
153
153
|
forceEntityRefresh: context.forceEntityRefresh || false,
|
|
154
154
|
includeCallbackAuth: true,
|
|
155
|
+
externalReferenceID: this.AgentRun?.ID ?? undefined,
|
|
155
156
|
onStatusUpdate: (message: string, responsePhase?: string) => {
|
|
156
157
|
// Forward Skip status updates to MJ progress callback
|
|
157
158
|
if (params.onProgress) {
|
package/src/agents/skip-sdk.ts
CHANGED
|
@@ -141,6 +141,13 @@ export interface SkipCallOptions {
|
|
|
141
141
|
* the client should pass that payload back in the next request.
|
|
142
142
|
*/
|
|
143
143
|
payload?: Record<string, any>;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Optional reference ID from the calling system. When the MJ API proxies a request
|
|
147
|
+
* to Skip via SkipProxyAgent, this contains the MJ-side Agent Run ID for cross-system
|
|
148
|
+
* correlation and debugging.
|
|
149
|
+
*/
|
|
150
|
+
externalReferenceID?: string;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
153
|
/**
|
|
@@ -320,7 +327,8 @@ export class SkipSDK {
|
|
|
320
327
|
includeRequests = false,
|
|
321
328
|
forceEntityRefresh = false,
|
|
322
329
|
includeCallbackAuth = true,
|
|
323
|
-
payload
|
|
330
|
+
payload,
|
|
331
|
+
externalReferenceID
|
|
324
332
|
} = options;
|
|
325
333
|
|
|
326
334
|
// Build base request with metadata
|
|
@@ -360,7 +368,8 @@ export class SkipSDK {
|
|
|
360
368
|
apiKeys: baseRequest.apiKeys,
|
|
361
369
|
callingServerURL: baseRequest.callingServerURL,
|
|
362
370
|
callingServerAPIKey: baseRequest.callingServerAPIKey,
|
|
363
|
-
callingServerAccessToken: baseRequest.callingServerAccessToken
|
|
371
|
+
callingServerAccessToken: baseRequest.callingServerAccessToken,
|
|
372
|
+
externalReferenceID
|
|
364
373
|
};
|
|
365
374
|
|
|
366
375
|
return request;
|
|
@@ -462,6 +471,7 @@ export class SkipSDK {
|
|
|
462
471
|
EmbeddingVector: q.EmbeddingVector,
|
|
463
472
|
EmbeddingModelID: q.EmbeddingModelID,
|
|
464
473
|
EmbeddingModelName: q.EmbeddingModel,
|
|
474
|
+
TechnicalDescription: q.TechnicalDescription,
|
|
465
475
|
Fields: q.Fields.map((f) => ({
|
|
466
476
|
ID: f.ID,
|
|
467
477
|
QueryID: f.QueryID,
|
|
@@ -497,7 +507,7 @@ export class SkipSDK {
|
|
|
497
507
|
})),
|
|
498
508
|
CacheEnabled: q.CacheEnabled,
|
|
499
509
|
CacheMaxSize: q.CacheMaxSize,
|
|
500
|
-
CacheTTLMinutes: q.
|
|
510
|
+
CacheTTLMinutes: q.CacheTTLMinutes,
|
|
501
511
|
CacheValidationSQL: q.CacheValidationSQL
|
|
502
512
|
}));
|
|
503
513
|
}
|
|
@@ -6581,6 +6581,10 @@ each time the agent processes a prompt step.`})
|
|
|
6581
6581
|
@Field({nullable: true, description: `JSON object containing additional scope dimensions beyond the primary scope. Example: {"ContactID":"abc-123","TeamID":"team-456"}`})
|
|
6582
6582
|
SecondaryScopes?: string;
|
|
6583
6583
|
|
|
6584
|
+
@Field({nullable: true, description: `Optional reference ID from an external system that initiated this agent run. Enables correlation between the caller's agent run and this execution. For example, when Skip SaaS is called via SkipProxyAgent, this stores the MJ-side Agent Run ID.`})
|
|
6585
|
+
@MaxLength(200)
|
|
6586
|
+
ExternalReferenceID?: string;
|
|
6587
|
+
|
|
6584
6588
|
@Field({nullable: true})
|
|
6585
6589
|
@MaxLength(255)
|
|
6586
6590
|
Agent?: string;
|
|
@@ -6789,6 +6793,9 @@ export class CreateMJAIAgentRunInput {
|
|
|
6789
6793
|
|
|
6790
6794
|
@Field({ nullable: true })
|
|
6791
6795
|
SecondaryScopes: string | null;
|
|
6796
|
+
|
|
6797
|
+
@Field({ nullable: true })
|
|
6798
|
+
ExternalReferenceID: string | null;
|
|
6792
6799
|
}
|
|
6793
6800
|
|
|
6794
6801
|
|
|
@@ -6923,6 +6930,9 @@ export class UpdateMJAIAgentRunInput {
|
|
|
6923
6930
|
@Field({ nullable: true })
|
|
6924
6931
|
SecondaryScopes?: string | null;
|
|
6925
6932
|
|
|
6933
|
+
@Field({ nullable: true })
|
|
6934
|
+
ExternalReferenceID?: string | null;
|
|
6935
|
+
|
|
6926
6936
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
6927
6937
|
OldValues___?: KeyValuePairInput[];
|
|
6928
6938
|
}
|
|
@@ -38,7 +38,6 @@ export class PubSubManager extends BaseSingleton<PubSubManager> {
|
|
|
38
38
|
*/
|
|
39
39
|
public Publish(topic: string, payload: Record<string, unknown>): void {
|
|
40
40
|
if (this._pubSub) {
|
|
41
|
-
console.log(`[PubSubManager] Publishing to topic "${topic}":`, JSON.stringify(payload).substring(0, 200));
|
|
42
41
|
this._pubSub.publish(topic, payload);
|
|
43
42
|
} else {
|
|
44
43
|
console.warn(`[PubSubManager] Cannot publish to "${topic}" — PubSubEngine not set`);
|
|
@@ -66,12 +66,17 @@ export class ResolverBase {
|
|
|
66
66
|
* @returns The processed data object
|
|
67
67
|
*/
|
|
68
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
|
+
|
|
69
74
|
// for the given entity name provided, check to see if there are any fields
|
|
70
75
|
// where the code name is different from the field name, and for just those
|
|
71
76
|
// fields, iterate through the dataObject and REPLACE the property that has the field name
|
|
72
77
|
// with the CodeName, because we can't transfer those via GraphQL as they are not
|
|
73
78
|
// valid property names in GraphQL
|
|
74
|
-
|
|
79
|
+
{
|
|
75
80
|
const md = new Metadata();
|
|
76
81
|
const entityInfo = md.Entities.find((e) => e.Name === entityName);
|
|
77
82
|
if (!entityInfo) throw new Error(`Entity ${entityName} not found in metadata`);
|
|
@@ -1116,8 +1121,9 @@ export class ResolverBase {
|
|
|
1116
1121
|
entityObject.SetMany(input);
|
|
1117
1122
|
}
|
|
1118
1123
|
} else {
|
|
1119
|
-
//
|
|
1120
|
-
|
|
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`, {
|
|
1121
1127
|
extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
|
|
1122
1128
|
});
|
|
1123
1129
|
}
|
|
@@ -1352,7 +1358,14 @@ export class ResolverBase {
|
|
|
1352
1358
|
if (await this.BeforeDelete(provider, key)) {
|
|
1353
1359
|
// fire event and proceed if it wasn't cancelled
|
|
1354
1360
|
const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
|
|
1355
|
-
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
|
+
}
|
|
1356
1369
|
const returnValue = entityObject.GetAll(); // grab the values before we delete so we can return last state before delete if we are successful.
|
|
1357
1370
|
|
|
1358
1371
|
this.ListenForEntityMessages(entityObject, pubSub, userPayload);
|
package/src/index.ts
CHANGED
|
@@ -122,6 +122,7 @@ export * from './resolvers/UserFavoriteResolver.js';
|
|
|
122
122
|
export * from './resolvers/UserResolver.js';
|
|
123
123
|
export * from './resolvers/UserViewResolver.js';
|
|
124
124
|
export * from './resolvers/VersionHistoryResolver.js';
|
|
125
|
+
export * from './resolvers/CurrentUserContextResolver.js';
|
|
125
126
|
export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
|
|
126
127
|
|
|
127
128
|
export * from './generated/generated.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver for the `CurrentUserTenantContext` GraphQL query.
|
|
3
|
+
*
|
|
4
|
+
* Returns the server-set `TenantContext` from the authenticated user's `UserInfo`.
|
|
5
|
+
* This is populated by post-auth middleware (e.g., BCSaaS's `bcTenantContextMiddleware`)
|
|
6
|
+
* and serialized as JSON so the client can auto-stamp `UserInfo.TenantContext`.
|
|
7
|
+
*
|
|
8
|
+
* On the client, `GraphQLDataProvider.GetCurrentUser()` batches this query alongside
|
|
9
|
+
* `CurrentUser` — making plugins stack-layer agnostic without any client-side code.
|
|
10
|
+
*
|
|
11
|
+
* Returns `null` when no middleware has set TenantContext (vanilla MJ deployment).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Query, Resolver, Ctx } from 'type-graphql';
|
|
15
|
+
import { GraphQLJSONObject } from 'graphql-type-json';
|
|
16
|
+
import { AppContext } from '../types.js';
|
|
17
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
18
|
+
|
|
19
|
+
@Resolver()
|
|
20
|
+
export class CurrentUserContextResolver extends ResolverBase {
|
|
21
|
+
@Query(() => GraphQLJSONObject, {
|
|
22
|
+
nullable: true,
|
|
23
|
+
description: 'Returns the server-set TenantContext for the authenticated user. Null when no tenant middleware is active.',
|
|
24
|
+
})
|
|
25
|
+
async CurrentUserTenantContext(
|
|
26
|
+
@Ctx() context: AppContext
|
|
27
|
+
): Promise<Record<string, unknown> | null> {
|
|
28
|
+
await this.CheckAPIKeyScopeAuthorization('user:read', '*', context.userPayload);
|
|
29
|
+
|
|
30
|
+
const userRecord = context.userPayload.userRecord;
|
|
31
|
+
if (!userRecord?.TenantContext) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Serialize the full TenantContext (which may be an extended type like BCTenantContext).
|
|
36
|
+
// JSON serialization captures all enumerable properties including those from subtypes.
|
|
37
|
+
return { ...userRecord.TenantContext } as Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Dynamic Plugin for Phase 2 DynamicPackageLoader testing.
|
|
3
|
+
*
|
|
4
|
+
* This simple plugin validates the DynamicPackageLoader → MJServer pipeline:
|
|
5
|
+
* - Startup function is called and returns extensibility config
|
|
6
|
+
* - Post-auth middleware is injected and runs on each authenticated request
|
|
7
|
+
* - Middleware has access to the authenticated user via req.userPayload
|
|
8
|
+
*
|
|
9
|
+
* To enable: add to dynamicPackages.server in packages/MJAPI/mj.config.cjs
|
|
10
|
+
* To disable: set Enabled: false or remove the entry
|
|
11
|
+
*/
|
|
12
|
+
import type { RequestHandler } from 'express';
|
|
13
|
+
|
|
14
|
+
interface UserPayload {
|
|
15
|
+
email?: string;
|
|
16
|
+
userRecord?: { ID?: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Post-auth middleware that logs authenticated requests */
|
|
20
|
+
const testPostAuthMiddleware: RequestHandler = (req, _res, next) => {
|
|
21
|
+
const userPayload = (req as { userPayload?: UserPayload }).userPayload;
|
|
22
|
+
const email = userPayload?.email ?? 'unknown';
|
|
23
|
+
console.log(`[TestPlugin] Post-auth middleware: ${req.method} ${req.path} (user: ${email})`);
|
|
24
|
+
next();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Startup function called by DynamicPackageLoader.
|
|
29
|
+
* Returns extensibility config that gets merged into server options.
|
|
30
|
+
*/
|
|
31
|
+
export function LoadTestPlugin(): Record<string, unknown> {
|
|
32
|
+
console.log('[TestPlugin] Startup function called');
|
|
33
|
+
return {
|
|
34
|
+
ExpressMiddlewarePostAuth: [testPostAuthMiddleware],
|
|
35
|
+
};
|
|
36
|
+
}
|