@memberjunction/server 2.32.1 → 2.33.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.
@@ -0,0 +1,547 @@
1
+ import { Resolver, Mutation, Arg, Ctx } from "type-graphql";
2
+ import { ActionEngineServer } from "@memberjunction/actions";
3
+ import { EntityActionEngineServer } from "@memberjunction/actions";
4
+ import { Metadata, UserInfo, BaseEntity, CompositeKey, KeyValuePair, LogError } from "@memberjunction/core";
5
+ import { ActionParam } from "@memberjunction/actions-base";
6
+ import { Field, InputType, ObjectType } from "type-graphql";
7
+ import { KeyValuePairInput } from "../generic/KeyValuePairInput";
8
+
9
+ /**
10
+ * Input type for action parameters
11
+ * Used to pass parameters to actions when invoking them
12
+ */
13
+ @InputType()
14
+ export class ActionParamInput {
15
+ /**
16
+ * The name of the parameter
17
+ */
18
+ @Field()
19
+ Name: string;
20
+
21
+ /**
22
+ * The value of the parameter
23
+ * Complex objects should be serialized to JSON strings
24
+ */
25
+ @Field({ nullable: true })
26
+ Value: string;
27
+
28
+ /**
29
+ * The data type of the parameter
30
+ * Used for type conversion on the server
31
+ */
32
+ @Field()
33
+ Type: string;
34
+ }
35
+
36
+ /**
37
+ * Input type for running an action
38
+ */
39
+ @InputType()
40
+ export class RunActionInput {
41
+ /**
42
+ * The ID of the action to run
43
+ */
44
+ @Field()
45
+ ActionID: string;
46
+
47
+ /**
48
+ * Parameters to pass to the action
49
+ */
50
+ @Field(() => [ActionParamInput], { nullable: true })
51
+ Params?: ActionParamInput[];
52
+
53
+ /**
54
+ * Whether to skip logging the action execution
55
+ * Defaults to false
56
+ */
57
+ @Field(() => Boolean, { nullable: true })
58
+ SkipActionLog?: boolean;
59
+ }
60
+
61
+ /**
62
+ * Represents a collection of key-value pairs that make up a composite key
63
+ * Used for both primary keys and foreign keys
64
+ */
65
+ @InputType()
66
+ export class CompositeKeyInput {
67
+ /**
68
+ * The collection of key-value pairs that make up the composite key
69
+ */
70
+ @Field(() => [KeyValuePairInput])
71
+ KeyValuePairs: KeyValuePairInput[];
72
+ }
73
+
74
+ /**
75
+ * Input type for running entity actions
76
+ */
77
+ @InputType()
78
+ export class EntityActionInput {
79
+ /**
80
+ * The ID of the entity action to run
81
+ */
82
+ @Field()
83
+ EntityActionID: string;
84
+
85
+ /**
86
+ * The type of invocation (SingleRecord, View, List, etc.)
87
+ */
88
+ @Field()
89
+ InvocationType: string;
90
+
91
+ /**
92
+ * The name of the entity
93
+ * This is the preferred way to identify an entity as it's more human-readable than EntityID
94
+ */
95
+ @Field(() => String, { nullable: true })
96
+ EntityName?: string;
97
+
98
+ /**
99
+ * The ID of the entity
100
+ * Use EntityName instead when possible for better code readability
101
+ * @deprecated Use EntityName instead when possible
102
+ */
103
+ @Field(() => String, { nullable: true })
104
+ EntityID?: string;
105
+
106
+ /**
107
+ * The primary key of the entity record to act on
108
+ * This is used for SingleRecord invocation types
109
+ */
110
+ @Field(() => CompositeKeyInput, { nullable: true })
111
+ PrimaryKey?: CompositeKeyInput;
112
+
113
+ /**
114
+ * The ID of the list to operate on
115
+ * This is used for List invocation types
116
+ */
117
+ @Field(() => String, { nullable: true })
118
+ ListID?: string;
119
+
120
+ /**
121
+ * The ID of the view to operate on
122
+ * This is used for View invocation types
123
+ */
124
+ @Field(() => String, { nullable: true })
125
+ ViewID?: string;
126
+
127
+ /**
128
+ * Additional parameters to pass to the action
129
+ */
130
+ @Field(() => [ActionParamInput], { nullable: true })
131
+ Params?: ActionParamInput[];
132
+ }
133
+
134
+ /**
135
+ * Output type for action results
136
+ * Used to return results from actions to clients
137
+ */
138
+ @ObjectType()
139
+ export class ActionResultOutput {
140
+ /**
141
+ * Whether the action was executed successfully
142
+ */
143
+ @Field()
144
+ Success: boolean;
145
+
146
+ /**
147
+ * Optional message describing the result of the action
148
+ */
149
+ @Field({ nullable: true })
150
+ Message?: string;
151
+
152
+ /**
153
+ * Optional result code from the action
154
+ */
155
+ @Field(() => String, { nullable: true })
156
+ ResultCode?: string;
157
+
158
+ /**
159
+ * Optional result data from the action
160
+ * Complex objects are serialized to JSON strings
161
+ */
162
+ @Field(() => String, { nullable: true })
163
+ ResultData?: string;
164
+ }
165
+
166
+ /**
167
+ * Resolver for action-related GraphQL operations
168
+ * Handles running actions and entity actions through GraphQL
169
+ */
170
+ @Resolver()
171
+ export class ActionResolver {
172
+ /**
173
+ * Mutation for running an action
174
+ * @param input The input parameters for running the action
175
+ * @param ctx The GraphQL context containing user authentication information
176
+ * @returns The result of running the action
177
+ */
178
+ @Mutation(() => ActionResultOutput)
179
+ async RunAction(
180
+ @Arg("input") input: RunActionInput,
181
+ @Ctx() ctx: any
182
+ ): Promise<ActionResultOutput> {
183
+ try {
184
+ // Get the user from context
185
+ const user = this.getUserFromContext(ctx);
186
+
187
+ // Initialize the action engine
188
+ await ActionEngineServer.Instance.Config(false, user);
189
+
190
+ // Get the action by ID
191
+ const action = this.findActionById(input.ActionID);
192
+
193
+ // Parse the parameters
194
+ const params = this.parseActionParameters(input.Params);
195
+
196
+ // Run the action
197
+ const result = await this.executeAction(action, user, params, input.SkipActionLog);
198
+
199
+ // Return the result
200
+ return this.createActionResult(result);
201
+ } catch (e) {
202
+ return this.handleActionError(e);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Finds an action by its ID
208
+ * @param actionID The ID of the action to find
209
+ * @returns The action
210
+ * @throws Error if the action is not found
211
+ * @private
212
+ */
213
+ private findActionById(actionID: string): any {
214
+ const action = ActionEngineServer.Instance.Actions.find(a => a.ID === actionID);
215
+ if (!action) {
216
+ throw new Error(`Action with ID ${actionID} not found`);
217
+ }
218
+ return action;
219
+ }
220
+
221
+ /**
222
+ * Parses action parameters from the input
223
+ * @param inputParams The input parameters
224
+ * @returns The parsed parameters
225
+ * @private
226
+ */
227
+ private parseActionParameters(inputParams?: ActionParamInput[]): ActionParam[] {
228
+ if (!inputParams || inputParams.length === 0) {
229
+ return [];
230
+ }
231
+
232
+ return inputParams.map(p => {
233
+ let value: any = p.Value;
234
+
235
+ // Try to parse JSON for complex values
236
+ try {
237
+ if (p.Value && (p.Type === 'object' || p.Type === 'array')) {
238
+ value = JSON.parse(p.Value);
239
+ }
240
+ } catch (e) {
241
+ // If parsing fails, keep the original value
242
+ const error = e as Error;
243
+ LogError(`Failed to parse parameter value as JSON: ${error.message}`);
244
+ }
245
+
246
+ return {
247
+ Name: p.Name,
248
+ Value: value,
249
+ Type: 'Input' // Default to Input type since we're sending parameters
250
+ };
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Executes an action
256
+ * @param action The action to execute
257
+ * @param user The user context
258
+ * @param params The action parameters
259
+ * @param skipActionLog Whether to skip action logging
260
+ * @returns The action result
261
+ * @private
262
+ */
263
+ private async executeAction(
264
+ action: any,
265
+ user: UserInfo,
266
+ params: ActionParam[],
267
+ skipActionLog?: boolean
268
+ ): Promise<any> {
269
+ return await ActionEngineServer.Instance.RunAction({
270
+ Action: action,
271
+ ContextUser: user,
272
+ Params: params,
273
+ SkipActionLog: skipActionLog,
274
+ Filters: []
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Creates an action result from the execution result
280
+ * @param result The execution result
281
+ * @returns The formatted action result
282
+ * @private
283
+ */
284
+ private createActionResult(result: any): ActionResultOutput {
285
+ return {
286
+ Success: result.Success,
287
+ Message: result.Message,
288
+ ResultCode: result.Result?.ResultCode,
289
+ ResultData: result.Result ? JSON.stringify(result.Result) : undefined
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Handles errors in the action resolver
295
+ * @param e The error
296
+ * @returns An error result
297
+ * @private
298
+ */
299
+ private handleActionError(e: unknown): ActionResultOutput {
300
+ const error = e as Error;
301
+ LogError(`Error in RunAction resolver: ${error}`);
302
+ return {
303
+ Success: false,
304
+ Message: `Error executing action: ${error.message}`
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Mutation for running an entity action
310
+ * @param input The input parameters for running the entity action
311
+ * @param ctx The GraphQL context containing user authentication information
312
+ * @returns The result of running the entity action
313
+ */
314
+ @Mutation(() => ActionResultOutput)
315
+ async RunEntityAction(
316
+ @Arg("input") input: EntityActionInput,
317
+ @Ctx() ctx: any
318
+ ): Promise<ActionResultOutput> {
319
+ try {
320
+ // Get the user from context
321
+ const user = this.getUserFromContext(ctx);
322
+
323
+ // Initialize the entity action engine
324
+ await EntityActionEngineServer.Instance.Config(false, user);
325
+
326
+ // Get the entity action by ID
327
+ const entityAction = this.getEntityAction(input.EntityActionID);
328
+
329
+ // Create the base parameters
330
+ const params = this.createBaseParams(entityAction, input.InvocationType, user);
331
+
332
+ // Add entity object if we have entity information and primary key
333
+ if ((input.EntityID || input.EntityName) && input.PrimaryKey && input.PrimaryKey.KeyValuePairs.length > 0) {
334
+ await this.addEntityObject(params, input, user);
335
+ }
336
+
337
+ // Add other parameters
338
+ this.addOptionalParams(params, input);
339
+
340
+ // Run the entity action
341
+ const result = await EntityActionEngineServer.Instance.RunEntityAction(params);
342
+
343
+ // Return the result
344
+ return {
345
+ Success: result.Success,
346
+ Message: result.Message,
347
+ ResultData: JSON.stringify(result)
348
+ };
349
+ } catch (e) {
350
+ return this.handleError(e);
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Gets the authenticated user from the GraphQL context
356
+ * @param ctx The GraphQL context
357
+ * @returns The authenticated user
358
+ * @throws Error if user is not authenticated
359
+ * @private
360
+ */
361
+ private getUserFromContext(ctx: any): UserInfo {
362
+ const user = ctx.user as UserInfo;
363
+ if (!user) {
364
+ throw new Error("User not authenticated");
365
+ }
366
+ return user;
367
+ }
368
+
369
+ /**
370
+ * Gets an entity action by ID
371
+ * @param actionID The ID of the entity action
372
+ * @returns The entity action
373
+ * @throws Error if entity action is not found
374
+ * @private
375
+ */
376
+ private getEntityAction(actionID: string): any {
377
+ const entityAction = EntityActionEngineServer.Instance.EntityActions.find(ea => ea.ID === actionID);
378
+ if (!entityAction) {
379
+ throw new Error(`EntityAction with ID ${actionID} not found`);
380
+ }
381
+ return entityAction;
382
+ }
383
+
384
+ /**
385
+ * Creates the base parameters for the entity action
386
+ * @param entityAction The entity action
387
+ * @param invocationTypeName The invocation type name
388
+ * @param user The authenticated user
389
+ * @returns The base parameters
390
+ * @private
391
+ */
392
+ private createBaseParams(entityAction: any, invocationTypeName: string, user: UserInfo): any {
393
+ return {
394
+ EntityAction: entityAction,
395
+ InvocationType: { Name: invocationTypeName },
396
+ ContextUser: user,
397
+ Params: [],
398
+ };
399
+ }
400
+
401
+ /**
402
+ * Adds the entity object to the parameters
403
+ * @param params The parameters to add to
404
+ * @param input The input parameters
405
+ * @param user The authenticated user
406
+ * @private
407
+ */
408
+ private async addEntityObject(params: any, input: EntityActionInput, user: UserInfo): Promise<void> {
409
+ const md = new Metadata();
410
+
411
+ // Find the entity by ID or name
412
+ let entity;
413
+ if (input.EntityName) {
414
+ entity = md.Entities.find(e => e.Name === input.EntityName);
415
+ if (!entity) {
416
+ throw new Error(`Entity with name ${input.EntityName} not found`);
417
+ }
418
+ } else if (input.EntityID) {
419
+ entity = md.Entities.find(e => e.ID === input.EntityID);
420
+ if (!entity) {
421
+ throw new Error(`Entity with ID ${input.EntityID} not found`);
422
+ }
423
+ }
424
+
425
+ if (!entity) {
426
+ throw new Error("Entity information is required");
427
+ }
428
+
429
+ // Create a composite key and load the entity object
430
+ const compositeKey = this.createCompositeKey(entity, input.PrimaryKey);
431
+ const entityObject = await md.GetEntityObject(entity.Name);
432
+ await entityObject.InnerLoad(compositeKey);
433
+ params['EntityObject'] = entityObject;
434
+ }
435
+
436
+ /**
437
+ * Creates a composite key from the input
438
+ * @param entity The entity information
439
+ * @param primaryKey The primary key input
440
+ * @returns The composite key
441
+ * @private
442
+ */
443
+ private createCompositeKey(entity: any, primaryKey: CompositeKeyInput): CompositeKey {
444
+ const compositeKey = new CompositeKey();
445
+
446
+ for (const kvp of primaryKey.KeyValuePairs) {
447
+ // Convert value based on the field type if necessary
448
+ const field = entity.Fields.find(f => f.Name === kvp.Key);
449
+ let value: any = kvp.Value;
450
+
451
+ // If the field is found, try to convert to proper type
452
+ if (field) {
453
+ value = this.convertValueToProperType(value, field);
454
+ }
455
+
456
+ // Add to composite key
457
+ const kvPair = new KeyValuePair();
458
+ kvPair.FieldName = kvp.Key;
459
+ kvPair.Value = value;
460
+ compositeKey.KeyValuePairs.push(kvPair);
461
+ }
462
+
463
+ return compositeKey;
464
+ }
465
+
466
+ /**
467
+ * Converts a value to the proper type based on the field information
468
+ * @param value The value to convert
469
+ * @param field The field information
470
+ * @returns The converted value
471
+ * @private
472
+ */
473
+ private convertValueToProperType(value: any, field: any): any {
474
+ // Simple conversion, could be enhanced for other types
475
+ if (field.Type.toLowerCase().match(/int|decimal|float|money|numeric|real/) && !isNaN(Number(value))) {
476
+ return Number(value);
477
+ } else if (field.Type.toLowerCase().includes('date') && !isNaN(Date.parse(value))) {
478
+ return new Date(value);
479
+ }
480
+ return value;
481
+ }
482
+
483
+ /**
484
+ * Adds optional parameters to the entity action parameters
485
+ * @param params The parameters to add to
486
+ * @param input The input parameters
487
+ * @private
488
+ */
489
+ private addOptionalParams(params: any, input: EntityActionInput): void {
490
+ // Add list ID if provided
491
+ if (input.ListID) {
492
+ params['ListID'] = input.ListID;
493
+ }
494
+
495
+ // Add view ID if provided
496
+ if (input.ViewID) {
497
+ params['ViewID'] = input.ViewID;
498
+ }
499
+
500
+ // Add additional parameters if provided
501
+ if (input.Params && input.Params.length > 0) {
502
+ params.Params = input.Params.map(p => this.convertParameterValue(p));
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Converts a parameter value to the proper format
508
+ * @param p The parameter to convert
509
+ * @returns The converted parameter
510
+ * @private
511
+ */
512
+ private convertParameterValue(p: ActionParamInput): any {
513
+ let value: any = p.Value;
514
+
515
+ // Try to parse JSON for complex values
516
+ try {
517
+ if (p.Value && (p.Type === 'object' || p.Type === 'array')) {
518
+ value = JSON.parse(p.Value);
519
+ }
520
+ } catch (e) {
521
+ // If parsing fails, keep the original value
522
+ const error = e as Error;
523
+ LogError(`Failed to parse parameter value as JSON: ${error.message}`);
524
+ }
525
+
526
+ return {
527
+ Name: p.Name,
528
+ Value: value,
529
+ Type: 'Input' // Default to Input type since we're sending parameters
530
+ };
531
+ }
532
+
533
+ /**
534
+ * Handles errors in the entity action resolver
535
+ * @param e The error
536
+ * @returns An error result
537
+ * @private
538
+ */
539
+ private handleError(e: unknown): ActionResultOutput {
540
+ const error = e as Error;
541
+ LogError(`Error in RunEntityAction resolver: ${error}`);
542
+ return {
543
+ Success: false,
544
+ Message: `Error executing entity action: ${error.message}`
545
+ };
546
+ }
547
+ }
@@ -1,7 +1,7 @@
1
1
  import { Arg, Ctx, Field, ObjectType, Query } from "type-graphql";
2
- import { AppContext } from "../types";
2
+ import { AppContext } from "../types.js";
3
3
  import { DataContext } from "@memberjunction/data-context";
4
- import { GetReadOnlyDataSource } from "../util";
4
+ import { GetReadOnlyDataSource } from "../util.js";
5
5
  import { Metadata } from "@memberjunction/core";
6
6
  import { DataContextItemEntity } from "@memberjunction/core-entities";
7
7