@memberjunction/server 0.9.90 → 0.9.94
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/build.log.json +12 -0
- package/dist/generated/generated.js +18854 -0
- package/dist/generated/generated.js.map +1 -0
- package/dist/generic/PushStatusResolver.js +59 -0
- package/dist/generic/PushStatusResolver.js.map +1 -0
- package/dist/generic/ResolverBase.js +208 -0
- package/dist/generic/ResolverBase.js.map +1 -0
- package/dist/generic/RunViewResolver.js +401 -0
- package/dist/generic/RunViewResolver.js.map +1 -0
- package/dist/index.js +20 -3
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +247 -0
- package/dist/resolvers/AskSkipResolver.js.map +1 -0
- package/dist/resolvers/ColorResolver.js +94 -0
- package/dist/resolvers/ColorResolver.js.map +1 -0
- package/dist/resolvers/DatasetResolver.js +168 -0
- package/dist/resolvers/DatasetResolver.js.map +1 -0
- package/dist/resolvers/EntityRecordNameResolver.js +111 -0
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -0
- package/dist/resolvers/EntityResolver.js +60 -0
- package/dist/resolvers/EntityResolver.js.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.js +256 -0
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -0
- package/dist/resolvers/ReportResolver.js +74 -0
- package/dist/resolvers/ReportResolver.js.map +1 -0
- package/dist/resolvers/UserFavoriteResolver.js +185 -0
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -0
- package/dist/resolvers/UserResolver.js +70 -0
- package/dist/resolvers/UserResolver.js.map +1 -0
- package/dist/resolvers/UserViewResolver.js +102 -0
- package/dist/resolvers/UserViewResolver.js.map +1 -0
- package/package.json +7 -7
- package/src/generated/generated.ts +13928 -0
- package/src/generic/PushStatusResolver.ts +41 -0
- package/src/generic/ResolverBase.ts +285 -0
- package/src/generic/RunViewResolver.ts +350 -0
- package/src/index.ts +31 -3
- package/src/resolvers/AskSkipResolver.ts +237 -0
- package/src/resolvers/ColorResolver.ts +72 -0
- package/src/resolvers/DatasetResolver.ts +114 -0
- package/src/resolvers/EntityRecordNameResolver.ts +73 -0
- package/src/resolvers/EntityResolver.ts +34 -0
- package/src/resolvers/MergeRecordsResolver.ts +178 -0
- package/src/resolvers/ReportResolver.ts +56 -0
- package/src/resolvers/UserFavoriteResolver.ts +123 -0
- package/src/resolvers/UserResolver.ts +29 -0
- package/src/resolvers/UserViewResolver.ts +63 -0
package/src/index.ts
CHANGED
|
@@ -34,12 +34,40 @@ export * from './directives';
|
|
|
34
34
|
export * from './entitySubclasses/userViewEntity.server';
|
|
35
35
|
export * from './types';
|
|
36
36
|
|
|
37
|
+
export * from './generic/PushStatusResolver';
|
|
38
|
+
export * from './generic/ResolverBase';
|
|
39
|
+
export * from './generic/RunViewResolver';
|
|
40
|
+
|
|
41
|
+
export * from './resolvers/AskSkipResolver';
|
|
42
|
+
export * from './resolvers/ColorResolver';
|
|
43
|
+
export * from './resolvers/DatasetResolver';
|
|
44
|
+
export * from './resolvers/EntityRecordNameResolver';
|
|
45
|
+
//export * from '../../MJAPI/src/resolvers/EntityResolver';
|
|
46
|
+
export * from './resolvers/MergeRecordsResolver';
|
|
47
|
+
export * from './resolvers/ReportResolver';
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
//export * from '../../MJAPI/src/resolvers/UserFavoriteResolver';
|
|
51
|
+
//export * from '../../MJAPI/src/resolvers/UserViewResolver';
|
|
52
|
+
|
|
53
|
+
import { resolve } from 'node:path';
|
|
54
|
+
|
|
55
|
+
const localPath = (p: string) => resolve(__dirname, p);
|
|
56
|
+
|
|
37
57
|
export const serve = async (resolverPaths: Array<string>) => {
|
|
58
|
+
const localResolverPaths = [
|
|
59
|
+
'resolvers/**/*Resolver.{js,ts}',
|
|
60
|
+
'generic/*Resolver.{js,ts}',
|
|
61
|
+
'generated/generated.{js,ts}'
|
|
62
|
+
].map(localPath);
|
|
63
|
+
|
|
64
|
+
const combinedResolverPaths = [...resolverPaths, ...localResolverPaths];
|
|
65
|
+
|
|
38
66
|
const replaceBackslashes = sep === '\\';
|
|
39
|
-
const paths =
|
|
67
|
+
const paths = combinedResolverPaths.flatMap((path) => globSync(replaceBackslashes ? path.replace(/\\/g, '/') : path));
|
|
40
68
|
if (paths.length === 0) {
|
|
41
|
-
console.warn(`No resolvers found in ${
|
|
42
|
-
console.log({
|
|
69
|
+
console.warn(`No resolvers found in ${combinedResolverPaths.join(', ')}`);
|
|
70
|
+
console.log({ combinedResolverPaths, paths, cwd: process.cwd() });
|
|
43
71
|
}
|
|
44
72
|
|
|
45
73
|
const dataSource = new DataSource(orm(paths));
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver } from 'type-graphql';
|
|
2
|
+
import { SkipAnalyzeData, SkipExplainQuery } from '@memberjunction/ai';
|
|
3
|
+
import { Metadata } from '@memberjunction/core';
|
|
4
|
+
import { AppContext } from '../types';
|
|
5
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
|
|
8
|
+
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver';
|
|
9
|
+
|
|
10
|
+
@ObjectType()
|
|
11
|
+
export class AskSkipResultType {
|
|
12
|
+
@Field(() => Boolean)
|
|
13
|
+
Success: boolean;
|
|
14
|
+
|
|
15
|
+
@Field(() => String)
|
|
16
|
+
Status: string; // required
|
|
17
|
+
|
|
18
|
+
@Field(() => String)
|
|
19
|
+
Result: string;
|
|
20
|
+
|
|
21
|
+
@Field(() => Int)
|
|
22
|
+
ConversationId: number;
|
|
23
|
+
|
|
24
|
+
@Field(() => Int)
|
|
25
|
+
UserMessageConversationDetailId: number;
|
|
26
|
+
|
|
27
|
+
@Field(() => Int)
|
|
28
|
+
AIMessageConversationDetailId: number;
|
|
29
|
+
}
|
|
30
|
+
@Resolver(AskSkipResultType)
|
|
31
|
+
export class AskSkipResolver {
|
|
32
|
+
private static _defaultNewChatName = 'New Chat';
|
|
33
|
+
|
|
34
|
+
@Query(() => AskSkipResultType)
|
|
35
|
+
async ExecuteAskSkipQuery(
|
|
36
|
+
@Arg('UserQuestion', () => String) UserQuestion: string,
|
|
37
|
+
@Arg('ConversationId', () => Int) ConversationId: number,
|
|
38
|
+
@Ctx() { dataSource, userPayload }: AppContext,
|
|
39
|
+
@PubSub() pubSub: PubSubEngine
|
|
40
|
+
) {
|
|
41
|
+
try {
|
|
42
|
+
const md = new Metadata();
|
|
43
|
+
const user = UserCache.Instance.Users.find((u) => u.Email === userPayload.email);
|
|
44
|
+
if (!user) throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
45
|
+
|
|
46
|
+
const convoEntity = await md.GetEntityObject('Conversations', user);
|
|
47
|
+
if (!ConversationId || ConversationId <= 0) {
|
|
48
|
+
// create a new conversation id
|
|
49
|
+
convoEntity.NewRecord();
|
|
50
|
+
if (user) {
|
|
51
|
+
convoEntity.UserID = user.ID;
|
|
52
|
+
convoEntity.Name = AskSkipResolver._defaultNewChatName;
|
|
53
|
+
if (await convoEntity.Save()) ConversationId = convoEntity.ID;
|
|
54
|
+
else throw new Error(`Creating a new conversation failed`);
|
|
55
|
+
} else throw new Error(`User ${userPayload.email} not found in UserCache`);
|
|
56
|
+
} else {
|
|
57
|
+
await convoEntity.Load(ConversationId); // load the existing conversation, will need it later
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// now, create a conversation detail record for the user message
|
|
61
|
+
const convoDetailEntity = await md.GetEntityObject('Conversation Details', user);
|
|
62
|
+
convoDetailEntity.NewRecord();
|
|
63
|
+
convoDetailEntity.ConversationID = ConversationId;
|
|
64
|
+
convoDetailEntity.Message = UserQuestion;
|
|
65
|
+
convoDetailEntity.Role = 'User';
|
|
66
|
+
convoDetailEntity.Set('Sequence', 1); // using weakly typed here because we're going to get rid of this field soon
|
|
67
|
+
await convoDetailEntity.Save();
|
|
68
|
+
|
|
69
|
+
//const OrganizationId = 2 //HG 8/1/2023 TODO: Pull this from an environment variable
|
|
70
|
+
const OrganizationId = process.env.BOT_SCHEMA_ORGANIZATION_ID;
|
|
71
|
+
|
|
72
|
+
const input = { userInput: UserQuestion, conversationID: ConversationId, organizationID: OrganizationId };
|
|
73
|
+
//const url = 'https://tasioskipapi.azurewebsites.net/report';
|
|
74
|
+
const url = process.env.BOT_EXTERNAL_API_URL;
|
|
75
|
+
|
|
76
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
77
|
+
message: JSON.stringify({
|
|
78
|
+
type: 'AskSkip',
|
|
79
|
+
status: 'OK',
|
|
80
|
+
message: 'Sure, I can help with that, just give me a second and I will think about the best way to complete your request.',
|
|
81
|
+
}),
|
|
82
|
+
sessionId: userPayload.sessionId,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const response = await axios({
|
|
86
|
+
method: 'post',
|
|
87
|
+
url: process.env.BOT_EXTERNAL_API_URL,
|
|
88
|
+
data: input,
|
|
89
|
+
});
|
|
90
|
+
if (response.status === 200) {
|
|
91
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
92
|
+
message: JSON.stringify({
|
|
93
|
+
type: 'AskSkip',
|
|
94
|
+
status: 'OK',
|
|
95
|
+
message: 'I created the report structure, now I will get the data for you and analyze the results... Back to ya soon!',
|
|
96
|
+
}),
|
|
97
|
+
sessionId: userPayload.sessionId,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// it worked, run the SQL and return the results
|
|
101
|
+
const sql = response.data.params.sql;
|
|
102
|
+
const [{ result, analysis }, explanation] = await Promise.all([
|
|
103
|
+
this.getSkipDataAndAnalysis(dataSource, UserQuestion, sql),
|
|
104
|
+
this.getReportExplanation(UserQuestion, sql),
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const sTitle = response.data.params.reportTitle || response.data.params.chartTitle;
|
|
108
|
+
const sResult = JSON.stringify({
|
|
109
|
+
SQLResults: {
|
|
110
|
+
results: result,
|
|
111
|
+
sql: sql,
|
|
112
|
+
columns: response.data.params.columns,
|
|
113
|
+
},
|
|
114
|
+
UserMessage: '',
|
|
115
|
+
ReportExplanation: explanation,
|
|
116
|
+
Analysis: analysis,
|
|
117
|
+
ReportTitle: sTitle,
|
|
118
|
+
DisplayType: response.data.type,
|
|
119
|
+
DrillDownView: response.data.params.drillDownView,
|
|
120
|
+
DrillDownBaseViewField: response.data.params.drillDownBaseViewField,
|
|
121
|
+
DrillDownReportValueField: response.data.params.drillDownReportValueField,
|
|
122
|
+
ChartOptions: {
|
|
123
|
+
xAxis: response.data.params.xAxis,
|
|
124
|
+
xLabel: response.data.params.xLabel,
|
|
125
|
+
yAxis: response.data.params.yAxis,
|
|
126
|
+
yLabel: response.data.params.yLabel,
|
|
127
|
+
color: response.data.params.color,
|
|
128
|
+
yFormat: response.data.params.yFormat,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// now, create a conversation detail record for the Skip response
|
|
133
|
+
const convoDetailEntityAI = await md.GetEntityObject('Conversation Details', user);
|
|
134
|
+
convoDetailEntityAI.NewRecord();
|
|
135
|
+
convoDetailEntityAI.ConversationID = ConversationId;
|
|
136
|
+
convoDetailEntityAI.Message = sResult;
|
|
137
|
+
convoDetailEntityAI.Role = 'AI';
|
|
138
|
+
convoDetailEntityAI.Set('Sequence', 2); // using weakly typed here because we're going to get rid of this field soon
|
|
139
|
+
await convoDetailEntityAI.Save();
|
|
140
|
+
|
|
141
|
+
// finally update the convo name if it is still the default
|
|
142
|
+
if (convoEntity.Name === AskSkipResolver._defaultNewChatName) {
|
|
143
|
+
convoEntity.Name = response.data.params.reportTitle || response.data.params.chartTitle || AskSkipResolver._defaultNewChatName;
|
|
144
|
+
await convoEntity.Save();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// now create a notification for the user
|
|
148
|
+
const userNotification = await md.GetEntityObject('User Notifications', user);
|
|
149
|
+
userNotification.NewRecord();
|
|
150
|
+
userNotification.UserID = user.ID;
|
|
151
|
+
userNotification.Title = 'Report Created: ' + sTitle;
|
|
152
|
+
userNotification.Message = `Good news! Skip finished creating a report for you, click on this notification to jump back into the conversation.`;
|
|
153
|
+
userNotification.Unread = true;
|
|
154
|
+
userNotification.ResourceConfiguration = JSON.stringify({
|
|
155
|
+
type: 'askskip',
|
|
156
|
+
conversationId: ConversationId,
|
|
157
|
+
});
|
|
158
|
+
await userNotification.Save();
|
|
159
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
160
|
+
message: JSON.stringify({
|
|
161
|
+
type: 'UserNotifications',
|
|
162
|
+
status: 'OK',
|
|
163
|
+
details: {
|
|
164
|
+
action: 'create',
|
|
165
|
+
recordId: userNotification.ID,
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
sessionId: userPayload.sessionId,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
Success: true,
|
|
173
|
+
Status: 'OK',
|
|
174
|
+
ConversationId: ConversationId,
|
|
175
|
+
UserMessageConversationDetailId: convoDetailEntity.ID,
|
|
176
|
+
AIMessageConversationDetailId: convoDetailEntityAI.ID,
|
|
177
|
+
Result: sResult,
|
|
178
|
+
};
|
|
179
|
+
} else return { Success: false, Status: 'Error', Result: `User Question ${UserQuestion} didn't work!` };
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(`Error occurred: ${error}`);
|
|
182
|
+
if (error.response) {
|
|
183
|
+
// The request was made and the server responded with a status code
|
|
184
|
+
// that falls out of the range of 2xx
|
|
185
|
+
console.log(error.response.data);
|
|
186
|
+
console.log(error.response.status);
|
|
187
|
+
console.log(error.response.headers);
|
|
188
|
+
} else if (error.request) {
|
|
189
|
+
// The request was made but no response was received
|
|
190
|
+
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
|
191
|
+
// http.ClientRequest in node.js
|
|
192
|
+
console.log(error.request);
|
|
193
|
+
} else {
|
|
194
|
+
// Something happened in setting up the request that triggered an Error
|
|
195
|
+
console.log('Error', error.message);
|
|
196
|
+
}
|
|
197
|
+
console.log(error.config);
|
|
198
|
+
return {
|
|
199
|
+
Success: false,
|
|
200
|
+
Status: 'Error',
|
|
201
|
+
Result: `User Question ${UserQuestion} didn't work!`,
|
|
202
|
+
ConversationId: ConversationId,
|
|
203
|
+
UserMessageConversationDetailId: 0,
|
|
204
|
+
AIMessageConversationDetailId: 0,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
protected async getSkipDataAndAnalysis(dataSource: any, userQuestion: string, sql: string): Promise<{ result: any[]; analysis: string }> {
|
|
210
|
+
const result = await dataSource.query(sql);
|
|
211
|
+
const stringResult = JSON.stringify(result);
|
|
212
|
+
// next get average string length of each row in result
|
|
213
|
+
const maxStringLength = 2000;
|
|
214
|
+
const avgRowStringLength = stringResult.length / result.length;
|
|
215
|
+
// next get the subset we actually want to use, either entire result
|
|
216
|
+
// of if too long, then a subset of the rows from the result
|
|
217
|
+
let sampleDataJSON = stringResult;
|
|
218
|
+
if (stringResult.length > maxStringLength) {
|
|
219
|
+
const rowsToUse = result.length / (maxStringLength / avgRowStringLength);
|
|
220
|
+
const subsetResult = result.slice(0, rowsToUse);
|
|
221
|
+
sampleDataJSON = JSON.stringify(subsetResult);
|
|
222
|
+
}
|
|
223
|
+
// now get the analysis since we have the sample data
|
|
224
|
+
const analysis = await this.getAnalysis(userQuestion, sql, sampleDataJSON);
|
|
225
|
+
return { result, analysis };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
protected async getAnalysis(userQuestion: string, sql: string, sampleDataJSON: string): Promise<string> {
|
|
229
|
+
return await SkipAnalyzeData(userQuestion, sql, sampleDataJSON);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
protected async getReportExplanation(userQuestion: string, sql: string): Promise<string> {
|
|
233
|
+
return await SkipExplainQuery(userQuestion, sql);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default AskSkipResolver;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppContext,
|
|
3
|
+
Ctx,
|
|
4
|
+
Field,
|
|
5
|
+
Int,
|
|
6
|
+
ObjectType,
|
|
7
|
+
PubSub,
|
|
8
|
+
PubSubEngine,
|
|
9
|
+
Public,
|
|
10
|
+
Query,
|
|
11
|
+
Resolver,
|
|
12
|
+
Root,
|
|
13
|
+
Subscription,
|
|
14
|
+
} from '@memberjunction/server';
|
|
15
|
+
|
|
16
|
+
@ObjectType()
|
|
17
|
+
export class Color {
|
|
18
|
+
@Field(() => Int)
|
|
19
|
+
@Public()
|
|
20
|
+
ID: number;
|
|
21
|
+
|
|
22
|
+
@Field(() => String)
|
|
23
|
+
@Public()
|
|
24
|
+
name: string;
|
|
25
|
+
|
|
26
|
+
@Field(() => String)
|
|
27
|
+
@Public()
|
|
28
|
+
createdZ: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@ObjectType()
|
|
32
|
+
export class ColorNotification {
|
|
33
|
+
@Public()
|
|
34
|
+
@Field(() => String, { nullable: true })
|
|
35
|
+
message?: string;
|
|
36
|
+
|
|
37
|
+
@Public()
|
|
38
|
+
@Field((_type) => Date)
|
|
39
|
+
date!: Date;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ColorNotificationPayload {
|
|
43
|
+
message?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Resolver(Color)
|
|
47
|
+
export class ColorResolver {
|
|
48
|
+
@Subscription(() => ColorNotification, { topics: 'COLOR' })
|
|
49
|
+
@Public()
|
|
50
|
+
colorSubscription(@Root() { message }: ColorNotificationPayload): ColorNotification {
|
|
51
|
+
return { message, date: new Date() };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Query(() => [Color])
|
|
55
|
+
@Public()
|
|
56
|
+
async colors(@Ctx() _ctx: AppContext, @PubSub() pubSub: PubSubEngine) {
|
|
57
|
+
const createdZ = new Date().toISOString();
|
|
58
|
+
|
|
59
|
+
pubSub.publish('COLOR', {
|
|
60
|
+
message: 'Colors were requested!',
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
{ ID: 1, name: 'Red', createdZ },
|
|
65
|
+
{ ID: 2, name: 'Orange', createdZ },
|
|
66
|
+
{ ID: 3, name: 'Yellow', createdZ },
|
|
67
|
+
{ ID: 4, name: 'Green', createdZ },
|
|
68
|
+
{ ID: 5, name: 'Blue', createdZ },
|
|
69
|
+
{ ID: 6, name: 'Purple', createdZ },
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Arg, Ctx, Field, InputType, Int, ObjectType, Query, Resolver } from 'type-graphql';
|
|
2
|
+
import { AppContext } from '../types';
|
|
3
|
+
import { LogError, Metadata } from '@memberjunction/core';
|
|
4
|
+
|
|
5
|
+
@ObjectType()
|
|
6
|
+
export class DatasetResultType {
|
|
7
|
+
@Field(() => Int)
|
|
8
|
+
DatasetID: number;
|
|
9
|
+
|
|
10
|
+
@Field(() => String)
|
|
11
|
+
DatasetName: string;
|
|
12
|
+
|
|
13
|
+
@Field(() => Boolean)
|
|
14
|
+
Success: boolean;
|
|
15
|
+
|
|
16
|
+
@Field(() => String)
|
|
17
|
+
Status: string;
|
|
18
|
+
|
|
19
|
+
@Field(() => Date)
|
|
20
|
+
LatestUpdateDate: Date;
|
|
21
|
+
|
|
22
|
+
@Field(() => String)
|
|
23
|
+
Results: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@InputType()
|
|
27
|
+
export class DatasetItemFilterTypeGQL {
|
|
28
|
+
@Field(() => String)
|
|
29
|
+
ItemCode: string;
|
|
30
|
+
|
|
31
|
+
@Field(() => String)
|
|
32
|
+
Filter: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Resolver(DatasetResultType)
|
|
36
|
+
export class DatasetResolver {
|
|
37
|
+
@Query(() => DatasetResultType)
|
|
38
|
+
async GetDatasetByName(
|
|
39
|
+
@Arg('DatasetName', () => String) DatasetName: string,
|
|
40
|
+
@Ctx() {}: AppContext,
|
|
41
|
+
@Arg('ItemFilters', () => [DatasetItemFilterTypeGQL], { nullable: 'itemsAndList' }) ItemFilters?: DatasetItemFilterTypeGQL[]
|
|
42
|
+
) {
|
|
43
|
+
try {
|
|
44
|
+
const md = new Metadata();
|
|
45
|
+
const result = await md.GetDatasetByName(DatasetName, ItemFilters);
|
|
46
|
+
if (result) {
|
|
47
|
+
return {
|
|
48
|
+
DatasetID: result.DatasetID,
|
|
49
|
+
DatasetName: result.DatasetName,
|
|
50
|
+
Success: result.Success,
|
|
51
|
+
Status: result.Status,
|
|
52
|
+
LatestUpdateDate: result.LatestUpdateDate,
|
|
53
|
+
Results: JSON.stringify(result.Results),
|
|
54
|
+
};
|
|
55
|
+
} else {
|
|
56
|
+
throw new Error('Error retrieving Dataset: ' + DatasetName);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
LogError(err);
|
|
60
|
+
throw new Error('Error retrieving Dataset: ' + DatasetName + '\n\n' + err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@ObjectType()
|
|
66
|
+
export class DatasetStatusResultType {
|
|
67
|
+
@Field(() => Int)
|
|
68
|
+
DatasetID: number;
|
|
69
|
+
|
|
70
|
+
@Field(() => String)
|
|
71
|
+
DatasetName: string;
|
|
72
|
+
|
|
73
|
+
@Field(() => Boolean)
|
|
74
|
+
Success: boolean;
|
|
75
|
+
|
|
76
|
+
@Field(() => String)
|
|
77
|
+
Status: string;
|
|
78
|
+
|
|
79
|
+
@Field(() => Date)
|
|
80
|
+
LatestUpdateDate: Date;
|
|
81
|
+
|
|
82
|
+
@Field(() => String)
|
|
83
|
+
EntityUpdateDates: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Resolver(DatasetStatusResultType)
|
|
87
|
+
export class DatasetStatusResolver {
|
|
88
|
+
@Query(() => DatasetStatusResultType)
|
|
89
|
+
async GetDatasetStatusByName(
|
|
90
|
+
@Arg('DatasetName', () => String) DatasetName: string,
|
|
91
|
+
@Ctx() {}: AppContext,
|
|
92
|
+
@Arg('ItemFilters', () => [DatasetItemFilterTypeGQL], { nullable: 'itemsAndList' }) ItemFilters?: DatasetItemFilterTypeGQL[]
|
|
93
|
+
) {
|
|
94
|
+
try {
|
|
95
|
+
const md = new Metadata();
|
|
96
|
+
const result = await md.GetDatasetStatusByName(DatasetName, ItemFilters);
|
|
97
|
+
if (result) {
|
|
98
|
+
return {
|
|
99
|
+
DatasetID: result.DatasetID,
|
|
100
|
+
DatasetName: result.DatasetName,
|
|
101
|
+
Success: result.Success,
|
|
102
|
+
Status: result.Status,
|
|
103
|
+
LatestUpdateDate: result.LatestUpdateDate,
|
|
104
|
+
EntityUpdateDates: JSON.stringify(result.EntityUpdateDates),
|
|
105
|
+
};
|
|
106
|
+
} else {
|
|
107
|
+
throw new Error('Error retrieving Dataset Status: ' + DatasetName);
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
LogError(err);
|
|
111
|
+
throw new Error('Error retrieving Dataset Status: ' + DatasetName + '\n\n' + err);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Metadata } from '@memberjunction/core';
|
|
2
|
+
import { Arg, Ctx, Field, InputType, ObjectType, Query, Resolver } from 'type-graphql';
|
|
3
|
+
import { AppContext } from '../types';
|
|
4
|
+
|
|
5
|
+
@InputType()
|
|
6
|
+
export class EntityRecordNameInput {
|
|
7
|
+
@Field(() => String)
|
|
8
|
+
EntityName: string;
|
|
9
|
+
|
|
10
|
+
@Field(() => String)
|
|
11
|
+
RecordID: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@ObjectType()
|
|
15
|
+
export class EntityRecordNameResult {
|
|
16
|
+
@Field(() => Boolean)
|
|
17
|
+
Success: boolean;
|
|
18
|
+
|
|
19
|
+
@Field(() => String)
|
|
20
|
+
Status: string;
|
|
21
|
+
|
|
22
|
+
@Field(() => String)
|
|
23
|
+
RecordID: string;
|
|
24
|
+
|
|
25
|
+
@Field(() => String)
|
|
26
|
+
EntityName: string;
|
|
27
|
+
|
|
28
|
+
@Field(() => String, { nullable: true })
|
|
29
|
+
RecordName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Resolver(EntityRecordNameResult)
|
|
33
|
+
export class EntityRecordNameResolver {
|
|
34
|
+
@Query(() => EntityRecordNameResult)
|
|
35
|
+
async GetEntityRecordName(
|
|
36
|
+
@Arg('EntityName', () => String) EntityName: string,
|
|
37
|
+
@Arg('RecordID', () => String) RecordID: string,
|
|
38
|
+
@Ctx() {}: AppContext
|
|
39
|
+
): Promise<EntityRecordNameResult> {
|
|
40
|
+
const md = new Metadata();
|
|
41
|
+
return await this.InnerGetEntityRecordName(md, EntityName, RecordID);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Query(() => [EntityRecordNameResult])
|
|
45
|
+
async GetEntityRecordNames(
|
|
46
|
+
@Arg('info', () => [EntityRecordNameInput]) info: EntityRecordNameInput[],
|
|
47
|
+
@Ctx() {}: AppContext
|
|
48
|
+
): Promise<EntityRecordNameResult[]> {
|
|
49
|
+
const result: EntityRecordNameResult[] = [];
|
|
50
|
+
const md = new Metadata();
|
|
51
|
+
for (const i of info) {
|
|
52
|
+
result.push(await this.InnerGetEntityRecordName(md, i.EntityName, i.RecordID));
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async InnerGetEntityRecordName(md: Metadata, EntityName: string, RecordID: string): Promise<EntityRecordNameResult> {
|
|
58
|
+
const e = md.Entities.find((e) => e.Name === EntityName);
|
|
59
|
+
if (e) {
|
|
60
|
+
const recordName = await md.GetEntityRecordName(e.Name, RecordID);
|
|
61
|
+
if (recordName) return { Success: true, Status: 'OK', RecordID: RecordID, RecordName: recordName, EntityName: EntityName };
|
|
62
|
+
else
|
|
63
|
+
return {
|
|
64
|
+
Success: false,
|
|
65
|
+
Status: `Name for record, or record ${RecordID} itself not found, could be an access issue if user doesn't have Row Level Access (RLS) if RLS is enabled for this entity`,
|
|
66
|
+
RecordID: RecordID,
|
|
67
|
+
EntityName: EntityName,
|
|
68
|
+
};
|
|
69
|
+
} else return { Success: false, Status: `Entity ${EntityName} not found`, RecordID: RecordID, EntityName: EntityName };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default EntityRecordNameResolver;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EntityPermissionType } from '@memberjunction/core';
|
|
2
|
+
import { AppContext, Arg, Ctx, Query, Resolver } from '@memberjunction/server';
|
|
3
|
+
import { Entity_, EntityResolverBase } from '../generated/generated';
|
|
4
|
+
|
|
5
|
+
@Resolver(Entity_)
|
|
6
|
+
export class EntityResolver extends EntityResolverBase {
|
|
7
|
+
@Query(() => [Entity_])
|
|
8
|
+
EntitiesBySchemas(
|
|
9
|
+
@Ctx() { dataSource, userPayload }: AppContext,
|
|
10
|
+
@Arg('IncludeSchemas', () => [String], { nullable: true }) IncludeSchemas?: string[],
|
|
11
|
+
@Arg('ExcludeSchemas', () => [String], { nullable: true }) ExcludeSchemas?: string[]
|
|
12
|
+
) {
|
|
13
|
+
this.CheckUserReadPermissions('Entities', userPayload);
|
|
14
|
+
const rlsWhere = this.getRowLevelSecurityWhereClause('Entities', userPayload, EntityPermissionType.Read, ' WHERE');
|
|
15
|
+
const includeSchemaSQL =
|
|
16
|
+
IncludeSchemas && IncludeSchemas.length > 0 ? `SchemaName IN (${IncludeSchemas.map((s) => `'${s}'`).join(',')})` : '';
|
|
17
|
+
const excludeSchemaSQL =
|
|
18
|
+
ExcludeSchemas && ExcludeSchemas.length > 0 ? `SchemaName NOT IN (${ExcludeSchemas.map((s) => `'${s}'`).join(',')})` : '';
|
|
19
|
+
let schemaSQL = '';
|
|
20
|
+
if (includeSchemaSQL) schemaSQL = includeSchemaSQL;
|
|
21
|
+
if (excludeSchemaSQL) {
|
|
22
|
+
if (schemaSQL) schemaSQL = `${schemaSQL} AND ${excludeSchemaSQL}`;
|
|
23
|
+
else schemaSQL = excludeSchemaSQL;
|
|
24
|
+
}
|
|
25
|
+
let totalWhere = '';
|
|
26
|
+
if (schemaSQL) totalWhere = ` WHERE ${schemaSQL}`;
|
|
27
|
+
if (rlsWhere) {
|
|
28
|
+
if (totalWhere) totalWhere = `${totalWhere} AND ${rlsWhere}`;
|
|
29
|
+
else totalWhere = ` WHERE ${rlsWhere}`;
|
|
30
|
+
}
|
|
31
|
+
const sSQL = `SELECT * FROM [${this.MJCoreSchema}].vwEntities${totalWhere}`;
|
|
32
|
+
return dataSource.query(sSQL);
|
|
33
|
+
}
|
|
34
|
+
}
|