@memberjunction/server 5.1.0 → 5.3.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 (59) hide show
  1. package/README.md +2 -1
  2. package/dist/agents/skip-sdk.d.ts +7 -0
  3. package/dist/agents/skip-sdk.d.ts.map +1 -1
  4. package/dist/agents/skip-sdk.js +105 -4
  5. package/dist/agents/skip-sdk.js.map +1 -1
  6. package/dist/entitySubclasses/{entityPermissions.server.d.ts → MJEntityPermissionEntityServer.server.d.ts} +2 -2
  7. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -0
  8. package/dist/entitySubclasses/{entityPermissions.server.js → MJEntityPermissionEntityServer.server.js} +9 -9
  9. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -0
  10. package/dist/generated/generated.d.ts +36 -10
  11. package/dist/generated/generated.d.ts.map +1 -1
  12. package/dist/generated/generated.js +160 -36
  13. package/dist/generated/generated.js.map +1 -1
  14. package/dist/generic/ResolverBase.d.ts +2 -2
  15. package/dist/generic/ResolverBase.d.ts.map +1 -1
  16. package/dist/generic/ResolverBase.js.map +1 -1
  17. package/dist/generic/RunViewResolver.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/resolvers/AdhocQueryResolver.d.ts +28 -0
  23. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -0
  24. package/dist/resolvers/AdhocQueryResolver.js +140 -0
  25. package/dist/resolvers/AdhocQueryResolver.js.map +1 -0
  26. package/dist/resolvers/CreateQueryResolver.js +2 -2
  27. package/dist/resolvers/RunAIAgentResolver.d.ts +11 -2
  28. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  29. package/dist/resolvers/RunAIAgentResolver.js +55 -11
  30. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  31. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  32. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  33. package/dist/resolvers/RunTestResolver.d.ts +5 -0
  34. package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
  35. package/dist/resolvers/RunTestResolver.js +32 -2
  36. package/dist/resolvers/RunTestResolver.js.map +1 -1
  37. package/dist/resolvers/UserViewResolver.js.map +1 -1
  38. package/dist/services/TaskOrchestrator.js.map +1 -1
  39. package/dist/types.d.ts +2 -2
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +53 -52
  42. package/src/__tests__/AdhocQueryResolver.test.ts +175 -0
  43. package/src/agents/skip-sdk.ts +130 -5
  44. package/src/entitySubclasses/{entityPermissions.server.ts → MJEntityPermissionEntityServer.server.ts} +3 -3
  45. package/src/generated/generated.ts +126 -28
  46. package/src/generic/ResolverBase.ts +9 -9
  47. package/src/generic/RunViewResolver.ts +4 -4
  48. package/src/index.ts +1 -1
  49. package/src/resolvers/AdhocQueryResolver.ts +126 -0
  50. package/src/resolvers/CreateQueryResolver.ts +5 -5
  51. package/src/resolvers/RunAIAgentResolver.ts +98 -15
  52. package/src/resolvers/RunAIPromptResolver.ts +7 -7
  53. package/src/resolvers/RunTemplateResolver.ts +2 -2
  54. package/src/resolvers/RunTestResolver.ts +41 -3
  55. package/src/resolvers/UserViewResolver.ts +2 -2
  56. package/src/services/TaskOrchestrator.ts +5 -5
  57. package/src/types.ts +2 -2
  58. package/dist/entitySubclasses/entityPermissions.server.d.ts.map +0 -1
  59. package/dist/entitySubclasses/entityPermissions.server.js.map +0 -1
@@ -4,7 +4,7 @@ import { ResolverBase } from './ResolverBase.js';
4
4
  import { LogError, LogStatus, EntityInfo, RunViewWithCacheCheckResult, RunViewsWithCacheCheckResponse, RunViewWithCacheCheckParams, AggregateResult } from '@memberjunction/core';
5
5
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
6
6
  import { GetReadOnlyProvider } from '../util.js';
7
- import { UserViewEntityExtended } from '@memberjunction/core-entities';
7
+ import { MJUserViewEntityExtended } from '@memberjunction/core-entities';
8
8
  import { KeyValuePairOutputType } from './KeyInputOutputTypes.js';
9
9
  import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
10
10
 
@@ -652,7 +652,7 @@ export class RunViewResolver extends ResolverBase {
652
652
  if (rawData === null)
653
653
  return null;
654
654
 
655
- const viewInfo = super.safeFirstArrayElement<UserViewEntityExtended>(await super.findBy<UserViewEntityExtended>(provider, "MJ: User Views", { Name: input.ViewName }, userPayload.userRecord));
655
+ const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { Name: input.ViewName }, userPayload.userRecord));
656
656
  const entity = provider.Entities.find((e) => e.ID === viewInfo.EntityID);
657
657
  const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
658
658
  return {
@@ -683,7 +683,7 @@ export class RunViewResolver extends ResolverBase {
683
683
  if (rawData === null)
684
684
  return null;
685
685
 
686
- const viewInfo = super.safeFirstArrayElement<UserViewEntityExtended>(await super.findBy<UserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
686
+ const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
687
687
  const entity = provider.Entities.find((e) => e.ID === viewInfo.EntityID);
688
688
  const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
689
689
  return {
@@ -837,7 +837,7 @@ export class RunViewResolver extends ResolverBase {
837
837
  };
838
838
  }
839
839
 
840
- const viewInfo = super.safeFirstArrayElement<UserViewEntityExtended>(await super.findBy<UserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
840
+ const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
841
841
  const entity = provider.Entities.find((e) => e.ID === viewInfo.EntityID);
842
842
  const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
843
843
  return {
package/src/index.ts CHANGED
@@ -44,7 +44,7 @@ export * from 'type-graphql';
44
44
  export { NewUserBase } from './auth/newUsers.js';
45
45
  export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
46
46
  export * from './directives/index.js';
47
- export * from './entitySubclasses/entityPermissions.server.js';
47
+ export * from './entitySubclasses/MJEntityPermissionEntityServer.server.js';
48
48
  export * from './types.js';
49
49
  export {
50
50
  TokenExpiredError,
@@ -0,0 +1,126 @@
1
+ import { Arg, Ctx, Query, Resolver, Field, Int, InputType } from 'type-graphql';
2
+ import { LogError } from '@memberjunction/core';
3
+ import { SQLExpressionValidator } from '@memberjunction/global';
4
+ import { AppContext } from '../types.js';
5
+ import { GetReadOnlyDataSource } from '../util.js';
6
+ import { ResolverBase } from '../generic/ResolverBase.js';
7
+ import { RunQueryResultType } from './QueryResolver.js';
8
+ import sql from 'mssql';
9
+
10
+ /**
11
+ * Input type for executing ad-hoc SQL queries directly.
12
+ * The SQL is validated server-side to ensure it's a safe SELECT/WITH statement.
13
+ */
14
+ @InputType()
15
+ class AdhocQueryInput {
16
+ @Field(() => String, { description: 'SQL query to execute. Must be a SELECT or WITH (CTE) statement.' })
17
+ SQL: string;
18
+
19
+ @Field(() => Int, { nullable: true, description: 'Query timeout in seconds. Defaults to 30.' })
20
+ TimeoutSeconds?: number;
21
+ }
22
+
23
+ /**
24
+ * Resolver for executing ad-hoc (unsaved) SQL queries.
25
+ *
26
+ * Security:
27
+ * - SQL validated via SQLExpressionValidator (full_query context) — blocks mutations, dangerous operations
28
+ * - Executes on read-only connection pool only (no fallback to read-write)
29
+ * - Configurable timeout (default 30s)
30
+ * - Requires authenticated user (standard GraphQL auth, no @RequireSystemUser)
31
+ *
32
+ * Auto-discovered by MJServer's dynamic resolver import.
33
+ */
34
+ @Resolver()
35
+ export class AdhocQueryResolver extends ResolverBase {
36
+ @Query(() => RunQueryResultType)
37
+ async ExecuteAdhocQuery(
38
+ @Arg('input', () => AdhocQueryInput) input: AdhocQueryInput,
39
+ @Ctx() context: AppContext
40
+ ): Promise<RunQueryResultType> {
41
+ const startTime = Date.now();
42
+
43
+ try {
44
+ // 1. Security: validate SQL using SQLExpressionValidator
45
+ const validator = SQLExpressionValidator.Instance;
46
+ const validation = validator.validateFullQuery(input.SQL);
47
+ if (!validation.valid) {
48
+ return this.buildErrorResult(validation.error || 'SQL validation failed');
49
+ }
50
+
51
+ // 2. Get READ-ONLY data source (no fallback to read-write)
52
+ let readOnlyDS: sql.ConnectionPool;
53
+ try {
54
+ readOnlyDS = GetReadOnlyDataSource(context.dataSources, { allowFallbackToReadWrite: false });
55
+ } catch {
56
+ return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
57
+ }
58
+
59
+ // 3. Execute with timeout
60
+ const timeoutMs = (input.TimeoutSeconds ?? 30) * 1000;
61
+ const request = new sql.Request(readOnlyDS);
62
+
63
+ const result = await Promise.race([
64
+ request.query(input.SQL),
65
+ new Promise<never>((_, reject) =>
66
+ setTimeout(() => reject(new Error('Query timeout exceeded')), timeoutMs)
67
+ )
68
+ ]);
69
+ const executionTimeMs = Date.now() - startTime;
70
+
71
+ // 4. Return as RunQueryResultType
72
+ return {
73
+ QueryID: '',
74
+ QueryName: 'Ad-Hoc Query',
75
+ Success: true,
76
+ Results: JSON.stringify(result.recordset ?? []),
77
+ RowCount: result.recordset?.length ?? 0,
78
+ TotalRowCount: result.recordset?.length ?? 0,
79
+ ExecutionTime: executionTimeMs,
80
+ ErrorMessage: ''
81
+ };
82
+ } catch (err: unknown) {
83
+ const executionTimeMs = Date.now() - startTime;
84
+ const errorMessage = err instanceof Error ? err.message : String(err);
85
+
86
+ // Handle timeout
87
+ if (errorMessage.includes('timeout') || errorMessage.includes('Timeout')) {
88
+ return {
89
+ QueryID: '',
90
+ QueryName: 'Ad-Hoc Query',
91
+ Success: false,
92
+ Results: '[]',
93
+ RowCount: 0,
94
+ TotalRowCount: 0,
95
+ ExecutionTime: executionTimeMs,
96
+ ErrorMessage: `Query execution exceeded ${input.TimeoutSeconds ?? 30} second timeout`
97
+ };
98
+ }
99
+
100
+ LogError(`Ad-hoc query execution failed: ${errorMessage}`);
101
+ return {
102
+ QueryID: '',
103
+ QueryName: 'Ad-Hoc Query',
104
+ Success: false,
105
+ Results: '[]',
106
+ RowCount: 0,
107
+ TotalRowCount: 0,
108
+ ExecutionTime: executionTimeMs,
109
+ ErrorMessage: `Query execution failed: ${errorMessage}`
110
+ };
111
+ }
112
+ }
113
+
114
+ private buildErrorResult(errorMessage: string): RunQueryResultType {
115
+ return {
116
+ QueryID: '',
117
+ QueryName: 'Ad-Hoc Query',
118
+ Success: false,
119
+ Results: '[]',
120
+ RowCount: 0,
121
+ TotalRowCount: 0,
122
+ ExecutionTime: 0,
123
+ ErrorMessage: errorMessage
124
+ };
125
+ }
126
+ }
@@ -6,7 +6,7 @@ import { MJQueryCategoryEntity, MJQueryPermissionEntity } from '@memberjunction/
6
6
  import { MJQueryResolver } from '../generated/generated.js';
7
7
  import { GetReadOnlyProvider, GetReadWriteProvider } from '../util.js';
8
8
  import { DeleteOptionsInput } from '../generic/DeleteOptionsInput.js';
9
- import { QueryEntityExtended } from '@memberjunction/core-entities-server';
9
+ import { MJQueryEntityServer } from '@memberjunction/core-entities-server';
10
10
 
11
11
  /**
12
12
  * Query status enumeration for GraphQL
@@ -425,8 +425,8 @@ export class MJQueryResolverExtended extends MJQueryResolver {
425
425
  };
426
426
  }
427
427
 
428
- // Use QueryEntityExtended which handles AI processing
429
- const record = await provider.GetEntityObject<QueryEntityExtended>("MJ: Queries", context.userPayload.userRecord);
428
+ // Use MJQueryEntityServer which handles AI processing
429
+ const record = await provider.GetEntityObject<MJQueryEntityServer>("MJ: Queries", context.userPayload.userRecord);
430
430
 
431
431
  // Set the fields from input, handling CategoryPath resolution
432
432
  const fieldsToSet = {
@@ -638,9 +638,9 @@ export class MJQueryResolverExtended extends MJQueryResolver {
638
638
  @PubSub() pubSub: PubSubEngine
639
639
  ): Promise<UpdateQueryResultType> {
640
640
  try {
641
- // Load the existing query using QueryEntityExtended
641
+ // Load the existing query using MJQueryEntityServer
642
642
  const provider = GetReadWriteProvider(context.providers);
643
- const queryEntity = await provider.GetEntityObject<QueryEntityExtended>('MJ: Queries', context.userPayload.userRecord);
643
+ const queryEntity = await provider.GetEntityObject<MJQueryEntityServer>('MJ: Queries', context.userPayload.userRecord);
644
644
  if (!queryEntity || !await queryEntity.Load(input.ID)) {
645
645
  return {
646
646
  Success: false,
@@ -3,7 +3,7 @@ import { AppContext, UserPayload } from '../types.js';
3
3
  import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
4
  import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity } from '@memberjunction/core-entities';
5
5
  import { AgentRunner } from '@memberjunction/ai-agents';
6
- import { AIAgentEntityExtended, AIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
6
+ import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
7
7
  import { AIEngine } from '@memberjunction/aiengine';
8
8
  import { ChatMessage } from '@memberjunction/ai';
9
9
  import { ResolverBase } from '../generic/ResolverBase.js';
@@ -207,12 +207,12 @@ export class RunAIAgentResolver extends ResolverBase {
207
207
  /**
208
208
  * Validate the agent entity
209
209
  */
210
- private async validateAgent(agentId: string, currentUser: any): Promise<AIAgentEntityExtended> {
210
+ private async validateAgent(agentId: string, currentUser: any): Promise<MJAIAgentEntityExtended> {
211
211
  // Use AIEngine to get cached agent data
212
212
  await AIEngine.Instance.Config(false, currentUser);
213
213
 
214
214
  // Find agent in cached collection
215
- const agentEntity = AIEngine.Instance.Agents.find((a: AIAgentEntityExtended) => a.ID === agentId);
215
+ const agentEntity = AIEngine.Instance.Agents.find((a: MJAIAgentEntityExtended) => a.ID === agentId);
216
216
 
217
217
  if (!agentEntity) {
218
218
  throw new Error(`AI Agent with ID ${agentId} not found`);
@@ -433,9 +433,6 @@ export class RunAIAgentResolver extends ResolverBase {
433
433
 
434
434
  const executionTime = Date.now() - startTime;
435
435
 
436
- // Publish final events
437
- this.publishFinalEvents(pubSub, sessionId, userPayload, result);
438
-
439
436
  // Create notification if enabled and artifact was created successfully
440
437
  if (createNotification && result.success && artifactInfo && artifactInfo.artifactId && artifactInfo.versionId && artifactInfo.versionNumber) {
441
438
  await this.createCompletionNotification(
@@ -456,6 +453,9 @@ export class RunAIAgentResolver extends ResolverBase {
456
453
  const sanitizedResult = this.sanitizeAgentResult(result);
457
454
  const returnResult = JSON.stringify(sanitizedResult);
458
455
 
456
+ // Publish final events with enriched result data for fire-and-forget clients
457
+ this.publishFinalEvents(pubSub, sessionId, userPayload, result, returnResult);
458
+
459
459
  // Log completion
460
460
  if (result.success) {
461
461
  LogStatus(`=== AI AGENT RUN COMPLETED FOR: ${agentEntity.Name} (${executionTime}ms) ===`);
@@ -491,9 +491,17 @@ export class RunAIAgentResolver extends ResolverBase {
491
491
  }
492
492
 
493
493
  /**
494
- * Publish final streaming events (partial result and completion)
494
+ * Publish final streaming events (partial result and completion).
495
+ * The completion event includes the full result JSON so clients using
496
+ * fire-and-forget mode can receive the result via WebSocket.
495
497
  */
496
- private publishFinalEvents(pubSub: PubSubEngine, sessionId: string, userPayload: UserPayload, result: ExecuteAgentResult) {
498
+ private publishFinalEvents(
499
+ pubSub: PubSubEngine,
500
+ sessionId: string,
501
+ userPayload: UserPayload,
502
+ result: ExecuteAgentResult,
503
+ resultJson?: string
504
+ ) {
497
505
  if (result.agentRun) {
498
506
  // Get the last step from agent run
499
507
  let lastStep = 'Completed';
@@ -519,15 +527,19 @@ export class RunAIAgentResolver extends ResolverBase {
519
527
  this.PublishStreamingUpdate(pubSub, partialMsg, userPayload);
520
528
  }
521
529
 
522
- // Publish completion with conversationDetailId for client-side routing
523
- const completeMsg: AgentExecutionStreamMessage = {
530
+ // Publish completion with conversationDetailId for client-side routing.
531
+ // Include result data so fire-and-forget clients can receive the full result via WebSocket.
532
+ const completionData: Record<string, unknown> = {
524
533
  sessionId,
525
534
  agentRunId: result.agentRun?.ID || 'unknown',
526
535
  type: 'complete',
527
536
  timestamp: new Date(),
528
- conversationDetailId: result.agentRun?.ConversationDetailID
537
+ conversationDetailId: result.agentRun?.ConversationDetailID,
538
+ success: result.success,
539
+ errorMessage: result.agentRun?.ErrorMessage || undefined,
540
+ result: resultJson || undefined
529
541
  };
530
- this.PublishStreamingUpdate(pubSub, completeMsg, userPayload);
542
+ this.PublishStreamingUpdate(pubSub, completionData, userPayload);
531
543
  }
532
544
 
533
545
  /**
@@ -630,7 +642,7 @@ export class RunAIAgentResolver extends ResolverBase {
630
642
  * Notification includes navigation link back to the conversation
631
643
  */
632
644
  private async createCompletionNotification(
633
- agentRun: AIAgentRunEntityExtended,
645
+ agentRun: MJAIAgentRunEntityExtended,
634
646
  artifactInfo: { artifactId: string; versionId: string; versionNumber: number },
635
647
  conversationDetailId: string,
636
648
  contextUser: UserInfo,
@@ -742,7 +754,8 @@ export class RunAIAgentResolver extends ResolverBase {
742
754
  @Arg('createArtifacts', { nullable: true }) createArtifacts?: boolean,
743
755
  @Arg('createNotification', { nullable: true }) createNotification?: boolean,
744
756
  @Arg('sourceArtifactId', { nullable: true }) sourceArtifactId?: string,
745
- @Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string
757
+ @Arg('sourceArtifactVersionId', { nullable: true }) sourceArtifactVersionId?: string,
758
+ @Arg('fireAndForget', { nullable: true }) fireAndForget?: boolean
746
759
  ): Promise<AIAgentRunResult> {
747
760
  // Check API key scope authorization for agent execution
748
761
  await this.CheckAPIKeyScopeAuthorization('agent:execute', agentId, userPayload);
@@ -769,7 +782,25 @@ export class RunAIAgentResolver extends ResolverBase {
769
782
  // Convert to JSON string for the existing executeAIAgent method
770
783
  const messagesJson = JSON.stringify(messages);
771
784
 
772
- // Delegate to existing implementation
785
+ if (fireAndForget) {
786
+ // Fire-and-forget mode: start execution in background, return immediately.
787
+ // The client will receive the result via WebSocket PubSub completion event.
788
+ this.executeAgentInBackground(
789
+ p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
790
+ data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
791
+ conversationDetailId, createArtifacts || false, createNotification || false,
792
+ sourceArtifactId, sourceArtifactVersionId
793
+ );
794
+
795
+ LogStatus(`🔥 Fire-and-forget: Agent ${agentId} execution started in background for session ${sessionId}`);
796
+
797
+ return {
798
+ success: true,
799
+ result: JSON.stringify({ accepted: true, fireAndForget: true })
800
+ };
801
+ }
802
+
803
+ // Synchronous mode (default): wait for execution to complete
773
804
  return this.executeAIAgent(
774
805
  p,
775
806
  dataSource,
@@ -801,6 +832,58 @@ export class RunAIAgentResolver extends ResolverBase {
801
832
  }
802
833
  }
803
834
 
835
+ /**
836
+ * Execute agent in background (fire-and-forget).
837
+ * Handles errors by publishing error completion events via PubSub,
838
+ * so the client receives them via WebSocket even though the HTTP response
839
+ * has already been sent.
840
+ */
841
+ private executeAgentInBackground(
842
+ p: DatabaseProviderBase,
843
+ dataSource: unknown,
844
+ agentId: string,
845
+ userPayload: UserPayload,
846
+ messagesJson: string,
847
+ sessionId: string,
848
+ pubSub: PubSubEngine,
849
+ data?: string,
850
+ payload?: string,
851
+ lastRunId?: string,
852
+ autoPopulateLastRunPayload?: boolean,
853
+ configurationId?: string,
854
+ conversationDetailId?: string,
855
+ createArtifacts: boolean = false,
856
+ createNotification: boolean = false,
857
+ sourceArtifactId?: string,
858
+ sourceArtifactVersionId?: string
859
+ ): void {
860
+ // Execute in background - errors are handled within, not propagated
861
+ this.executeAIAgent(
862
+ p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
863
+ data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
864
+ configurationId, conversationDetailId, createArtifacts, createNotification,
865
+ sourceArtifactId, sourceArtifactVersionId
866
+ ).catch((error: unknown) => {
867
+ // Background execution failed unexpectedly (executeAIAgent has its own try-catch,
868
+ // so this would only fire for truly unexpected errors).
869
+ const errorMessage = (error instanceof Error) ? error.message : 'Unknown background execution error';
870
+ LogError(`🔥 Fire-and-forget background execution failed: ${errorMessage}`, undefined, error);
871
+
872
+ // Publish error completion event so the client knows the agent failed
873
+ const errorCompletionData: Record<string, unknown> = {
874
+ sessionId,
875
+ agentRunId: 'unknown',
876
+ type: 'complete',
877
+ timestamp: new Date(),
878
+ conversationDetailId,
879
+ success: false,
880
+ errorMessage,
881
+ result: JSON.stringify({ success: false, errorMessage })
882
+ };
883
+ this.PublishStreamingUpdate(pubSub, errorCompletionData, userPayload);
884
+ });
885
+ }
886
+
804
887
  /**
805
888
  * Load conversation history with attachments from database.
806
889
  * Builds ChatMessage[] with multimodal content blocks for attachments.
@@ -1,7 +1,7 @@
1
1
  import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, Field, Int } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
3
  import { DatabaseProviderBase, LogError, LogStatus, Metadata } from '@memberjunction/core';
4
- import { AIPromptEntityExtended, AIModelEntityExtended } from '@memberjunction/ai-core-plus';
4
+ import { MJAIPromptEntityExtended, MJAIModelEntityExtended } from '@memberjunction/ai-core-plus';
5
5
  import { AIPromptRunner } from '@memberjunction/ai-prompts';
6
6
  import { AIPromptParams } from '@memberjunction/ai-core-plus';
7
7
  import { ResolverBase } from '../generic/ResolverBase.js';
@@ -156,7 +156,7 @@ export class RunAIPromptResolver extends ResolverBase {
156
156
  }
157
157
 
158
158
  // Load the AI prompt entity
159
- const promptEntity = await p.GetEntityObject<AIPromptEntityExtended>('MJ: AI Prompts', currentUser);
159
+ const promptEntity = await p.GetEntityObject<MJAIPromptEntityExtended>('MJ: AI Prompts', currentUser);
160
160
  await promptEntity.Load(promptId);
161
161
 
162
162
  if (!promptEntity.IsSaved) {
@@ -402,7 +402,7 @@ export class RunAIPromptResolver extends ResolverBase {
402
402
  preferredModels: string[] | undefined,
403
403
  modelPower: string,
404
404
  contextUser: any
405
- ): Promise<AIModelEntityExtended> {
405
+ ): Promise<MJAIModelEntityExtended> {
406
406
  // Ensure AI Engine is configured
407
407
  await AIEngine.Instance.Config(false, contextUser);
408
408
 
@@ -413,7 +413,7 @@ export class RunAIPromptResolver extends ResolverBase {
413
413
  );
414
414
 
415
415
  // Filter to only models with valid API keys
416
- const modelsWithKeys: AIModelEntityExtended[] = [];
416
+ const modelsWithKeys: MJAIModelEntityExtended[] = [];
417
417
  for (const model of allModels) {
418
418
  const apiKey = GetAIAPIKey(model.DriverClass);
419
419
  if (apiKey && apiKey.trim().length > 0) {
@@ -443,7 +443,7 @@ export class RunAIPromptResolver extends ResolverBase {
443
443
  // Sort by PowerRank for power-based selection
444
444
  modelsWithKeys.sort((a, b) => (b.PowerRank || 0) - (a.PowerRank || 0));
445
445
 
446
- let selectedModel: AIModelEntityExtended;
446
+ let selectedModel: MJAIModelEntityExtended;
447
447
  switch (modelPower) {
448
448
  case 'lowest':
449
449
  selectedModel = modelsWithKeys[modelsWithKeys.length - 1];
@@ -466,7 +466,7 @@ export class RunAIPromptResolver extends ResolverBase {
466
466
  * Helper method to select an embedding model by size
467
467
  * @private
468
468
  */
469
- private selectEmbeddingModelBySize(modelSize: string): AIModelEntityExtended {
469
+ private selectEmbeddingModelBySize(modelSize: string): MJAIModelEntityExtended {
470
470
  const localModels = AIEngine.Instance.LocalEmbeddingModels;
471
471
 
472
472
  if (!localModels || localModels.length === 0) {
@@ -541,7 +541,7 @@ export class RunAIPromptResolver extends ResolverBase {
541
541
  * Helper method to format simple prompt result
542
542
  * @private
543
543
  */
544
- private formatSimpleResult(chatResult: any, model: AIModelEntityExtended, executionTime: number): SimplePromptResult {
544
+ private formatSimpleResult(chatResult: any, model: MJAIModelEntityExtended, executionTime: number): SimplePromptResult {
545
545
  if (!chatResult || !chatResult.success) {
546
546
  return {
547
547
  success: false,
@@ -1,7 +1,7 @@
1
1
  import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
3
  import { LogError, LogStatus, Metadata, RunView } from '@memberjunction/core';
4
- import { MJTemplateContentEntity, TemplateEntityExtended } from '@memberjunction/core-entities';
4
+ import { MJTemplateContentEntity, MJTemplateEntityExtended } from '@memberjunction/core-entities';
5
5
  import { TemplateEngineServer } from '@memberjunction/templates';
6
6
  import { ResolverBase } from '../generic/ResolverBase.js';
7
7
  import { GetReadWriteProvider } from '../util.js';
@@ -63,7 +63,7 @@ export class RunTemplateResolver extends ResolverBase {
63
63
 
64
64
  const p = GetReadWriteProvider(providers);
65
65
  // Load the template entity
66
- const templateEntity = await p.GetEntityObject<TemplateEntityExtended>('MJ: Templates', currentUser);
66
+ const templateEntity = await p.GetEntityObject<MJTemplateEntityExtended>('MJ: Templates', currentUser);
67
67
  await templateEntity.Load(templateId);
68
68
 
69
69
  if (!templateEntity.IsSaved) {
@@ -16,7 +16,7 @@ import { LogError, LogStatus } from '@memberjunction/core';
16
16
  import { TestEngine } from '@memberjunction/testing-engine';
17
17
  import { ResolverBase } from '../generic/ResolverBase.js';
18
18
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
19
- import { TestRunVariables } from '@memberjunction/testing-engine-base';
19
+ import { TestRunVariables, TestLogMessage } from '@memberjunction/testing-engine-base';
20
20
 
21
21
  // ===== GraphQL Types =====
22
22
 
@@ -131,6 +131,11 @@ export class RunTestResolver extends ResolverBase {
131
131
  this.createProgressCallback(pubSub, userPayload, testId) :
132
132
  undefined;
133
133
 
134
+ // Create log callback to stream driver/engine logs to the UI in real-time
135
+ const logCallback = pubSub ?
136
+ this.createLogCallback(pubSub, userPayload, testId) :
137
+ undefined;
138
+
134
139
  // Parse variables from JSON string if provided
135
140
  let parsedVariables: TestRunVariables | undefined;
136
141
  if (variables) {
@@ -147,7 +152,8 @@ export class RunTestResolver extends ResolverBase {
147
152
  environment,
148
153
  tags,
149
154
  variables: parsedVariables,
150
- progressCallback
155
+ progressCallback,
156
+ logCallback
151
157
  };
152
158
 
153
159
  const result = await engine.RunTest(testId, options, user);
@@ -251,6 +257,11 @@ export class RunTestResolver extends ResolverBase {
251
257
  this.createProgressCallback(pubSub, userPayload, suiteId) :
252
258
  undefined;
253
259
 
260
+ // Create log callback to stream driver/engine logs to the UI in real-time
261
+ const logCallback = pubSub ?
262
+ this.createLogCallback(pubSub, userPayload, suiteId) :
263
+ undefined;
264
+
254
265
  // Parse selectedTestIds from JSON string if provided
255
266
  let parsedSelectedTestIds: string[] | undefined;
256
267
  if (selectedTestIds) {
@@ -280,7 +291,8 @@ export class RunTestResolver extends ResolverBase {
280
291
  selectedTestIds: parsedSelectedTestIds,
281
292
  sequenceStart,
282
293
  sequenceEnd,
283
- progressCallback
294
+ progressCallback,
295
+ logCallback
284
296
  };
285
297
 
286
298
  const result = await engine.RunSuite(suiteId, options, user);
@@ -364,6 +376,32 @@ export class RunTestResolver extends ResolverBase {
364
376
  };
365
377
  }
366
378
 
379
+ /**
380
+ * Create log callback that streams driver/engine log messages to the UI
381
+ * as progress updates, so they appear in the execution log in real-time.
382
+ */
383
+ private createLogCallback(
384
+ pubSub: PubSubEngine,
385
+ userPayload: UserPayload,
386
+ testId: string
387
+ ) {
388
+ return (message: TestLogMessage) => {
389
+ const progressMsg: TestExecutionStreamMessage = {
390
+ sessionId: userPayload.sessionId || '',
391
+ testRunId: testId,
392
+ type: 'progress',
393
+ progress: {
394
+ currentStep: 'driver_log',
395
+ percentage: -1, // Signal that percentage should not be updated
396
+ message: message.message,
397
+ },
398
+ timestamp: message.timestamp
399
+ };
400
+
401
+ this.publishProgress(pubSub, progressMsg, userPayload);
402
+ };
403
+ }
404
+
367
405
  private publishProgress(pubSub: PubSubEngine, data: TestExecutionStreamMessage, userPayload: UserPayload) {
368
406
  pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
369
407
  message: JSON.stringify({
@@ -3,7 +3,7 @@ import { EntitySaveOptions, Metadata } from '@memberjunction/core';
3
3
  import { AppContext, Arg, Ctx, Int, Query, Resolver, UserPayload } from '@memberjunction/server';
4
4
  import { MJUserView_, MJUserViewResolverBase } from '../generated/generated.js';
5
5
  import { UserResolver } from './UserResolver.js';
6
- import { MJUserViewEntity, UserViewEntityExtended } from '@memberjunction/core-entities';
6
+ import { MJUserViewEntity, MJUserViewEntityExtended } from '@memberjunction/core-entities';
7
7
  import { GetReadOnlyProvider } from '../util.js';
8
8
 
9
9
  @Resolver(MJUserView_)
@@ -54,7 +54,7 @@ export class UserViewResolver extends MJUserViewResolverBase {
54
54
  // filter state which in turn will be used to update the where clause in the entity sub-class.
55
55
  const p = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true});
56
56
  const u = this.GetUserFromPayload(userPayload);
57
- const viewEntity = <UserViewEntityExtended>await p.GetEntityObject('MJ: User Views', u);
57
+ const viewEntity = <MJUserViewEntityExtended>await p.GetEntityObject('MJ: User Views', u);
58
58
  await viewEntity.Load(ID);
59
59
  viewEntity.UpdateWhereClause();
60
60
 
@@ -5,7 +5,7 @@ import { ChatMessageRole } from '@memberjunction/ai';
5
5
  import { PubSubEngine } from 'type-graphql';
6
6
  import { UserPayload } from '../types.js';
7
7
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
8
- import { AIAgentEntityExtended } from '@memberjunction/ai-core-plus';
8
+ import { MJAIAgentEntityExtended } from '@memberjunction/ai-core-plus';
9
9
 
10
10
  /**
11
11
  * Task definition from LLM response
@@ -268,9 +268,9 @@ export class TaskOrchestrator {
268
268
  /**
269
269
  * Find agent by name
270
270
  */
271
- private async findAgentByName(agentName: string): Promise<AIAgentEntityExtended | null> {
271
+ private async findAgentByName(agentName: string): Promise<MJAIAgentEntityExtended | null> {
272
272
  const rv = new RunView();
273
- const result = await rv.RunView<AIAgentEntityExtended>({
273
+ const result = await rv.RunView<MJAIAgentEntityExtended>({
274
274
  EntityName: 'MJ: AI Agents',
275
275
  ExtraFilter: `Name='${agentName.replace(/'/g, "''")}'`,
276
276
  ResultType: 'entity_object'
@@ -475,7 +475,7 @@ export class TaskOrchestrator {
475
475
 
476
476
  // Load the agent entity
477
477
  const md = new Metadata();
478
- const agentEntity = await md.GetEntityObject<AIAgentEntityExtended>('MJ: AI Agents', this.contextUser);
478
+ const agentEntity = await md.GetEntityObject<MJAIAgentEntityExtended>('MJ: AI Agents', this.contextUser);
479
479
  const loaded = await agentEntity.Load(task.AgentID!);
480
480
  if (!loaded) {
481
481
  throw new Error(`Agent with ID ${task.AgentID} not found`);
@@ -695,7 +695,7 @@ export class TaskOrchestrator {
695
695
  private async createArtifactFromOutput(
696
696
  output: { type: 'message' | 'payload', content: any },
697
697
  conversationDetailId: string,
698
- agent: AIAgentEntityExtended,
698
+ agent: MJAIAgentEntityExtended,
699
699
  taskName: string
700
700
  ): Promise<void> {
701
701
  try {
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AggregateExpression, DatabaseProviderBase, UserInfo } from '@memberjunction/core';
2
- import { UserViewEntityExtended } from '@memberjunction/core-entities';
2
+ import { MJUserViewEntityExtended } from '@memberjunction/core-entities';
3
3
  import { GraphQLSchema } from 'graphql';
4
4
  import sql from 'mssql';
5
5
  import { getSystemUser } from './auth/index.js';
@@ -68,7 +68,7 @@ export type DirectiveBuilder = {
68
68
  };
69
69
 
70
70
  export type RunViewGenericParams = {
71
- viewInfo: UserViewEntityExtended;
71
+ viewInfo: MJUserViewEntityExtended;
72
72
  provider: DatabaseProviderBase;
73
73
  extraFilter: string;
74
74
  orderBy: string;
@@ -1 +0,0 @@
1
- {"version":3,"file":"entityPermissions.server.d.ts","sourceRoot":"","sources":["../../src/entitySubclasses/entityPermissions.server.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC1F,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAIzE;;;;;;GAMG;AACH,qBACa,8BAA+B,SAAQ,wBAAwB;IAC1E,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,CAAM;IAC/C,SAAS,CAAC,MAAM,CAAC,iBAAiB,EAAE,IAAI,GAAG,IAAI,CAAQ;IACvD,SAAS,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAQ;IAChE,SAAS,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAgC;IACzE,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAoB;IACrD,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAqB;IACnD,SAAS,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAA6B;IAGlE,SAAS,CAAC,MAAM,CAAC,gBAAgB,IAAI,MAAM;IAI3C,WAAkB,aAAa,IAAI,MAAM,EAAE,CAE1C;WAEa,UAAU,IAAI,IAAI;WAIlB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAMhD,SAAS,CAAC,MAAM,CAAC,yBAAyB,IAAI,IAAI;IAUlD,SAAS,CAAC,MAAM,CAAC,oBAAoB,IAAI,IAAI;qBAMtB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B1C,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAO7C,MAAM,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;CAQtE"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"entityPermissions.server.js","sourceRoot":"","sources":["../../src/entitySubclasses/entityPermissions.server.ts"],"names":[],"mappings":"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"}