@memberjunction/server 2.45.0 → 2.47.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/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
- package/dist/generated/generated.d.ts +7 -2
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +56 -24
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +5 -2
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/package.json +24 -23
- package/src/entitySubclasses/entityPermissions.server.ts +3 -0
- package/src/generated/generated.ts +40 -18
- package/src/index.ts +3 -3
- package/src/resolvers/AskSkipResolver.ts +5 -2
- package/dist/entitySubclasses/DuplicateRunEntity.server.d.ts +0 -6
- package/dist/entitySubclasses/DuplicateRunEntity.server.d.ts.map +0 -1
- package/dist/entitySubclasses/DuplicateRunEntity.server.js +0 -32
- package/dist/entitySubclasses/DuplicateRunEntity.server.js.map +0 -1
- package/dist/entitySubclasses/reportEntity.server.d.ts +0 -6
- package/dist/entitySubclasses/reportEntity.server.d.ts.map +0 -1
- package/dist/entitySubclasses/reportEntity.server.js +0 -103
- package/dist/entitySubclasses/reportEntity.server.js.map +0 -1
- package/dist/entitySubclasses/userViewEntity.server.d.ts +0 -13
- package/dist/entitySubclasses/userViewEntity.server.d.ts.map +0 -1
- package/dist/entitySubclasses/userViewEntity.server.js +0 -205
- package/dist/entitySubclasses/userViewEntity.server.js.map +0 -1
- package/src/entitySubclasses/DuplicateRunEntity.server.ts +0 -27
- package/src/entitySubclasses/reportEntity.server.ts +0 -130
- package/src/entitySubclasses/userViewEntity.server.ts +0 -231
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { BaseEntity, Metadata, RunView } from "@memberjunction/core";
|
|
2
|
-
import { RegisterClass, SafeJSONParse } from "@memberjunction/global";
|
|
3
|
-
import { ReportEntity, ReportSnapshotEntity, ReportVersionEntity } from "@memberjunction/core-entities";
|
|
4
|
-
import { SkipAPIAnalysisCompleteResponse } from "@memberjunction/skip-types";
|
|
5
|
-
|
|
6
|
-
@RegisterClass(BaseEntity, 'Reports')
|
|
7
|
-
export class ReportEntity_Server extends ReportEntity {
|
|
8
|
-
/**
|
|
9
|
-
* The server side Report Entity sub-class has a simple logic that will create a snapshot of the report when it is first created and each time it is modified, but only
|
|
10
|
-
* if it is either newly created or if the Configuration field has changed.
|
|
11
|
-
* @returns
|
|
12
|
-
*/
|
|
13
|
-
public async Save(): Promise<boolean> {
|
|
14
|
-
try {
|
|
15
|
-
// first let's get the actual parsed object for the Configuration field
|
|
16
|
-
const config = this.Configuration && this.Configuration.length > 0 ? SafeJSONParse(this.Configuration) : null;
|
|
17
|
-
const oldTextConfig = this.GetFieldByName('Configuration')?.OldValue;
|
|
18
|
-
const oldConfig = oldTextConfig && oldTextConfig.length > 0 ? SafeJSONParse(oldTextConfig) : null;
|
|
19
|
-
|
|
20
|
-
if ( (config && oldConfig) || (config && !this.IsSaved) ) {
|
|
21
|
-
let createSnapshot = false;
|
|
22
|
-
let createReportVersion = false;
|
|
23
|
-
if (this.IsSaved) {
|
|
24
|
-
// existing record
|
|
25
|
-
// we nave a configuration and an old configuration, so we can compare them, cast them to their strong types first
|
|
26
|
-
const castedConfig = config as SkipAPIAnalysisCompleteResponse;
|
|
27
|
-
const castedOldConfig = oldConfig as SkipAPIAnalysisCompleteResponse;
|
|
28
|
-
|
|
29
|
-
// Next, we need to see if the data context has changed at all. We do this by comparing the # of items in the data context. a Data context's items are immutable and we only add new Data Context Items
|
|
30
|
-
// so we can simply check to see if we have new data context items or not
|
|
31
|
-
const dataContextChanged = Object.keys(castedConfig.dataContext || {})?.length !== Object.keys(castedOldConfig.dataContext || {})?.length;
|
|
32
|
-
|
|
33
|
-
// next up, we check to see if anything in the execution results changed. The simple way to do that is convert the execution results back to a string and compare those strings
|
|
34
|
-
const executionResultsChanged = JSON.stringify(castedConfig.executionResults) !== JSON.stringify(castedOldConfig.executionResults);
|
|
35
|
-
|
|
36
|
-
// next up we check to see if the analysis changed, which would also represent a "data" change
|
|
37
|
-
const analysisChanged = castedConfig.analysis !== castedOldConfig.analysis;
|
|
38
|
-
|
|
39
|
-
// finally, we check to see if the configuration itself has OUTSIDE of the data elements we just checked. We do that by deleting those properties from those objects, converting to strings
|
|
40
|
-
// and comparing them
|
|
41
|
-
delete castedConfig.dataContext;
|
|
42
|
-
delete castedConfig.analysis;
|
|
43
|
-
delete castedConfig.executionResults;
|
|
44
|
-
|
|
45
|
-
delete castedOldConfig.dataContext;
|
|
46
|
-
delete castedOldConfig.executionResults;
|
|
47
|
-
delete castedOldConfig.analysis;
|
|
48
|
-
|
|
49
|
-
// we can now compare the two objects
|
|
50
|
-
const configChanged = JSON.stringify(castedConfig) !== JSON.stringify(castedOldConfig);
|
|
51
|
-
|
|
52
|
-
// now our logic is this - if the DATA changed, but the configuration did NOT change, that means we want to create a new SNAPSHOT of the report - which is the data changing side
|
|
53
|
-
// if the configuration changed at all we wnat to create a new Report Version. Sometimes both changd and we do both
|
|
54
|
-
createSnapshot = dataContextChanged || executionResultsChanged || analysisChanged;
|
|
55
|
-
createReportVersion = configChanged;
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
// we have a new record set createSnapshot to true and createReportVersion to true
|
|
59
|
-
createSnapshot = true;
|
|
60
|
-
createReportVersion = true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const wasNewRecord: boolean = !this.IsSaved;
|
|
64
|
-
const saveResult: boolean = await super.Save();
|
|
65
|
-
if (saveResult && (createSnapshot || createReportVersion)) {
|
|
66
|
-
// here we either have a new record or the configuration has changed, so we need to create a snapshot of the report
|
|
67
|
-
let success: boolean = true;
|
|
68
|
-
const md = new Metadata();
|
|
69
|
-
|
|
70
|
-
if (createReportVersion) {
|
|
71
|
-
const reportVersion = await md.GetEntityObject<ReportVersionEntity>('MJ: Report Versions', this.ContextCurrentUser);
|
|
72
|
-
reportVersion.ReportID = this.ID;
|
|
73
|
-
// const get highest new version number already in DB
|
|
74
|
-
let newVersionNumber: number = 1;
|
|
75
|
-
if (!wasNewRecord) {
|
|
76
|
-
const rv = new RunView();
|
|
77
|
-
const rvResult = await rv.RunView({
|
|
78
|
-
Fields: ['VersionNumber'],
|
|
79
|
-
EntityName: 'MJ: Report Versions',
|
|
80
|
-
ExtraFilter: `ReportID = '${this.ID}'`,
|
|
81
|
-
MaxRows: 1,
|
|
82
|
-
OrderBy: 'VersionNumber DESC',
|
|
83
|
-
}, this.ContextCurrentUser);
|
|
84
|
-
if (rvResult.Success && rvResult.Results.length > 0) {
|
|
85
|
-
newVersionNumber = parseInt(rvResult.Results[0].VersionNumber) + 1;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
newVersionNumber = 1; // this is an error state as we previously had a saved report, BUT there were no saved versions for the report, so we are going to use 1 here, but let's log a warning
|
|
89
|
-
console.warn('ReportEntity_Server.Save(): No report versions found for report ID:', this.ID);
|
|
90
|
-
console.warn('ReportEntity_Server.Save(): Using version number 1 for new report version');
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
reportVersion.Name = this.Name; // copy current name to the new version
|
|
94
|
-
reportVersion.Description = this.Description; // copy current description to the new version
|
|
95
|
-
reportVersion.VersionNumber = newVersionNumber;
|
|
96
|
-
reportVersion.Configuration = JSON.stringify(this.Configuration);
|
|
97
|
-
success = success && await reportVersion.Save();
|
|
98
|
-
if (!success) {
|
|
99
|
-
console.error('ReportEntity_Server.Save(): Error saving report version\n' + reportVersion.LatestResult.Message);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (createSnapshot) {
|
|
104
|
-
const snapshot = await md.GetEntityObject<ReportSnapshotEntity>('Report Snapshots', this.ContextCurrentUser);
|
|
105
|
-
snapshot.ReportID = this.ID;
|
|
106
|
-
snapshot.UserID = this.ContextCurrentUser.ID;
|
|
107
|
-
// in the snapshot entity the ResultSet column is the Configuration column from the Report entity
|
|
108
|
-
snapshot.ResultSet = JSON.stringify(this.Configuration);
|
|
109
|
-
success = success && await snapshot.Save();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return success;
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
return saveResult;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
// no configuration or no parseable configuration, so we just call super.Save();
|
|
120
|
-
return super.Save();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
catch (e) {
|
|
124
|
-
console.error('Error in ReportEntity_Server.Save():', e);
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function LoadReportEntityServerSubClass() {}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import { CleanJSON, MJGlobal, RegisterClass } from "@memberjunction/global";
|
|
2
|
-
import { BaseEntity, EntityInfo, LogError, Metadata } from "@memberjunction/core";
|
|
3
|
-
import { AIModelEntity, AIModelEntityExtended, UserViewEntityExtended } from '@memberjunction/core-entities'
|
|
4
|
-
import { BaseLLM, ChatParams, GetAIAPIKey } from "@memberjunction/ai";
|
|
5
|
-
import { AIEngine } from "@memberjunction/aiengine";
|
|
6
|
-
import { LoadOpenAILLM } from "@memberjunction/ai-openai";
|
|
7
|
-
LoadOpenAILLM(); // this is to prevent tree shaking since the openai package is not directly used and rather instantiated dynamically in the LoadOpenAILLM function. Since no static code path exists tree shaking can result in this class being optimized out
|
|
8
|
-
|
|
9
|
-
@RegisterClass(BaseEntity, 'User Views')
|
|
10
|
-
export class UserViewEntity_Server extends UserViewEntityExtended {
|
|
11
|
-
/**
|
|
12
|
-
* This property is hard-coded to true in this class because we DO support smart filters in this class. If you want to disable smart filters for a specific view you can override this property in your subclass and set it to false.
|
|
13
|
-
*/
|
|
14
|
-
protected override get SmartFilterImplemented(): boolean {
|
|
15
|
-
return true;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Default implementation simply returns 'OpenAI' - override this in your subclass if you are using a different AI vendor.
|
|
20
|
-
* @returns
|
|
21
|
-
*/
|
|
22
|
-
protected get AIVendorName(): string {
|
|
23
|
-
return 'OpenAI';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Default implementation simply grabs the first AI model that matches GetAIModelName().
|
|
28
|
-
* @returns
|
|
29
|
-
*/
|
|
30
|
-
protected async GetAIModel(): Promise<AIModelEntityExtended> {
|
|
31
|
-
await AIEngine.Instance.Config(false, this.ContextCurrentUser); // most of the time this is already loaded, but just in case it isn't we will load it here
|
|
32
|
-
const models = AIEngine.Instance.Models.filter(m => m.AIModelType.trim().toLowerCase() === 'llm' &&
|
|
33
|
-
m.Vendor.trim().toLowerCase() === this.AIVendorName.trim().toLowerCase())
|
|
34
|
-
// next, sort the models by the PowerRank field so that the highest power rank model is the first array element
|
|
35
|
-
models.sort((a, b) => b.PowerRank - a.PowerRank); // highest power rank first
|
|
36
|
-
return models[0];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* This method will use AI to return a valid WHERE clause based on the provided prompt. This is automatically called at the right time if the view has SmartFilterEnabled turned on and the SmartFilterPrompt is set. If you want
|
|
41
|
-
* to call this directly to get back a WHERE clause for other purposes you can call this method directly and provide both a prompt and the entity that the view is based on.
|
|
42
|
-
* @param prompt
|
|
43
|
-
*/
|
|
44
|
-
public async GenerateSmartFilterWhereClause(prompt: string, entityInfo: EntityInfo): Promise<{whereClause: string, userExplanation: string}> {
|
|
45
|
-
try {
|
|
46
|
-
const model = await this.GetAIModel();
|
|
47
|
-
const llm = MJGlobal.Instance.ClassFactory.CreateInstance<BaseLLM>(BaseLLM, model.DriverClass, GetAIAPIKey(model.DriverClass));
|
|
48
|
-
|
|
49
|
-
const chatParams: ChatParams = {
|
|
50
|
-
model: model.APINameOrName,
|
|
51
|
-
messages: [
|
|
52
|
-
{
|
|
53
|
-
role: 'system',
|
|
54
|
-
content: this.GenerateSysPrompt(entityInfo)
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
role: 'user',
|
|
58
|
-
content: `${prompt}`,
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
}
|
|
62
|
-
const result = await llm.ChatCompletion(chatParams);
|
|
63
|
-
if (result && result.data) {
|
|
64
|
-
const llmResponse = result.data.choices[0].message.content;
|
|
65
|
-
if (llmResponse) {
|
|
66
|
-
// try to parse it as JSON
|
|
67
|
-
try {
|
|
68
|
-
const cleansed = CleanJSON(llmResponse);
|
|
69
|
-
if (!cleansed)
|
|
70
|
-
throw new Error('Invalid JSON response from AI: ' + llmResponse);
|
|
71
|
-
|
|
72
|
-
const parsed = JSON.parse(cleansed);
|
|
73
|
-
if (parsed.whereClause && parsed.whereClause.length > 0) {
|
|
74
|
-
// we have the where clause. Sometimes the LLM prefixes it with WHERE and somtimes not, we need to strip WHERE if it is there
|
|
75
|
-
const trimmed = parsed.whereClause.trim();
|
|
76
|
-
let ret: string = '';
|
|
77
|
-
if (trimmed.toLowerCase().startsWith('where '))
|
|
78
|
-
ret = trimmed.substring(6);
|
|
79
|
-
else
|
|
80
|
-
ret = parsed.whereClause;
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
whereClause: ret,
|
|
84
|
-
userExplanation: parsed.userExplanationMessage
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
else if (parsed.whereClause !== undefined && parsed.whereClause !== null) {
|
|
88
|
-
return {
|
|
89
|
-
whereClause: '', // empty string is valid, it means no where clause
|
|
90
|
-
userExplanation: parsed.userExplanationMessage
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
// if we get here, no whereClause property was provided by the LLM, that is an error
|
|
95
|
-
throw new Error('Invalid response from AI, no whereClause property found in response: ' + llmResponse);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch (e) {
|
|
99
|
-
LogError(e);
|
|
100
|
-
throw new Error('Error parsing JSON response from AI: ' + llmResponse);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
else
|
|
104
|
-
throw new Error('Null response from AI');
|
|
105
|
-
}
|
|
106
|
-
else
|
|
107
|
-
throw new Error('No result returned from AI');
|
|
108
|
-
}
|
|
109
|
-
catch (e) {
|
|
110
|
-
LogError(e);
|
|
111
|
-
throw e;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
public GenerateSysPrompt(entityInfo: EntityInfo): string {
|
|
116
|
-
const processedViews: string[] = [entityInfo.BaseView];
|
|
117
|
-
const md = new Metadata();
|
|
118
|
-
const listsEntity = md.EntityByName("Lists");
|
|
119
|
-
const listDetailsEntity = md.EntityByName("List Details");
|
|
120
|
-
const gptSysPrompt: string = `You are an expert in SQL and Microsoft SQL Server.
|
|
121
|
-
You will be provided a user prompt representing how they want to filter the data.
|
|
122
|
-
You may *NOT* use JOINS, only sub-queries for related tables.
|
|
123
|
-
|
|
124
|
-
I am a bot and can only understand JSON. Your response must be parsable into this type:
|
|
125
|
-
const returnType = {
|
|
126
|
-
whereClause: string,
|
|
127
|
-
orderByClause: string
|
|
128
|
-
userExplanationMessage: string
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
In MemberJunction, we have a concept called "Entities" and these are metadata constructs that wrap SQL tables and views. The entity that we are currently
|
|
132
|
-
building a filter for is called "${entityInfo.Name}" and has an ID of "${entityInfo.ID}"
|
|
133
|
-
|
|
134
|
-
You won't be using this Entity name or ID in your SQL, unless you need to use it for List filtering (more on that below).
|
|
135
|
-
|
|
136
|
-
The view that the user is querying is called ${entityInfo.BaseView} and has these fields:
|
|
137
|
-
${entityInfo.Fields.map(f => {
|
|
138
|
-
let ret: string = `${f.Name} (${f.Type})`;
|
|
139
|
-
if (f.RelatedEntity) {
|
|
140
|
-
ret += ` (fkey to ${f.RelatedEntityBaseView})`;
|
|
141
|
-
}
|
|
142
|
-
return ret;
|
|
143
|
-
}).join(',')}`
|
|
144
|
-
|
|
145
|
-
const fkeyFields = entityInfo.Fields.filter(f => f.RelatedEntity && f.RelatedEntity.length > 0);
|
|
146
|
-
const fkeyBaseViewsDistinct = fkeyFields.map(f => f.RelatedEntityBaseView).filter((v, i, a) => a.indexOf(v) === i);
|
|
147
|
-
const relationships: string = `
|
|
148
|
-
In addition, ${entityInfo.BaseView} has links to other views, as shown here, you can use these views in sub-queries to achieve the request from the user.
|
|
149
|
-
If there are multiple filters related to a single related view, attempt to combine them into a single sub-query for efficiency.
|
|
150
|
-
${
|
|
151
|
-
// this part returns a list of all the related views and the fields that are related to the current view via fkeys in the current view
|
|
152
|
-
fkeyBaseViewsDistinct.map(v => {
|
|
153
|
-
if (processedViews.indexOf(v) === -1) {
|
|
154
|
-
const e = md.Entities.find(e => e.BaseView === v);
|
|
155
|
-
if (e) {
|
|
156
|
-
processedViews.push(v); // already processed this view now, so we won't repeat it
|
|
157
|
-
return `* ${e.SchemaName}.${e.BaseView}: ${e.Fields.map(ef => {
|
|
158
|
-
return ef.Name + ' (' + ef.Type + ')';
|
|
159
|
-
}).join(',') }`
|
|
160
|
-
}
|
|
161
|
-
else
|
|
162
|
-
return '';
|
|
163
|
-
}
|
|
164
|
-
else
|
|
165
|
-
return ''; // already did this at some point
|
|
166
|
-
}).join('\n')
|
|
167
|
-
}
|
|
168
|
-
${
|
|
169
|
-
// this part returns a list of all the related views and the fields that are related to the current view fkeys in THOSE views
|
|
170
|
-
entityInfo.RelatedEntities.map(r => {
|
|
171
|
-
const e = md.Entities.find(e => e.Name === r.RelatedEntity);
|
|
172
|
-
if (e) {
|
|
173
|
-
if (processedViews.indexOf(e.BaseView) === -1) {
|
|
174
|
-
processedViews.push(e.BaseView); // note that we are processing this view now, so we won't repeat it
|
|
175
|
-
return `* ${e.SchemaName}.${e.BaseView}: ${e.Fields.map(ef => {
|
|
176
|
-
let ret: string = `${ef.Name} (${ef.Type})`;
|
|
177
|
-
if (ef.RelatedEntity) {
|
|
178
|
-
ret += ` (fkey to ${ef.RelatedEntityBaseView})`;
|
|
179
|
-
}
|
|
180
|
-
return ret;
|
|
181
|
-
}).join(',') }`
|
|
182
|
-
}
|
|
183
|
-
else
|
|
184
|
-
return ''; // already did this at some point
|
|
185
|
-
}
|
|
186
|
-
else
|
|
187
|
-
return '';
|
|
188
|
-
}).join('\n')
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
<IMPORTANT - LISTS FEATURE>
|
|
192
|
-
In addition to the above related views, a user may talk about "Lists" in their request. A List is a static set of records modeled in our database with the ${listsEntity.SchemaName}.vwLists view and
|
|
193
|
-
the ${listsEntity.SchemaName}.vwListDetails view. ${listsEntity.SchemaName}.vwLists contains the list "header" information with these columns:
|
|
194
|
-
${listsEntity.Fields.map(f => {
|
|
195
|
-
return f.Name + ' (' + f.SQLFullType + ')';
|
|
196
|
-
}).join(', ')}
|
|
197
|
-
The vwListDetails view contains the list "detail" information with these columns which is basically the records that are part of the list. ${listsEntity.SchemaName}.vwListDetails contains these columns:
|
|
198
|
-
${listDetailsEntity.Fields.map(f => {
|
|
199
|
-
return f.Name + ' (' + f.SQLFullType + ')';
|
|
200
|
-
}).join(', ')}.
|
|
201
|
-
|
|
202
|
-
If a user is asking to use a list in creating a view, you need to use a sub-query along these lines:
|
|
203
|
-
|
|
204
|
-
ID IN (SELECT RecordID FROM ${listsEntity.SchemaName}.vwListDetails WHERE ListID='My List ID')
|
|
205
|
-
|
|
206
|
-
In this example we're assuming the user has asked us to filter to include only records that are part of the list with the ID of 'My List ID'. In reality the prompt you will have will have a UUID/GUID type ID not a text string like this.
|
|
207
|
-
You can use any fields at the detail level filter the records and of course combine this type of list-oriented sub-query with other filters as appropriate to satisfy the user's request.
|
|
208
|
-
|
|
209
|
-
It is also possible that a user will provide ONLY the name of the list they want to filter on. If they provide the List ID, use it with a query like the above. However, if ONLY a List name is provided, you can do as follows: use this style of query with a join to the vwLists view (the list "header") to filter on the name
|
|
210
|
-
of the view or other header information if they want to filter on other list header attributes you can do this. Here is an example:
|
|
211
|
-
|
|
212
|
-
ID IN (SELECT ld.RecordID FROM ${listsEntity.SchemaName}.vwListDetails ld INNER JOIN ${listsEntity.SchemaName}.vwLists l ON ld.ListID=l.ID WHERE l.Name='My List Name')
|
|
213
|
-
|
|
214
|
-
No need to use table aliasing if you're just using the vwListDetails view, in that simple subquery it is automatic and unnecessary. If you need to join to the vwLists view, you can use the aliases "l" and "ld", as shown in the example above.
|
|
215
|
-
|
|
216
|
-
</IMPORTANT - LISTS FEATURE>
|
|
217
|
-
<IMPORTANT - OTHER VIEWS>
|
|
218
|
-
The user might reference other "views" in their request. In the user's terminology a view is what we call a "User View" in MemberJunction system-speak. The idea is a user might ask for a filter that includes or excludes
|
|
219
|
-
records from another view. Unlike Lists, which are STATIC sets of records, User Views are dynamic and can change based on the underlying data. So, what we need to do is use a sub-query that pulls in the SQL for the other view
|
|
220
|
-
The user will be referring to the other view by name, so what you need to do when generating SQL for this scenario is to simply embed a sub-query along the lines of this example, updated of course in the context of the user's request and the rest of any filters you are building:
|
|
221
|
-
ID IN ({%UserView "ViewID"%}) -- this is for filtering the primary key using other User Views for this entity: "${entityInfo.Name}"
|
|
222
|
-
AccountID IN ({%UserView "ViewID"%}) -- this is an example where the foreign key relationship for the AccountID field in this entity (${entityInfo.Name}) links to another entity hypothetically called "Accounts" and we want to use an Accounts view for filtering in some way
|
|
223
|
-
By including this sub-query in your generated WHERE clause, it will give me what I need to dynamically replace the {%UserView "View Name"%} with the actual SQL for the view that the user is referring to. Since that actual SQL
|
|
224
|
-
can change over time, I don't want to embed it directly here but rather have it templatized and each time it is run, I will pre-process the SQL to replace the template with the actual SQL for the view.
|
|
225
|
-
</IMPORTANT - OTHER VIEWS>
|
|
226
|
-
`
|
|
227
|
-
|
|
228
|
-
return gptSysPrompt + (processedViews.length > 1 /*we always have 1 from the entity base view*/ ? relationships : '') + `
|
|
229
|
-
**** REMEMBER **** I am a BOT, do not return anything other than JSON to me or I will choke on your response!`;
|
|
230
|
-
}
|
|
231
|
-
}
|