@memberjunction/server 2.23.2 → 2.24.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.
@@ -27,9 +27,9 @@ import { FieldMapper } from '@memberjunction/graphql-dataprovider';
27
27
  import { Subscription } from 'rxjs';
28
28
 
29
29
  export class ResolverBase {
30
- private _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
31
- private _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
32
- private _eventSubscription: Subscription | null = null;
30
+ private static _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
31
+ private static _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
32
+ private static _eventSubscriptions = new Map<string, Subscription>;
33
33
 
34
34
  protected MapFieldNamesToCodeNames(entityName: string, dataObject: any) {
35
35
  // for the given entity name provided, check to see if there are any fields
@@ -245,7 +245,7 @@ export class ResolverBase {
245
245
  }
246
246
 
247
247
  protected async EmitCloudEvent({ component, event, eventCode, args }: MJEvent) {
248
- if (this._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
248
+ if (ResolverBase._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
249
249
  const extendedType = args instanceof BaseEntityEvent ? `.${args.type}` : '';
250
250
  const type = `MemberJunction.${event}${extendedType}`;
251
251
  const source = `${process.env.CLOUDEVENTS_SOURCE ?? 'MemberJunction'}`;
@@ -255,7 +255,7 @@ export class ResolverBase {
255
255
  const cloudEvent = new CloudEvent({ type, source, subject, data });
256
256
 
257
257
  try {
258
- const cloudeventTransportResponse = await this._emit(cloudEvent, { headers: this._cloudeventsHeaders });
258
+ const cloudeventTransportResponse = await ResolverBase._emit(cloudEvent, { headers: ResolverBase._cloudeventsHeaders });
259
259
  const cloudeventResponse = JSON.stringify(cloudeventTransportResponse);
260
260
  if (/error/i.test(cloudeventResponse)) {
261
261
  console.error('CloudEvent ERROR', cloudeventResponse);
@@ -548,27 +548,34 @@ export class ResolverBase {
548
548
  }
549
549
 
550
550
  protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
551
- if (!this._eventSubscription) {
551
+ // 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
552
+ // entity in the system. This is important because we don't want to have multiple listeners for the same entity as it could
553
+ // cause issues with multiple messages for the same event.
554
+ const uniqueKey = entityObject.EntityInfo.Name;
555
+
556
+ if (!ResolverBase._eventSubscriptions.has(uniqueKey)) {
552
557
  // listen for events from the entityObject in case it is a long running task and we can push messages back to the client via pubSub
553
- this._eventSubscription = MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
558
+ const theSub = MJGlobal.Instance.GetEventListener(false).subscribe(async (event: MJEvent) => {
554
559
  if (event) {
555
560
  await this.EmitCloudEvent(event);
556
561
 
557
- if (event.component === entityObject && event.args && event.args.message) {
562
+ if (event.args && event.args instanceof BaseEntityEvent) {
563
+ const baseEntityEvent = event.args as BaseEntityEvent;
558
564
  // message from our entity object, relay it to the client
559
565
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
560
566
  message: JSON.stringify({
561
567
  status: 'OK',
562
568
  type: 'EntityObjectStatusMessage',
563
- entityName: entityObject.EntityInfo.Name,
564
- primaryKey: entityObject.PrimaryKey,
565
- message: event.args.message,
569
+ entityName: baseEntityEvent.baseEntity.EntityInfo.Name,
570
+ primaryKey: baseEntityEvent.baseEntity.PrimaryKey,
571
+ message: event.args.payload,
566
572
  }),
567
573
  sessionId: userPayload.sessionId,
568
574
  });
569
575
  }
570
576
  }
571
577
  });
578
+ ResolverBase._eventSubscriptions.set(uniqueKey, theSub);
572
579
  }
573
580
  }
574
581
 
package/src/index.ts CHANGED
@@ -69,7 +69,7 @@ export { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
69
69
  export * from './generated/generated.js';
70
70
 
71
71
  import { resolve } from 'node:path';
72
- import { DataSourceInfo } from './types.js';
72
+ import { DataSourceInfo, raiseEvent } from './types.js';
73
73
 
74
74
  export type MJServerOptions = {
75
75
  onBeforeServe?: () => void | Promise<void>;
@@ -125,8 +125,8 @@ export const serve = async (resolverPaths: Array<string>, app = createApp(), opt
125
125
  console.log('Read-only Data Source has been initialized.');
126
126
  }
127
127
 
128
-
129
128
  setupComplete$.next(true);
129
+ raiseEvent('setupComplete', dataSources, null, this);
130
130
 
131
131
  /******TEST HARNESS FOR CHANGE DETECTION */
132
132
  /******TEST HARNESS FOR CHANGE DETECTION */
@@ -1456,6 +1456,17 @@ export class AskSkipResolver {
1456
1456
  LogError(`Error saving user notification entity for AI message: ${sResult}`, undefined, userNotification.LatestResult);
1457
1457
  }
1458
1458
 
1459
+ // check to see if Skip retrieved additional data on his own outside of the DATA_REQUEST phase/process. It is possible for Skip to call back
1460
+ // to the MJAPI in the instance using the GetData() query in the MJAPI. If Skip did this, we need to save the data context items here.
1461
+ if (apiResponse.newDataItems) {
1462
+ apiResponse.newDataItems.forEach((skipItem) => {
1463
+ const newItem = dataContext.AddDataContextItem();
1464
+ newItem.Type = 'sql';
1465
+ newItem.SQL = skipItem.text;
1466
+ newItem.AdditionalDescription = skipItem.description;
1467
+ });
1468
+ }
1469
+
1459
1470
  // Save the data context items...
1460
1471
  // FOR NOW, we don't want to store the data in the database, we will just load it from the data context when we need it
1461
1472
  // we need a better strategy to persist because the cost of storage and retrieval/parsing is higher than just running the query again in many/most cases
package/src/types.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import { UserInfo } from '@memberjunction/core';
1
2
  import { UserViewEntity } from '@memberjunction/core-entities';
2
3
  import { GraphQLSchema } from 'graphql';
3
4
  import { PubSubEngine } from 'type-graphql';
4
5
  import { DataSource, QueryRunner } from 'typeorm';
6
+ import { getSystemUser } from './auth';
7
+ import { MJEvent, MJEventType, MJGlobal } from '@memberjunction/global';
5
8
 
6
9
  export type UserPayload = {
7
10
  email: string;
@@ -69,3 +72,28 @@ export type RunViewGenericParams = {
69
72
  userPayload?: UserPayload;
70
73
  pubSub: PubSubEngine;
71
74
  };
75
+
76
+
77
+ export class MJServerEvent {
78
+ type: 'setupComplete' | 'requestReceived' | 'requestCompleted' | 'requestFailed';
79
+ dataSources: DataSourceInfo[];
80
+ userPayload: UserPayload;
81
+ systemUser: UserInfo;
82
+ }
83
+
84
+ export const MJ_SERVER_EVENT_CODE = 'MJ_SERVER_EVENT';
85
+
86
+ export async function raiseEvent(type: MJServerEvent['type'], dataSources: DataSourceInfo[], userPayload: UserPayload, component?: any) {
87
+ const event = new MJServerEvent();
88
+ event.type = type;
89
+ event.dataSources = dataSources;
90
+ event.userPayload = userPayload;
91
+ event.systemUser = await getSystemUser();
92
+
93
+ const mje = new MJEvent();
94
+ mje.args = event;
95
+ mje.component = component;
96
+ mje.event = MJEventType.ComponentEvent;
97
+ mje.eventCode = MJ_SERVER_EVENT_CODE;
98
+ MJGlobal.Instance.RaiseEvent(mje);
99
+ }