@memberjunction/server 2.20.2 → 2.21.0
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/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +122 -57
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1287 -694
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +8 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +14 -5
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +250 -61
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts +90 -0
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -0
- package/dist/resolvers/GetDataResolver.js +294 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -0
- package/dist/resolvers/SyncDataResolver.d.ts +47 -0
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -0
- package/dist/resolvers/SyncDataResolver.js +345 -0
- package/dist/resolvers/SyncDataResolver.js.map +1 -0
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/package.json +23 -22
- package/src/config.ts +2 -0
- package/src/generated/generated.ts +899 -610
- package/src/generic/ResolverBase.ts +14 -5
- package/src/index.ts +2 -0
- package/src/resolvers/AskSkipResolver.ts +300 -66
- package/src/resolvers/GetDataResolver.ts +245 -0
- package/src/resolvers/SyncDataResolver.ts +337 -0
- package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Arg, Ctx, Field, InputType, ObjectType, Query } from 'type-graphql';
|
|
2
|
+
import { AppContext } from '../types.js';
|
|
3
|
+
import { LogError, LogStatus, Metadata } from '@memberjunction/core';
|
|
4
|
+
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
|
|
7
|
+
@InputType()
|
|
8
|
+
export class GetDataInputType {
|
|
9
|
+
@Field(() => String)
|
|
10
|
+
Token: string;
|
|
11
|
+
|
|
12
|
+
@Field(() => [String])
|
|
13
|
+
Queries: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@ObjectType()
|
|
18
|
+
export class GetDataOutputType {
|
|
19
|
+
@Field(() => Boolean)
|
|
20
|
+
Success: boolean;
|
|
21
|
+
|
|
22
|
+
@Field(() => String)
|
|
23
|
+
ErrorMessage: string;
|
|
24
|
+
|
|
25
|
+
@Field(() => String)
|
|
26
|
+
SQL: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Each query's results will be converted to JSON and returned as a string
|
|
30
|
+
*/
|
|
31
|
+
@Field(() => [String])
|
|
32
|
+
Results: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@ObjectType()
|
|
36
|
+
export class SimpleEntityResultType {
|
|
37
|
+
@Field(() => Boolean)
|
|
38
|
+
Success: boolean;
|
|
39
|
+
|
|
40
|
+
@Field(() => String)
|
|
41
|
+
ErrorMessage: string;
|
|
42
|
+
|
|
43
|
+
@Field(() => [SimpleEntityOutputType])
|
|
44
|
+
Results: SimpleEntityOutputType[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@ObjectType()
|
|
48
|
+
export class SimpleEntityOutputType {
|
|
49
|
+
@Field(() => String)
|
|
50
|
+
ID: string;
|
|
51
|
+
|
|
52
|
+
@Field(() => String)
|
|
53
|
+
Name: string;
|
|
54
|
+
|
|
55
|
+
@Field(() => String)
|
|
56
|
+
Description: string;
|
|
57
|
+
|
|
58
|
+
@Field(() => String)
|
|
59
|
+
SchemaName: string;
|
|
60
|
+
|
|
61
|
+
@Field(() => String)
|
|
62
|
+
BaseView: string;
|
|
63
|
+
|
|
64
|
+
@Field(() => String)
|
|
65
|
+
BaseTable: string;
|
|
66
|
+
|
|
67
|
+
@Field(() => String)
|
|
68
|
+
CodeName: string;
|
|
69
|
+
|
|
70
|
+
@Field(() => String)
|
|
71
|
+
ClassName: string;
|
|
72
|
+
|
|
73
|
+
@Field(() => [SimpleEntityFieldOutputType])
|
|
74
|
+
Fields: SimpleEntityFieldOutputType[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@ObjectType()
|
|
78
|
+
export class SimpleEntityFieldOutputType {
|
|
79
|
+
@Field(() => String)
|
|
80
|
+
ID: string;
|
|
81
|
+
|
|
82
|
+
@Field(() => String)
|
|
83
|
+
Name: string;
|
|
84
|
+
|
|
85
|
+
@Field(() => String)
|
|
86
|
+
Description: string;
|
|
87
|
+
|
|
88
|
+
@Field(() => String)
|
|
89
|
+
Type: string;
|
|
90
|
+
|
|
91
|
+
@Field(() => Boolean)
|
|
92
|
+
AllowsNull: boolean;
|
|
93
|
+
|
|
94
|
+
@Field(() => Number)
|
|
95
|
+
MaxLength: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
export class GetDataResolver {
|
|
100
|
+
/**
|
|
101
|
+
* This mutation will sync the specified items with the existing system. Items will be processed in order and the results of each operation will be returned in the Results array within the return value.
|
|
102
|
+
* @param items - an array of ActionItemInputType objects that specify the action to be taken on the specified entity with the specified primary key and the JSON representation of the field values.
|
|
103
|
+
* @param token - the short-lived access token that is required to perform this operation.
|
|
104
|
+
*/
|
|
105
|
+
@RequireSystemUser()
|
|
106
|
+
@Query(() => GetDataOutputType)
|
|
107
|
+
async GetData(
|
|
108
|
+
@Arg('input', () => GetDataInputType) input: GetDataInputType,
|
|
109
|
+
@Ctx() context: AppContext
|
|
110
|
+
) {
|
|
111
|
+
try {
|
|
112
|
+
LogStatus(`GetDataResolver.GetData() ---- IMPORTANT - temporarily using the same connection as rest of the server, we need to separately create a READ ONLY CONNECTION and pass that in
|
|
113
|
+
the AppContext so we can use that special connection here to ensure we are using a lower privileged connection for this operation to prevent mutation from being possible.`);
|
|
114
|
+
LogStatus(`${JSON.stringify(input)}`);
|
|
115
|
+
|
|
116
|
+
// validate the token
|
|
117
|
+
if (!isTokenValid(input.Token)) {
|
|
118
|
+
throw new Error(`Token ${input.Token} is not valid or has expired`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// iterate through the items
|
|
122
|
+
let success: boolean = true;
|
|
123
|
+
const promises = input.Queries.map((q) => {
|
|
124
|
+
return context.dataSource.query(q);
|
|
125
|
+
});
|
|
126
|
+
const results = await Promise.all(promises); // run all the queries in parallel
|
|
127
|
+
|
|
128
|
+
// record the use of the token
|
|
129
|
+
recordTokenUse(input.Token, {request: input, results: results});
|
|
130
|
+
|
|
131
|
+
return { Success: success, Results: results.map((r) => JSON.stringify(r)) };
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
LogError(err);
|
|
135
|
+
return { Success: false, ErrorMessage: typeof err === 'string' ? err : (err as any).message, Results: [] };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@RequireSystemUser()
|
|
140
|
+
@Query(() => SimpleEntityResultType)
|
|
141
|
+
async GetAllEntities(
|
|
142
|
+
@Ctx() context: AppContext
|
|
143
|
+
) {
|
|
144
|
+
try {
|
|
145
|
+
const md = new Metadata();
|
|
146
|
+
const result = md.Entities.map((e) => {
|
|
147
|
+
return {
|
|
148
|
+
ID: e.ID,
|
|
149
|
+
Name: e.Name,
|
|
150
|
+
Description: e.Description,
|
|
151
|
+
SchemaName: e.SchemaName,
|
|
152
|
+
BaseView: e.BaseView,
|
|
153
|
+
BaseTable: e.BaseTable,
|
|
154
|
+
CodeName: e.CodeName,
|
|
155
|
+
ClassName: e.ClassName,
|
|
156
|
+
Fields: e.Fields.map((f) => {
|
|
157
|
+
return {
|
|
158
|
+
ID: f.ID,
|
|
159
|
+
Name: f.Name,
|
|
160
|
+
Description: f.Description,
|
|
161
|
+
Type: f.Type,
|
|
162
|
+
AllowsNull: f.AllowsNull,
|
|
163
|
+
MaxLength: f.MaxLength,
|
|
164
|
+
};
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return { Success: true, Results: result };
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
LogError(err);
|
|
172
|
+
return { Success: false, ErrorMessage: typeof err === 'string' ? err : (err as any).message, Results: [] };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
export class TokenUseLog {
|
|
181
|
+
Token: string;
|
|
182
|
+
UsedAt: Date;
|
|
183
|
+
UsePayload: any;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Used to track all active access tokens that are requested by anyone within the server to be able to send to external services that can
|
|
187
|
+
* in turn call back to the GetDataResolver to get data from the server. This is an extra security layer to ensure that tokens are short
|
|
188
|
+
* lived compared to the system level API key which rotates but less frequently.
|
|
189
|
+
*/
|
|
190
|
+
export class GetDataAccessToken {
|
|
191
|
+
Token: string;
|
|
192
|
+
ExpiresAt: Date;
|
|
193
|
+
RequstedAt: Date;
|
|
194
|
+
/**
|
|
195
|
+
* Can be used to store any payload to identify who requested the creation of the token, for example Skip might use this to put in a conversation ID to know which conversation a request is coming back for.
|
|
196
|
+
*/
|
|
197
|
+
RequestorPayload: any;
|
|
198
|
+
TokenUses: TokenUseLog[];
|
|
199
|
+
}
|
|
200
|
+
const __accessTokens: GetDataAccessToken[] = [];
|
|
201
|
+
const __defaultTokenLifeSpan = 1000 * 60 * 5; // 5 minutes
|
|
202
|
+
export function registerAccessToken(token?: string, lifeSpan: number = __defaultTokenLifeSpan, requestorPayload?: any): GetDataAccessToken {
|
|
203
|
+
const tokenToUse = token || uuidv4();
|
|
204
|
+
|
|
205
|
+
if (tokenExists(tokenToUse)) {
|
|
206
|
+
// should never happen if we used the uuidv4() function but could happen if someone tries to use a custom token
|
|
207
|
+
throw new Error(`Token ${tokenToUse} already exists`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const newToken = new GetDataAccessToken();
|
|
211
|
+
newToken.Token = tokenToUse;
|
|
212
|
+
newToken.ExpiresAt = new Date(new Date().getTime() + lifeSpan);
|
|
213
|
+
newToken.RequstedAt = new Date();
|
|
214
|
+
newToken.RequestorPayload = requestorPayload;
|
|
215
|
+
__accessTokens.push(newToken);
|
|
216
|
+
return newToken;
|
|
217
|
+
}
|
|
218
|
+
export function deleteAccessToken(token: string) {
|
|
219
|
+
const index = __accessTokens.findIndex((t) => t.Token === token);
|
|
220
|
+
if (index >= 0) {
|
|
221
|
+
__accessTokens.splice(index, 1);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
throw new Error(`Token ${token} does not exist`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export function tokenExists(token: string) {
|
|
228
|
+
return __accessTokens.find((t) => t.Token === token) !== undefined;
|
|
229
|
+
}
|
|
230
|
+
export function isTokenValid(token: string) {
|
|
231
|
+
const t = __accessTokens.find((t) => t.Token === token);
|
|
232
|
+
if (t) {
|
|
233
|
+
return t.ExpiresAt > new Date();
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
export function recordTokenUse(token: string, usePayload: any) {
|
|
238
|
+
const t = __accessTokens.find((t) => t.Token === token);
|
|
239
|
+
if (t) {
|
|
240
|
+
t.TokenUses.push({ Token: token, UsedAt: new Date(), UsePayload: usePayload });
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
throw new Error(`Token ${token} does not exist`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType } from 'type-graphql';
|
|
2
|
+
import { AppContext } from '../types.js';
|
|
3
|
+
import { BaseEntity, CompositeKey, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
|
+
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
|
+
import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* This type defines the possible list of actions that can be taken in syncing data: Create, Update, CreateOrUpdate, Delete, or DeleteWithFilter
|
|
11
|
+
* DeleteWithFilter is where you specify a valid SQL expression that can be used in a where clause to get a list of records in a given entity to delete
|
|
12
|
+
* this can be used to ensure cleaning out data from a subset of a given table.
|
|
13
|
+
*/
|
|
14
|
+
export enum SyncDataActionType {
|
|
15
|
+
Create = "Create",
|
|
16
|
+
Update = "Update",
|
|
17
|
+
CreateOrUpdate = "CreateOrUpdate",
|
|
18
|
+
Delete = "Delete",
|
|
19
|
+
DeleteWithFilter = "DeleteWithFilter"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
registerEnumType(SyncDataActionType, {
|
|
23
|
+
name: "SyncDataActionType", // GraphQL Enum Name
|
|
24
|
+
description: "Specifies the type of action to be taken in syncing, Create, Update, CreateOrUpdate, Delete" // Description,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@InputType()
|
|
29
|
+
export class ActionItemInputType {
|
|
30
|
+
@Field(() => String)
|
|
31
|
+
EntityName!: string;
|
|
32
|
+
|
|
33
|
+
@Field(() => CompositeKeyInputType, {nullable: true})
|
|
34
|
+
PrimaryKey?: CompositeKeyInputType;
|
|
35
|
+
|
|
36
|
+
@Field(() => CompositeKeyInputType, {nullable: true})
|
|
37
|
+
AlternateKey?: CompositeKeyInputType;
|
|
38
|
+
|
|
39
|
+
@Field(() => SyncDataActionType)
|
|
40
|
+
Type!: SyncDataActionType;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* This field is a JSON representation of the field values of the entity to be created or updated. It is used for all ActionTypes except for
|
|
44
|
+
*/
|
|
45
|
+
@Field(() => String, {nullable: true})
|
|
46
|
+
RecordJSON?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* This field is only provided when the Action Type is DeleteWithFilter. It is a valid SQL expression that can be used in a where clause to get a list of records in a given entity to delete
|
|
50
|
+
*/
|
|
51
|
+
@Field(() => String, {nullable: true})
|
|
52
|
+
DeleteFilter?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@ObjectType()
|
|
57
|
+
export class ActionItemOutputType {
|
|
58
|
+
@Field(() => Boolean)
|
|
59
|
+
Success: boolean;
|
|
60
|
+
|
|
61
|
+
@Field(() => String)
|
|
62
|
+
ErrorMessage: string;
|
|
63
|
+
|
|
64
|
+
@Field(() => String)
|
|
65
|
+
EntityName!: string;
|
|
66
|
+
|
|
67
|
+
@Field(() => CompositeKeyOutputType, {nullable: true})
|
|
68
|
+
PrimaryKey?: CompositeKeyOutputType;
|
|
69
|
+
|
|
70
|
+
@Field(() => CompositeKeyOutputType, {nullable: true})
|
|
71
|
+
AlternateKey?: CompositeKeyOutputType;
|
|
72
|
+
|
|
73
|
+
@Field(() => SyncDataActionType)
|
|
74
|
+
Type!: SyncDataActionType;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* This field is a JSON representation of the field values of the entity to be created or updated. It is used for all ActionTypes except for
|
|
78
|
+
*/
|
|
79
|
+
@Field(() => String, {nullable: true})
|
|
80
|
+
RecordJSON?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* This field is only provided when the Action Type is DeleteWithFilter. It is a valid SQL expression that can be used in a where clause to get a list of records in a given entity to delete
|
|
84
|
+
*/
|
|
85
|
+
@Field(() => String, {nullable: true})
|
|
86
|
+
DeleteFilter?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@ObjectType()
|
|
90
|
+
export class SyncDataResultType {
|
|
91
|
+
@Field(() => Boolean)
|
|
92
|
+
Success: boolean;
|
|
93
|
+
|
|
94
|
+
@Field(() => [ActionItemOutputType])
|
|
95
|
+
Results: ActionItemOutputType[] = [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class SyncDataResolver {
|
|
99
|
+
/**
|
|
100
|
+
* This mutation will sync the specified items with the existing system. Items will be processed in order and the results of each operation will be returned in the Results array within the return value.
|
|
101
|
+
* @param items - an array of ActionItemInputType objects that specify the action to be taken on the specified entity with the specified primary key and the JSON representation of the field values.
|
|
102
|
+
*/
|
|
103
|
+
@RequireSystemUser()
|
|
104
|
+
@Mutation(() => SyncDataResultType)
|
|
105
|
+
async SyncData(
|
|
106
|
+
@Arg('items', () => [ActionItemInputType] ) items: ActionItemInputType[],
|
|
107
|
+
@Ctx() context: AppContext
|
|
108
|
+
) {
|
|
109
|
+
try {
|
|
110
|
+
// iterate through the items
|
|
111
|
+
const md = new Metadata();
|
|
112
|
+
const results: ActionItemOutputType[] = [];
|
|
113
|
+
for (const item of items) {
|
|
114
|
+
results.push(await this.SyncSingleItem(item, context, md));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const overallSuccess = !results.some((r) => !r.Success); // if any element in the array of results has a Success value of false, then the overall success is false
|
|
118
|
+
return { Success: overallSuccess, Results: results };
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
LogError(err);
|
|
122
|
+
throw new Error('SyncDataResolver::SyncData --- Error Syncing Data\n\n' + err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected async SyncSingleItem(item: ActionItemInputType, context: AppContext, md: Metadata): Promise<ActionItemOutputType> {
|
|
127
|
+
const result = new ActionItemOutputType();
|
|
128
|
+
result.AlternateKey = item.AlternateKey;
|
|
129
|
+
result.PrimaryKey = item.PrimaryKey;
|
|
130
|
+
result.DeleteFilter = item.DeleteFilter;
|
|
131
|
+
result.EntityName = item.EntityName;
|
|
132
|
+
result.RecordJSON = item.RecordJSON;
|
|
133
|
+
result.Type = item.Type;
|
|
134
|
+
result.Success = false;
|
|
135
|
+
result.ErrorMessage = '';
|
|
136
|
+
try {
|
|
137
|
+
const e = md.Entities.find((e) => e.Name === item.EntityName);
|
|
138
|
+
if (e) {
|
|
139
|
+
const pk = item.PrimaryKey ? new CompositeKey(item.PrimaryKey.KeyValuePairs) : null;
|
|
140
|
+
const ak = item.AlternateKey ? new CompositeKey(item.AlternateKey.KeyValuePairs) : null;
|
|
141
|
+
const entityObject = item.Type === SyncDataActionType.DeleteWithFilter ? null : await md.GetEntityObject(e.Name, context.userPayload.userRecord);
|
|
142
|
+
const fieldValues = item.RecordJSON ? JSON.parse(item.RecordJSON) : {};
|
|
143
|
+
switch (item.Type) {
|
|
144
|
+
case SyncDataActionType.Create:
|
|
145
|
+
await this.SyncSingleItemCreate(entityObject, fieldValues, result);
|
|
146
|
+
break;
|
|
147
|
+
case SyncDataActionType.Update:
|
|
148
|
+
await this.SyncSingleItemUpdate(entityObject, pk, ak, fieldValues, result);
|
|
149
|
+
break;
|
|
150
|
+
case SyncDataActionType.CreateOrUpdate:
|
|
151
|
+
// in this case we attempt to load the item first, if it is not possible to load the item, then we create it
|
|
152
|
+
await this.SyncSingleItemCreateOrUpdate(entityObject, pk, ak, fieldValues, result);
|
|
153
|
+
break;
|
|
154
|
+
case SyncDataActionType.Delete:
|
|
155
|
+
await this.SyncSingleItemDelete(entityObject, pk, ak, result);
|
|
156
|
+
break;
|
|
157
|
+
case SyncDataActionType.DeleteWithFilter:
|
|
158
|
+
await this.SyncSingleItemDeleteWithFilter(item.EntityName, item.DeleteFilter, result, context.userPayload.userRecord);
|
|
159
|
+
break;
|
|
160
|
+
default:
|
|
161
|
+
throw new Error('Invalid SyncDataActionType');
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
throw new Error('Entity not found');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
result.ErrorMessage = typeof err === 'string' ? err : (err as any).message;
|
|
169
|
+
LogError(err);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
protected async SyncSingleItemDeleteWithFilter(entityName: string, filter: string, result: ActionItemOutputType, user: UserInfo) {
|
|
178
|
+
try {
|
|
179
|
+
// here we will iterate through the result of a RunView on the entityname/filter and delete each matching record
|
|
180
|
+
let overallSuccess: boolean = true;
|
|
181
|
+
let combinedErrorMessage: string = "";
|
|
182
|
+
const rv = new RunView();
|
|
183
|
+
const data = await rv.RunView<BaseEntity>({
|
|
184
|
+
EntityName: entityName,
|
|
185
|
+
ExtraFilter: filter,
|
|
186
|
+
ResultType: 'entity_object'
|
|
187
|
+
}, user);
|
|
188
|
+
if (data && data.Success) {
|
|
189
|
+
for (const entityObject of data.Results) {
|
|
190
|
+
if (!await entityObject.Delete()) {
|
|
191
|
+
overallSuccess = false;
|
|
192
|
+
combinedErrorMessage += 'Failed to delete the item :' + entityObject.LatestResult.Message + '\n';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
result.Success = overallSuccess
|
|
196
|
+
if (!overallSuccess) {
|
|
197
|
+
result.ErrorMessage = combinedErrorMessage
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
result.Success = false;
|
|
202
|
+
result.ErrorMessage = 'Failed to run the view to get the list of items to delete for entity: ' + entityName + ' with filter: ' + filter + '\n';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
result.ErrorMessage = typeof e === 'string' ? e : (e as any).message;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
protected async LoadFromAlternateKey(entityName: string, alternateKey: CompositeKey, user: UserInfo): Promise<BaseEntity> {
|
|
211
|
+
try {
|
|
212
|
+
// no primary key provided, attempt to look up the primary key based on the
|
|
213
|
+
const rv = new RunView();
|
|
214
|
+
const md = new Metadata();
|
|
215
|
+
const entity = md.EntityByName(entityName);
|
|
216
|
+
const r = await rv.RunView<BaseEntity>({
|
|
217
|
+
EntityName: entityName,
|
|
218
|
+
ExtraFilter: alternateKey.KeyValuePairs.map((kvp) => {
|
|
219
|
+
const fieldInfo = entity.Fields.find((f) => f.Name === kvp.FieldName);
|
|
220
|
+
const quotes = fieldInfo.NeedsQuotes ? "'" : '';
|
|
221
|
+
return `${kvp.FieldName} = ${quotes}${kvp.Value}${quotes}`;
|
|
222
|
+
}).join(' AND '),
|
|
223
|
+
ResultType: 'entity_object'
|
|
224
|
+
}, user);
|
|
225
|
+
if (r && r.Success && r.Results.length === 1) {
|
|
226
|
+
return r.Results[0];
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
//LogError (`Failed to load the item with alternate key: ${alternateKey.KeyValuePairs.map((kvp) => `${kvp.FieldName} = ${kvp.Value}`).join(' AND ')}. Result: ${r.Success} and ${r.Results?.length} items returned`);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
LogError(e);
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
protected async SyncSingleItemCreateOrUpdate(entityObject: BaseEntity, pk: CompositeKey, ak: CompositeKey, fieldValues: any, result: ActionItemOutputType) {
|
|
240
|
+
if (!pk || pk.KeyValuePairs.length === 0) {
|
|
241
|
+
// no primary key try to load from alt key
|
|
242
|
+
const altKeyResult = await this.LoadFromAlternateKey(entityObject.EntityInfo.Name, ak, entityObject.ContextCurrentUser);
|
|
243
|
+
if (!altKeyResult) {
|
|
244
|
+
// no record found, create a new one
|
|
245
|
+
await this.SyncSingleItemCreate(entityObject, fieldValues, result);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
await this.InnerSyncSingleItemUpdate(altKeyResult, fieldValues, result);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// have a primary key do the usual load
|
|
253
|
+
if (await entityObject.InnerLoad(pk)) {
|
|
254
|
+
await this.InnerSyncSingleItemUpdate(entityObject, fieldValues, result);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
await this.SyncSingleItemCreate(entityObject, fieldValues, result);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
protected async SyncSingleItemDelete(entityObject: BaseEntity, pk: CompositeKey, ak: CompositeKey, result: ActionItemOutputType) {
|
|
263
|
+
if (!pk || pk.KeyValuePairs.length === 0) {
|
|
264
|
+
const altKeyResult = await this.LoadFromAlternateKey(entityObject.EntityInfo.Name, ak, entityObject.ContextCurrentUser);
|
|
265
|
+
if (!altKeyResult) {
|
|
266
|
+
result.ErrorMessage = 'Failed to load the item, it is possible the record with the specified primary key does not exist';
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
if (await altKeyResult.Delete()) {
|
|
270
|
+
result.Success = true;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
result.ErrorMessage = 'Failed to delete the item :' + entityObject.LatestResult.Message;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else if (await entityObject.InnerLoad(pk)) {
|
|
278
|
+
if (await entityObject.Delete()) {
|
|
279
|
+
result.Success = true;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
result.ErrorMessage = 'Failed to delete the item :' + entityObject.LatestResult.Message;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
result.ErrorMessage = 'Failed to load the item, it is possible the record with the specified primary key does not exist';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
protected async SyncSingleItemCreate(entityObject: BaseEntity, fieldValues: any, result: ActionItemOutputType) {
|
|
291
|
+
// make sure we strip out the primary key from fieldValues before we pass it in because otherwise it will appear to be an existing record to the BaseEntity
|
|
292
|
+
const noPKValues = {...fieldValues};
|
|
293
|
+
entityObject.EntityInfo.PrimaryKeys.forEach((pk) => {
|
|
294
|
+
delete noPKValues[pk.Name];
|
|
295
|
+
});
|
|
296
|
+
entityObject.SetMany(noPKValues);
|
|
297
|
+
if (await entityObject.Save()) {
|
|
298
|
+
result.Success = true;
|
|
299
|
+
result.PrimaryKey = new CompositeKey(entityObject.PrimaryKeys.map((pk) => ({FieldName: pk.Name, Value: pk.Value})));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
result.ErrorMessage = 'Failed to create the item :' + entityObject.LatestResult.Message;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
protected async SyncSingleItemUpdate(entityObject: BaseEntity, pk: CompositeKey, ak: CompositeKey, fieldValues: any, result: ActionItemOutputType) {
|
|
307
|
+
if (!pk || pk.KeyValuePairs.length === 0) {
|
|
308
|
+
// no pk, attempt to load by alt key
|
|
309
|
+
const altKeyResult = await this.LoadFromAlternateKey(entityObject.EntityInfo.Name, ak, entityObject.ContextCurrentUser);
|
|
310
|
+
if (!altKeyResult) {
|
|
311
|
+
// no record found, create a new one
|
|
312
|
+
result.ErrorMessage = 'Failed to load the item, it is possible the record with the specified alternate key does not exist';
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
await this.InnerSyncSingleItemUpdate(altKeyResult, fieldValues, result);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (await entityObject.InnerLoad(pk)) {
|
|
319
|
+
await this.InnerSyncSingleItemUpdate(entityObject, fieldValues, result);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// failed to load the item
|
|
323
|
+
result.ErrorMessage = 'Failed to load the item, it is possible the record with the specified primary key does not exist';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
protected async InnerSyncSingleItemUpdate(entityObject: BaseEntity, fieldValues: any, result: ActionItemOutputType) {
|
|
328
|
+
entityObject.SetMany(fieldValues);
|
|
329
|
+
if (await entityObject.Save()) {
|
|
330
|
+
result.Success = true;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
result.ErrorMessage = 'Failed to update the item :' + entityObject.LatestResult.Message;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Arg, Ctx, Field, InputType,
|
|
1
|
+
import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType } from 'type-graphql';
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
|
-
import { LogError, Metadata, RunView,
|
|
3
|
+
import { LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { RoleEntity, UserEntity, UserRoleEntity } from '@memberjunction/core-entities';
|
|
6
6
|
|