@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.
- package/CHANGELOG.json +359 -1
- package/CHANGELOG.md +78 -2
- package/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/entitySubclasses/EntityBehavior.server.d.ts +29 -0
- package/dist/entitySubclasses/EntityBehavior.server.d.ts.map +1 -0
- package/dist/entitySubclasses/EntityBehavior.server.js +213 -0
- package/dist/entitySubclasses/EntityBehavior.server.js.map +1 -0
- package/dist/generated/generated.d.ts +2235 -1334
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +7461 -3779
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/DeleteOptionsInput.d.ts +5 -0
- package/dist/generic/DeleteOptionsInput.d.ts.map +1 -0
- package/dist/generic/DeleteOptionsInput.js +28 -0
- package/dist/generic/DeleteOptionsInput.js.map +1 -0
- package/dist/generic/KeyInputOutputTypes.d.ts +16 -0
- package/dist/generic/KeyInputOutputTypes.d.ts.map +1 -0
- package/dist/generic/KeyInputOutputTypes.js +62 -0
- package/dist/generic/KeyInputOutputTypes.js.map +1 -0
- package/dist/generic/KeyValuePairInput.d.ts +5 -0
- package/dist/generic/KeyValuePairInput.d.ts.map +1 -0
- package/dist/generic/KeyValuePairInput.js +28 -0
- package/dist/generic/KeyValuePairInput.js.map +1 -0
- package/dist/generic/ResolverBase.d.ts +17 -5
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +176 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +3 -3
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +29 -16
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +3 -2
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +13 -12
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.d.ts +2 -2
- package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +23 -11
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +2 -2
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +6 -5
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +1 -9
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +8 -36
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +2 -8
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +5 -25
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts +5 -5
- package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +11 -10
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/package.json +20 -15
- package/src/auth/exampleNewUserSubClass.ts +1 -1
- package/src/entitySubclasses/EntityBehavior.server.ts +241 -0
- package/src/generated/generated.ts +6590 -5576
- package/src/generic/DeleteOptionsInput.ts +13 -0
- package/src/generic/KeyInputOutputTypes.ts +35 -0
- package/src/generic/KeyValuePairInput.ts +14 -0
- package/src/generic/ResolverBase.ts +239 -1
- package/src/index.ts +10 -0
- package/src/resolvers/AskSkipResolver.ts +40 -16
- package/src/resolvers/EntityRecordNameResolver.ts +49 -11
- package/src/resolvers/FileCategoryResolver.ts +34 -10
- package/src/resolvers/FileResolver.ts +3 -2
- package/src/resolvers/MergeRecordsResolver.ts +1 -20
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -13
- 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 {
|
|
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)
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 '
|
|
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)
|
|
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,
|
|
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,
|
|
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,
|
|
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 ${
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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,
|
|
59
|
+
await this.AfterDelete(dataSource, key); // fire event
|
|
36
60
|
return returnValue;
|
|
37
61
|
}
|
|
38
62
|
}
|