@memberjunction/server 0.9.165 → 0.9.168

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.
@@ -2,7 +2,8 @@ import { Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver
2
2
  import { LogError, LogStatus, Metadata, RunQuery, RunQueryParams, RunView, UserInfo } from '@memberjunction/core';
3
3
  import { AppContext, UserPayload } from '../types';
4
4
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
5
- import { SkipDataContext, SkipDataContextItem, SkipAPIRequest, SkipAPIResponse, SkipMessage, SkipAPIAnalysisCompleteResponse, SkipAPIDataRequestResponse, SkipAPIClarifyingQuestionResponse } from '@memberjunction/skip-types';
5
+ import { DataContext, DataContextItem } from '@memberjunction/data-context'
6
+ import { SkipAPIRequest, SkipAPIResponse, SkipMessage, SkipAPIAnalysisCompleteResponse, SkipAPIDataRequestResponse, SkipAPIClarifyingQuestionResponse } from '@memberjunction/skip-types';
6
7
  import axios from 'axios';
7
8
  import zlib from 'zlib';
8
9
  import { promisify } from 'util';
@@ -10,7 +11,7 @@ import { promisify } from 'util';
10
11
  const gzip = promisify(zlib.gzip);
11
12
 
12
13
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver';
13
- import { ConversationDetailEntity, ConversationEntity, DataContextEntity, DataContextItemEntity, UserNotificationEntity, UserViewEntityExtended } from '@memberjunction/core-entities';
14
+ import { ConversationDetailEntity, ConversationEntity, DataContextEntity, DataContextItemEntity, UserNotificationEntity, UserViewEntity, UserViewEntityExtended } from '@memberjunction/core-entities';
14
15
  import { DataSource } from 'typeorm';
15
16
  import { ___skipAPIOrgId, ___skipAPIurl } from '../config';
16
17
 
@@ -64,16 +65,16 @@ export class AskSkipResolver {
64
65
  @Query(() => AskSkipResultType)
65
66
  async ExecuteAskSkipAnalysisQuery(
66
67
  @Arg('UserQuestion', () => String) UserQuestion: string,
67
- @Arg('ViewId', () => Int) ViewId: number,
68
68
  @Arg('ConversationId', () => Int) ConversationId: number,
69
69
  @Ctx() { dataSource, userPayload }: AppContext,
70
- @PubSub() pubSub: PubSubEngine
70
+ @PubSub() pubSub: PubSubEngine,
71
+ @Arg('DataContextId', () => Int, { nullable: true }) DataContextId?: number
71
72
  ) {
72
73
  const md = new Metadata();
73
74
  const user = UserCache.Instance.Users.find((u) => u.Email === userPayload.email);
74
75
  if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
75
76
 
76
- const {convoEntity, dataContextEntity, convoDetailEntity, dataContext} = await this.HandleCreationOfEntityObjects(dataSource, ConversationId, UserQuestion, user, userPayload, md, ViewId);
77
+ const {convoEntity, dataContextEntity, convoDetailEntity, dataContext} = await this.HandleSkipInitialObjectLoading(dataSource, ConversationId, UserQuestion, user, userPayload, md, DataContextId);
77
78
 
78
79
  const OrganizationId = ___skipAPIOrgId;
79
80
 
@@ -100,16 +101,17 @@ export class AskSkipResolver {
100
101
  return this.HandleSkipRequest(input, UserQuestion, user, dataSource, ConversationId, userPayload, pubSub, md, convoEntity, convoDetailEntity, dataContext, dataContextEntity);
101
102
  }
102
103
 
103
- protected async HandleCreationOfEntityObjects(dataSource: DataSource,
104
- ConversationId: number,
105
- UserQuestion: string,
106
- user: UserInfo,
107
- userPayload: UserPayload,
108
- md: Metadata,
109
- ViewId: number): Promise<{convoEntity: ConversationEntity,
110
- dataContextEntity: DataContextEntity,
111
- convoDetailEntity: ConversationDetailEntity,
112
- dataContext: SkipDataContext}> {
104
+
105
+ protected async HandleSkipInitialObjectLoading(dataSource: DataSource,
106
+ ConversationId: number,
107
+ UserQuestion: string,
108
+ user: UserInfo,
109
+ userPayload: UserPayload,
110
+ md: Metadata,
111
+ DataContextId: number): Promise<{convoEntity: ConversationEntity,
112
+ dataContextEntity: DataContextEntity,
113
+ convoDetailEntity: ConversationDetailEntity,
114
+ dataContext: DataContext}> {
113
115
  const convoEntity = <ConversationEntity>await md.GetEntityObject('Conversations', user);
114
116
  let dataContextEntity: DataContextEntity;
115
117
 
@@ -121,21 +123,27 @@ export class AskSkipResolver {
121
123
  convoEntity.Name = AskSkipResolver._defaultNewChatName;
122
124
 
123
125
  dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
124
- dataContextEntity.NewRecord();
125
- dataContextEntity.UserID = user.ID;
126
- dataContextEntity.Name = 'Data Context for Skip Conversation';
127
- if (await dataContextEntity.Save()) {
128
- convoEntity.DataContextID = dataContextEntity.ID;
129
- if (await convoEntity.Save()) {
130
- ConversationId = convoEntity.ID;
126
+ if (!DataContextId || DataContextId <= 0) {
127
+ dataContextEntity.NewRecord();
128
+ dataContextEntity.UserID = user.ID;
129
+ dataContextEntity.Name = 'Data Context for Skip Conversation';
130
+ if (!await dataContextEntity.Save())
131
+ throw new Error(`Creating a new data context failed`);
132
+ }
133
+ else {
134
+ await dataContextEntity.Load(DataContextId);
135
+ }
136
+ convoEntity.DataContextID = dataContextEntity.ID;
137
+ if (await convoEntity.Save()) {
138
+ ConversationId = convoEntity.ID;
139
+ if (!DataContextId || dataContextEntity.ID <= 0) {
140
+ // only do this if we created a new data context for this conversation
131
141
  dataContextEntity.Name += ` ${ConversationId}`;
132
- await dataContextEntity.Save();
142
+ await dataContextEntity.Save();
133
143
  }
134
- else
135
- throw new Error(`Creating a new conversation failed`);
136
144
  }
137
- else
138
- throw new Error(`Creating a new data context failed`);
145
+ else
146
+ throw new Error(`Creating a new conversation failed`);
139
147
  }
140
148
  else {
141
149
  throw new Error(`User ${userPayload.email} not found in UserCache`);
@@ -143,6 +151,11 @@ export class AskSkipResolver {
143
151
  } else {
144
152
  await convoEntity.Load(ConversationId); // load the existing conversation, will need it later
145
153
  dataContextEntity = await md.GetEntityObject<DataContextEntity>('Data Contexts', user);
154
+
155
+ // note - we ignore the parameter DataContextId if it is passed in, we will use the data context from the conversation that is saved. If a user wants to change the data context for a convo, they can do that elsewhere
156
+ if (DataContextId && DataContextId > 0 && DataContextId !== convoEntity.DataContextID)
157
+ console.log(`AskSkipResolver: DataContextId ${DataContextId} was passed in but it was ignored because it was different than the DataContextID in the conversation ${convoEntity.DataContextID}`);
158
+
146
159
  await dataContextEntity.Load(convoEntity.DataContextID);
147
160
  }
148
161
 
@@ -156,7 +169,7 @@ export class AskSkipResolver {
156
169
  convoDetailEntity.Set('Sequence', 1); // using weakly typed here because we're going to get rid of this field soon
157
170
  await convoDetailEntity.Save();
158
171
 
159
- const dataContext: SkipDataContext = new SkipDataContext();
172
+ const dataContext: DataContext = new DataContext();
160
173
  const dciEntityInfo = md.Entities.find((e) => e.Name === 'Data Context Items');
161
174
  if (!dciEntityInfo)
162
175
  throw new Error(`Data Context Items entity not found`);
@@ -167,25 +180,52 @@ export class AskSkipResolver {
167
180
  throw new Error(`Error running SQL: ${sql}`);
168
181
  else {
169
182
  for (const r of result) {
170
- const item = new SkipDataContextItem();
183
+ const item = new DataContextItem();
171
184
  item.Type = r.Type;
172
- item.RecordID = r.RecordID;
173
- item.RecordName = r.RecordName;
185
+ switch (item.Type) {
186
+ case 'full_entity':
187
+ item.EntityID = r.EntityID;
188
+ break;
189
+ case 'single_record':
190
+ item.RecordID = r.RecordID;
191
+ item.EntityID = r.EntityID;
192
+ break;
193
+ case 'query':
194
+ item.QueryID = r.QueryID; // map the QueryID in our database to the RecordID field in the object model for runtime use
195
+ break;
196
+ case 'sql':
197
+ item.SQL = r.SQL;
198
+ break;
199
+ case 'view':
200
+ item.ViewID = r.ViewID;
201
+ item.EntityID = r.EntityID;
202
+ break;
203
+ }
204
+ if (item.EntityID) {
205
+ item.Entity = md.Entities.find((e) => e.ID === item.EntityID);
206
+ item.EntityName = item.Entity.Name;
207
+ if (item.Type === 'full_entity')
208
+ item.RecordName = item.EntityName;
209
+ }
210
+ if (item.Type === 'query' && item.QueryID) {
211
+ const q = md.Queries.find((q) => q.ID === item.QueryID);
212
+ item.RecordName = q?.Name;
213
+ }
214
+ if (item.Type === 'view' && item.ViewID) {
215
+ const v = await md.GetEntityObject<UserViewEntityExtended>('User Views', user);
216
+ await v.Load(item.ViewID);
217
+ item.RecordName = v.Name;
218
+ item.ViewEntity = v;
219
+ }
174
220
  item.Data = r.Data && r.Data.length > 0 ? JSON.parse(r.Data) : item.Data; // parse the stored data if it was saved, otherwise leave it to whatever the object's default is
175
221
  item.AdditionalDescription = r.AdditionalDescription;
222
+ item.DataContextItemID = r.ID;
176
223
  dataContext.Items.push(item);
177
224
  }
178
225
  }
179
226
 
180
- // now if we don't already have this view in our data context, we will add it
181
- if (ViewId && !dataContext.Items.find(i => i.Type === 'view' && i.RecordID === ViewId))
182
- dataContext.Items.push(
183
- {
184
- Type: 'view',
185
- RecordID: ViewId,
186
- Data: await this.getViewData(ViewId, user),
187
- } as SkipDataContextItem
188
- );
227
+
228
+ /// TODO next up we need to modify MJExplorer to handle the data context stuff and then we can finish this method
189
229
 
190
230
  return {dataContext, convoEntity, dataContextEntity, convoDetailEntity};
191
231
  }
@@ -247,7 +287,7 @@ export class AskSkipResolver {
247
287
  protected async HandleSkipRequest(input: SkipAPIRequest, UserQuestion: string, user: UserInfo, dataSource: DataSource,
248
288
  ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, md: Metadata,
249
289
  convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
250
- dataContext: SkipDataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
290
+ dataContext: DataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
251
291
  LogStatus(`Sending request to Skip API: ${___skipAPIurl}`)
252
292
 
253
293
  // Convert JSON payload to a Buffer and compress it
@@ -331,7 +371,7 @@ export class AskSkipResolver {
331
371
 
332
372
  protected async HandleAnalysisComplete(apiRequest: SkipAPIRequest, apiResponse: SkipAPIAnalysisCompleteResponse, UserQuestion: string, user: UserInfo, dataSource: DataSource,
333
373
  ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
334
- dataContext: SkipDataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
374
+ dataContext: DataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
335
375
  // analysis is complete
336
376
  // all done, wrap things up
337
377
  const md = new Metadata();
@@ -383,7 +423,7 @@ export class AskSkipResolver {
383
423
 
384
424
  protected async HandleDataRequestPhase(apiRequest: SkipAPIRequest, apiResponse: SkipAPIDataRequestResponse, UserQuestion: string, user: UserInfo, dataSource: DataSource,
385
425
  ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
386
- dataContext: SkipDataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
426
+ dataContext: DataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
387
427
  // 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
388
428
  try {
389
429
  const _maxDataGatheringRetries = 5;
@@ -407,10 +447,10 @@ export class AskSkipResolver {
407
447
  if (!result)
408
448
  throw new Error(`Error running SQL: ${sql}`);
409
449
 
410
- const item = new SkipDataContextItem();
450
+ const item = new DataContextItem();
411
451
  item.Type = 'sql';
412
452
  item.Data = result;
413
- item.RecordName = dr.text;
453
+ item.SQL = dr.text;
414
454
  item.AdditionalDescription = dr.description;
415
455
  dataContext.Items.push(item);
416
456
  break;
@@ -421,10 +461,10 @@ export class AskSkipResolver {
421
461
  const rq = new RunQuery();
422
462
  const result = await rq.RunQuery({QueryID: query.ID}, user)
423
463
  if (result && result.Success) {
424
- const item = new SkipDataContextItem();
464
+ const item = new DataContextItem();
425
465
  item.Type = 'query';
426
466
  item.Data = result.Results;
427
- item.RecordID = query.ID;
467
+ item.QueryID = query.ID;
428
468
  item.RecordName = query.Name;
429
469
  item.AdditionalDescription = dr.description;
430
470
  dataContext.Items.push(item);
@@ -491,7 +531,7 @@ export class AskSkipResolver {
491
531
  * @param userPayload
492
532
  * @returns
493
533
  */
494
- protected async FinishConversationAndNotifyUser(apiResponse: SkipAPIAnalysisCompleteResponse, dataContext: SkipDataContext, dataContextEntity: DataContextEntity, md: Metadata, user: UserInfo, convoEntity: ConversationEntity, pubSub: PubSubEngine, userPayload: UserPayload): Promise<{AIMessageConversationDetailID: number}> {
534
+ protected async FinishConversationAndNotifyUser(apiResponse: SkipAPIAnalysisCompleteResponse, dataContext: DataContext, dataContextEntity: DataContextEntity, md: Metadata, user: UserInfo, convoEntity: ConversationEntity, pubSub: PubSubEngine, userPayload: UserPayload): Promise<{AIMessageConversationDetailID: number}> {
495
535
  const sTitle = apiResponse.reportTitle;
496
536
  const sResult = JSON.stringify(apiResponse);
497
537
 
@@ -532,9 +572,24 @@ export class AskSkipResolver {
532
572
  dciEntity.NewRecord();
533
573
  dciEntity.DataContextID = dataContextEntity.ID;
534
574
  dciEntity.Type = item.Type;
535
- dciEntity.RecordID = item.RecordID;
536
- if (item.Type === 'sql')
537
- dciEntity.SQL = item.RecordName; // the SQL field in the database is where we store the SQL, in the object model it ends up in the RecordName property, mapping it here
575
+ switch (item.Type) {
576
+ case 'full_entity':
577
+ case 'single_record':
578
+ const e = item.Entity || md.Entities.find((e) => e.Name === item.EntityName);
579
+ dciEntity.EntityID = e.ID;
580
+ if (item.Type === 'single_record')
581
+ dciEntity.RecordID = item.RecordID;
582
+ break;
583
+ case 'view':
584
+ dciEntity.ViewID = item.ViewID;
585
+ break;
586
+ case 'query':
587
+ dciEntity.QueryID = item.QueryID;
588
+ break;
589
+ case 'sql':
590
+ dciEntity.SQL = item.SQL;
591
+ break;
592
+ }
538
593
  dciEntity.DataJSON = JSON.stringify(item.Data);
539
594
  await dciEntity.Save();
540
595
  }