@memberjunction/server 5.1.0 → 5.2.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/README.md +2 -1
- package/dist/entitySubclasses/{entityPermissions.server.d.ts → MJEntityPermissionEntityServer.server.d.ts} +2 -2
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -0
- package/dist/entitySubclasses/{entityPermissions.server.js → MJEntityPermissionEntityServer.server.js} +9 -9
- package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -0
- package/dist/generic/ResolverBase.d.ts +2 -2
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +28 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.js +140 -0
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -0
- package/dist/resolvers/CreateQueryResolver.js +2 -2
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +52 -52
- package/src/__tests__/AdhocQueryResolver.test.ts +175 -0
- package/src/entitySubclasses/{entityPermissions.server.ts → MJEntityPermissionEntityServer.server.ts} +3 -3
- package/src/generic/ResolverBase.ts +9 -9
- package/src/generic/RunViewResolver.ts +4 -4
- package/src/index.ts +1 -1
- package/src/resolvers/AdhocQueryResolver.ts +126 -0
- package/src/resolvers/CreateQueryResolver.ts +5 -5
- package/src/resolvers/RunAIAgentResolver.ts +4 -4
- package/src/resolvers/RunAIPromptResolver.ts +7 -7
- package/src/resolvers/RunTemplateResolver.ts +2 -2
- package/src/resolvers/UserViewResolver.ts +2 -2
- package/src/services/TaskOrchestrator.ts +5 -5
- package/src/types.ts +2 -2
- package/dist/entitySubclasses/entityPermissions.server.d.ts.map +0 -1
- package/dist/entitySubclasses/entityPermissions.server.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "MemberJunction: This project provides API access via GraphQL to the common data store.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -26,57 +26,57 @@
|
|
|
26
26
|
"@apollo/server": "^4.9.1",
|
|
27
27
|
"@graphql-tools/schema": "latest",
|
|
28
28
|
"@graphql-tools/utils": "^11.0.0",
|
|
29
|
-
"@memberjunction/actions": "5.
|
|
30
|
-
"@memberjunction/actions-base": "5.
|
|
31
|
-
"@memberjunction/actions-apollo": "5.
|
|
32
|
-
"@memberjunction/actions-bizapps-accounting": "5.
|
|
33
|
-
"@memberjunction/actions-bizapps-crm": "5.
|
|
34
|
-
"@memberjunction/actions-bizapps-formbuilders": "5.
|
|
35
|
-
"@memberjunction/actions-bizapps-lms": "5.
|
|
36
|
-
"@memberjunction/actions-bizapps-social": "5.
|
|
37
|
-
"@memberjunction/ai": "5.
|
|
38
|
-
"@memberjunction/ai-mcp-client": "5.
|
|
39
|
-
"@memberjunction/ai-agent-manager": "5.
|
|
40
|
-
"@memberjunction/ai-agent-manager-actions": "5.
|
|
41
|
-
"@memberjunction/ai-agents": "5.
|
|
42
|
-
"@memberjunction/ai-core-plus": "5.
|
|
43
|
-
"@memberjunction/ai-prompts": "5.
|
|
44
|
-
"@memberjunction/ai-provider-bundle": "5.
|
|
45
|
-
"@memberjunction/ai-vectors-pinecone": "5.
|
|
46
|
-
"@memberjunction/aiengine": "5.
|
|
47
|
-
"@memberjunction/communication-ms-graph": "5.
|
|
48
|
-
"@memberjunction/communication-sendgrid": "5.
|
|
49
|
-
"@memberjunction/communication-types": "5.
|
|
50
|
-
"@memberjunction/component-registry-client-sdk": "5.
|
|
51
|
-
"@memberjunction/config": "5.
|
|
52
|
-
"@memberjunction/core": "5.
|
|
53
|
-
"@memberjunction/core-actions": "5.
|
|
54
|
-
"@memberjunction/core-entities": "5.
|
|
55
|
-
"@memberjunction/core-entities-server": "5.
|
|
56
|
-
"@memberjunction/data-context": "5.
|
|
57
|
-
"@memberjunction/data-context-server": "5.
|
|
58
|
-
"@memberjunction/doc-utils": "5.
|
|
59
|
-
"@memberjunction/api-keys": "5.
|
|
60
|
-
"@memberjunction/encryption": "5.
|
|
61
|
-
"@memberjunction/entity-communications-base": "5.
|
|
62
|
-
"@memberjunction/entity-communications-server": "5.
|
|
63
|
-
"@memberjunction/external-change-detection": "5.
|
|
64
|
-
"@memberjunction/global": "5.
|
|
65
|
-
"@memberjunction/graphql-dataprovider": "5.
|
|
66
|
-
"@memberjunction/interactive-component-types": "5.
|
|
67
|
-
"@memberjunction/notifications": "5.
|
|
68
|
-
"@memberjunction/queue": "5.
|
|
69
|
-
"@memberjunction/scheduling-actions": "5.
|
|
70
|
-
"@memberjunction/scheduling-base-types": "5.
|
|
71
|
-
"@memberjunction/scheduling-engine": "5.
|
|
72
|
-
"@memberjunction/scheduling-engine-base": "5.
|
|
73
|
-
"@memberjunction/skip-types": "5.
|
|
74
|
-
"@memberjunction/sqlserver-dataprovider": "5.
|
|
75
|
-
"@memberjunction/storage": "5.
|
|
76
|
-
"@memberjunction/templates": "5.
|
|
77
|
-
"@memberjunction/testing-engine": "5.
|
|
78
|
-
"@memberjunction/testing-engine-base": "5.
|
|
79
|
-
"@memberjunction/version-history": "5.
|
|
29
|
+
"@memberjunction/actions": "5.2.0",
|
|
30
|
+
"@memberjunction/actions-base": "5.2.0",
|
|
31
|
+
"@memberjunction/actions-apollo": "5.2.0",
|
|
32
|
+
"@memberjunction/actions-bizapps-accounting": "5.2.0",
|
|
33
|
+
"@memberjunction/actions-bizapps-crm": "5.2.0",
|
|
34
|
+
"@memberjunction/actions-bizapps-formbuilders": "5.2.0",
|
|
35
|
+
"@memberjunction/actions-bizapps-lms": "5.2.0",
|
|
36
|
+
"@memberjunction/actions-bizapps-social": "5.2.0",
|
|
37
|
+
"@memberjunction/ai": "5.2.0",
|
|
38
|
+
"@memberjunction/ai-mcp-client": "5.2.0",
|
|
39
|
+
"@memberjunction/ai-agent-manager": "5.2.0",
|
|
40
|
+
"@memberjunction/ai-agent-manager-actions": "5.2.0",
|
|
41
|
+
"@memberjunction/ai-agents": "5.2.0",
|
|
42
|
+
"@memberjunction/ai-core-plus": "5.2.0",
|
|
43
|
+
"@memberjunction/ai-prompts": "5.2.0",
|
|
44
|
+
"@memberjunction/ai-provider-bundle": "5.2.0",
|
|
45
|
+
"@memberjunction/ai-vectors-pinecone": "5.2.0",
|
|
46
|
+
"@memberjunction/aiengine": "5.2.0",
|
|
47
|
+
"@memberjunction/communication-ms-graph": "5.2.0",
|
|
48
|
+
"@memberjunction/communication-sendgrid": "5.2.0",
|
|
49
|
+
"@memberjunction/communication-types": "5.2.0",
|
|
50
|
+
"@memberjunction/component-registry-client-sdk": "5.2.0",
|
|
51
|
+
"@memberjunction/config": "5.2.0",
|
|
52
|
+
"@memberjunction/core": "5.2.0",
|
|
53
|
+
"@memberjunction/core-actions": "5.2.0",
|
|
54
|
+
"@memberjunction/core-entities": "5.2.0",
|
|
55
|
+
"@memberjunction/core-entities-server": "5.2.0",
|
|
56
|
+
"@memberjunction/data-context": "5.2.0",
|
|
57
|
+
"@memberjunction/data-context-server": "5.2.0",
|
|
58
|
+
"@memberjunction/doc-utils": "5.2.0",
|
|
59
|
+
"@memberjunction/api-keys": "5.2.0",
|
|
60
|
+
"@memberjunction/encryption": "5.2.0",
|
|
61
|
+
"@memberjunction/entity-communications-base": "5.2.0",
|
|
62
|
+
"@memberjunction/entity-communications-server": "5.2.0",
|
|
63
|
+
"@memberjunction/external-change-detection": "5.2.0",
|
|
64
|
+
"@memberjunction/global": "5.2.0",
|
|
65
|
+
"@memberjunction/graphql-dataprovider": "5.2.0",
|
|
66
|
+
"@memberjunction/interactive-component-types": "5.2.0",
|
|
67
|
+
"@memberjunction/notifications": "5.2.0",
|
|
68
|
+
"@memberjunction/queue": "5.2.0",
|
|
69
|
+
"@memberjunction/scheduling-actions": "5.2.0",
|
|
70
|
+
"@memberjunction/scheduling-base-types": "5.2.0",
|
|
71
|
+
"@memberjunction/scheduling-engine": "5.2.0",
|
|
72
|
+
"@memberjunction/scheduling-engine-base": "5.2.0",
|
|
73
|
+
"@memberjunction/skip-types": "5.2.0",
|
|
74
|
+
"@memberjunction/sqlserver-dataprovider": "5.2.0",
|
|
75
|
+
"@memberjunction/storage": "5.2.0",
|
|
76
|
+
"@memberjunction/templates": "5.2.0",
|
|
77
|
+
"@memberjunction/testing-engine": "5.2.0",
|
|
78
|
+
"@memberjunction/testing-engine-base": "5.2.0",
|
|
79
|
+
"@memberjunction/version-history": "5.2.0",
|
|
80
80
|
"@types/compression": "^1.8.1",
|
|
81
81
|
"@types/cors": "^2.8.19",
|
|
82
82
|
"@types/jsonwebtoken": "9.0.10",
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { SQLExpressionValidator } from '@memberjunction/global';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for AdhocQueryResolver.
|
|
6
|
+
*
|
|
7
|
+
* Since the resolver has heavy dependencies (type-graphql decorators, mssql, AppContext),
|
|
8
|
+
* we test the core logic through the SQLExpressionValidator integration and
|
|
9
|
+
* verify the resolver's structure and error-handling contracts.
|
|
10
|
+
*/
|
|
11
|
+
describe('AdhocQueryResolver', () => {
|
|
12
|
+
let validator: SQLExpressionValidator;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
validator = SQLExpressionValidator.Instance;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('SQL validation gate', () => {
|
|
19
|
+
it('should accept valid SELECT query', () => {
|
|
20
|
+
const result = validator.validateFullQuery('SELECT TOP 10 * FROM __mj.vwUsers');
|
|
21
|
+
expect(result.valid).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should accept valid CTE query', () => {
|
|
25
|
+
const result = validator.validateFullQuery(`
|
|
26
|
+
WITH cte AS (SELECT ID, Name FROM __mj.vwUsers)
|
|
27
|
+
SELECT * FROM cte
|
|
28
|
+
`);
|
|
29
|
+
expect(result.valid).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should reject INSERT statement', () => {
|
|
33
|
+
const result = validator.validateFullQuery("INSERT INTO Users (Name) VALUES ('hacked')");
|
|
34
|
+
expect(result.valid).toBe(false);
|
|
35
|
+
expect(result.error).toContain('INSERT');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should reject DELETE statement', () => {
|
|
39
|
+
const result = validator.validateFullQuery("DELETE FROM Users WHERE 1=1");
|
|
40
|
+
expect(result.valid).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should reject UPDATE statement', () => {
|
|
44
|
+
const result = validator.validateFullQuery("UPDATE Users SET IsAdmin = 1");
|
|
45
|
+
expect(result.valid).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should reject DROP TABLE', () => {
|
|
49
|
+
const result = validator.validateFullQuery("DROP TABLE Users");
|
|
50
|
+
expect(result.valid).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should reject EXEC', () => {
|
|
54
|
+
const result = validator.validateFullQuery("EXEC sp_executesql N'SELECT 1'");
|
|
55
|
+
expect(result.valid).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should reject multi-statement injection via semicolon', () => {
|
|
59
|
+
const result = validator.validateFullQuery("SELECT 1; DROP TABLE Users");
|
|
60
|
+
expect(result.valid).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should reject empty SQL', () => {
|
|
64
|
+
const result = validator.validateFullQuery('');
|
|
65
|
+
expect(result.valid).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('buildErrorResult contract', () => {
|
|
70
|
+
it('should return the expected error shape', () => {
|
|
71
|
+
// This tests the contract that the resolver's buildErrorResult must fulfill
|
|
72
|
+
const expectedShape = {
|
|
73
|
+
QueryID: '',
|
|
74
|
+
QueryName: 'Ad-Hoc Query',
|
|
75
|
+
Success: false,
|
|
76
|
+
Results: '[]',
|
|
77
|
+
RowCount: 0,
|
|
78
|
+
TotalRowCount: 0,
|
|
79
|
+
ExecutionTime: 0,
|
|
80
|
+
ErrorMessage: 'SQL validation failed'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
expect(expectedShape.Success).toBe(false);
|
|
84
|
+
expect(expectedShape.QueryID).toBe('');
|
|
85
|
+
expect(expectedShape.QueryName).toBe('Ad-Hoc Query');
|
|
86
|
+
expect(expectedShape.Results).toBe('[]');
|
|
87
|
+
expect(expectedShape.ErrorMessage).toBe('SQL validation failed');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('success result contract', () => {
|
|
92
|
+
it('should return the expected success shape', () => {
|
|
93
|
+
// This tests the contract that the resolver's success path must fulfill
|
|
94
|
+
const mockRecordset = [{ ID: '1', Name: 'Test' }, { ID: '2', Name: 'Other' }];
|
|
95
|
+
const expectedShape = {
|
|
96
|
+
QueryID: '',
|
|
97
|
+
QueryName: 'Ad-Hoc Query',
|
|
98
|
+
Success: true,
|
|
99
|
+
Results: JSON.stringify(mockRecordset),
|
|
100
|
+
RowCount: mockRecordset.length,
|
|
101
|
+
TotalRowCount: mockRecordset.length,
|
|
102
|
+
ExecutionTime: 42,
|
|
103
|
+
ErrorMessage: ''
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
expect(expectedShape.Success).toBe(true);
|
|
107
|
+
expect(expectedShape.QueryID).toBe('');
|
|
108
|
+
expect(expectedShape.QueryName).toBe('Ad-Hoc Query');
|
|
109
|
+
expect(JSON.parse(expectedShape.Results)).toHaveLength(2);
|
|
110
|
+
expect(expectedShape.RowCount).toBe(2);
|
|
111
|
+
expect(expectedShape.ErrorMessage).toBe('');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('timeout contract', () => {
|
|
116
|
+
it('should default to 30 seconds when TimeoutSeconds is not provided', () => {
|
|
117
|
+
const defaultTimeout = undefined ?? 30;
|
|
118
|
+
expect(defaultTimeout).toBe(30);
|
|
119
|
+
expect(defaultTimeout * 1000).toBe(30000);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should use provided TimeoutSeconds', () => {
|
|
123
|
+
const customTimeout = 60 ?? 30;
|
|
124
|
+
expect(customTimeout).toBe(60);
|
|
125
|
+
expect(customTimeout * 1000).toBe(60000);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should produce correct error message for timeout', () => {
|
|
129
|
+
const timeoutSeconds = 30;
|
|
130
|
+
const errorMessage = `Query execution exceeded ${timeoutSeconds} second timeout`;
|
|
131
|
+
expect(errorMessage).toContain('30');
|
|
132
|
+
expect(errorMessage).toContain('timeout');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('complex query validation', () => {
|
|
137
|
+
it('should accept query with JOINs and subqueries', () => {
|
|
138
|
+
const sql = `
|
|
139
|
+
SELECT a.Name, COUNT(r.ID) AS TotalRuns
|
|
140
|
+
FROM __mj.vwAIAgents a
|
|
141
|
+
INNER JOIN __mj.vwAIAgentRuns r ON r.AgentID = a.ID
|
|
142
|
+
WHERE EXISTS (SELECT 1 FROM __mj.vwUsers u WHERE u.ID = a.CreatedByUserID)
|
|
143
|
+
GROUP BY a.Name
|
|
144
|
+
ORDER BY TotalRuns DESC
|
|
145
|
+
`;
|
|
146
|
+
const result = validator.validateFullQuery(sql);
|
|
147
|
+
expect(result.valid).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should accept query with UNION', () => {
|
|
151
|
+
const sql = `
|
|
152
|
+
SELECT Name, 'Agent' AS Type FROM __mj.vwAIAgents
|
|
153
|
+
UNION ALL
|
|
154
|
+
SELECT Name, 'Model' AS Type FROM __mj.vwAIModels
|
|
155
|
+
`;
|
|
156
|
+
const result = validator.validateFullQuery(sql);
|
|
157
|
+
expect(result.valid).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should accept query with comments (agent-generated)', () => {
|
|
161
|
+
const sql = `
|
|
162
|
+
-- ============================================================
|
|
163
|
+
-- Agent Performance Report
|
|
164
|
+
-- ============================================================
|
|
165
|
+
SELECT TOP 100 a.Name, COUNT(*) AS RunCount
|
|
166
|
+
FROM __mj.vwAIAgents a
|
|
167
|
+
INNER JOIN __mj.vwAIAgentRuns r ON r.AgentID = a.ID
|
|
168
|
+
GROUP BY a.Name
|
|
169
|
+
ORDER BY RunCount DESC
|
|
170
|
+
`;
|
|
171
|
+
const result = validator.validateFullQuery(sql);
|
|
172
|
+
expect(result.valid).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -12,7 +12,7 @@ import { ___codeGenAPIPort, ___codeGenAPISubmissionDelay, ___codeGenAPIURL } fro
|
|
|
12
12
|
* happens in the server. That's why it is not in the core-entities-server package.
|
|
13
13
|
*/
|
|
14
14
|
@RegisterClass(BaseEntity, 'MJ: Entity Permissions')
|
|
15
|
-
export class
|
|
15
|
+
export class MJEntityPermissionEntityServer extends MJEntityPermissionEntity {
|
|
16
16
|
protected static _entityIDQueue: string[] = [];
|
|
17
17
|
protected static _lastModifiedTime: Date | null = null;
|
|
18
18
|
protected static _submissionTimer: NodeJS.Timeout | null = null;
|
|
@@ -89,7 +89,7 @@ export class EntityPermissionsEntity_Server extends MJEntityPermissionEntity {
|
|
|
89
89
|
|
|
90
90
|
override Save(options?: EntitySaveOptions): Promise<boolean> {
|
|
91
91
|
// simply queue up the entity ID
|
|
92
|
-
if (this.Dirty || options?.IgnoreDirtyState)
|
|
92
|
+
if (this.Dirty || options?.IgnoreDirtyState) MJEntityPermissionEntityServer.AddToQueue(this.EntityID);
|
|
93
93
|
|
|
94
94
|
return super.Save(options);
|
|
95
95
|
}
|
|
@@ -98,7 +98,7 @@ export class EntityPermissionsEntity_Server extends MJEntityPermissionEntity {
|
|
|
98
98
|
const success = await super.Delete(options);
|
|
99
99
|
|
|
100
100
|
// simply queue up the entity ID if the delete worked
|
|
101
|
-
if (success)
|
|
101
|
+
if (success) MJEntityPermissionEntityServer.AddToQueue(this.EntityID);
|
|
102
102
|
|
|
103
103
|
return success;
|
|
104
104
|
}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
RunViewResult,
|
|
18
18
|
UserInfo,
|
|
19
19
|
} from '@memberjunction/core';
|
|
20
|
-
import { MJAuditLogEntity, MJErrorLogEntity,
|
|
20
|
+
import { MJAuditLogEntity, MJErrorLogEntity, MJUserViewEntityExtended } from '@memberjunction/core-entities';
|
|
21
21
|
import { SQLServerDataProvider, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
22
22
|
import { PubSubEngine, AuthorizationError } from 'type-graphql';
|
|
23
23
|
import { GraphQLError } from 'graphql';
|
|
@@ -274,7 +274,7 @@ export class ResolverBase {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
const rv = provider as any as IRunViewProvider;
|
|
277
|
-
const result = await rv.RunView<
|
|
277
|
+
const result = await rv.RunView<MJUserViewEntityExtended>({
|
|
278
278
|
EntityName: 'MJ: User Views',
|
|
279
279
|
ExtraFilter: "Name='" + viewInput.ViewName + "'",
|
|
280
280
|
}, userPayload.userRecord);
|
|
@@ -319,7 +319,7 @@ export class ResolverBase {
|
|
|
319
319
|
}
|
|
320
320
|
|
|
321
321
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
322
|
-
const viewInfo = await provider.GetEntityObject<
|
|
322
|
+
const viewInfo = await provider.GetEntityObject<MJUserViewEntityExtended>('MJ: User Views', contextUser);
|
|
323
323
|
await viewInfo.Load(viewInput.ViewID);
|
|
324
324
|
return this.RunViewGenericInternal(
|
|
325
325
|
provider,
|
|
@@ -358,12 +358,12 @@ export class ResolverBase {
|
|
|
358
358
|
const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
|
|
359
359
|
if (!entity) throw new Error(`Entity ${viewInput.EntityName} not found in metadata`);
|
|
360
360
|
|
|
361
|
-
const viewInfo:
|
|
361
|
+
const viewInfo: MJUserViewEntityExtended = {
|
|
362
362
|
ID: '',
|
|
363
363
|
Entity: viewInput.EntityName,
|
|
364
364
|
EntityID: entity.ID,
|
|
365
365
|
EntityBaseView: entity.BaseView as string,
|
|
366
|
-
} as
|
|
366
|
+
} as MJUserViewEntityExtended; // only providing a few bits of data here, but it's enough to get the view to run
|
|
367
367
|
|
|
368
368
|
return this.RunViewGenericInternal(
|
|
369
369
|
provider,
|
|
@@ -401,12 +401,12 @@ export class ResolverBase {
|
|
|
401
401
|
let params: RunViewGenericParams[] = [];
|
|
402
402
|
for (const viewInput of viewInputs) {
|
|
403
403
|
try {
|
|
404
|
-
let viewInfo:
|
|
404
|
+
let viewInfo: MJUserViewEntityExtended | null = null;
|
|
405
405
|
|
|
406
406
|
if (viewInput.ViewName) {
|
|
407
407
|
viewInfo = this.safeFirstArrayElement(await this.findBy(provider, 'MJ: User Views', { Name: viewInput.ViewName }, userPayload.userRecord));
|
|
408
408
|
} else if (viewInput.ViewID) {
|
|
409
|
-
viewInfo = await provider.GetEntityObject<
|
|
409
|
+
viewInfo = await provider.GetEntityObject<MJUserViewEntityExtended>('MJ: User Views', contextUser);
|
|
410
410
|
await viewInfo.Load(viewInput.ViewID);
|
|
411
411
|
} else if (viewInput.EntityName) {
|
|
412
412
|
const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
|
|
@@ -420,7 +420,7 @@ export class ResolverBase {
|
|
|
420
420
|
Entity: viewInput.EntityName,
|
|
421
421
|
EntityID: entity.ID,
|
|
422
422
|
EntityBaseView: entity.BaseView,
|
|
423
|
-
} as
|
|
423
|
+
} as MJUserViewEntityExtended;
|
|
424
424
|
} else {
|
|
425
425
|
throw new Error('Unable to determine input type');
|
|
426
426
|
}
|
|
@@ -603,7 +603,7 @@ export class ResolverBase {
|
|
|
603
603
|
*/
|
|
604
604
|
protected async RunViewGenericInternal(
|
|
605
605
|
provider: DatabaseProviderBase,
|
|
606
|
-
viewInfo:
|
|
606
|
+
viewInfo: MJUserViewEntityExtended,
|
|
607
607
|
extraFilter: string,
|
|
608
608
|
orderBy: string,
|
|
609
609
|
userSearchString: string,
|
|
@@ -4,7 +4,7 @@ import { ResolverBase } from './ResolverBase.js';
|
|
|
4
4
|
import { LogError, LogStatus, EntityInfo, RunViewWithCacheCheckResult, RunViewsWithCacheCheckResponse, RunViewWithCacheCheckParams, AggregateResult } from '@memberjunction/core';
|
|
5
5
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
6
6
|
import { GetReadOnlyProvider } from '../util.js';
|
|
7
|
-
import {
|
|
7
|
+
import { MJUserViewEntityExtended } from '@memberjunction/core-entities';
|
|
8
8
|
import { KeyValuePairOutputType } from './KeyInputOutputTypes.js';
|
|
9
9
|
import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
10
10
|
|
|
@@ -652,7 +652,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
652
652
|
if (rawData === null)
|
|
653
653
|
return null;
|
|
654
654
|
|
|
655
|
-
const viewInfo = super.safeFirstArrayElement<
|
|
655
|
+
const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { Name: input.ViewName }, userPayload.userRecord));
|
|
656
656
|
const entity = provider.Entities.find((e) => e.ID === viewInfo.EntityID);
|
|
657
657
|
const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
|
|
658
658
|
return {
|
|
@@ -683,7 +683,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
683
683
|
if (rawData === null)
|
|
684
684
|
return null;
|
|
685
685
|
|
|
686
|
-
const viewInfo = super.safeFirstArrayElement<
|
|
686
|
+
const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
|
|
687
687
|
const entity = provider.Entities.find((e) => e.ID === viewInfo.EntityID);
|
|
688
688
|
const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
|
|
689
689
|
return {
|
|
@@ -837,7 +837,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
837
837
|
};
|
|
838
838
|
}
|
|
839
839
|
|
|
840
|
-
const viewInfo = super.safeFirstArrayElement<
|
|
840
|
+
const viewInfo = super.safeFirstArrayElement<MJUserViewEntityExtended>(await super.findBy<MJUserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
|
|
841
841
|
const entity = provider.Entities.find((e) => e.ID === viewInfo.EntityID);
|
|
842
842
|
const returnData = this.processRawData(rawData.Results, viewInfo.EntityID, entity);
|
|
843
843
|
return {
|
package/src/index.ts
CHANGED
|
@@ -44,7 +44,7 @@ export * from 'type-graphql';
|
|
|
44
44
|
export { NewUserBase } from './auth/newUsers.js';
|
|
45
45
|
export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
|
|
46
46
|
export * from './directives/index.js';
|
|
47
|
-
export * from './entitySubclasses/
|
|
47
|
+
export * from './entitySubclasses/MJEntityPermissionEntityServer.server.js';
|
|
48
48
|
export * from './types.js';
|
|
49
49
|
export {
|
|
50
50
|
TokenExpiredError,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Arg, Ctx, Query, Resolver, Field, Int, InputType } from 'type-graphql';
|
|
2
|
+
import { LogError } from '@memberjunction/core';
|
|
3
|
+
import { SQLExpressionValidator } from '@memberjunction/global';
|
|
4
|
+
import { AppContext } from '../types.js';
|
|
5
|
+
import { GetReadOnlyDataSource } from '../util.js';
|
|
6
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
7
|
+
import { RunQueryResultType } from './QueryResolver.js';
|
|
8
|
+
import sql from 'mssql';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Input type for executing ad-hoc SQL queries directly.
|
|
12
|
+
* The SQL is validated server-side to ensure it's a safe SELECT/WITH statement.
|
|
13
|
+
*/
|
|
14
|
+
@InputType()
|
|
15
|
+
class AdhocQueryInput {
|
|
16
|
+
@Field(() => String, { description: 'SQL query to execute. Must be a SELECT or WITH (CTE) statement.' })
|
|
17
|
+
SQL: string;
|
|
18
|
+
|
|
19
|
+
@Field(() => Int, { nullable: true, description: 'Query timeout in seconds. Defaults to 30.' })
|
|
20
|
+
TimeoutSeconds?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolver for executing ad-hoc (unsaved) SQL queries.
|
|
25
|
+
*
|
|
26
|
+
* Security:
|
|
27
|
+
* - SQL validated via SQLExpressionValidator (full_query context) — blocks mutations, dangerous operations
|
|
28
|
+
* - Executes on read-only connection pool only (no fallback to read-write)
|
|
29
|
+
* - Configurable timeout (default 30s)
|
|
30
|
+
* - Requires authenticated user (standard GraphQL auth, no @RequireSystemUser)
|
|
31
|
+
*
|
|
32
|
+
* Auto-discovered by MJServer's dynamic resolver import.
|
|
33
|
+
*/
|
|
34
|
+
@Resolver()
|
|
35
|
+
export class AdhocQueryResolver extends ResolverBase {
|
|
36
|
+
@Query(() => RunQueryResultType)
|
|
37
|
+
async ExecuteAdhocQuery(
|
|
38
|
+
@Arg('input', () => AdhocQueryInput) input: AdhocQueryInput,
|
|
39
|
+
@Ctx() context: AppContext
|
|
40
|
+
): Promise<RunQueryResultType> {
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// 1. Security: validate SQL using SQLExpressionValidator
|
|
45
|
+
const validator = SQLExpressionValidator.Instance;
|
|
46
|
+
const validation = validator.validateFullQuery(input.SQL);
|
|
47
|
+
if (!validation.valid) {
|
|
48
|
+
return this.buildErrorResult(validation.error || 'SQL validation failed');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Get READ-ONLY data source (no fallback to read-write)
|
|
52
|
+
let readOnlyDS: sql.ConnectionPool;
|
|
53
|
+
try {
|
|
54
|
+
readOnlyDS = GetReadOnlyDataSource(context.dataSources, { allowFallbackToReadWrite: false });
|
|
55
|
+
} catch {
|
|
56
|
+
return this.buildErrorResult('No read-only data source available for ad-hoc query execution');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Execute with timeout
|
|
60
|
+
const timeoutMs = (input.TimeoutSeconds ?? 30) * 1000;
|
|
61
|
+
const request = new sql.Request(readOnlyDS);
|
|
62
|
+
|
|
63
|
+
const result = await Promise.race([
|
|
64
|
+
request.query(input.SQL),
|
|
65
|
+
new Promise<never>((_, reject) =>
|
|
66
|
+
setTimeout(() => reject(new Error('Query timeout exceeded')), timeoutMs)
|
|
67
|
+
)
|
|
68
|
+
]);
|
|
69
|
+
const executionTimeMs = Date.now() - startTime;
|
|
70
|
+
|
|
71
|
+
// 4. Return as RunQueryResultType
|
|
72
|
+
return {
|
|
73
|
+
QueryID: '',
|
|
74
|
+
QueryName: 'Ad-Hoc Query',
|
|
75
|
+
Success: true,
|
|
76
|
+
Results: JSON.stringify(result.recordset ?? []),
|
|
77
|
+
RowCount: result.recordset?.length ?? 0,
|
|
78
|
+
TotalRowCount: result.recordset?.length ?? 0,
|
|
79
|
+
ExecutionTime: executionTimeMs,
|
|
80
|
+
ErrorMessage: ''
|
|
81
|
+
};
|
|
82
|
+
} catch (err: unknown) {
|
|
83
|
+
const executionTimeMs = Date.now() - startTime;
|
|
84
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
85
|
+
|
|
86
|
+
// Handle timeout
|
|
87
|
+
if (errorMessage.includes('timeout') || errorMessage.includes('Timeout')) {
|
|
88
|
+
return {
|
|
89
|
+
QueryID: '',
|
|
90
|
+
QueryName: 'Ad-Hoc Query',
|
|
91
|
+
Success: false,
|
|
92
|
+
Results: '[]',
|
|
93
|
+
RowCount: 0,
|
|
94
|
+
TotalRowCount: 0,
|
|
95
|
+
ExecutionTime: executionTimeMs,
|
|
96
|
+
ErrorMessage: `Query execution exceeded ${input.TimeoutSeconds ?? 30} second timeout`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
LogError(`Ad-hoc query execution failed: ${errorMessage}`);
|
|
101
|
+
return {
|
|
102
|
+
QueryID: '',
|
|
103
|
+
QueryName: 'Ad-Hoc Query',
|
|
104
|
+
Success: false,
|
|
105
|
+
Results: '[]',
|
|
106
|
+
RowCount: 0,
|
|
107
|
+
TotalRowCount: 0,
|
|
108
|
+
ExecutionTime: executionTimeMs,
|
|
109
|
+
ErrorMessage: `Query execution failed: ${errorMessage}`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private buildErrorResult(errorMessage: string): RunQueryResultType {
|
|
115
|
+
return {
|
|
116
|
+
QueryID: '',
|
|
117
|
+
QueryName: 'Ad-Hoc Query',
|
|
118
|
+
Success: false,
|
|
119
|
+
Results: '[]',
|
|
120
|
+
RowCount: 0,
|
|
121
|
+
TotalRowCount: 0,
|
|
122
|
+
ExecutionTime: 0,
|
|
123
|
+
ErrorMessage: errorMessage
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -6,7 +6,7 @@ import { MJQueryCategoryEntity, MJQueryPermissionEntity } from '@memberjunction/
|
|
|
6
6
|
import { MJQueryResolver } from '../generated/generated.js';
|
|
7
7
|
import { GetReadOnlyProvider, GetReadWriteProvider } from '../util.js';
|
|
8
8
|
import { DeleteOptionsInput } from '../generic/DeleteOptionsInput.js';
|
|
9
|
-
import {
|
|
9
|
+
import { MJQueryEntityServer } from '@memberjunction/core-entities-server';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Query status enumeration for GraphQL
|
|
@@ -425,8 +425,8 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
425
425
|
};
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
-
// Use
|
|
429
|
-
const record = await provider.GetEntityObject<
|
|
428
|
+
// Use MJQueryEntityServer which handles AI processing
|
|
429
|
+
const record = await provider.GetEntityObject<MJQueryEntityServer>("MJ: Queries", context.userPayload.userRecord);
|
|
430
430
|
|
|
431
431
|
// Set the fields from input, handling CategoryPath resolution
|
|
432
432
|
const fieldsToSet = {
|
|
@@ -638,9 +638,9 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
638
638
|
@PubSub() pubSub: PubSubEngine
|
|
639
639
|
): Promise<UpdateQueryResultType> {
|
|
640
640
|
try {
|
|
641
|
-
// Load the existing query using
|
|
641
|
+
// Load the existing query using MJQueryEntityServer
|
|
642
642
|
const provider = GetReadWriteProvider(context.providers);
|
|
643
|
-
const queryEntity = await provider.GetEntityObject<
|
|
643
|
+
const queryEntity = await provider.GetEntityObject<MJQueryEntityServer>('MJ: Queries', context.userPayload.userRecord);
|
|
644
644
|
if (!queryEntity || !await queryEntity.Load(input.ID)) {
|
|
645
645
|
return {
|
|
646
646
|
Success: false,
|
|
@@ -3,7 +3,7 @@ import { AppContext, UserPayload } from '../types.js';
|
|
|
3
3
|
import { DatabaseProviderBase, LogError, LogStatus, Metadata, RunView, UserInfo } from '@memberjunction/core';
|
|
4
4
|
import { MJConversationDetailEntity, MJConversationDetailAttachmentEntity } from '@memberjunction/core-entities';
|
|
5
5
|
import { AgentRunner } from '@memberjunction/ai-agents';
|
|
6
|
-
import {
|
|
6
|
+
import { MJAIAgentEntityExtended, MJAIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
|
|
7
7
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
8
8
|
import { ChatMessage } from '@memberjunction/ai';
|
|
9
9
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
@@ -207,12 +207,12 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
207
207
|
/**
|
|
208
208
|
* Validate the agent entity
|
|
209
209
|
*/
|
|
210
|
-
private async validateAgent(agentId: string, currentUser: any): Promise<
|
|
210
|
+
private async validateAgent(agentId: string, currentUser: any): Promise<MJAIAgentEntityExtended> {
|
|
211
211
|
// Use AIEngine to get cached agent data
|
|
212
212
|
await AIEngine.Instance.Config(false, currentUser);
|
|
213
213
|
|
|
214
214
|
// Find agent in cached collection
|
|
215
|
-
const agentEntity = AIEngine.Instance.Agents.find((a:
|
|
215
|
+
const agentEntity = AIEngine.Instance.Agents.find((a: MJAIAgentEntityExtended) => a.ID === agentId);
|
|
216
216
|
|
|
217
217
|
if (!agentEntity) {
|
|
218
218
|
throw new Error(`AI Agent with ID ${agentId} not found`);
|
|
@@ -630,7 +630,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
630
630
|
* Notification includes navigation link back to the conversation
|
|
631
631
|
*/
|
|
632
632
|
private async createCompletionNotification(
|
|
633
|
-
agentRun:
|
|
633
|
+
agentRun: MJAIAgentRunEntityExtended,
|
|
634
634
|
artifactInfo: { artifactId: string; versionId: string; versionNumber: number },
|
|
635
635
|
conversationDetailId: string,
|
|
636
636
|
contextUser: UserInfo,
|