@memberjunction/server 1.3.3 → 1.4.1

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 (76) hide show
  1. package/CHANGELOG.json +359 -1
  2. package/CHANGELOG.md +78 -2
  3. package/dist/auth/exampleNewUserSubClass.js +1 -1
  4. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  5. package/dist/entitySubclasses/EntityBehavior.server.d.ts +29 -0
  6. package/dist/entitySubclasses/EntityBehavior.server.d.ts.map +1 -0
  7. package/dist/entitySubclasses/EntityBehavior.server.js +213 -0
  8. package/dist/entitySubclasses/EntityBehavior.server.js.map +1 -0
  9. package/dist/generated/generated.d.ts +2235 -1334
  10. package/dist/generated/generated.d.ts.map +1 -1
  11. package/dist/generated/generated.js +7461 -3779
  12. package/dist/generated/generated.js.map +1 -1
  13. package/dist/generic/DeleteOptionsInput.d.ts +5 -0
  14. package/dist/generic/DeleteOptionsInput.d.ts.map +1 -0
  15. package/dist/generic/DeleteOptionsInput.js +28 -0
  16. package/dist/generic/DeleteOptionsInput.js.map +1 -0
  17. package/dist/generic/KeyInputOutputTypes.d.ts +16 -0
  18. package/dist/generic/KeyInputOutputTypes.d.ts.map +1 -0
  19. package/dist/generic/KeyInputOutputTypes.js +62 -0
  20. package/dist/generic/KeyInputOutputTypes.js.map +1 -0
  21. package/dist/generic/KeyValuePairInput.d.ts +5 -0
  22. package/dist/generic/KeyValuePairInput.d.ts.map +1 -0
  23. package/dist/generic/KeyValuePairInput.js +28 -0
  24. package/dist/generic/KeyValuePairInput.js.map +1 -0
  25. package/dist/generic/ResolverBase.d.ts +17 -5
  26. package/dist/generic/ResolverBase.d.ts.map +1 -1
  27. package/dist/generic/ResolverBase.js +176 -0
  28. package/dist/generic/ResolverBase.js.map +1 -1
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +8 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/resolvers/AskSkipResolver.d.ts +3 -3
  34. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  35. package/dist/resolvers/AskSkipResolver.js +29 -16
  36. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  37. package/dist/resolvers/EntityRecordNameResolver.d.ts +3 -2
  38. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  39. package/dist/resolvers/EntityRecordNameResolver.js +13 -12
  40. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  41. package/dist/resolvers/FileCategoryResolver.d.ts +2 -2
  42. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  43. package/dist/resolvers/FileCategoryResolver.js +23 -11
  44. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  45. package/dist/resolvers/FileResolver.d.ts +2 -2
  46. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  47. package/dist/resolvers/FileResolver.js +6 -5
  48. package/dist/resolvers/FileResolver.js.map +1 -1
  49. package/dist/resolvers/MergeRecordsResolver.d.ts +1 -9
  50. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  51. package/dist/resolvers/MergeRecordsResolver.js +8 -36
  52. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  53. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +2 -8
  54. package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
  55. package/dist/resolvers/PotentialDuplicateRecordResolver.js +5 -25
  56. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  57. package/dist/resolvers/UserFavoriteResolver.d.ts +5 -5
  58. package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
  59. package/dist/resolvers/UserFavoriteResolver.js +11 -10
  60. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  61. package/package.json +20 -15
  62. package/src/auth/exampleNewUserSubClass.ts +1 -1
  63. package/src/entitySubclasses/EntityBehavior.server.ts +241 -0
  64. package/src/generated/generated.ts +6590 -5576
  65. package/src/generic/DeleteOptionsInput.ts +13 -0
  66. package/src/generic/KeyInputOutputTypes.ts +35 -0
  67. package/src/generic/KeyValuePairInput.ts +14 -0
  68. package/src/generic/ResolverBase.ts +239 -1
  69. package/src/index.ts +10 -0
  70. package/src/resolvers/AskSkipResolver.ts +40 -16
  71. package/src/resolvers/EntityRecordNameResolver.ts +49 -11
  72. package/src/resolvers/FileCategoryResolver.ts +34 -10
  73. package/src/resolvers/FileResolver.ts +3 -2
  74. package/src/resolvers/MergeRecordsResolver.ts +1 -20
  75. package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -13
  76. package/src/resolvers/UserFavoriteResolver.ts +10 -8
@@ -0,0 +1,13 @@
1
+ import { Field, InputType } from "type-graphql";
2
+
3
+ /**
4
+ * GraphQL InputType for the DeleteOptions
5
+ */
6
+ @InputType()
7
+ export class DeleteOptionsInput {
8
+ @Field(() => Boolean)
9
+ SkipEntityAIActions: boolean;
10
+
11
+ @Field(() => Boolean)
12
+ SkipEntityActions: boolean;
13
+ }
@@ -0,0 +1,35 @@
1
+ import { KeyValuePair } from "@memberjunction/core";
2
+ import { Field, InputType, ObjectType } from "type-graphql";
3
+
4
+
5
+
6
+ @InputType()
7
+ export class KeyValuePairInputType {
8
+ @Field(() => String)
9
+ FieldName: string;
10
+
11
+ @Field(() => String)
12
+ Value: string;
13
+ }
14
+
15
+ @ObjectType()
16
+ export class KeyValuePairOutputType {
17
+ @Field(() => String)
18
+ FieldName: string;
19
+
20
+ @Field(() => String)
21
+ Value: string;
22
+ }
23
+
24
+
25
+ @InputType()
26
+ export class CompositeKeyInputType {
27
+ @Field(() => [KeyValuePairInputType])
28
+ KeyValuePairs: KeyValuePair[];
29
+ }
30
+
31
+ @ObjectType()
32
+ export class CompositeKeyOutputType {
33
+ @Field(() => [KeyValuePairOutputType])
34
+ KeyValuePairs: KeyValuePair[];
35
+ }
@@ -0,0 +1,14 @@
1
+ import { Field, InputType } from "type-graphql";
2
+
3
+ /**
4
+ * GraphQL InputType for the KeyValuePairInput - used for various situations where an input
5
+ * is required that has a combination of Key/Value pairs
6
+ */
7
+ @InputType()
8
+ export class KeyValuePairInput {
9
+ @Field(() => String)
10
+ Key: string;
11
+
12
+ @Field(() => String, { nullable: true})
13
+ Value?: string;
14
+ }
@@ -1,11 +1,15 @@
1
- import { EntityPermissionType, Metadata, RunView, UserInfo } from '@memberjunction/core';
1
+ import { BaseEntity, CompositeKey, EntityFieldTSType, EntityPermissionType, Metadata, RunView, UserInfo } from '@memberjunction/core';
2
2
  import { AuditLogEntity, UserViewEntity } from '@memberjunction/core-entities';
3
3
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
4
4
  import { PubSubEngine } from 'type-graphql';
5
+ import { GraphQLError } from 'graphql';
5
6
  import { DataSource } from 'typeorm';
6
7
 
7
8
  import { UserPayload } from '../types';
8
9
  import { RunDynamicViewInput, RunViewByIDInput, RunViewByNameInput } from './RunViewResolver';
10
+ import { DeleteOptionsInput } from './DeleteOptionsInput';
11
+ import { MJGlobal } from '@memberjunction/global';
12
+ import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver';
9
13
 
10
14
  export class ResolverBase {
11
15
 
@@ -348,4 +352,238 @@ export class ResolverBase {
348
352
  public get MJCoreSchema(): string {
349
353
  return Metadata.Provider.ConfigData.MJCoreSchemaName;
350
354
  }
355
+
356
+ protected ListenForEntityMessages(entityObject: BaseEntity, pubSub: PubSubEngine, userPayload: UserPayload) {
357
+ // 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
358
+ MJGlobal.Instance.GetEventListener(false).subscribe((event) => {
359
+ if (event) {
360
+ if (event.component === entityObject && event.args && event.args.message) {
361
+ // message from our entity object, relay it to the client
362
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
363
+ message: JSON.stringify({
364
+ status: 'OK',
365
+ type: 'EntityObjectStatusMessage',
366
+ entityName: entityObject.EntityInfo.Name,
367
+ primaryKey: entityObject.PrimaryKey,
368
+ message: event.args.message
369
+ }),
370
+ sessionId: userPayload.sessionId
371
+ });
372
+ }
373
+ }
374
+ });
375
+ }
376
+
377
+ protected async CreateRecord(entityName: string, input: any, dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
378
+ if (await this.BeforeCreate(dataSource, input)) { // fire event and proceed if it wasn't cancelled
379
+ const entityObject = await new Metadata().GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
380
+ entityObject.NewRecord();
381
+ entityObject.SetMany(input);
382
+
383
+ this.ListenForEntityMessages(entityObject, pubSub, userPayload);
384
+
385
+ if (await entityObject.Save()) {
386
+ // save worked, fire the AfterCreate event and then return all the data
387
+ await this.AfterCreate(dataSource, input); // fire event
388
+ return entityObject.GetAll();
389
+ }
390
+ else
391
+ // save failed, return null
392
+ throw entityObject.LatestResult.Message;
393
+ }
394
+ else
395
+ return null;
396
+ }
397
+
398
+ // Before/After CREATE Event Hooks for Sub-Classes to Override
399
+ protected async BeforeCreate(dataSource: DataSource, input: any): Promise<boolean> {
400
+ return true;
401
+ }
402
+ protected async AfterCreate(dataSource: DataSource, input: any) {
403
+ }
404
+
405
+
406
+ protected async UpdateRecord(entityName: string, input: any, dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
407
+ if (await this.BeforeUpdate(dataSource, input)) { // fire event and proceed if it wasn't cancelled
408
+ const md = new Metadata();
409
+ const userInfo = this.GetUserFromPayload(userPayload)
410
+ const entityObject = await md.GetEntityObject(entityName, userInfo);
411
+ const entityInfo = entityObject.EntityInfo;
412
+ const clientNewValues = {};
413
+ Object.keys(input).forEach((key) => { if (key !== 'OldValues___') clientNewValues[key] = input[key]; }); // grab all the props except for the OldValues property
414
+
415
+ if (entityInfo.TrackRecordChanges || !input.OldValues___) {
416
+ // the entity tracks record changes, so we need to load the old values from the DB to make sure they are not inconsistent
417
+ // with the old values from the input.OldValues property. If they are different, but on different fields, we allow it
418
+ // but if they are different on fields that the current UpdateRecord call is trying to update, we throw an error.
419
+ const cKey = new CompositeKey(entityInfo.PrimaryKeys.map((pk) => {
420
+ return {
421
+ FieldName: pk.Name,
422
+ Value: input[pk.CodeName]
423
+ }
424
+ }));
425
+
426
+ if (await entityObject.InnerLoad(cKey)) {
427
+ // load worked, now, if we HAVE OldValues, we need to check them against the values in the DB we just loaded.
428
+ if (!input.OldValues___) {
429
+ // no OldValues, so we can just set the new values from input
430
+ entityObject.SetMany(input);
431
+ }
432
+ else {
433
+ // we DO have OldValues, so we need to do a more in depth analysis
434
+ this.TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject);
435
+ }
436
+ }
437
+ else {
438
+ throw new Error(`Record not found for ${entityName} with key ${JSON.stringify(cKey)}`);
439
+ }
440
+ }
441
+ else {
442
+ // not tracking changes and we DO have OldValues, so we can load from them
443
+ const oldValues = {};
444
+ // for each item in the oldValues array, add it to the oldValues object
445
+ input.OldValues___?.forEach((item) => oldValues[item.Key] = item.Value);
446
+
447
+ // 1) load the old values, this will be the initial state of the object
448
+ entityObject.LoadFromData(oldValues);
449
+
450
+ // 2) set the new values from the input, not including the OldValues property
451
+ entityObject.SetMany(clientNewValues);
452
+ }
453
+
454
+ this.ListenForEntityMessages(entityObject, pubSub, userPayload);
455
+ if (await entityObject.Save()) {
456
+ // save worked, fire afterevent and return all the data
457
+ await this.AfterUpdate(dataSource, input); // fire event
458
+ return entityObject.GetAll();
459
+ }
460
+ else {
461
+ // save failed, return null
462
+ throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
463
+ extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
464
+ });
465
+ }
466
+ }
467
+ else
468
+ return null; // update canceled by the BeforeUpdate event, return null
469
+ }
470
+
471
+ /**
472
+ * This routine compares the OldValues property in the input object to the values in the DB that we just loaded. If there are differences, we need to check to see if the client
473
+ * is trying to update any of those fields (e.g. overlap). If there is overlap, we throw an error. If there is no overlap, we can proceed with the update even if the DB Values
474
+ * and the ClientOldValues are not 100% the same, so long as there is no overlap in the specific FIELDS that are different.
475
+ *
476
+ * ASSUMES: input object has an OldValues___ property that is an array of Key/Value pairs that represent the old values of the record that the client is trying to update.
477
+ */
478
+ protected TestAndSetClientOldValuesToDBValues(input: any, clientNewValues: any, entityObject: BaseEntity) {
479
+ // we have OldValues, so we need to compare them to the values we just loaded from the DB
480
+ const clientOldValues = {};
481
+ // for each item in the oldValues array, add it to the clientOldValues object
482
+ input.OldValues___.forEach((item) => {
483
+ // we need to do a quick transform on the values to make sure they match the TS Type for the given field because item.Value will always be a string
484
+ const field = entityObject.EntityInfo.Fields.find((f) => f.CodeName === item.Key);
485
+ let val = item.Value;
486
+ if ( (val === null || val === undefined) && field.DefaultValue !== null && field.DefaultValue !== undefined)
487
+ val = field.DefaultValue; // set default value as the field was never set
488
+
489
+ if (field) {
490
+ switch (field.TSType) {
491
+ case EntityFieldTSType.Number:
492
+ val = val !== null && val !== undefined ? parseInt(val) : null;
493
+ break;
494
+ case EntityFieldTSType.Boolean:
495
+ val = (val === null || val === undefined || val === 'false' || val === '0' || parseInt(val) === 0) ? false : true;
496
+ break;
497
+ case EntityFieldTSType.Date:
498
+ val = val !== null && val !== undefined ? new Date(val) : null;
499
+ break;
500
+ default:
501
+ break; // already a string
502
+ }
503
+ }
504
+ clientOldValues[item.Key] = val
505
+ });
506
+
507
+ // clientOldValues now has all of the oldValues the CLIENT passed us. Now we need to build the same kind of object
508
+ // with the DB values
509
+ const dbValues = entityObject.GetAll();
510
+
511
+ // now we need to compare clientOldValues and dbValues and have a new array that has entries for any differences and have FieldName, clientOldValue and dbValue as properties
512
+ const dbDifferences = [];
513
+ Object.keys(clientOldValues).forEach((key) => {
514
+ const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
515
+ if (clientOldValues[key] !== dbValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey ) {
516
+ // only include updateable fields
517
+ dbDifferences.push({
518
+ FieldName: key,
519
+ ClientOldValue: clientOldValues[key],
520
+ DBValue: dbValues[key]
521
+ });
522
+ }
523
+ });
524
+
525
+ if (dbDifferences.length > 0) {
526
+ // now we have an array of any dbDifferences with length > 0, between the clientOldValues and the dbValues, we need to check to see if any of the differences are on fields that the client is trying to update
527
+ // first step is to get clientNewValues into an object that is like clientOldValues, get the diff and then compare that diff to the differences array that shows diff between DB and ClientOld
528
+ const clientDifferences = [];
529
+ Object.keys(clientOldValues).forEach((key) => {
530
+ const f = entityObject.EntityInfo.Fields.find((f) => f.CodeName === key);
531
+ if (clientOldValues[key] !== clientNewValues[key] && f && f.AllowUpdateAPI && !f.IsPrimaryKey) {
532
+ // only include updateable fields
533
+ clientDifferences.push({
534
+ FieldName: key,
535
+ ClientOldValue: clientOldValues[key],
536
+ ClientNewValue: clientNewValues[key]
537
+ });
538
+ }
539
+ });
540
+
541
+ // now we have clientDifferences which shows what the client thinks they are changing. And, we have the dbDifferences array that shows changes between the clientOldValues and the dbValues
542
+ // if there is ANY overlap in the FIELDS that appear in both arrays, we need to throw an error
543
+ const overlap = clientDifferences.filter((cd) => dbDifferences.find((dd) => dd.FieldName === cd.FieldName));
544
+ if (overlap.length > 0) {
545
+ const msg = {
546
+ Message: 'Inconsistency between old values provided for changed fields, and the values of one or more of those fields in the database. Update operation cancelled.',
547
+ ClientDifferences: clientDifferences,
548
+ DBDifferences: dbDifferences,
549
+ Overlap: overlap
550
+ };
551
+ throw new Error(JSON.stringify(msg));
552
+ }
553
+ }
554
+
555
+ // If we get here that means we've not thrown an exception, so there is
556
+ // NO OVERLAP, so we can set the new values from the data provided from the client now...
557
+ entityObject.SetMany(clientNewValues);
558
+ }
559
+
560
+ protected async DeleteRecord(entityName: string, key: CompositeKey, options: DeleteOptionsInput, dataSource: DataSource, userPayload: UserPayload, pubSub: PubSubEngine) {
561
+ if (await this.BeforeDelete(dataSource, key)) { // fire event and proceed if it wasn't cancelled
562
+ const entityObject = await new Metadata().GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
563
+ await entityObject.InnerLoad(key);
564
+ const returnValue = entityObject.GetAll(); // grab the values before we delete so we can return last state before delete if we are successful.
565
+ if (await entityObject.Delete(options)) {
566
+ await this.AfterDelete(dataSource, key); // fire event
567
+ return returnValue;
568
+ }
569
+ else
570
+ return null; // delete failed, this will cause an exception
571
+ }
572
+ else
573
+ return null; // BeforeDelete canceled the operation, this will cause an exception
574
+ }
575
+
576
+ // Before/After DELETE Event Hooks for Sub-Classes to Override
577
+ protected async BeforeDelete(dataSource: DataSource, key: CompositeKey,): Promise<boolean> {
578
+ return true;
579
+ }
580
+ protected async AfterDelete(dataSource: DataSource, key: CompositeKey,) {
581
+ }
582
+
583
+ // Before/After UPDATE Event Hooks for Sub-Classes to Override
584
+ protected async BeforeUpdate(dataSource: DataSource, input: any): Promise<boolean> {
585
+ return true;
586
+ }
587
+ protected async AfterUpdate(dataSource: DataSource, input: any) {
588
+ }
351
589
  }
package/src/index.ts CHANGED
@@ -24,6 +24,12 @@ import { contextFunction, getUserPayload } from './context';
24
24
  import { publicDirective } from './directives';
25
25
  import orm from './orm';
26
26
 
27
+ import { LoadActionEntityServer } from '@memberjunction/actions';
28
+ LoadActionEntityServer(); // prevent tree shaking for this dynamic module
29
+
30
+ import { LoadGeneratedActions } from '@memberjunction/core-actions';
31
+ LoadGeneratedActions(); // prevent tree shaking for this dynamic module
32
+
27
33
  const cacheRefreshInterval = configInfo.databaseSettings.metadataCacheRefreshInterval;
28
34
 
29
35
  export { MaxLength } from 'class-validator';
@@ -34,12 +40,16 @@ export * from './directives';
34
40
  export * from './entitySubclasses/userViewEntity.server';
35
41
  export * from './entitySubclasses/entityPermissions.server';
36
42
  export * from './entitySubclasses/DuplicateRunEntity.server';
43
+ export * from './entitySubclasses/EntityBehavior.server';
37
44
  export * from './types';
38
45
  export { TokenExpiredError } from './auth';
39
46
 
40
47
  export * from './generic/PushStatusResolver';
41
48
  export * from './generic/ResolverBase';
42
49
  export * from './generic/RunViewResolver';
50
+ export * from './generic/KeyValuePairInput';
51
+ export * from './generic/KeyInputOutputTypes'
52
+ export * from './generic/DeleteOptionsInput';
43
53
 
44
54
  export * from './resolvers/AskSkipResolver';
45
55
  export * from './resolvers/ColorResolver';
@@ -18,8 +18,7 @@ import { registerEnumType } from "type-graphql";
18
18
  import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
19
19
  import { sendPostRequest } from '../util';
20
20
  import { GetAIAPIKey } from '@memberjunction/ai';
21
- import { KeyValuePairInputType } from './MergeRecordsResolver';
22
- import { CompositeKeyInputType } from './PotentialDuplicateRecordResolver';
21
+ import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes';
23
22
 
24
23
 
25
24
  enum SkipResponsePhase {
@@ -74,7 +73,7 @@ export class AskSkipResolver {
74
73
  async ExecuteAskSkipRecordChat(@Arg('UserQuestion', () => String) UserQuestion: string,
75
74
  @Arg('ConversationId', () => Int) ConversationId: number,
76
75
  @Arg('EntityName', () => String) EntityName: string,
77
- @Arg('CompositeKey', () => CompositeKeyInputType) CompositeKey: CompositeKeyInputType,
76
+ @Arg('CompositeKey', () => CompositeKeyInputType) compositeKey: CompositeKeyInputType,
78
77
  @Ctx() { dataSource, userPayload }: AppContext,
79
78
  @PubSub() pubSub: PubSubEngine) {
80
79
  // In this function we're simply going to call the Skip API and pass along the message from the user
@@ -97,7 +96,9 @@ export class AskSkipResolver {
97
96
  dci.DataContextID = dataContext.ID;
98
97
  dci.Type = 'single_record';
99
98
  dci.EntityID = md.Entities.find((e) => e.Name === EntityName)?.ID;
100
- dci.RecordID = CompositeKey.Values();
99
+ const ck = new CompositeKey();
100
+ ck.KeyValuePairs = compositeKey.KeyValuePairs;
101
+ dci.RecordID = ck.Values();
101
102
  await dci.Save();
102
103
 
103
104
  await dataContext.Load(dataContext.ID, dataSource, false, true, 10, user); // load again because we added a new data context item
@@ -105,7 +106,7 @@ export class AskSkipResolver {
105
106
 
106
107
  // also, in the situation for a new convo, we need to update the Conversation ID to have a LinkedEntity and LinkedRecord
107
108
  convoEntity.LinkedEntityID = dci.EntityID;
108
- convoEntity.LinkedRecordID = CompositeKey.Values();
109
+ convoEntity.LinkedRecordID = ck.Values();
109
110
  convoEntity.DataContextID = dataContext.ID;
110
111
  await convoEntity.Save();
111
112
  }
@@ -120,7 +121,7 @@ export class AskSkipResolver {
120
121
  }
121
122
 
122
123
  protected async handleSimpleSkipPostRequest(input: SkipAPIRequest, conversationID: number = 0, UserMessageConversationDetailId: number = 0, createAIMessageConversationDetail: boolean = false, user: UserInfo = null): Promise<AskSkipResultType> {
123
- LogStatus(` >>> Sending request to Skip API: ${___skipAPIurl}`)
124
+ LogStatus(` >>> HandleSimpleSkipPostRequest Sending request to Skip API: ${___skipAPIurl}`);
124
125
 
125
126
  const response = await sendPostRequest(___skipAPIurl, input, true, null);
126
127
 
@@ -222,7 +223,11 @@ export class AskSkipResolver {
222
223
  {
223
224
  vendorDriverName: 'GroqLLM',
224
225
  apiKey: GetAIAPIKey('GroqLLM')
225
- }
226
+ },
227
+ {
228
+ vendorDriverName: 'MistralLLM',
229
+ apiKey: GetAIAPIKey('MistralLLM')
230
+ },
226
231
  ];
227
232
  }
228
233
 
@@ -527,18 +532,34 @@ export class AskSkipResolver {
527
532
  ConversationId: number, userPayload: UserPayload, pubSub: PubSubEngine, md: Metadata,
528
533
  convoEntity: ConversationEntity, convoDetailEntity: ConversationDetailEntity,
529
534
  dataContext: DataContext, dataContextEntity: DataContextEntity): Promise<AskSkipResultType> {
530
- LogStatus(` >>> Sending request to Skip API: ${___skipAPIurl}`)
535
+ LogStatus(` >>> HandleSkipRequest: Sending request to Skip API: ${___skipAPIurl}`);
531
536
 
532
- const response = await sendPostRequest(___skipAPIurl, input, true, null, (message) => {
533
- if (message.type==='status_update')
537
+ const response = await sendPostRequest(___skipAPIurl, input, true, null, (message:
538
+ {
539
+ type: string,
540
+ value: {
541
+ success: boolean,
542
+ error: string,
543
+ responsePhase: string,
544
+ messages: {
545
+ role: string,
546
+ content: string
547
+ }[]
548
+ }
549
+ }) => {
550
+ LogStatus(JSON.stringify(message, null, 4));
551
+ if (message.type ==='status_update'){
534
552
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
535
553
  message: JSON.stringify({
536
554
  type: 'AskSkip',
537
555
  status: 'OK',
556
+ conversationID: ConversationId,
557
+ ResponsePhase: message.value.responsePhase,
538
558
  message: message.value.messages[0].content
539
559
  }),
540
- sessionId: userPayload.sessionId,
560
+ sessionId: userPayload.sessionId
541
561
  });
562
+ }
542
563
  });
543
564
 
544
565
  if (response && response.length > 0) { // response.status === 200) {
@@ -546,7 +567,7 @@ export class AskSkipResolver {
546
567
  const apiResponse = <SkipAPIResponse>response[response.length - 1].value;
547
568
  //const apiResponse = <SkipAPIResponse>response.data;
548
569
  LogStatus(` Skip API response: ${apiResponse.responsePhase}`)
549
- this.PublishApiResponseUserUpdateMessage(apiResponse, userPayload, pubSub);
570
+ this.PublishApiResponseUserUpdateMessage(apiResponse, userPayload, ConversationId, pubSub);
550
571
 
551
572
  // now, based on the result type, we will either wait for the next phase or we will process the results
552
573
  if (apiResponse.responsePhase === 'data_request') {
@@ -569,9 +590,10 @@ export class AskSkipResolver {
569
590
  message: JSON.stringify({
570
591
  type: 'AskSkip',
571
592
  status: 'Error',
593
+ conversationID: ConversationId,
572
594
  message: 'Analysis failed to run, please try again later and if this continues, contact your support desk.',
573
595
  }),
574
- sessionId: userPayload.sessionId,
596
+ sessionId: userPayload.sessionId
575
597
  });
576
598
 
577
599
  return {
@@ -586,7 +608,7 @@ export class AskSkipResolver {
586
608
  }
587
609
  }
588
610
 
589
- protected async PublishApiResponseUserUpdateMessage(apiResponse: SkipAPIResponse, userPayload: UserPayload, pubSub: PubSubEngine) {
611
+ protected async PublishApiResponseUserUpdateMessage(apiResponse: SkipAPIResponse, userPayload: UserPayload, conversationID: number, pubSub: PubSubEngine) {
590
612
  let sUserMessage: string = '';
591
613
  switch (apiResponse.responsePhase) {
592
614
  case 'data_request':
@@ -606,9 +628,10 @@ export class AskSkipResolver {
606
628
  message: JSON.stringify({
607
629
  type: 'AskSkip',
608
630
  status: 'OK',
631
+ conversationID,
609
632
  message: sUserMessage,
610
633
  }),
611
- sessionId: userPayload.sessionId,
634
+ sessionId: userPayload.sessionId
612
635
  });
613
636
  }
614
637
 
@@ -837,10 +860,11 @@ export class AskSkipResolver {
837
860
  message: JSON.stringify({
838
861
  type: 'UserNotifications',
839
862
  status: 'OK',
863
+ conversationID: convoEntity.ID,
840
864
  details: {
841
865
  action: 'create',
842
866
  recordId: userNotification.ID,
843
- },
867
+ }
844
868
  }),
845
869
  sessionId: userPayload.sessionId,
846
870
  });
@@ -1,7 +1,10 @@
1
- import { Metadata, CompositeKey } from '@memberjunction/core';
1
+ import { Metadata, CompositeKey, UserInfo } from '@memberjunction/core';
2
2
  import { Arg, Ctx, Field, InputType, ObjectType, Query, Resolver } from 'type-graphql';
3
3
  import { AppContext } from '../types';
4
- import { CompositeKeyInputType, CompositeKeyOutputType } from './PotentialDuplicateRecordResolver';
4
+ import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes';
5
+ import { CommunicationEngine } from '@memberjunction/communication-core';
6
+ import { DocumentationEngine } from '@memberjunction/doc-utils';
7
+ import { TemplateEngineService } from '@memberjunction/templates';
5
8
 
6
9
  @InputType()
7
10
  export class EntityRecordNameInput {
@@ -35,11 +38,16 @@ export class EntityRecordNameResolver {
35
38
  @Query(() => EntityRecordNameResult)
36
39
  async GetEntityRecordName(
37
40
  @Arg('EntityName', () => String) EntityName: string,
38
- @Arg('CompositeKey', () => CompositeKeyInputType) CompositeKey: CompositeKey,
39
- @Ctx() {}: AppContext
41
+ @Arg('CompositeKey', () => CompositeKeyInputType) primaryKey: CompositeKey,
42
+ @Ctx() {userPayload}: AppContext
40
43
  ): Promise<EntityRecordNameResult> {
44
+ //TEMPORARY: test harness for communication framework - dumb place but quick test grounds, will delete
45
+ //this.TestCommunicationFramework(userPayload.userRecord, EntityName, primaryKey);
46
+ //this.TestDocLibraries(userPayload.userRecord);
47
+ //this.TestTemplates();
48
+
41
49
  const md = new Metadata();
42
- return await this.InnerGetEntityRecordName(md, EntityName, CompositeKey);
50
+ return await this.InnerGetEntityRecordName(md, EntityName, primaryKey);
43
51
  }
44
52
 
45
53
  @Query(() => [EntityRecordNameResult])
@@ -55,23 +63,53 @@ export class EntityRecordNameResolver {
55
63
  return result;
56
64
  }
57
65
 
58
- async InnerGetEntityRecordName(md: Metadata, EntityName: string, CompositeKey: CompositeKey): Promise<EntityRecordNameResult> {
66
+ async InnerGetEntityRecordName(md: Metadata, EntityName: string, primaryKey: CompositeKeyInputType): Promise<EntityRecordNameResult> {
67
+ const pk = new CompositeKey(primaryKey.KeyValuePairs);
59
68
  const e = md.Entities.find((e) => e.Name === EntityName);
60
69
  if (e) {
61
- const recordName = await md.GetEntityRecordName(e.Name, CompositeKey);
70
+ const recordName = await md.GetEntityRecordName(e.Name, pk);
62
71
  if (recordName)
63
- return { Success: true, Status: 'OK', CompositeKey, RecordName: recordName, EntityName };
72
+ return { Success: true, Status: 'OK', CompositeKey: pk, RecordName: recordName, EntityName };
64
73
  else
65
74
  return {
66
75
  Success: false,
67
- Status: `Name for record, or record ${CompositeKey.ToString()} itself not found, could be an access issue if user doesn't have Row Level Access (RLS) if RLS is enabled for this entity`,
68
- CompositeKey,
76
+ Status: `Name for record, or record ${pk.ToString()} itself not found, could be an access issue if user doesn't have Row Level Access (RLS) if RLS is enabled for this entity`,
77
+ CompositeKey: pk,
69
78
  EntityName
70
79
  };
71
80
  }
72
81
  else
73
- return { Success: false, Status: `Entity ${EntityName} not found`, CompositeKey, EntityName };
82
+ return { Success: false, Status: `Entity ${EntityName} not found`, CompositeKey: pk, EntityName };
74
83
  }
84
+
85
+ // private async TestCommunicationFramework(user: UserInfo, EntityName: string, primaryKey: CompositeKeyInputType) {
86
+ // const engine = CommunicationEngine.Instance;
87
+ // await engine.Config(false, user);
88
+ // await engine.SendSingleMessage('SendGrid', 'Email', {
89
+ // To: 'user@domain.com',
90
+ // Subject: `MJServer Notification: GetEntityRecordName Called For: ${EntityName}`,
91
+ // Body: `Entity: ${EntityName}, Key: ${JSON.stringify(primaryKey)}`,
92
+ // MessageType: null
93
+ // });
94
+ // }
95
+
96
+ // private async TestDocLibraries(user: UserInfo) {
97
+ // const engine = DocumentationEngine.Instance;
98
+ // await engine.Config(false, user)
99
+ // console.log(JSON.stringify(engine.Libraries));
100
+ // }
101
+
102
+
103
+ // private async TestTemplates() {
104
+ // const templateEngine = new TemplateEngineService('server'); // Provide 'server'
105
+
106
+ // const template = `
107
+ // <h1>Hello, {{context.name}}!</h1>
108
+ // `;
109
+
110
+ // const renderedHtml = await templateEngine.render(template, { name: 'World' });
111
+ // console.log(renderedHtml);
112
+ // }
75
113
  }
76
114
 
77
115
  export default EntityRecordNameResolver;
@@ -1,17 +1,19 @@
1
- import { EntityPermissionType, Metadata } from '@memberjunction/core';
1
+ import { CompositeKey, EntityPermissionType, Metadata, RunView } from '@memberjunction/core';
2
2
  import { FileCategoryEntity, FileEntity } from '@memberjunction/core-entities';
3
- import { AppContext, Arg, Ctx, Int, Mutation } from '@memberjunction/server';
3
+ import { AppContext, Arg, Ctx, DeleteOptionsInput, Int, Mutation } from '@memberjunction/server';
4
4
  import { mj_core_schema } from '../config';
5
5
  import { FileCategoryResolver as FileCategoryResolverBase, FileCategory_ } from '../generated/generated';
6
6
 
7
7
  export class FileResolver extends FileCategoryResolverBase {
8
8
  @Mutation(() => FileCategory_)
9
- async DeleteFileCategory(@Arg('ID', () => Int) ID: number, @Ctx() { dataSource, userPayload }: AppContext) {
10
- if (!(await this.BeforeDelete(dataSource, ID))) {
9
+ async DeleteFileCategory(@Arg('ID', () => Int) ID: number, @Arg('options___', () => DeleteOptionsInput) options: DeleteOptionsInput, @Ctx() { dataSource, userPayload }: AppContext) {
10
+ const key = new CompositeKey();
11
+ key.LoadFromSingleKeyValuePair('ID', ID);
12
+ if (!(await this.BeforeDelete(dataSource, key))) {
11
13
  return null;
12
14
  }
13
15
 
14
- const md = await new Metadata();
16
+ const md = new Metadata();
15
17
  const user = this.GetUserFromPayload(userPayload);
16
18
  const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
17
19
  const fileCategoryEntity = await md.GetEntityObject<FileCategoryEntity>('File Categories', user);
@@ -24,15 +26,37 @@ export class FileResolver extends FileCategoryResolverBase {
24
26
 
25
27
  // Any files using the deleted category fall back to its parent
26
28
  await dataSource.transaction(async () => {
27
- const sSQL = `UPDATE [${mj_core_schema}].[File]
28
- SET [CategoryID]=${fileCategoryEntity.ParentID}
29
- WHERE [CategoryID]=${fileCategoryEntity.ID}`;
29
+ // SHOULD USE BaseEntity for each of these records to ensure object model
30
+ // is used everywhere - new code below. The below is SLOWER than a single
31
+ // Update statement, but it ensures that the object model is used everywhere
32
+ // in case there are sub-classes and business logic/etc for the updates
33
+ // the direct SQL would bypass that logic.
30
34
 
31
- await dataSource.query(sSQL);
35
+ // const sSQL = `UPDATE [${mj_core_schema}].[File]
36
+ // SET [CategoryID]=${fileCategoryEntity.ParentID}
37
+ // WHERE [CategoryID]=${fileCategoryEntity.ID}`;
38
+
39
+ // await dataSource.query(sSQL);
40
+ const rv = new RunView();
41
+ const filesResult = await rv.RunView({
42
+ EntityName: 'Files',
43
+ ExtraFilter: 'CategoryID=' + fileCategoryEntity.ID,
44
+ ResultType: 'entity_object'
45
+ }, user);
46
+ if (filesResult) {
47
+ // iterate through each of the files in filesResult.Results
48
+ // and update the CategoryID to fileCategoryEntity.ParentID
49
+ for (const file of filesResult.Results) {
50
+ const fileEntity = await md.GetEntityObject<FileEntity>('Files', user);
51
+ await fileEntity.Load(file.ID);
52
+ fileEntity.CategoryID = fileCategoryEntity.ParentID;
53
+ await fileEntity.Save();
54
+ }
55
+ }
32
56
  await fileCategoryEntity.Delete();
33
57
  });
34
58
 
35
- await this.AfterDelete(dataSource, ID); // fire event
59
+ await this.AfterDelete(dataSource, key); // fire event
36
60
  return returnValue;
37
61
  }
38
62
  }