@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.
- package/dist/generated/generated.d.ts +78 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +821 -368
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +31 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +1 -0
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +10 -0
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts +33 -0
- package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -0
- package/dist/resolvers/CreateQueryResolver.js +251 -0
- package/dist/resolvers/CreateQueryResolver.js.map +1 -0
- package/dist/resolvers/QueryResolver.d.ts +7 -5
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +133 -64
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/package.json +34 -34
- package/src/generated/generated.ts +652 -369
- package/src/generic/ResolverBase.ts +37 -6
- package/src/index.ts +4 -1
- package/src/resolvers/AskSkipResolver.ts +15 -0
- package/src/resolvers/CreateQueryResolver.ts +257 -0
- package/src/resolvers/QueryResolver.ts +128 -56
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
32
|
-
export class
|
|
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
|
|
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:
|
|
50
|
-
Success: result.Success,
|
|
51
|
-
Results: JSON.stringify(result.Results),
|
|
52
|
-
RowCount: result.RowCount,
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
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:
|
|
101
|
-
Success: result.Success,
|
|
102
|
-
Results: JSON.stringify(result.Results),
|
|
103
|
-
RowCount: result.RowCount,
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
}
|