@memberjunction/server 2.20.2 → 2.20.3

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.
Files changed (38) hide show
  1. package/dist/config.d.ts +5 -1
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +2 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/generated/generated.d.ts +122 -57
  6. package/dist/generated/generated.d.ts.map +1 -1
  7. package/dist/generated/generated.js +1287 -694
  8. package/dist/generated/generated.js.map +1 -1
  9. package/dist/generic/ResolverBase.d.ts.map +1 -1
  10. package/dist/generic/ResolverBase.js +8 -4
  11. package/dist/generic/ResolverBase.js.map +1 -1
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/resolvers/AskSkipResolver.d.ts +14 -5
  17. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  18. package/dist/resolvers/AskSkipResolver.js +250 -61
  19. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  20. package/dist/resolvers/GetDataResolver.d.ts +90 -0
  21. package/dist/resolvers/GetDataResolver.d.ts.map +1 -0
  22. package/dist/resolvers/GetDataResolver.js +294 -0
  23. package/dist/resolvers/GetDataResolver.js.map +1 -0
  24. package/dist/resolvers/SyncDataResolver.d.ts +47 -0
  25. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -0
  26. package/dist/resolvers/SyncDataResolver.js +345 -0
  27. package/dist/resolvers/SyncDataResolver.js.map +1 -0
  28. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  29. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  30. package/package.json +23 -22
  31. package/src/config.ts +2 -0
  32. package/src/generated/generated.ts +899 -610
  33. package/src/generic/ResolverBase.ts +14 -5
  34. package/src/index.ts +2 -0
  35. package/src/resolvers/AskSkipResolver.ts +300 -66
  36. package/src/resolvers/GetDataResolver.ts +245 -0
  37. package/src/resolvers/SyncDataResolver.ts +337 -0
  38. package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
@@ -269,16 +269,25 @@ export class ResolverBase {
269
269
  protected CheckUserReadPermissions(entityName: string, userPayload: UserPayload | null) {
270
270
  const md = new Metadata();
271
271
  const entityInfo = md.Entities.find((e) => e.Name === entityName);
272
- if (!userPayload) throw new Error(`userPayload is null`);
273
-
272
+ if (!userPayload) {
273
+ throw new Error(`userPayload is null`);
274
+ }
275
+
274
276
  // first check permissions, the logged in user must have read permissions on the entity to run the view
275
277
  if (entityInfo) {
276
278
  const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim()); // get the user record from MD so we have ROLES attached, don't use the one from payload directly
277
- if (!userInfo) throw new Error(`User ${userPayload.email} not found in metadata`);
279
+ if (!userInfo) {
280
+ throw new Error(`User ${userPayload.email} not found in metadata`);
281
+ }
278
282
 
279
283
  const userPermissions = entityInfo.GetUserPermisions(userInfo);
280
- if (!userPermissions.CanRead) throw new Error(`User ${userPayload.email} does not have read permissions on ${entityInfo.Name}`);
281
- } else throw new Error(`Entity not found in metadata`);
284
+ if (!userPermissions.CanRead) {
285
+ throw new Error(`User ${userPayload.email} does not have read permissions on ${entityInfo.Name}`);
286
+ }
287
+ }
288
+ else {
289
+ throw new Error(`Entity not found in metadata`);
290
+ }
282
291
  }
283
292
 
284
293
  protected async RunViewGenericInternal(
package/src/index.ts CHANGED
@@ -61,6 +61,8 @@ export * from './resolvers/EntityRecordNameResolver.js';
61
61
  export * from './resolvers/MergeRecordsResolver.js';
62
62
  export * from './resolvers/ReportResolver.js';
63
63
  export * from './resolvers/SyncRolesUsersResolver.js';
64
+ export * from './resolvers/SyncDataResolver.js';
65
+ export * from './resolvers/GetDataResolver.js';
64
66
 
65
67
  export * from './generated/generated.js';
66
68
 
@@ -1,6 +1,8 @@
1
1
  import { Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
2
- import { LogError, LogStatus, Metadata, KeyValuePair, RunView, UserInfo, CompositeKey } from '@memberjunction/core';
2
+ import { LogError, LogStatus, Metadata, KeyValuePair, RunView, UserInfo, CompositeKey, EntityFieldInfo, EntityInfo, EntityRelationshipInfo } from '@memberjunction/core';
3
3
  import { AppContext, UserPayload } from '../types.js';
4
+ import { BehaviorSubject } from 'rxjs';
5
+ import { take } from 'rxjs/operators';
4
6
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
5
7
  import { DataContext } from '@memberjunction/data-context';
6
8
  import { LoadDataContextItemsServer } from '@memberjunction/data-context-server';
@@ -20,6 +22,9 @@ import {
20
22
  SkipRequestPhase,
21
23
  SkipAPIAgentNote,
22
24
  SkipAPIAgentNoteType,
25
+ SkipEntityFieldInfo,
26
+ SkipEntityRelationshipInfo,
27
+ SkipEntityFieldValueInfo,
23
28
  } from '@memberjunction/skip-types';
24
29
 
25
30
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
@@ -31,7 +36,7 @@ import {
31
36
  UserNotificationEntity,
32
37
  } from '@memberjunction/core-entities';
33
38
  import { DataSource } from 'typeorm';
34
- import { ___skipAPIOrgId, ___skipAPIurl, configInfo, mj_core_schema } from '../config.js';
39
+ import { ___skipAPIOrgId, ___skipAPIurl, apiKey, baseUrl, configInfo, graphqlPort, mj_core_schema } from '../config.js';
35
40
 
36
41
  import { registerEnumType } from 'type-graphql';
37
42
  import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
@@ -39,6 +44,7 @@ import { sendPostRequest } from '../util.js';
39
44
  import { GetAIAPIKey } from '@memberjunction/ai';
40
45
  import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
41
46
  import { AIAgentEntityExtended, AIEngine } from '@memberjunction/aiengine';
47
+ import { deleteAccessToken, GetDataAccessToken, registerAccessToken, tokenExists } from './GetDataResolver.js';
42
48
 
43
49
  enum SkipResponsePhase {
44
50
  ClarifyingQuestion = 'clarifying_question',
@@ -149,7 +155,7 @@ export class AskSkipResolver {
149
155
  }
150
156
  }
151
157
 
152
- const input = await this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'chat_with_a_record', false, false, false, user);
158
+ const input = await this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'chat_with_a_record', false, false, false, user, dataSource, false, false);
153
159
  messages.push({
154
160
  content: UserQuestion,
155
161
  role: 'user',
@@ -230,11 +236,32 @@ export class AskSkipResolver {
230
236
  includeEntities: boolean,
231
237
  includeQueries: boolean,
232
238
  includeNotes: boolean,
233
- contextUser: UserInfo
239
+ contextUser: UserInfo,
240
+ dataSource: DataSource,
241
+ forceEntitiesRefresh: boolean = false,
242
+ includeCallBackKeyAndAccessToken: boolean = false
234
243
  ): Promise<SkipAPIRequest> {
235
- const entities = includeEntities ? this.BuildSkipEntities() : [];
244
+ const entities = includeEntities ? await this.BuildSkipEntities(dataSource, forceEntitiesRefresh) : [];
236
245
  const queries = includeQueries ? this.BuildSkipQueries() : [];
237
246
  const {notes, noteTypes} = includeNotes ? await this.BuildSkipAgentNotes(contextUser) : {notes: [], noteTypes: []};
247
+
248
+ // setup a secure access token that is short lived for use with the Skip API
249
+ let accessToken: GetDataAccessToken;
250
+ if (includeCallBackKeyAndAccessToken) {
251
+ accessToken = registerAccessToken(
252
+ undefined,
253
+ 1000 * 60 * 10 /*10 minutes*/,
254
+ {
255
+ type: 'skip_api_request',
256
+ userEmail: contextUser.Email,
257
+ userName: contextUser.Name,
258
+ userID: contextUser.ID,
259
+ conversationId: conversationId,
260
+ requestPhase: requestPhase,
261
+ }
262
+ );
263
+ }
264
+
238
265
  const input: SkipAPIRequest = {
239
266
  apiKeys: this.buildSkipAPIKeys(),
240
267
  organizationInfo: configInfo?.askSkip?.organizationInfo,
@@ -246,7 +273,10 @@ export class AskSkipResolver {
246
273
  entities: entities,
247
274
  queries: queries,
248
275
  notes: notes,
249
- noteTypes: noteTypes
276
+ noteTypes: noteTypes,
277
+ callingServerURL: accessToken ? `${baseUrl}:${graphqlPort}` : undefined,
278
+ callingServerAPIKey: accessToken ? apiKey : undefined,
279
+ callingServerAccessToken: accessToken ? accessToken.Token : undefined
250
280
  };
251
281
  return input;
252
282
  }
@@ -268,7 +298,7 @@ export class AskSkipResolver {
268
298
  if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
269
299
  const dataContext: DataContext = new DataContext();
270
300
  await dataContext.Load(DataContextId, dataSource, true, false, 0, user);
271
- const input = <SkipAPIRunScriptRequest>await this.buildSkipAPIRequest([], '', dataContext, 'run_existing_script', false, false, false, user);
301
+ const input = <SkipAPIRunScriptRequest>await this.buildSkipAPIRequest([], '', dataContext, 'run_existing_script', false, false, false, user, dataSource, false, false);
272
302
  input.scriptText = ScriptText;
273
303
  return this.handleSimpleSkipPostRequest(input);
274
304
  }
@@ -304,7 +334,8 @@ export class AskSkipResolver {
304
334
  @Arg('ConversationId', () => String) ConversationId: string,
305
335
  @Ctx() { dataSource, userPayload }: AppContext,
306
336
  @PubSub() pubSub: PubSubEngine,
307
- @Arg('DataContextId', () => String, { nullable: true }) DataContextId?: string
337
+ @Arg('DataContextId', () => String, { nullable: true }) DataContextId?: string,
338
+ @Arg('ForceEntityRefresh', () => Boolean, { nullable: true }) ForceEntityRefresh?: boolean
308
339
  ) {
309
340
  const md = new Metadata();
310
341
  const user = UserCache.Instance.Users.find((u) => u.Email.trim().toLowerCase() === userPayload.email.trim().toLowerCase());
@@ -328,7 +359,7 @@ export class AskSkipResolver {
328
359
  );
329
360
 
330
361
  const conversationDetailCount = 1
331
- const input = await this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'initial_request', true, true, true, user);
362
+ const input = await this.buildSkipAPIRequest(messages, ConversationId, dataContext, 'initial_request', true, true, true, user, dataSource, ForceEntityRefresh === undefined ? false : ForceEntityRefresh, true);
332
363
 
333
364
  return this.HandleSkipRequest(
334
365
  input,
@@ -436,77 +467,274 @@ export class AskSkipResolver {
436
467
  }
437
468
  }
438
469
  catch (e) {
439
- LogError(e);
470
+ LogError(`AskSkipResolver::BuildSkipAgentNotes: ${e}`);
440
471
  return {notes: [], noteTypes: []}; // non- fatal error just return empty arrays
441
472
  }
442
473
  }
443
474
 
444
- protected BuildSkipEntities(): SkipEntityInfo[] {
445
- // build the entity info for skip in its format which is
446
- // narrower in scope than our native MJ metadata
447
- // don't pass the mj_core_schema entities by default, but allow flexibilty
448
- // to include specific entities from the MJAPI config.json
449
-
450
- const includedEntities = (configInfo.askSkip?.entitiesToSendSkip?.includeEntitiesFromExcludedSchemas ?? []).map((e) => e.trim().toLowerCase());
451
- const md = new Metadata();
452
- return md.Entities.filter((e) => e.SchemaName !== mj_core_schema || includedEntities.includes(e.Name.trim().toLowerCase())).map((e) => {
475
+ protected async PackEntityRows(e: EntityInfo, dataSource: DataSource): Promise<any[]> {
476
+ try {
477
+ if (e.RowsToPackWithSchema === 'None')
478
+ return [];
479
+
480
+ // only include columns that have a scopes including either All and/or AI or have Null for ScopeDefault
481
+ const fields = e.Fields.filter((f) => {
482
+ const scopes = f.ScopeDefault?.split(',').map((s) => s.trim().toLowerCase());
483
+ return !scopes || scopes.length === 0 || scopes.includes('all') || scopes.includes('ai');
484
+ }).map(f => `[${f.Name}]`).join(',');
485
+
486
+ // now run the query based on the row packing method
487
+ let sql: string = '';
488
+ switch (e.RowsToPackWithSchema) {
489
+ case 'All':
490
+ sql = `SELECT ${fields} FROM ${e.SchemaName}.${e.BaseView}`;
491
+ break;
492
+ case 'Sample':
493
+ switch (e.RowsToPackSampleMethod) {
494
+ case 'random':
495
+ sql = `SELECT TOP ${e.RowsToPackSampleCount} ${fields} FROM [${e.SchemaName}].[${e.BaseView}] ORDER BY newid()`; // SQL Server newid() function returns a new uniqueidentifier value for each row and when sorted it will be random
496
+ break;
497
+ case 'top n':
498
+ const orderBy = e.RowsToPackSampleOrder ? ` ORDER BY [${e.RowsToPackSampleOrder}]` : '';
499
+ sql = `SELECT TOP ${e.RowsToPackSampleCount} ${fields} FROM [${e.SchemaName}].[${e.BaseView}]${orderBy}`;
500
+ break;
501
+ case 'bottom n':
502
+ const firstPrimaryKey = e.FirstPrimaryKey.Name;
503
+ const innerOrderBy = e.RowsToPackSampleOrder ? `[${e.RowsToPackSampleOrder}]` : `[${firstPrimaryKey}] DESC`;
504
+ sql = `SELECT * FROM (
505
+ SELECT TOP ${e.RowsToPackSampleCount} ${fields}
506
+ FROM [${e.SchemaName}].[${e.BaseView}]
507
+ ORDER BY ${innerOrderBy}
508
+ ) sub
509
+ ORDER BY [${firstPrimaryKey}] ASC;`;
510
+ break;
511
+ }
512
+ }
513
+ const result = await dataSource.query(sql);
514
+ if (!result) {
515
+ return [];
516
+ }
517
+ else {
518
+ return result;
519
+ }
520
+ }
521
+ catch (e) {
522
+ LogError(`AskSkipResolver::PackEntityRows: ${e}`);
523
+ return [];
524
+ }
525
+ }
526
+
527
+ protected async PackFieldPossibleValues(f: EntityFieldInfo, dataSource: DataSource): Promise<SkipEntityFieldValueInfo[]> {
528
+ try {
529
+ if (f.ValuesToPackWithSchema === 'None') {
530
+ return []; // don't pack anything
531
+ }
532
+ else if (f.ValuesToPackWithSchema === 'All') {
533
+ // wants ALL of the distinct values
534
+ return await this.GetFieldDistinctValues(f, dataSource);
535
+ }
536
+ else if (f.ValuesToPackWithSchema === 'Auto') {
537
+ // default setting - pack based on the ValueListType
538
+ if (f.ValueListTypeEnum === 'List') {
539
+ // simple list of values in the Entity Field Values table
540
+ return f.EntityFieldValues.map((v) => {
541
+ return {value: v.Value, displayValue: v.Value};
542
+ });
543
+ }
544
+ else if (f.ValueListTypeEnum === 'ListOrUserEntry') {
545
+ // could be a user provided value, OR the values in the list of possible values.
546
+ // get the distinct list of values from the DB and concat that with the f.EntityFieldValues array - deduped and return
547
+ const values = await this.GetFieldDistinctValues(f, dataSource);
548
+ if (!values || values.length === 0) {
549
+ // no result, just return the EntityFieldValues
550
+ return f.EntityFieldValues.map((v) => {
551
+ return {value: v.Value, displayValue: v.Value};
552
+ });
553
+ }
554
+ else {
555
+ return [...new Set([...f.EntityFieldValues.map((v) => {
556
+ return {value: v.Value, displayValue: v.Value};
557
+ }), ...values])];
558
+ }
559
+ }
560
+ }
561
+ return []; // if we get here, nothing to pack
562
+ }
563
+ catch (e) {
564
+ LogError(`AskSkipResolver::PackFieldPossibleValues: ${e}`);
565
+ return [];
566
+ }
567
+ }
568
+
569
+ protected async GetFieldDistinctValues(f: EntityFieldInfo, dataSource: DataSource): Promise<SkipEntityFieldValueInfo[]> {
570
+ try {
571
+ const sql = `SELECT DISTINCT ${f.Name} FROM ${f.SchemaName}.${f.BaseView}`;
572
+ const result = await dataSource.query(sql);
573
+ if (!result) {
574
+ return [];
575
+ }
576
+ else {
577
+ return result.map((r) => {
578
+ return {
579
+ value: r[f.Name],
580
+ displayValue: r[f.Name]
581
+ };
582
+ });
583
+ }
584
+ }
585
+ catch (e) {
586
+ LogError(`AskSkipResolver::GetFieldDistinctValues: ${e}`);
587
+ return [];
588
+ }
589
+ }
590
+
591
+
592
+ // SKIP ENTITIES CACHING
593
+ // Static variables shared across all instances
594
+ private static __skipEntitiesCache$: BehaviorSubject<Promise<SkipEntityInfo[]> | null> = new BehaviorSubject<Promise<SkipEntityInfo[]> | null>(null);
595
+ private static __lastRefreshTime: number = 0;
596
+
597
+ private async refreshSkipEntities(dataSource: DataSource): Promise<SkipEntityInfo[]> {
598
+ try {
599
+ const md = new Metadata();
600
+ const skipSpecialIncludeEntities = (configInfo.askSkip?.entitiesToSendSkip?.includeEntitiesFromExcludedSchemas ?? [])
601
+ .map((e) => e.trim().toLowerCase());
602
+
603
+ // get the list of entities
604
+ const entities = md.Entities.filter((e) => {
605
+ if (e.SchemaName !== mj_core_schema || skipSpecialIncludeEntities.includes(e.Name.trim().toLowerCase())) {
606
+ const scopes = e.ScopeDefault?.split(',').map((s) => s.trim().toLowerCase()) ?? ['all'];
607
+ return !scopes || scopes.length === 0 || scopes.includes('all') || scopes.includes('ai') || skipSpecialIncludeEntities.includes(e.Name.trim().toLowerCase());
608
+ }
609
+ return false;
610
+ });
611
+
612
+ // now we have our list of entities, pack em up
613
+ const result = await Promise.all(entities.map((e) => this.PackSingleSkipEntityInfo(e, dataSource)));
614
+
615
+ AskSkipResolver.__lastRefreshTime = Date.now(); // Update last refresh time
616
+ return result;
617
+ }
618
+ catch (e) {
619
+ LogError(`AskSkipResolver::refreshSkipEntities: ${e}`);
620
+ return [];
621
+ }
622
+ }
623
+
624
+ public async BuildSkipEntities(dataSource: DataSource, forceRefresh: boolean = false, refreshIntervalMinutes: number = 15): Promise<SkipEntityInfo[]> {
625
+ try {
626
+ const now = Date.now();
627
+ const cacheExpired = (now - AskSkipResolver.__lastRefreshTime) > (refreshIntervalMinutes * 60 * 1000);
628
+
629
+ // If force refresh is requested OR cache expired OR cache is empty, refresh
630
+ if (forceRefresh || cacheExpired || AskSkipResolver.__skipEntitiesCache$.value === null) {
631
+ console.log(`Forcing Skip Entities refresh: ${forceRefresh}, Cache Expired: ${cacheExpired}`);
632
+ const newData = this.refreshSkipEntities(dataSource);
633
+ AskSkipResolver.__skipEntitiesCache$.next(newData);
634
+ }
635
+
636
+ return AskSkipResolver.__skipEntitiesCache$.pipe(take(1)).toPromise();
637
+ }
638
+ catch (e) {
639
+ LogError(`AskSkipResolver::BuildSkipEntities: ${e}`);
640
+ return [];
641
+ }
642
+ }
643
+
644
+ protected async PackSingleSkipEntityInfo(e: EntityInfo, dataSource: DataSource): Promise<SkipEntityInfo> {
645
+ try {
453
646
  const ret: SkipEntityInfo = {
454
647
  id: e.ID,
455
648
  name: e.Name,
456
649
  schemaName: e.SchemaName,
457
650
  baseView: e.BaseView,
458
651
  description: e.Description,
459
- fields: e.Fields.map((f) => {
460
- return {
461
- id: f.ID,
462
- entityID: f.EntityID,
463
- sequence: f.Sequence,
464
- name: f.Name,
465
- displayName: f.DisplayName,
466
- category: f.Category,
467
- type: f.Type,
468
- description: f.Description,
469
- isPrimaryKey: f.IsPrimaryKey,
470
- allowsNull: f.AllowsNull,
471
- isUnique: f.IsUnique,
472
- length: f.Length,
473
- precision: f.Precision,
474
- scale: f.Scale,
475
- sqlFullType: f.SQLFullType,
476
- defaultValue: f.DefaultValue,
477
- autoIncrement: f.AutoIncrement,
478
- valueListType: f.ValueListType,
479
- extendedType: f.ExtendedType,
480
- defaultInView: f.DefaultInView,
481
- defaultColumnWidth: f.DefaultColumnWidth,
482
- isVirtual: f.IsVirtual,
483
- isNameField: f.IsNameField,
484
- relatedEntityID: f.RelatedEntityID,
485
- relatedEntityFieldName: f.RelatedEntityFieldName,
486
- relatedEntity: f.RelatedEntity,
487
- relatedEntitySchemaName: f.RelatedEntitySchemaName,
488
- relatedEntityBaseView: f.RelatedEntityBaseView,
489
- };
490
- }),
652
+
653
+ fields: await Promise.all(e.Fields.filter(f => {
654
+ // we want to check the scopes for the field level and make sure it is either All or AI or has both
655
+ const scopes = f.ScopeDefault?.split(',').map((s) => s.trim().toLowerCase());
656
+ return !scopes || scopes.length === 0 || scopes.includes('all') || scopes.includes('ai');
657
+ }).map(f => {
658
+ return this.PackSingleSkipEntityField(f, dataSource);
659
+ })),
660
+
491
661
  relatedEntities: e.RelatedEntities.map((r) => {
492
- return {
493
- entityID: r.EntityID,
494
- relatedEntityID: r.RelatedEntityID,
495
- type: r.Type,
496
- entityKeyField: r.EntityKeyField,
497
- relatedEntityJoinField: r.RelatedEntityJoinField,
498
- joinView: r.JoinView,
499
- joinEntityJoinField: r.JoinEntityJoinField,
500
- joinEntityInverseJoinField: r.JoinEntityInverseJoinField,
501
- entity: r.Entity,
502
- entityBaseView: r.EntityBaseView,
503
- relatedEntity: r.RelatedEntity,
504
- relatedEntityBaseView: r.RelatedEntityBaseView,
505
- };
662
+ return this.PackSingleSkipEntityRelationship(r);
506
663
  }),
664
+
665
+ rowsPacked: e.RowsToPackWithSchema,
666
+ rowsSampleMethod: e.RowsToPackSampleMethod,
667
+ rows: await this.PackEntityRows(e, dataSource)
507
668
  };
508
669
  return ret;
509
- });
670
+ }
671
+ catch (e) {
672
+ LogError(`AskSkipResolver::PackSingleSkipEntityInfo: ${e}`);
673
+ return null;
674
+ }
675
+ }
676
+
677
+ protected PackSingleSkipEntityRelationship(r: EntityRelationshipInfo): SkipEntityRelationshipInfo {
678
+ try {
679
+ return {
680
+ entityID: r.EntityID,
681
+ relatedEntityID: r.RelatedEntityID,
682
+ type: r.Type,
683
+ entityKeyField: r.EntityKeyField,
684
+ relatedEntityJoinField: r.RelatedEntityJoinField,
685
+ joinView: r.JoinView,
686
+ joinEntityJoinField: r.JoinEntityJoinField,
687
+ joinEntityInverseJoinField: r.JoinEntityInverseJoinField,
688
+ entity: r.Entity,
689
+ entityBaseView: r.EntityBaseView,
690
+ relatedEntity: r.RelatedEntity,
691
+ relatedEntityBaseView: r.RelatedEntityBaseView,
692
+ };
693
+ }
694
+ catch (e) {
695
+ LogError(`AskSkipResolver::PackSingleSkipEntityRelationship: ${e}`);
696
+ return null;
697
+ }
698
+ }
699
+
700
+ protected async PackSingleSkipEntityField(f: EntityFieldInfo, dataSource: DataSource): Promise<SkipEntityFieldInfo> {
701
+ try {
702
+ return {
703
+ //id: f.ID,
704
+ entityID: f.EntityID,
705
+ sequence: f.Sequence,
706
+ name: f.Name,
707
+ displayName: f.DisplayName,
708
+ category: f.Category,
709
+ type: f.Type,
710
+ description: f.Description,
711
+ isPrimaryKey: f.IsPrimaryKey,
712
+ allowsNull: f.AllowsNull,
713
+ isUnique: f.IsUnique,
714
+ length: f.Length,
715
+ precision: f.Precision,
716
+ scale: f.Scale,
717
+ sqlFullType: f.SQLFullType,
718
+ defaultValue: f.DefaultValue,
719
+ autoIncrement: f.AutoIncrement,
720
+ valueListType: f.ValueListType,
721
+ extendedType: f.ExtendedType,
722
+ defaultInView: f.DefaultInView,
723
+ defaultColumnWidth: f.DefaultColumnWidth,
724
+ isVirtual: f.IsVirtual,
725
+ isNameField: f.IsNameField,
726
+ relatedEntityID: f.RelatedEntityID,
727
+ relatedEntityFieldName: f.RelatedEntityFieldName,
728
+ relatedEntity: f.RelatedEntity,
729
+ relatedEntitySchemaName: f.RelatedEntitySchemaName,
730
+ relatedEntityBaseView: f.RelatedEntityBaseView,
731
+ possibleValues: await this.PackFieldPossibleValues(f, dataSource),
732
+ };
733
+ }
734
+ catch (e) {
735
+ LogError(`AskSkipResolver::PackSingleSkipEntityField: ${e}`);
736
+ return null;
737
+ }
510
738
  }
511
739
 
512
740
  protected async HandleSkipInitialObjectLoading(
@@ -940,6 +1168,12 @@ export class AskSkipResolver {
940
1168
  // analysis is complete
941
1169
  // all done, wrap things up
942
1170
  const md = new Metadata();
1171
+
1172
+ // if we created an access token, it will expire soon anyway but let's remove it for extra safety now
1173
+ if (apiRequest.callingServerAccessToken && tokenExists(apiRequest.callingServerAccessToken)) {
1174
+ deleteAccessToken(apiRequest.callingServerAccessToken);
1175
+ }
1176
+
943
1177
  const { AIMessageConversationDetailID } = await this.FinishConversationAndNotifyUser(
944
1178
  apiResponse,
945
1179
  dataContext,