@memberjunction/server 2.43.0 → 2.45.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.
Files changed (34) hide show
  1. package/README.md +228 -2
  2. package/dist/generated/generated.d.ts +227 -5
  3. package/dist/generated/generated.d.ts.map +1 -1
  4. package/dist/generated/generated.js +1409 -29
  5. package/dist/generated/generated.js.map +1 -1
  6. package/dist/generic/ResolverBase.d.ts.map +1 -1
  7. package/dist/generic/ResolverBase.js.map +1 -1
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/resolvers/AskSkipResolver.d.ts +1 -1
  13. package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
  14. package/dist/resolvers/AskSkipResolver.js +79 -48
  15. package/dist/resolvers/AskSkipResolver.js.map +1 -1
  16. package/dist/resolvers/ReportResolver.d.ts.map +1 -1
  17. package/dist/resolvers/ReportResolver.js +2 -1
  18. package/dist/resolvers/ReportResolver.js.map +1 -1
  19. package/dist/resolvers/RunAIPromptResolver.d.ts +19 -0
  20. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -0
  21. package/dist/resolvers/RunAIPromptResolver.js +188 -0
  22. package/dist/resolvers/RunAIPromptResolver.js.map +1 -0
  23. package/dist/resolvers/RunTemplateResolver.d.ts +14 -0
  24. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -0
  25. package/dist/resolvers/RunTemplateResolver.js +138 -0
  26. package/dist/resolvers/RunTemplateResolver.js.map +1 -0
  27. package/package.json +23 -22
  28. package/src/generated/generated.ts +880 -21
  29. package/src/generic/ResolverBase.ts +2 -1
  30. package/src/index.ts +2 -0
  31. package/src/resolvers/AskSkipResolver.ts +120 -76
  32. package/src/resolvers/ReportResolver.ts +4 -1
  33. package/src/resolvers/RunAIPromptResolver.ts +169 -0
  34. package/src/resolvers/RunTemplateResolver.ts +130 -0
@@ -742,6 +742,7 @@ export class ResolverBase {
742
742
  if (await entityObject.Save()) {
743
743
  // save worked, fire afterevent and return all the data
744
744
  await this.AfterUpdate(dataSource, input); // fire event
745
+
745
746
  return this.MapFieldNamesToCodeNames(entityName, entityObject.GetAll());
746
747
  } else {
747
748
  throw new GraphQLError(entityObject.LatestResult?.Message ?? 'Unknown error', {
@@ -753,7 +754,7 @@ export class ResolverBase {
753
754
  extensions: { code: 'SAVE_ENTITY_ERROR', entityName },
754
755
  });
755
756
  }
756
-
757
+
757
758
  /**
758
759
  * 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
759
760
  * 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
package/src/index.ts CHANGED
@@ -52,6 +52,8 @@ export { TokenExpiredError, getSystemUser } from './auth/index.js';
52
52
  export * from './generic/PushStatusResolver.js';
53
53
  export * from './generic/ResolverBase.js';
54
54
  export * from './generic/RunViewResolver.js';
55
+ export * from './resolvers/RunTemplateResolver.js';
56
+ export * from './resolvers/RunAIPromptResolver.js';
55
57
  export * from './generic/KeyValuePairInput.js';
56
58
  export * from './generic/KeyInputOutputTypes.js';
57
59
  export * from './generic/DeleteOptionsInput.js';
@@ -51,6 +51,7 @@ import {
51
51
  DataContextEntity,
52
52
  DataContextItemEntity,
53
53
  UserNotificationEntity,
54
+ AIAgentEntityExtended
54
55
  } from '@memberjunction/core-entities';
55
56
  import { DataSource } from 'typeorm';
56
57
  import { apiKey, baseUrl, configInfo, graphqlPort, mj_core_schema } from '../config.js';
@@ -60,7 +61,7 @@ import { MJGlobal, CopyScalarsAndArrays } from '@memberjunction/global';
60
61
  import { sendPostRequest } from '../util.js';
61
62
  import { GetAIAPIKey } from '@memberjunction/ai';
62
63
  import { CompositeKeyInputType } from '../generic/KeyInputOutputTypes.js';
63
- import { AIAgentEntityExtended, AIEngine } from '@memberjunction/aiengine';
64
+ import { AIEngine } from '@memberjunction/aiengine';
64
65
  import { deleteAccessToken, GetDataAccessToken, registerAccessToken, tokenExists } from './GetDataResolver.js';
65
66
  import e from 'express';
66
67
 
@@ -414,7 +415,7 @@ export class AskSkipResolver {
414
415
  conversationDetailID: convoDetailEntity.ID,
415
416
  });
416
417
 
417
- return this.handleSimpleSkipChatPostRequest(input, convoEntity.ID, convoDetailEntity.ID, true, user);
418
+ return this.handleSimpleSkipChatPostRequest(input, convoEntity, convoDetailEntity, true, user);
418
419
  }
419
420
 
420
421
  /**
@@ -641,51 +642,69 @@ export class AskSkipResolver {
641
642
  * Sends the chat request and processes the response
642
643
  *
643
644
  * @param input The chat request payload
644
- * @param conversationID ID of the conversation, or empty for a new conversation
645
- * @param UserMessageConversationDetailId ID of the user's message in the conversation
645
+ * @param convoEntity The conversation entity object
646
+ * @param convoDetailEntity The conversation detail entity object
646
647
  * @param createAIMessageConversationDetail Whether to create a conversation detail for the AI response
647
648
  * @param user User context for the request
648
649
  * @returns Result of the Skip interaction
649
650
  */
650
651
  protected async handleSimpleSkipChatPostRequest(
651
652
  input: SkipAPIRequest,
652
- conversationID: string = '',
653
- UserMessageConversationDetailId: string = '',
653
+ convoEntity: ConversationEntity = null,
654
+ convoDetailEntity: ConversationDetailEntity = null,
654
655
  createAIMessageConversationDetail: boolean = false,
655
656
  user: UserInfo = null
656
657
  ): Promise<AskSkipResultType> {
657
658
  const skipConfigInfo = configInfo.askSkip;
658
659
  LogStatus(` >>> HandleSimpleSkipChatPostRequest Sending request to Skip API: ${skipConfigInfo.chatURL}`);
659
660
 
660
- const response = await sendPostRequest(skipConfigInfo.chatURL, input, true, null);
661
-
662
- if (response && response.length > 0) {
663
- // the last object in the response array is the final response from the Skip API
664
- const apiResponse = <SkipAPIResponse>response[response.length - 1].value;
665
- const AIMessageConversationDetailID = createAIMessageConversationDetail
666
- ? await this.CreateAIMessageConversationDetail(apiResponse, conversationID, user)
667
- : '';
668
- // const apiResponse = <SkipAPIResponse>response.data;
669
- LogStatus(` Skip API response: ${apiResponse.responsePhase}`);
670
- return {
671
- Success: true,
672
- Status: 'OK',
673
- ResponsePhase: SkipResponsePhase.AnalysisComplete,
674
- ConversationId: conversationID,
675
- UserMessageConversationDetailId: UserMessageConversationDetailId,
676
- AIMessageConversationDetailId: AIMessageConversationDetailID,
677
- Result: JSON.stringify(apiResponse),
678
- };
679
- } else {
680
- return {
681
- Success: false,
682
- Status: 'Error',
683
- Result: `Request failed`,
684
- ResponsePhase: SkipResponsePhase.AnalysisComplete,
685
- ConversationId: conversationID,
686
- UserMessageConversationDetailId: UserMessageConversationDetailId,
687
- AIMessageConversationDetailId: '',
688
- };
661
+ try {
662
+ const response = await sendPostRequest(skipConfigInfo.chatURL, input, true, null);
663
+
664
+ if (response && response.length > 0) {
665
+ // the last object in the response array is the final response from the Skip API
666
+ const apiResponse = <SkipAPIResponse>response[response.length - 1].value;
667
+ const AIMessageConversationDetailID = createAIMessageConversationDetail && convoEntity
668
+ ? await this.CreateAIMessageConversationDetail(apiResponse, convoEntity.ID, user)
669
+ : '';
670
+ // const apiResponse = <SkipAPIResponse>response.data;
671
+ LogStatus(` Skip API response: ${apiResponse.responsePhase}`);
672
+ return {
673
+ Success: true,
674
+ Status: 'OK',
675
+ ResponsePhase: SkipResponsePhase.AnalysisComplete,
676
+ ConversationId: convoEntity ? convoEntity.ID : '',
677
+ UserMessageConversationDetailId: convoDetailEntity ? convoDetailEntity.ID : '',
678
+ AIMessageConversationDetailId: AIMessageConversationDetailID,
679
+ Result: JSON.stringify(apiResponse),
680
+ };
681
+ } else {
682
+ // Set conversation status to Available on failure so user can try again (if conversation exists)
683
+ if (convoEntity) {
684
+ await this.setConversationStatus(convoEntity, 'Available');
685
+ }
686
+
687
+ return {
688
+ Success: false,
689
+ Status: 'Error',
690
+ Result: `Request failed`,
691
+ ResponsePhase: SkipResponsePhase.AnalysisComplete,
692
+ ConversationId: convoEntity ? convoEntity.ID : '',
693
+ UserMessageConversationDetailId: convoDetailEntity ? convoDetailEntity.ID : '',
694
+ AIMessageConversationDetailId: '',
695
+ };
696
+ }
697
+ } catch (error) {
698
+ // Set conversation status to Available on error so user can try again (if conversation exists)
699
+ if (convoEntity) {
700
+ await this.setConversationStatus(convoEntity, 'Available');
701
+ }
702
+
703
+ // Log the error for debugging
704
+ LogError(`Error in handleSimpleSkipChatPostRequest: ${error}`);
705
+
706
+ // Re-throw the error to propagate it up the stack
707
+ throw error;
689
708
  }
690
709
  }
691
710
 
@@ -1343,7 +1362,7 @@ cycle.`);
1343
1362
  );
1344
1363
 
1345
1364
  // Set the conversation status to 'Processing' when a request is initiated
1346
- this.setConversationStatus(convoEntity, 'Processing');
1365
+ await this.setConversationStatus(convoEntity, 'Processing');
1347
1366
 
1348
1367
  // now load up the messages. We will load up ALL of the messages for this conversation, and then pass them to the Skip API
1349
1368
  const messages: SkipMessage[] = await this.LoadConversationDetailsIntoSkipMessages(
@@ -1641,7 +1660,8 @@ cycle.`);
1641
1660
 
1642
1661
  // get the list of entities
1643
1662
  const entities = md.Entities.filter((e) => {
1644
- if (e.SchemaName !== mj_core_schema || skipSpecialIncludeEntities.includes(e.Name.trim().toLowerCase())) {
1663
+ if (!configInfo.askSkip.entitiesToSend.excludeSchemas.includes(e.SchemaName) ||
1664
+ skipSpecialIncludeEntities.includes(e.Name.trim().toLowerCase())) {
1645
1665
  const sd = e.ScopeDefault?.trim();
1646
1666
  if (sd && sd.length > 0) {
1647
1667
  const scopes = sd.split(',').map((s) => s.trim().toLowerCase()) ?? ['all'];
@@ -1941,6 +1961,7 @@ cycle.`);
1941
1961
  const convoDetailEntity = await md.GetEntityObject<ConversationDetailEntity>('Conversation Details', user);
1942
1962
  convoDetailEntity.NewRecord();
1943
1963
  convoDetailEntity.ConversationID = ConversationId;
1964
+ convoDetailEntity.UserID = user.ID;
1944
1965
  convoDetailEntity.Message = UserQuestion;
1945
1966
  convoDetailEntity.Role = 'User';
1946
1967
  convoDetailEntity.HiddenToUser = false;
@@ -2141,38 +2162,61 @@ cycle.`);
2141
2162
  };
2142
2163
  }
2143
2164
 
2144
- const response = await sendPostRequest(
2145
- skipConfigInfo.chatURL,
2146
- input,
2147
- true,
2148
- null,
2149
- (message: {
2150
- type: string;
2151
- value: {
2152
- success: boolean;
2153
- error: string;
2154
- responsePhase: string;
2155
- messages: {
2156
- role: string;
2157
- content: string;
2158
- }[];
2159
- };
2160
- }) => {
2161
- LogStatus(JSON.stringify(message, null, 4));
2162
- if (message.type === 'status_update') {
2163
- pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2164
- message: JSON.stringify({
2165
- type: 'AskSkip',
2166
- status: 'OK',
2167
- conversationID: ConversationId,
2168
- ResponsePhase: message.value.responsePhase,
2169
- message: message.value.messages[0].content,
2170
- }),
2171
- sessionId: userPayload.sessionId,
2172
- });
2165
+ let response;
2166
+ try {
2167
+ response = await sendPostRequest(
2168
+ skipConfigInfo.chatURL,
2169
+ input,
2170
+ true,
2171
+ null,
2172
+ (message: {
2173
+ type: string;
2174
+ value: {
2175
+ success: boolean;
2176
+ error: string;
2177
+ responsePhase: string;
2178
+ messages: {
2179
+ role: string;
2180
+ content: string;
2181
+ }[];
2182
+ };
2183
+ }) => {
2184
+ LogStatus(JSON.stringify(message, null, 4));
2185
+ if (message.type === 'status_update') {
2186
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2187
+ message: JSON.stringify({
2188
+ type: 'AskSkip',
2189
+ status: 'OK',
2190
+ conversationID: ConversationId,
2191
+ ResponsePhase: message.value.responsePhase,
2192
+ message: message.value.messages[0].content,
2193
+ }),
2194
+ sessionId: userPayload.sessionId,
2195
+ });
2196
+ }
2173
2197
  }
2174
- }
2175
- );
2198
+ );
2199
+ } catch (error) {
2200
+ // Set conversation status to Available on error so user can try again
2201
+ await this.setConversationStatus(convoEntity, 'Available');
2202
+
2203
+ // Log the error for debugging
2204
+ LogError(`Error in HandleSkipChatRequest sendPostRequest: ${error}`);
2205
+
2206
+ // Publish error status update to user
2207
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
2208
+ message: JSON.stringify({
2209
+ type: 'AskSkip',
2210
+ status: 'Error',
2211
+ conversationID: ConversationId,
2212
+ message: 'Request failed. Please try again later and if this continues, contact your support desk.',
2213
+ }),
2214
+ sessionId: userPayload.sessionId,
2215
+ });
2216
+
2217
+ // Re-throw the error to propagate it up the stack
2218
+ throw error;
2219
+ }
2176
2220
 
2177
2221
  if (response && response.length > 0) {
2178
2222
  // response.status === 200) {
@@ -2409,7 +2453,7 @@ cycle.`);
2409
2453
  convoDetailEntityAI.CompletionTime = endTime.getTime() - startTime.getTime();
2410
2454
 
2411
2455
  // Set conversation status back to Available since we need user input for the clarifying question
2412
- this.setConversationStatus(convoEntity, 'Available');
2456
+ await this.setConversationStatus(convoEntity, 'Available');
2413
2457
 
2414
2458
  if (await convoDetailEntityAI.Save()) {
2415
2459
  return {
@@ -2637,7 +2681,7 @@ cycle.`);
2637
2681
  dataSource: DataSource,
2638
2682
  startTime: Date
2639
2683
  ): Promise<{ AIMessageConversationDetailID: string }> {
2640
- const sTitle = apiResponse.reportTitle;
2684
+ const sTitle = apiResponse.title;
2641
2685
  const sResult = JSON.stringify(apiResponse);
2642
2686
 
2643
2687
  // first up, let's see if Skip asked us to create an artifact or add a new version to an existing artifact, or NOT
@@ -2804,12 +2848,12 @@ cycle.`);
2804
2848
 
2805
2849
  private async setConversationStatus(convoEntity: ConversationEntity, status: 'Processing' | 'Available'): Promise<boolean> {
2806
2850
  if (convoEntity.Status !== status) {
2807
- convoEntity.Status = status;
2808
- const convoSaveResult = await convoEntity.Save();
2809
- if (!convoSaveResult) {
2810
- LogError(`Error updating conversation status to '${status}'`, undefined, convoEntity.LatestResult);
2811
- }
2812
- return convoSaveResult;
2851
+ convoEntity.Status = status;
2852
+ const convoSaveResult = await convoEntity.Save();
2853
+ if (!convoSaveResult) {
2854
+ LogError(`Error updating conversation status to '${status}'`, undefined, convoEntity.LatestResult);
2855
+ }
2856
+ return convoSaveResult;
2813
2857
  }
2814
2858
  return true;
2815
2859
  }
@@ -96,7 +96,10 @@ export class ReportResolverExtended {
96
96
 
97
97
  const report = await md.GetEntityObject<ReportEntity>('Reports', u);
98
98
  report.NewRecord();
99
- report.Name = skipData.reportTitle ? skipData.reportTitle : 'Untitled Report';
99
+ // support the legacy report title as old conversation details had a reportTitle property
100
+ // but the new SkipData object has a title property, so favor the title property
101
+ const title = skipData.title ? skipData.title : skipData.reportTitle ? skipData.reportTitle : 'Untitled Report';
102
+ report.Name = title;
100
103
  report.Description = skipData.userExplanation ? skipData.userExplanation : '';
101
104
  report.ConversationID = result[0].ConversationID;
102
105
  report.ConversationDetailID = ConversationDetailID;
@@ -0,0 +1,169 @@
1
+ import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
2
+ import { UserPayload } from '../types.js';
3
+ import { LogError, LogStatus, Metadata } from '@memberjunction/core';
4
+ import { AIPromptEntity } from '@memberjunction/core-entities';
5
+ import { AIPromptRunner, AIPromptParams } from '@memberjunction/ai-prompts';
6
+ import { ResolverBase } from '../generic/ResolverBase.js';
7
+
8
+ @ObjectType()
9
+ export class AIPromptRunResult {
10
+ @Field()
11
+ success: boolean;
12
+
13
+ @Field({ nullable: true })
14
+ output?: string;
15
+
16
+ @Field({ nullable: true })
17
+ parsedResult?: string;
18
+
19
+ @Field({ nullable: true })
20
+ error?: string;
21
+
22
+ @Field({ nullable: true })
23
+ executionTimeMs?: number;
24
+
25
+ @Field({ nullable: true })
26
+ tokensUsed?: number;
27
+
28
+ @Field({ nullable: true })
29
+ promptRunId?: string;
30
+
31
+ @Field({ nullable: true })
32
+ rawResult?: string;
33
+
34
+ @Field({ nullable: true })
35
+ validationResult?: string;
36
+ }
37
+
38
+ @Resolver()
39
+ export class RunAIPromptResolver extends ResolverBase {
40
+ @Mutation(() => AIPromptRunResult)
41
+ async RunAIPrompt(
42
+ @Arg('promptId') promptId: string,
43
+ @Ctx() { userPayload }: { userPayload: UserPayload },
44
+ @Arg('data', { nullable: true }) data?: string,
45
+ @Arg('modelId', { nullable: true }) modelId?: string,
46
+ @Arg('vendorId', { nullable: true }) vendorId?: string,
47
+ @Arg('configurationId', { nullable: true }) configurationId?: string,
48
+ @Arg('skipValidation', { nullable: true }) skipValidation?: boolean,
49
+ @Arg('templateData', { nullable: true }) templateData?: string
50
+ ): Promise<AIPromptRunResult> {
51
+ const startTime = Date.now();
52
+
53
+ try {
54
+ LogStatus(`=== RUNNING AI PROMPT FOR ID: ${promptId} ===`);
55
+
56
+ // Parse data contexts (JSON strings)
57
+ let parsedData = {};
58
+ let parsedTemplateData = {};
59
+
60
+ if (data) {
61
+ try {
62
+ parsedData = JSON.parse(data);
63
+ } catch (parseError) {
64
+ return {
65
+ success: false,
66
+ error: `Invalid JSON in data: ${(parseError as Error).message}`,
67
+ executionTimeMs: Date.now() - startTime
68
+ };
69
+ }
70
+ }
71
+
72
+ if (templateData) {
73
+ try {
74
+ parsedTemplateData = JSON.parse(templateData);
75
+ } catch (parseError) {
76
+ return {
77
+ success: false,
78
+ error: `Invalid JSON in template data: ${(parseError as Error).message}`,
79
+ executionTimeMs: Date.now() - startTime
80
+ };
81
+ }
82
+ }
83
+
84
+ // Get current user from payload
85
+ const currentUser = this.GetUserFromPayload(userPayload);
86
+ if (!currentUser) {
87
+ return {
88
+ success: false,
89
+ error: 'Unable to determine current user',
90
+ executionTimeMs: Date.now() - startTime
91
+ };
92
+ }
93
+
94
+ const md = new Metadata();
95
+
96
+ // Load the AI prompt entity
97
+ const promptEntity = await md.GetEntityObject<AIPromptEntity>('AI Prompts', currentUser);
98
+ await promptEntity.Load(promptId);
99
+
100
+ if (!promptEntity.IsSaved) {
101
+ return {
102
+ success: false,
103
+ error: `AI Prompt with ID ${promptId} not found`,
104
+ executionTimeMs: Date.now() - startTime
105
+ };
106
+ }
107
+
108
+ // Check if prompt is active
109
+ if (promptEntity.Status !== 'Active') {
110
+ return {
111
+ success: false,
112
+ error: `AI Prompt "${promptEntity.Name}" is not active (Status: ${promptEntity.Status})`,
113
+ executionTimeMs: Date.now() - startTime
114
+ };
115
+ }
116
+
117
+ // Create AI prompt runner and execute
118
+ const promptRunner = new AIPromptRunner();
119
+
120
+ // Build execution parameters
121
+ const promptParams = new AIPromptParams();
122
+ promptParams.prompt = promptEntity;
123
+ promptParams.data = parsedData;
124
+ promptParams.templateData = parsedTemplateData;
125
+ promptParams.modelId = modelId;
126
+ promptParams.vendorId = vendorId;
127
+ promptParams.configurationId = configurationId;
128
+ promptParams.contextUser = currentUser;
129
+ promptParams.skipValidation = skipValidation || false;
130
+
131
+ // Execute the prompt
132
+ const result = await promptRunner.ExecutePrompt(promptParams);
133
+
134
+ const executionTime = Date.now() - startTime;
135
+
136
+ if (result.success) {
137
+ LogStatus(`=== AI PROMPT RUN COMPLETED FOR: ${promptEntity.Name} (${executionTime}ms) ===`);
138
+
139
+ return {
140
+ success: true,
141
+ output: result.rawResult,
142
+ parsedResult: typeof result.result === 'string' ? result.result : JSON.stringify(result.result),
143
+ rawResult: result.rawResult,
144
+ executionTimeMs: executionTime,
145
+ tokensUsed: result.tokensUsed,
146
+ promptRunId: result.promptRun?.ID,
147
+ validationResult: result.validationResult ? JSON.stringify(result.validationResult) : undefined
148
+ };
149
+ } else {
150
+ LogError(`AI Prompt run failed for ${promptEntity.Name}: ${result.errorMessage}`);
151
+ return {
152
+ success: false,
153
+ error: result.errorMessage,
154
+ executionTimeMs: executionTime,
155
+ promptRunId: result.promptRun?.ID
156
+ };
157
+ }
158
+
159
+ } catch (error) {
160
+ const executionTime = Date.now() - startTime;
161
+ LogError(`AI Prompt run failed:`, undefined, error);
162
+ return {
163
+ success: false,
164
+ error: (error as Error).message || 'Unknown error occurred',
165
+ executionTimeMs: executionTime
166
+ };
167
+ }
168
+ }
169
+ }
@@ -0,0 +1,130 @@
1
+ import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
2
+ import { UserPayload } from '../types.js';
3
+ import { LogError, LogStatus, Metadata, RunView } from '@memberjunction/core';
4
+ import { TemplateContentEntity } from '@memberjunction/core-entities';
5
+ import { TemplateEngineServer } from '@memberjunction/templates';
6
+ import { TemplateEntityExtended } from '@memberjunction/templates-base-types';
7
+ import { ResolverBase } from '../generic/ResolverBase.js';
8
+
9
+ @ObjectType()
10
+ export class TemplateRunResult {
11
+ @Field()
12
+ success: boolean;
13
+
14
+ @Field({ nullable: true })
15
+ output?: string;
16
+
17
+ @Field({ nullable: true })
18
+ error?: string;
19
+
20
+ @Field({ nullable: true })
21
+ executionTimeMs?: number;
22
+ }
23
+
24
+ @Resolver()
25
+ export class RunTemplateResolver extends ResolverBase {
26
+ @Mutation(() => TemplateRunResult)
27
+ async RunTemplate(
28
+ @Arg('templateId') templateId: string,
29
+ @Ctx() { userPayload }: { userPayload: UserPayload },
30
+ @Arg('contextData', { nullable: true }) contextData?: string
31
+ ): Promise<TemplateRunResult> {
32
+ const startTime = Date.now();
33
+
34
+ try {
35
+ LogStatus(`=== RUNNING TEMPLATE FOR ID: ${templateId} ===`);
36
+
37
+ // Parse context data (JSON string)
38
+ let data = {};
39
+ if (contextData) {
40
+ try {
41
+ data = JSON.parse(contextData);
42
+ } catch (parseError) {
43
+ return {
44
+ success: false,
45
+ error: `Invalid JSON in context data: ${(parseError as Error).message}`,
46
+ executionTimeMs: Date.now() - startTime
47
+ };
48
+ }
49
+ }
50
+
51
+ // Get current user from payload
52
+ const currentUser = this.GetUserFromPayload(userPayload);
53
+ if (!currentUser) {
54
+ return {
55
+ success: false,
56
+ error: 'Unable to determine current user',
57
+ executionTimeMs: Date.now() - startTime
58
+ };
59
+ }
60
+
61
+ const md = new Metadata();
62
+
63
+ // Load the template entity
64
+ const templateEntity = await md.GetEntityObject<TemplateEntityExtended>('Templates', currentUser);
65
+ await templateEntity.Load(templateId);
66
+
67
+ if (!templateEntity.IsSaved) {
68
+ return {
69
+ success: false,
70
+ error: `Template with ID ${templateId} not found`,
71
+ executionTimeMs: Date.now() - startTime
72
+ };
73
+ }
74
+
75
+ // Load template content (get the first/highest priority content)
76
+ const rv = new RunView();
77
+ const templateContentResult = await rv.RunView<TemplateContentEntity>({
78
+ EntityName: 'Template Contents',
79
+ ExtraFilter: `TemplateID = '${templateId}'`,
80
+ OrderBy: 'Priority ASC',
81
+ MaxRows: 1,
82
+ ResultType: 'entity_object'
83
+ }, currentUser);
84
+
85
+ if (!templateContentResult.Results || templateContentResult.Results.length === 0) {
86
+ return {
87
+ success: false,
88
+ error: `No template content found for template ${templateEntity.Name}`,
89
+ executionTimeMs: Date.now() - startTime
90
+ };
91
+ }
92
+
93
+ // Configure and render the template
94
+ await TemplateEngineServer.Instance.Config(true /*always refresh to get latest templates*/, currentUser);
95
+ const result = await TemplateEngineServer.Instance.RenderTemplate(
96
+ templateEntity,
97
+ templateContentResult.Results[0],
98
+ data,
99
+ true // skip validation for execution
100
+ );
101
+
102
+ const executionTime = Date.now() - startTime;
103
+
104
+ if (result.Success) {
105
+ LogStatus(`=== TEMPLATE RUN COMPLETED FOR: ${templateEntity.Name} (${executionTime}ms) ===`);
106
+ return {
107
+ success: true,
108
+ output: result.Output,
109
+ executionTimeMs: executionTime
110
+ };
111
+ } else {
112
+ LogError(`Template run failed for ${templateEntity.Name}: ${result.Message}`);
113
+ return {
114
+ success: false,
115
+ error: result.Message,
116
+ executionTimeMs: executionTime
117
+ };
118
+ }
119
+
120
+ } catch (error) {
121
+ const executionTime = Date.now() - startTime;
122
+ LogError(`Template run failed:`, undefined, error);
123
+ return {
124
+ success: false,
125
+ error: (error as Error).message || 'Unknown error occurred',
126
+ executionTimeMs: executionTime
127
+ };
128
+ }
129
+ }
130
+ }