@memberjunction/server 2.23.1 → 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
@@ -118,7 +118,7 @@ export class SyncDataResolver {
118
118
  results.push(await this.SyncSingleItem(item, context, md));
119
119
  }
120
120
 
121
- if (await this.DoSyncItemsAffectMetadata(items)) {
121
+ if (await this.DoSyncItemsAffectMetadata(context.userPayload.userRecord, items)) {
122
122
  await md.Refresh(); // force refesh the metadata which will cause a reload from the DB
123
123
  }
124
124
 
@@ -131,13 +131,13 @@ export class SyncDataResolver {
131
131
  }
132
132
  }
133
133
 
134
- protected async GetLowercaseMetadatEntitiesList(forceRefresh: boolean = false): Promise<string[]> {
134
+ protected async GetLowercaseMetadataEntitiesList(user: UserInfo, forceRefresh: boolean = false): Promise<string[]> {
135
135
  if (forceRefresh || __metadata_DatasetItems.length === 0) {
136
136
  const rv = new RunView(); // cache this, veyr simple - should use an engine for this stuff later
137
137
  const result = await rv.RunView<DatasetItemEntity>({
138
138
  EntityName: "Dataset Items",
139
139
  ExtraFilter: "Dataset = 'MJ_Metadata'",
140
- })
140
+ }, user)
141
141
  if (result && result.Success) {
142
142
  __metadata_DatasetItems.length = 0;
143
143
  __metadata_DatasetItems.push(...result.Results.map((r) => {
@@ -149,9 +149,9 @@ export class SyncDataResolver {
149
149
  return __metadata_DatasetItems;
150
150
  }
151
151
 
152
- protected async DoSyncItemsAffectMetadata(items: ActionItemInputType[]): Promise<boolean> {
152
+ protected async DoSyncItemsAffectMetadata(user: UserInfo, items: ActionItemInputType[]): Promise<boolean> {
153
153
  // check to see if any of the items affect any of these entities:
154
- const entitiesToCheck = await this.GetLowercaseMetadatEntitiesList(false);
154
+ const entitiesToCheck = await this.GetLowercaseMetadataEntitiesList(user, false);
155
155
  for (const item of items) {
156
156
  if (entitiesToCheck.find(e => e === item.EntityName.trim().toLowerCase()) ) {
157
157
  return true;
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
+ }