@memberjunction/server 2.72.0 → 2.74.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.
@@ -13,7 +13,7 @@ import {
13
13
  RunViewResult,
14
14
  UserInfo,
15
15
  } from '@memberjunction/core';
16
- import { AuditLogEntity, UserViewEntity } from '@memberjunction/core-entities';
16
+ import { AuditLogEntity, ErrorLogEntity, UserViewEntity } from '@memberjunction/core-entities';
17
17
  import { SQLServerDataProvider, UserCache } from '@memberjunction/sqlserver-dataprovider';
18
18
  import { PubSubEngine } from 'type-graphql';
19
19
  import { GraphQLError } from 'graphql';
@@ -727,7 +727,7 @@ export class ResolverBase {
727
727
  // load worked, now, only IF we have OldValues, we need to check them against the values in the DB we just loaded.
728
728
  if (input.OldValues___) {
729
729
  // we DO have OldValues, so we need to do a more in depth analysis
730
- this.TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject);
730
+ await this.TestAndSetClientOldValuesToDBValues(input, clientNewValues, entityObject, userInfo);
731
731
  } else {
732
732
  // no OldValues, so we can just set the new values from input
733
733
  entityObject.SetMany(input);
@@ -775,7 +775,7 @@ export class ResolverBase {
775
775
  *
776
776
  * ASSUMES: input object has an OldValues___ property that is an array of Key/Value pairs that represent the old values of the record that the client is trying to update.
777
777
  */
778
- protected TestAndSetClientOldValuesToDBValues(input: any, clientNewValues: any, entityObject: BaseEntity) {
778
+ protected async TestAndSetClientOldValuesToDBValues(input: any, clientNewValues: any, entityObject: BaseEntity, contextUser: UserInfo) {
779
779
  // we have OldValues, so we need to compare them to the values we just loaded from the DB
780
780
  const clientOldValues = {};
781
781
  // for each item in the oldValues array, add it to the clientOldValues object
@@ -883,17 +883,48 @@ export class ResolverBase {
883
883
  });
884
884
 
885
885
  // now we have clientDifferences which shows what the client thinks they are changing. And, we have the dbDifferences array that shows changes between the clientOldValues and the dbValues
886
- // if there is ANY overlap in the FIELDS that appear in both arrays, we need to throw an error
886
+ // if there is ANY overlap in the FIELDS that appear in both arrays, we need to log a warning but allow the save to continue
887
887
  const overlap = clientDifferences.filter((cd) => dbDifferences.find((dd) => dd.FieldName === cd.FieldName));
888
888
  if (overlap.length > 0) {
889
889
  const msg = {
890
890
  Message:
891
- 'Inconsistency between old values provided for changed fields, and the values of one or more of those fields in the database. Update operation cancelled.',
891
+ 'Inconsistency between old values provided for changed fields, and the values of one or more of those fields in the database. Save operation continued with warning.',
892
892
  ClientDifferences: clientDifferences,
893
893
  DBDifferences: dbDifferences,
894
894
  Overlap: overlap,
895
895
  };
896
- throw new Error(JSON.stringify(msg));
896
+
897
+ // Log as warning to console and ErrorLog table instead of throwing error
898
+ console.warn('Entity save inconsistency detected but allowing save to continue:', JSON.stringify(msg));
899
+ LogError({
900
+ service: 'ResolverBase',
901
+ operation: 'TestAndSetClientOldValuesToDBValues',
902
+ error: `Entity save inconsistency detected: ${JSON.stringify(msg)}`,
903
+ details: {
904
+ entityName: entityObject.EntityInfo.Name,
905
+ clientDifferences: clientDifferences,
906
+ dbDifferences: dbDifferences,
907
+ overlap: overlap
908
+ }
909
+ });
910
+
911
+ // Create ErrorLog record in the database
912
+ try {
913
+ const md = new Metadata();
914
+ const errorLogEntity = await md.GetEntityObject<ErrorLogEntity>('Error Logs', contextUser);
915
+ errorLogEntity.Code = 'ENTITY_SAVE_INCONSISTENCY';
916
+ errorLogEntity.Message = `Entity save inconsistency detected for ${entityObject.EntityInfo.Name}: ${JSON.stringify(msg)}`;
917
+ errorLogEntity.Status = 'Warning';
918
+ errorLogEntity.Category = 'Entity Save';
919
+ errorLogEntity.CreatedBy = contextUser.Email || contextUser.Name;
920
+
921
+ const saveResult = await errorLogEntity.Save();
922
+ if (!saveResult) {
923
+ console.error('Failed to save ErrorLog record');
924
+ }
925
+ } catch (errorLogError) {
926
+ console.error('Error creating ErrorLog record:', errorLogError);
927
+ }
897
928
  }
898
929
  }
899
930
 
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ LoadAgentManagementActions();
40
40
 
41
41
  import { resolve } from 'node:path';
42
42
  import { DataSourceInfo, raiseEvent } from './types.js';
43
+ import { LoadAIEngine } from '@memberjunction/aiengine';
43
44
  import { LoadOpenAILLM } from '@memberjunction/ai-openai';
44
45
  import { LoadAnthropicLLM } from '@memberjunction/ai-anthropic';
45
46
  import { LoadGroqLLM } from '@memberjunction/ai-groq';
@@ -48,6 +49,7 @@ import { LoadMistralLLM } from '@memberjunction/ai-mistral';
48
49
  // Load AI LLMs and Base AI Engine
49
50
  // These imports are necessary to ensure the LLMs are registered in the MemberJunction AI
50
51
  // system. They are not tree-shaken because they are dynamically loaded at runtime.
52
+ LoadAIEngine();
51
53
  LoadOpenAILLM();
52
54
  LoadAnthropicLLM();
53
55
  LoadGroqLLM();
@@ -83,13 +85,14 @@ export * from './resolvers/DatasetResolver.js';
83
85
  export * from './resolvers/EntityRecordNameResolver.js';
84
86
  export * from './resolvers/MergeRecordsResolver.js';
85
87
  export * from './resolvers/ReportResolver.js';
88
+ export * from './resolvers/QueryResolver.js';
86
89
  export * from './resolvers/SqlLoggingConfigResolver.js';
87
90
  export * from './resolvers/SyncRolesUsersResolver.js';
88
91
  export * from './resolvers/SyncDataResolver.js';
89
92
  export * from './resolvers/GetDataResolver.js';
90
93
  export * from './resolvers/GetDataContextDataResolver.js';
91
94
  export * from './resolvers/TransactionGroupResolver.js';
92
-
95
+ export * from './resolvers/CreateQueryResolver.js';
93
96
  export { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
94
97
 
95
98
  export * from './generated/generated.js';
@@ -1396,6 +1396,20 @@ cycle.`);
1396
1396
  );
1397
1397
  }
1398
1398
 
1399
+
1400
+ /**
1401
+ * Recursively builds the category path for a query
1402
+ * @param md
1403
+ * @param categoryID
1404
+ */
1405
+ protected buildQueryCategoryPath(md: Metadata, categoryID: string): string {
1406
+ const cat = md.QueryCategories.find((c) => c.ID === categoryID);
1407
+ if (!cat) return '';
1408
+ if (!cat.ParentID) return cat.Name; // base case, no parent, just return the name
1409
+ const parentPath = this.buildQueryCategoryPath(md, cat.ParentID); // build the path recursively
1410
+ return parentPath ? `${parentPath}/${cat.Name}` : cat.Name;
1411
+ }
1412
+
1399
1413
  /**
1400
1414
  * Packages up queries from the metadata based on their status
1401
1415
  * Used to provide Skip with information about available queries
@@ -1412,6 +1426,7 @@ cycle.`);
1412
1426
  name: q.Name,
1413
1427
  description: q.Description,
1414
1428
  category: q.Category,
1429
+ categoryPath: this.buildQueryCategoryPath(md, q.CategoryID),
1415
1430
  sql: q.SQL,
1416
1431
  originalSQL: q.OriginalSQL,
1417
1432
  feedback: q.Feedback,
@@ -0,0 +1,257 @@
1
+ import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType } from 'type-graphql';
2
+ import { AppContext } from '../types.js';
3
+ import { LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
+ import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
+ import { QueryEntity, QueryCategoryEntity } from '@memberjunction/core-entities';
6
+
7
+ /**
8
+ * Query status enumeration for GraphQL
9
+ */
10
+ export enum QueryStatus {
11
+ Pending = "Pending",
12
+ Approved = "Approved",
13
+ Rejected = "Rejected",
14
+ Expired = "Expired"
15
+ }
16
+
17
+ registerEnumType(QueryStatus, {
18
+ name: "QueryStatus",
19
+ description: "Status of a query: Pending, Approved, Rejected, or Expired"
20
+ });
21
+
22
+ @InputType()
23
+ export class CreateQueryInputType {
24
+ @Field(() => String)
25
+ Name!: string;
26
+
27
+ @Field(() => String, { nullable: true })
28
+ CategoryID?: string;
29
+
30
+ @Field(() => String, { nullable: true })
31
+ CategoryPath?: string;
32
+
33
+ @Field(() => String, { nullable: true })
34
+ UserQuestion?: string;
35
+
36
+ @Field(() => String, { nullable: true })
37
+ Description?: string;
38
+
39
+ @Field(() => String, { nullable: true })
40
+ SQL?: string;
41
+
42
+ @Field(() => String, { nullable: true })
43
+ TechnicalDescription?: string;
44
+
45
+ @Field(() => String, { nullable: true })
46
+ OriginalSQL?: string;
47
+
48
+ @Field(() => String, { nullable: true })
49
+ Feedback?: string;
50
+
51
+ @Field(() => QueryStatus, { nullable: true, defaultValue: QueryStatus.Pending })
52
+ Status?: QueryStatus;
53
+
54
+ @Field(() => Number, { nullable: true })
55
+ QualityRank?: number;
56
+
57
+ @Field(() => Number, { nullable: true })
58
+ ExecutionCostRank?: number;
59
+
60
+ @Field(() => Boolean, { nullable: true })
61
+ UsesTemplate?: boolean;
62
+ }
63
+
64
+ @ObjectType()
65
+ export class CreateQueryResultType {
66
+ @Field(() => Boolean)
67
+ Success!: boolean;
68
+
69
+ @Field(() => String, { nullable: true })
70
+ ErrorMessage?: string;
71
+
72
+ @Field(() => String, { nullable: true })
73
+ QueryData?: string;
74
+ }
75
+
76
+ export class CreateQueryResolver {
77
+ /**
78
+ * Creates a new query with the provided attributes. This mutation is restricted to system users only.
79
+ * @param input - CreateQueryInputType containing all the query attributes
80
+ * @param context - Application context containing user information
81
+ * @returns CreateQueryResultType with success status and query data
82
+ */
83
+ @RequireSystemUser()
84
+ @Mutation(() => CreateQueryResultType)
85
+ async CreateQuery(
86
+ @Arg('input', () => CreateQueryInputType) input: CreateQueryInputType,
87
+ @Ctx() context: AppContext
88
+ ): Promise<CreateQueryResultType> {
89
+ try {
90
+ const md = new Metadata();
91
+ const newQuery = await md.GetEntityObject<QueryEntity>("Queries", context.userPayload.userRecord);
92
+
93
+ // Handle CategoryPath if provided
94
+ let finalCategoryID = input.CategoryID;
95
+ if (input.CategoryPath) {
96
+ finalCategoryID = await this.findOrCreateCategoryPath(input.CategoryPath, md, context.userPayload.userRecord);
97
+ }
98
+
99
+ // Populate the query fields from input
100
+ newQuery.Name = input.Name;
101
+
102
+ if (finalCategoryID != null) {
103
+ newQuery.CategoryID = finalCategoryID;
104
+ }
105
+
106
+ if (input.UserQuestion != null) {
107
+ newQuery.UserQuestion = input.UserQuestion;
108
+ }
109
+
110
+ if (input.Description != null) {
111
+ newQuery.Description = input.Description;
112
+ }
113
+
114
+ if (input.SQL != null) {
115
+ newQuery.SQL = input.SQL;
116
+ }
117
+
118
+ if (input.TechnicalDescription != null) {
119
+ newQuery.TechnicalDescription = input.TechnicalDescription;
120
+ }
121
+
122
+ if (input.OriginalSQL != null) {
123
+ newQuery.OriginalSQL = input.OriginalSQL;
124
+ }
125
+
126
+ if (input.Feedback != null) {
127
+ newQuery.Feedback = input.Feedback;
128
+ }
129
+
130
+ if (input.Status != null) {
131
+ newQuery.Status = input.Status;
132
+ }
133
+
134
+ if (input.QualityRank != null) {
135
+ newQuery.QualityRank = input.QualityRank;
136
+ }
137
+
138
+ if (input.ExecutionCostRank != null) {
139
+ newQuery.ExecutionCostRank = input.ExecutionCostRank;
140
+ }
141
+
142
+ if (input.UsesTemplate != null) {
143
+ newQuery.UsesTemplate = input.UsesTemplate;
144
+ }
145
+
146
+ // Save the query
147
+ const saveResult = await newQuery.Save();
148
+
149
+ if (saveResult) {
150
+ return {
151
+ Success: true,
152
+ QueryData: JSON.stringify(newQuery.GetAll())
153
+ };
154
+ } else {
155
+ return {
156
+ Success: false,
157
+ ErrorMessage: `Failed to save query: ${newQuery.LatestResult?.Message || 'Unknown error'}`
158
+ };
159
+ }
160
+
161
+ } catch (err) {
162
+ LogError(err);
163
+ return {
164
+ Success: false,
165
+ ErrorMessage: `CreateQueryResolver::CreateQuery --- Error creating query: ${err instanceof Error ? err.message : String(err)}`
166
+ };
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Finds or creates a category hierarchy based on the provided path.
172
+ * Path format: "Parent/Child/Grandchild" - case insensitive lookup and creation.
173
+ * @param categoryPath - Slash-separated category path
174
+ * @param md - Metadata instance
175
+ * @param contextUser - User context for operations
176
+ * @returns The ID of the final category in the path
177
+ */
178
+ private async findOrCreateCategoryPath(categoryPath: string, md: Metadata, contextUser: UserInfo): Promise<string> {
179
+ if (!categoryPath || categoryPath.trim() === '') {
180
+ throw new Error('CategoryPath cannot be empty');
181
+ }
182
+
183
+ const pathParts = categoryPath.split('/').map(part => part.trim()).filter(part => part.length > 0);
184
+ if (pathParts.length === 0) {
185
+ throw new Error('CategoryPath must contain at least one valid category name');
186
+ }
187
+
188
+ let currentParentID: string | null = null;
189
+ let currentCategoryID: string | null = null;
190
+
191
+ for (let i = 0; i < pathParts.length; i++) {
192
+ const categoryName = pathParts[i];
193
+
194
+ // Look for existing category at this level
195
+ const existingCategory = await this.findCategoryByNameAndParent(categoryName, currentParentID, contextUser);
196
+
197
+ if (existingCategory) {
198
+ currentCategoryID = existingCategory.ID;
199
+ currentParentID = existingCategory.ID;
200
+ } else {
201
+ // Create new category
202
+ const newCategory = await md.GetEntityObject<QueryCategoryEntity>("Query Categories", contextUser);
203
+ newCategory.Name = categoryName;
204
+ newCategory.ParentID = currentParentID;
205
+ newCategory.UserID = contextUser.ID;
206
+ newCategory.Description = `Auto-created category from path: ${categoryPath}`;
207
+
208
+ const saveResult = await newCategory.Save();
209
+ if (!saveResult) {
210
+ throw new Error(`Failed to create category '${categoryName}': ${newCategory.LatestResult?.Message || 'Unknown error'}`);
211
+ }
212
+
213
+ currentCategoryID = newCategory.ID;
214
+ currentParentID = newCategory.ID;
215
+
216
+ // Refresh metadata after each category creation to ensure it's available for subsequent lookups
217
+ await md.Refresh();
218
+ }
219
+ }
220
+
221
+ if (!currentCategoryID) {
222
+ throw new Error('Failed to determine final category ID');
223
+ }
224
+
225
+ return currentCategoryID;
226
+ }
227
+
228
+ /**
229
+ * Finds a category by name and parent ID using case-insensitive comparison via RunView.
230
+ * @param categoryName - Name of the category to find
231
+ * @param parentID - Parent category ID (null for root level)
232
+ * @param contextUser - User context for database operations
233
+ * @returns The matching category entity or null if not found
234
+ */
235
+ private async findCategoryByNameAndParent(categoryName: string, parentID: string | null, contextUser: UserInfo): Promise<QueryCategoryEntity | null> {
236
+ try {
237
+ const rv = new RunView();
238
+ const parentFilter = parentID ? `ParentID='${parentID}'` : 'ParentID IS NULL';
239
+ const nameFilter = `LOWER(Name) = LOWER('${categoryName.replace(/'/g, "''")}')`; // Escape single quotes
240
+
241
+ const result = await rv.RunView<QueryCategoryEntity>({
242
+ EntityName: 'Query Categories',
243
+ ExtraFilter: `${nameFilter} AND ${parentFilter}`,
244
+ ResultType: 'entity_object'
245
+ }, contextUser);
246
+
247
+ if (result.Success && result.Results && result.Results.length > 0) {
248
+ return result.Results[0];
249
+ }
250
+
251
+ return null;
252
+ } catch (error) {
253
+ LogError(error);
254
+ return null;
255
+ }
256
+ }
257
+ }
@@ -1,8 +1,10 @@
1
- // Queries are MemberJunction primitive operations that are used to retrieve data from the server from any stored query
2
- import { RunQuery } from '@memberjunction/core';
3
- import { Arg, Ctx, Field, Int, ObjectType, Query, Resolver } from 'type-graphql';
1
+ import { Arg, Ctx, ObjectType, Query, Resolver, Field, Int } from 'type-graphql';
2
+ import { RunQuery, QueryInfo } from '@memberjunction/core';
4
3
  import { AppContext } from '../types.js';
5
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
+ import { GraphQLJSONObject } from 'graphql-type-json';
6
+ import { QueryEntity } from '@memberjunction/core-entities';
7
+ import { Metadata } from '@memberjunction/core';
6
8
 
7
9
  @ObjectType()
8
10
  export class RunQueryResultType {
@@ -21,37 +23,104 @@ export class RunQueryResultType {
21
23
  @Field()
22
24
  RowCount: number;
23
25
 
26
+ @Field()
27
+ TotalRowCount: number;
28
+
24
29
  @Field()
25
30
  ExecutionTime: number;
26
31
 
27
32
  @Field()
28
33
  ErrorMessage: string;
34
+
35
+ @Field(() => String, { nullable: true })
36
+ AppliedParameters?: string;
29
37
  }
30
38
 
31
- @Resolver(RunQueryResultType)
32
- export class ReportResolver {
39
+ @Resolver()
40
+ export class RunQueryResolver {
41
+ private async findQuery(QueryID: string, QueryName?: string, CategoryID?: string, CategoryName?: string, refreshMetadataIfNotFound: boolean = false): Promise<QueryInfo | null> {
42
+ const md = new Metadata();
43
+
44
+ // Filter queries based on provided criteria
45
+ const queries = md.Queries.filter(q => {
46
+ if (QueryID) {
47
+ return q.ID.trim().toLowerCase() === QueryID.trim().toLowerCase();
48
+ } else if (QueryName) {
49
+ let matches = q.Name.trim().toLowerCase() === QueryName.trim().toLowerCase();
50
+ if (CategoryID) {
51
+ matches = matches && q.CategoryID?.trim().toLowerCase() === CategoryID.trim().toLowerCase();
52
+ }
53
+ if (CategoryName) {
54
+ matches = matches && q.Category?.trim().toLowerCase() === CategoryName.trim().toLowerCase();
55
+ }
56
+ return matches;
57
+ }
58
+ return false;
59
+ });
60
+
61
+ if (queries.length === 0) {
62
+ if (refreshMetadataIfNotFound) {
63
+ // If we didn't find the query, refresh metadata and try again
64
+ await md.Refresh();
65
+ return this.findQuery(QueryID, QueryName, CategoryID, CategoryName, false); // change the refresh flag to false so we don't loop infinitely
66
+ }
67
+ else {
68
+ return null; // No query found and not refreshing metadata
69
+ }
70
+ }
71
+ else {
72
+ return queries[0];
73
+ }
74
+ }
33
75
  @Query(() => RunQueryResultType)
34
76
  async GetQueryData(@Arg('QueryID', () => String) QueryID: string,
35
77
  @Ctx() context: AppContext,
36
78
  @Arg('CategoryID', () => String, {nullable: true}) CategoryID?: string,
37
- @Arg('CategoryName', () => String, {nullable: true}) CategoryName?: string): Promise<RunQueryResultType> {
79
+ @Arg('CategoryName', () => String, {nullable: true}) CategoryName?: string,
80
+ @Arg('Parameters', () => GraphQLJSONObject, {nullable: true}) Parameters?: Record<string, any>,
81
+ @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
82
+ @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number): Promise<RunQueryResultType> {
38
83
  const runQuery = new RunQuery();
84
+ console.log('GetQueryData called with:', { QueryID, Parameters, MaxRows, StartRow });
39
85
  const result = await runQuery.RunQuery(
40
86
  {
41
87
  QueryID: QueryID,
42
88
  CategoryID: CategoryID,
43
- CategoryName: CategoryName
89
+ CategoryName: CategoryName,
90
+ Parameters: Parameters,
91
+ MaxRows: MaxRows,
92
+ StartRow: StartRow
44
93
  },
45
94
  context.userPayload.userRecord);
95
+ console.log('RunQuery result:', {
96
+ Success: result.Success,
97
+ ErrorMessage: result.ErrorMessage,
98
+ AppliedParameters: result.AppliedParameters
99
+ });
100
+
101
+ // If QueryName is not populated by the provider, use efficient lookup
102
+ let queryName = result.QueryName;
103
+ if (!queryName) {
104
+ try {
105
+ const queryInfo = await this.findQuery(QueryID, undefined, CategoryID, CategoryName, true);
106
+ if (queryInfo) {
107
+ queryName = queryInfo.Name;
108
+ }
109
+ } catch (error) {
110
+ console.error('Error finding query to get name:', error);
111
+ }
112
+ }
46
113
 
47
114
  return {
48
115
  QueryID: QueryID,
49
- QueryName: result.QueryName,
50
- Success: result.Success,
51
- Results: JSON.stringify(result.Results),
52
- RowCount: result.RowCount,
53
- ExecutionTime: result.ExecutionTime,
54
- ErrorMessage: result.ErrorMessage,
116
+ QueryName: queryName || 'Unknown Query',
117
+ Success: result.Success ?? false,
118
+ Results: JSON.stringify(result.Results ?? null),
119
+ RowCount: result.RowCount ?? 0,
120
+ TotalRowCount: result.TotalRowCount ?? 0,
121
+ ExecutionTime: result.ExecutionTime ?? 0,
122
+ ErrorMessage: result.ErrorMessage || '',
123
+ AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined
55
124
  };
56
125
  }
57
126
 
@@ -59,24 +128,32 @@ export class ReportResolver {
59
128
  async GetQueryDataByName(@Arg('QueryName', () => String) QueryName: string,
60
129
  @Ctx() context: AppContext,
61
130
  @Arg('CategoryID', () => String, {nullable: true}) CategoryID?: string,
62
- @Arg('CategoryName', () => String, {nullable: true}) CategoryName?: string): Promise<RunQueryResultType> {
131
+ @Arg('CategoryName', () => String, {nullable: true}) CategoryName?: string,
132
+ @Arg('Parameters', () => GraphQLJSONObject, {nullable: true}) Parameters?: Record<string, any>,
133
+ @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
134
+ @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number): Promise<RunQueryResultType> {
63
135
  const runQuery = new RunQuery();
64
136
  const result = await runQuery.RunQuery(
65
137
  {
66
138
  QueryName: QueryName,
67
139
  CategoryID: CategoryID,
68
- CategoryName: CategoryName
140
+ CategoryName: CategoryName,
141
+ Parameters: Parameters,
142
+ MaxRows: MaxRows,
143
+ StartRow: StartRow
69
144
  },
70
145
  context.userPayload.userRecord);
71
146
 
72
147
  return {
73
- QueryID: result.QueryID,
148
+ QueryID: result.QueryID || '',
74
149
  QueryName: QueryName,
75
- Success: result.Success,
76
- Results: JSON.stringify(result.Results),
77
- RowCount: result.RowCount,
78
- ExecutionTime: result.ExecutionTime,
79
- ErrorMessage: result.ErrorMessage,
150
+ Success: result.Success ?? false,
151
+ Results: JSON.stringify(result.Results ?? null),
152
+ RowCount: result.RowCount ?? 0,
153
+ TotalRowCount: result.TotalRowCount ?? 0,
154
+ ExecutionTime: result.ExecutionTime ?? 0,
155
+ ErrorMessage: result.ErrorMessage || '',
156
+ AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined
80
157
  };
81
158
  }
82
159
 
@@ -85,50 +162,45 @@ export class ReportResolver {
85
162
  async GetQueryDataSystemUser(@Arg('QueryID', () => String) QueryID: string,
86
163
  @Ctx() context: AppContext,
87
164
  @Arg('CategoryID', () => String, {nullable: true}) CategoryID?: string,
88
- @Arg('CategoryName', () => String, {nullable: true}) CategoryName?: string): Promise<RunQueryResultType> {
165
+ @Arg('CategoryName', () => String, {nullable: true}) CategoryName?: string,
166
+ @Arg('Parameters', () => GraphQLJSONObject, {nullable: true}) Parameters?: Record<string, any>,
167
+ @Arg('MaxRows', () => Int, {nullable: true}) MaxRows?: number,
168
+ @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number): Promise<RunQueryResultType> {
89
169
  const runQuery = new RunQuery();
90
170
  const result = await runQuery.RunQuery(
91
171
  {
92
172
  QueryID: QueryID,
93
173
  CategoryID: CategoryID,
94
- CategoryName: CategoryName
174
+ CategoryName: CategoryName,
175
+ Parameters: Parameters,
176
+ MaxRows: MaxRows,
177
+ StartRow: StartRow
95
178
  },
96
179
  context.userPayload.userRecord);
97
180
 
181
+ // If QueryName is not populated by the provider, use efficient lookup
182
+ let queryName = result.QueryName;
183
+ if (!queryName) {
184
+ try {
185
+ const queryInfo = await this.findQuery(QueryID, undefined, CategoryID, CategoryName, true);
186
+ if (queryInfo) {
187
+ queryName = queryInfo.Name;
188
+ }
189
+ } catch (error) {
190
+ console.error('Error finding query to get name:', error);
191
+ }
192
+ }
193
+
98
194
  return {
99
195
  QueryID: QueryID,
100
- QueryName: result.QueryName,
101
- Success: result.Success,
102
- Results: JSON.stringify(result.Results),
103
- RowCount: result.RowCount,
104
- ExecutionTime: result.ExecutionTime,
105
- ErrorMessage: result.ErrorMessage,
196
+ QueryName: queryName || 'Unknown Query',
197
+ Success: result.Success ?? false,
198
+ Results: JSON.stringify(result.Results ?? null),
199
+ RowCount: result.RowCount ?? 0,
200
+ TotalRowCount: result.TotalRowCount ?? 0,
201
+ ExecutionTime: result.ExecutionTime ?? 0,
202
+ ErrorMessage: result.ErrorMessage || '',
203
+ AppliedParameters: result.AppliedParameters ? JSON.stringify(result.AppliedParameters) : undefined
106
204
  };
107
205
  }
108
-
109
- @RequireSystemUser()
110
- @Query(() => RunQueryResultType)
111
- async GetQueryDataByNameSystemUser(@Arg('QueryName', () => String) QueryName: string,
112
- @Ctx() context: AppContext,
113
- @Arg('CategoryID', () => String, {nullable: true}) CategoryID?: string,
114
- @Arg('CategoryName', () => String, {nullable: true}) CategoryName?: string): Promise<RunQueryResultType> {
115
- const runQuery = new RunQuery();
116
- const result = await runQuery.RunQuery(
117
- {
118
- QueryName: QueryName,
119
- CategoryID: CategoryID,
120
- CategoryName: CategoryName
121
- },
122
- context.userPayload.userRecord);
123
-
124
- return {
125
- QueryID: result.QueryID,
126
- QueryName: QueryName,
127
- Success: result.Success,
128
- Results: JSON.stringify(result.Results),
129
- RowCount: result.RowCount,
130
- ExecutionTime: result.ExecutionTime,
131
- ErrorMessage: result.ErrorMessage,
132
- };
133
- }
134
- }
206
+ }