@memberjunction/server 2.13.4 → 2.15.2

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.
@@ -1,18 +1,34 @@
1
- import { BaseEntity, CompositeKey, EntityFieldTSType, EntityPermissionType, LogError, Metadata, RunView, RunViewParams, RunViewResult, UserInfo } from '@memberjunction/core';
1
+ import {
2
+ BaseEntity,
3
+ BaseEntityEvent,
4
+ CompositeKey,
5
+ EntityFieldTSType,
6
+ EntityPermissionType,
7
+ LogError,
8
+ Metadata,
9
+ RunView,
10
+ RunViewParams,
11
+ RunViewResult,
12
+ UserInfo,
13
+ } from '@memberjunction/core';
2
14
  import { AuditLogEntity, UserViewEntity } from '@memberjunction/core-entities';
3
15
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
4
16
  import { PubSubEngine } from 'type-graphql';
5
17
  import { GraphQLError } from 'graphql';
6
18
  import { DataSource } from 'typeorm';
19
+ import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
7
20
 
8
21
  import { RunViewGenericParams, UserPayload } from '../types.js';
9
22
  import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver.js';
10
23
  import { DeleteOptionsInput } from './DeleteOptionsInput.js';
11
- import { MJGlobal } from '@memberjunction/global';
24
+ import { MJEvent, MJEventType, MJGlobal } from '@memberjunction/global';
12
25
  import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver.js';
13
26
  import { FieldMapper } from '@memberjunction/graphql-dataprovider';
14
27
 
15
28
  export class ResolverBase {
29
+ private _emit = process.env.CLOUDEVENTS_HTTP_TRANSPORT ? emitterFor(httpTransport(process.env.CLOUDEVENTS_HTTP_TRANSPORT)) : null;
30
+ private _cloudeventsHeaders = process.env.CLOUDEVENTS_HTTP_HEADERS ? JSON.parse(process.env.CLOUDEVENTS_HTTP_HEADERS) : {};
31
+
16
32
  protected MapFieldNamesToCodeNames(entityName: string, dataObject: any) {
17
33
  // for the given entity name provided, check to see if there are any fields
18
34
  // where the code name is different from the field name, and for just those
@@ -28,7 +44,7 @@ export class ResolverBase {
28
44
  entityInfo.Fields.forEach((f) => {
29
45
  if (dataObject.hasOwnProperty(f.Name)) {
30
46
  // GraphQL doesn't allow us to pass back fields with __ so we are mapping our special field cases that start with __mj_ to _mj__ for transport - they are converted back on the other side automatically
31
- const mappedFieldName = mapper.MapFieldName(f.CodeName)
47
+ const mappedFieldName = mapper.MapFieldName(f.CodeName);
32
48
  if (mappedFieldName !== f.Name) {
33
49
  dataObject[mappedFieldName] = dataObject[f.Name];
34
50
  delete dataObject[f.Name];
@@ -134,7 +150,7 @@ export class ResolverBase {
134
150
  if (!entity) throw new Error(`Entity ${viewInput.EntityName} not found in metadata`);
135
151
 
136
152
  const viewInfo: UserViewEntity = {
137
- ID: "",
153
+ ID: '',
138
154
  Entity: viewInput.EntityName,
139
155
  EntityID: entity.ID,
140
156
  EntityBaseView: entity.BaseView as string,
@@ -164,22 +180,23 @@ export class ResolverBase {
164
180
  }
165
181
  }
166
182
 
167
- async RunViewsGeneric(viewInputs: (RunViewByNameInput & RunViewByIDInput & RunDynamicViewInput)[], dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
183
+ async RunViewsGeneric(
184
+ viewInputs: (RunViewByNameInput & RunViewByIDInput & RunDynamicViewInput)[],
185
+ dataSource: DataSource,
186
+ userPayload: UserPayload,
187
+ pubSub: PubSubEngine
188
+ ) {
168
189
  let md: Metadata | null = null;
169
190
  let params: RunViewGenericParams[] = [];
170
- for(const viewInput of viewInputs) {
191
+ for (const viewInput of viewInputs) {
171
192
  try {
172
193
  let viewInfo: UserViewEntity | null = null;
173
194
 
174
- if(viewInput.ViewName) {
175
- viewInfo = this.safeFirstArrayElement(
176
- await this.findBy(dataSource, 'User Views', { Name: viewInput.ViewName })
177
- );
178
- }
179
- else if(viewInput.ViewID) {
195
+ if (viewInput.ViewName) {
196
+ viewInfo = this.safeFirstArrayElement(await this.findBy(dataSource, 'User Views', { Name: viewInput.ViewName }));
197
+ } else if (viewInput.ViewID) {
180
198
  viewInfo = this.safeFirstArrayElement(await this.findBy(dataSource, 'User Views', { ID: viewInput.ViewID }));
181
- }
182
- else if(viewInput.EntityName) {
199
+ } else if (viewInput.EntityName) {
183
200
  md = md || new Metadata();
184
201
  const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
185
202
  if (!entity) {
@@ -188,14 +205,13 @@ export class ResolverBase {
188
205
 
189
206
  // only providing a few bits of data here, but it's enough to get the view to run
190
207
  viewInfo = {
191
- ID: "",
208
+ ID: '',
192
209
  Entity: viewInput.EntityName,
193
210
  EntityID: entity.ID,
194
211
  EntityBaseView: entity.BaseView,
195
212
  } as UserViewEntity;
196
- }
197
- else{
198
- throw new Error("Unable to determine input type");
213
+ } else {
214
+ throw new Error('Unable to determine input type');
199
215
  }
200
216
 
201
217
  params.push({
@@ -214,9 +230,8 @@ export class ResolverBase {
214
230
  auditLogDescription: viewInput.AuditLogDescription,
215
231
  resultType: viewInput.ResultType,
216
232
  userPayload,
217
- pubSub
233
+ pubSub,
218
234
  });
219
-
220
235
  } catch (err) {
221
236
  LogError(err);
222
237
  return null;
@@ -227,7 +242,26 @@ export class ResolverBase {
227
242
  return results;
228
243
  }
229
244
 
245
+ protected async EmitCloudEvent({ component, event, eventCode, args }: MJEvent) {
246
+ if (this._emit && event === MJEventType.ComponentEvent && eventCode === BaseEntity.BaseEventCode) {
247
+ const extendedType = args instanceof BaseEntityEvent ? `.${args.type}` : '';
248
+ const type = `MemberJunction.${event}${extendedType}`;
249
+ const source = `${process.env.CLOUDEVENTS_SOURCE ?? 'MemberJunction'}`;
250
+ const [subject, rawData] = args instanceof BaseEntityEvent ? [args.baseEntity.EntityInfo.CodeName, args.payload] : [undefined, args];
251
+ const data = (typeof rawData === 'object' ? rawData : { payload: rawData }) ?? {};
252
+ const cloudEvent = new CloudEvent({ source, subject, type, data });
230
253
 
254
+ try {
255
+ const cloudeventTransportResponse = await this._emit(cloudEvent, { headers: this._cloudeventsHeaders });
256
+ const cloudeventResponse = JSON.stringify(cloudeventTransportResponse);
257
+ if (/error/i.test(cloudeventResponse)) {
258
+ console.error('CloudEvent ERROR', cloudeventResponse);
259
+ }
260
+ } catch (e) {
261
+ console.error('CloudEvent ERROR', JSON.stringify(e));
262
+ }
263
+ }
264
+ }
231
265
 
232
266
  protected CheckUserReadPermissions(entityName: string, userPayload: UserPayload | null) {
233
267
  const md = new Metadata();
@@ -326,10 +360,12 @@ export class ResolverBase {
326
360
  const rv = new RunView();
327
361
  let RunViewParams: RunViewParams[] = [];
328
362
  let contextUser: UserInfo | null = null;
329
- for(const param of params){
363
+ for (const param of params) {
330
364
  if (param.viewInfo && param.userPayload) {
331
365
  md = md || new Metadata();
332
- const user: UserInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === param.userPayload?.email.toLowerCase().trim());
366
+ const user: UserInfo = UserCache.Users.find(
367
+ (u) => u.Email.toLowerCase().trim() === param.userPayload?.email.toLowerCase().trim()
368
+ );
333
369
  if (!user) {
334
370
  throw new Error(`User ${param.userPayload?.email} not found in metadata`);
335
371
  }
@@ -337,7 +373,7 @@ export class ResolverBase {
337
373
  contextUser = contextUser || user;
338
374
 
339
375
  const entityInfo = md.Entities.find((e) => e.Name === param.viewInfo.Entity);
340
- if (!entityInfo){
376
+ if (!entityInfo) {
341
377
  throw new Error(`Entity ${param.viewInfo.Entity} not found in metadata`);
342
378
  }
343
379
  }
@@ -381,7 +417,7 @@ export class ResolverBase {
381
417
 
382
418
  // go through the result and convert all fields that start with __mj_*** to _mj__*** for GraphQL transport
383
419
  const mapper = new FieldMapper();
384
- for(const runViewResult of runViewResults){
420
+ for (const runViewResult of runViewResults) {
385
421
  if (runViewResult && runViewResult.Success) {
386
422
  for (const result of runViewResult.Results) {
387
423
  mapper.MapFields(result);
@@ -390,8 +426,7 @@ export class ResolverBase {
390
426
  }
391
427
 
392
428
  return runViewResults;
393
- }
394
- catch (err) {
429
+ } catch (err) {
395
430
  console.log(err);
396
431
  throw err;
397
432
  }
@@ -453,8 +488,9 @@ export class ResolverBase {
453
488
  auditLog.UserID = userInfo.ID;
454
489
  auditLog.AuditLogTypeID = auditLogType.ID;
455
490
 
456
- if (authorization)
491
+ if (authorization) {
457
492
  auditLog.AuthorizationID = authorization.ID;
493
+ }
458
494
 
459
495
  if (status?.trim().toLowerCase() === 'success') auditLog.Status = 'Success';
460
496
  else auditLog.Status = 'Failed';
@@ -501,8 +537,10 @@ export class ResolverBase {
501
537
 
502
538
  protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
503
539
  // 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
504
- MJGlobal.Instance.GetEventListener(false).subscribe((event) => {
540
+ MJGlobal.Instance.GetEventListener(false).subscribe(async (event) => {
505
541
  if (event) {
542
+ await this.EmitCloudEvent(event);
543
+
506
544
  if (event.component === entityObject && event.args && event.args.message) {
507
545
  // message from our entity object, relay it to the client
508
546
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
@@ -554,8 +592,9 @@ export class ResolverBase {
554
592
  const entityInfo = entityObject.EntityInfo;
555
593
  const clientNewValues = {};
556
594
  Object.keys(input).forEach((key) => {
557
- if (key !== 'OldValues___')
595
+ if (key !== 'OldValues___') {
558
596
  clientNewValues[key] = input[key];
597
+ }
559
598
  }); // grab all the props except for the OldValues property
560
599
 
561
600
  if (entityInfo.TrackRecordChanges || !input.OldValues___) {
@@ -574,8 +613,7 @@ export class ResolverBase {
574
613
  if (input.OldValues___) {
575
614
  // we DO have OldValues, so we need to do a more in depth analysis
576
615
  this.TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject);
577
- }
578
- else {
616
+ } else {
579
617
  // no OldValues, so we can just set the new values from input
580
618
  entityObject.SetMany(input);
581
619
  }
@@ -585,8 +623,7 @@ export class ResolverBase {
585
623
  extensions: { code: 'LOAD_ENTITY_ERROR', entityName },
586
624
  });
587
625
  }
588
- }
589
- else {
626
+ } else {
590
627
  // we get here if we are NOT tracking changes and we DO have OldValues, so we can load from them
591
628
  const oldValues = {};
592
629
  // for each item in the oldValues array, add it to the oldValues object
@@ -318,6 +318,7 @@ export class AskSkipResolver {
318
318
  AskSkipResolver._maxHistoricalMessages
319
319
  );
320
320
 
321
+ const conversationDetailCount = 1
321
322
  const input = this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'initial_request', true, true);
322
323
 
323
324
  return this.HandleSkipRequest(
@@ -332,7 +333,8 @@ export class AskSkipResolver {
332
333
  convoEntity,
333
334
  convoDetailEntity,
334
335
  dataContext,
335
- dataContextEntity
336
+ dataContextEntity,
337
+ conversationDetailCount,
336
338
  );
337
339
  }
338
340
 
@@ -677,10 +679,34 @@ export class AskSkipResolver {
677
679
  convoEntity: ConversationEntity,
678
680
  convoDetailEntity: ConversationDetailEntity,
679
681
  dataContext: DataContext,
680
- dataContextEntity: DataContextEntity
682
+ dataContextEntity: DataContextEntity,
683
+ conversationDetailCount: number
681
684
  ): Promise<AskSkipResultType> {
682
685
  LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${___skipAPIurl}`);
683
686
 
687
+ if (conversationDetailCount > 10) {
688
+ // At this point it is likely that we are stuck in a loop, so we stop here
689
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
690
+ message: JSON.stringify({
691
+ type: 'AskSkip',
692
+ status: 'Error',
693
+ conversationID: ConversationId,
694
+ message: 'Analysis failed to run, please try again later and if this continues, contact your support desk.',
695
+ }),
696
+ sessionId: userPayload.sessionId,
697
+ });
698
+
699
+ return {
700
+ Success: false,
701
+ Status: 'Error',
702
+ Result: `Exceeded maximum attempts to answer the question ${UserQuestion}`,
703
+ ResponsePhase: SkipResponsePhase.AnalysisComplete,
704
+ ConversationId: ConversationId,
705
+ UserMessageConversationDetailId: '',
706
+ AIMessageConversationDetailId: '',
707
+ };
708
+ }
709
+
684
710
  const response = await sendPostRequest(
685
711
  ___skipAPIurl,
686
712
  input,
@@ -736,7 +762,8 @@ export class AskSkipResolver {
736
762
  convoEntity,
737
763
  convoDetailEntity,
738
764
  dataContext,
739
- dataContextEntity
765
+ dataContextEntity,
766
+ conversationDetailCount
740
767
  );
741
768
  } else if (apiResponse.responsePhase === 'clarifying_question') {
742
769
  // need to send the request back to the user for a clarifying question
@@ -925,7 +952,8 @@ export class AskSkipResolver {
925
952
  convoEntity: ConversationEntity,
926
953
  convoDetailEntity: ConversationDetailEntity,
927
954
  dataContext: DataContext,
928
- dataContextEntity: DataContextEntity
955
+ dataContextEntity: DataContextEntity,
956
+ conversationDetailCount: number
929
957
  ): Promise<AskSkipResultType> {
930
958
  // our job in this method is to go through each of the data requests from the Skip API, get the data, and then go back to the Skip API again and to the next phase
931
959
  try {
@@ -1039,6 +1067,7 @@ export class AskSkipResolver {
1039
1067
  apiRequest.dataContext = <DataContext>CopyScalarsAndArrays(dataContext); // we are casting this to DataContext as we're pushing this to the Skip API, and we don't want to send the real DataContext object, just a copy of the scalar and array properties
1040
1068
  apiRequest.requestPhase = 'data_gathering_response';
1041
1069
  }
1070
+ conversationDetailCount++;
1042
1071
  // we have all of the data now, add it to the data context and then submit it back to the Skip API
1043
1072
  return this.HandleSkipRequest(
1044
1073
  apiRequest,
@@ -1052,7 +1081,8 @@ export class AskSkipResolver {
1052
1081
  convoEntity,
1053
1082
  convoDetailEntity,
1054
1083
  dataContext,
1055
- dataContextEntity
1084
+ dataContextEntity,
1085
+ conversationDetailCount
1056
1086
  );
1057
1087
  } catch (e) {
1058
1088
  LogError(e);