@memberjunction/server 0.9.93 → 0.9.95

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 (47) hide show
  1. package/build.log.json +6 -0
  2. package/dist/generated/generated.js +18854 -0
  3. package/dist/generated/generated.js.map +1 -0
  4. package/dist/generic/PushStatusResolver.js +59 -0
  5. package/dist/generic/PushStatusResolver.js.map +1 -0
  6. package/dist/generic/ResolverBase.js +208 -0
  7. package/dist/generic/ResolverBase.js.map +1 -0
  8. package/dist/generic/RunViewResolver.js +401 -0
  9. package/dist/generic/RunViewResolver.js.map +1 -0
  10. package/dist/index.js +20 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/resolvers/AskSkipResolver.js +247 -0
  13. package/dist/resolvers/AskSkipResolver.js.map +1 -0
  14. package/dist/resolvers/ColorResolver.js +94 -0
  15. package/dist/resolvers/ColorResolver.js.map +1 -0
  16. package/dist/resolvers/DatasetResolver.js +168 -0
  17. package/dist/resolvers/DatasetResolver.js.map +1 -0
  18. package/dist/resolvers/EntityRecordNameResolver.js +111 -0
  19. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -0
  20. package/dist/resolvers/EntityResolver.js +60 -0
  21. package/dist/resolvers/EntityResolver.js.map +1 -0
  22. package/dist/resolvers/MergeRecordsResolver.js +256 -0
  23. package/dist/resolvers/MergeRecordsResolver.js.map +1 -0
  24. package/dist/resolvers/ReportResolver.js +74 -0
  25. package/dist/resolvers/ReportResolver.js.map +1 -0
  26. package/dist/resolvers/UserFavoriteResolver.js +185 -0
  27. package/dist/resolvers/UserFavoriteResolver.js.map +1 -0
  28. package/dist/resolvers/UserResolver.js +70 -0
  29. package/dist/resolvers/UserResolver.js.map +1 -0
  30. package/dist/resolvers/UserViewResolver.js +102 -0
  31. package/dist/resolvers/UserViewResolver.js.map +1 -0
  32. package/package.json +6 -6
  33. package/src/generated/generated.ts +13928 -0
  34. package/src/generic/PushStatusResolver.ts +41 -0
  35. package/src/generic/ResolverBase.ts +285 -0
  36. package/src/generic/RunViewResolver.ts +350 -0
  37. package/src/index.ts +31 -3
  38. package/src/resolvers/AskSkipResolver.ts +237 -0
  39. package/src/resolvers/ColorResolver.ts +72 -0
  40. package/src/resolvers/DatasetResolver.ts +114 -0
  41. package/src/resolvers/EntityRecordNameResolver.ts +73 -0
  42. package/src/resolvers/EntityResolver.ts +34 -0
  43. package/src/resolvers/MergeRecordsResolver.ts +178 -0
  44. package/src/resolvers/ReportResolver.ts +56 -0
  45. package/src/resolvers/UserFavoriteResolver.ts +123 -0
  46. package/src/resolvers/UserResolver.ts +29 -0
  47. 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 = resolverPaths.flatMap((path) => globSync(replaceBackslashes ? path.replace(/\\/g, '/') : path));
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 ${resolverPaths.join(', ')}`);
42
- console.log({ resolverPaths, paths, cwd: process.cwd() });
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
+ }