@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.
Files changed (41) hide show
  1. package/README.md +2 -1
  2. package/dist/entitySubclasses/{entityPermissions.server.d.ts → MJEntityPermissionEntityServer.server.d.ts} +2 -2
  3. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -0
  4. package/dist/entitySubclasses/{entityPermissions.server.js → MJEntityPermissionEntityServer.server.js} +9 -9
  5. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -0
  6. package/dist/generic/ResolverBase.d.ts +2 -2
  7. package/dist/generic/ResolverBase.d.ts.map +1 -1
  8. package/dist/generic/ResolverBase.js.map +1 -1
  9. package/dist/generic/RunViewResolver.js.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/resolvers/AdhocQueryResolver.d.ts +28 -0
  15. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -0
  16. package/dist/resolvers/AdhocQueryResolver.js +140 -0
  17. package/dist/resolvers/AdhocQueryResolver.js.map +1 -0
  18. package/dist/resolvers/CreateQueryResolver.js +2 -2
  19. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  20. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  21. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  22. package/dist/resolvers/UserViewResolver.js.map +1 -1
  23. package/dist/services/TaskOrchestrator.js.map +1 -1
  24. package/dist/types.d.ts +2 -2
  25. package/dist/types.d.ts.map +1 -1
  26. package/package.json +52 -52
  27. package/src/__tests__/AdhocQueryResolver.test.ts +175 -0
  28. package/src/entitySubclasses/{entityPermissions.server.ts → MJEntityPermissionEntityServer.server.ts} +3 -3
  29. package/src/generic/ResolverBase.ts +9 -9
  30. package/src/generic/RunViewResolver.ts +4 -4
  31. package/src/index.ts +1 -1
  32. package/src/resolvers/AdhocQueryResolver.ts +126 -0
  33. package/src/resolvers/CreateQueryResolver.ts +5 -5
  34. package/src/resolvers/RunAIAgentResolver.ts +4 -4
  35. package/src/resolvers/RunAIPromptResolver.ts +7 -7
  36. package/src/resolvers/RunTemplateResolver.ts +2 -2
  37. package/src/resolvers/UserViewResolver.ts +2 -2
  38. package/src/services/TaskOrchestrator.ts +5 -5
  39. package/src/types.ts +2 -2
  40. package/dist/entitySubclasses/entityPermissions.server.d.ts.map +0 -1
  41. 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.1.0",
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.1.0",
30
- "@memberjunction/actions-base": "5.1.0",
31
- "@memberjunction/actions-apollo": "5.1.0",
32
- "@memberjunction/actions-bizapps-accounting": "5.1.0",
33
- "@memberjunction/actions-bizapps-crm": "5.1.0",
34
- "@memberjunction/actions-bizapps-formbuilders": "5.1.0",
35
- "@memberjunction/actions-bizapps-lms": "5.1.0",
36
- "@memberjunction/actions-bizapps-social": "5.1.0",
37
- "@memberjunction/ai": "5.1.0",
38
- "@memberjunction/ai-mcp-client": "5.1.0",
39
- "@memberjunction/ai-agent-manager": "5.1.0",
40
- "@memberjunction/ai-agent-manager-actions": "5.1.0",
41
- "@memberjunction/ai-agents": "5.1.0",
42
- "@memberjunction/ai-core-plus": "5.1.0",
43
- "@memberjunction/ai-prompts": "5.1.0",
44
- "@memberjunction/ai-provider-bundle": "5.1.0",
45
- "@memberjunction/ai-vectors-pinecone": "5.1.0",
46
- "@memberjunction/aiengine": "5.1.0",
47
- "@memberjunction/communication-ms-graph": "5.1.0",
48
- "@memberjunction/communication-sendgrid": "5.1.0",
49
- "@memberjunction/communication-types": "5.1.0",
50
- "@memberjunction/component-registry-client-sdk": "5.1.0",
51
- "@memberjunction/config": "5.1.0",
52
- "@memberjunction/core": "5.1.0",
53
- "@memberjunction/core-actions": "5.1.0",
54
- "@memberjunction/core-entities": "5.1.0",
55
- "@memberjunction/core-entities-server": "5.1.0",
56
- "@memberjunction/data-context": "5.1.0",
57
- "@memberjunction/data-context-server": "5.1.0",
58
- "@memberjunction/doc-utils": "5.1.0",
59
- "@memberjunction/api-keys": "5.1.0",
60
- "@memberjunction/encryption": "5.1.0",
61
- "@memberjunction/entity-communications-base": "5.1.0",
62
- "@memberjunction/entity-communications-server": "5.1.0",
63
- "@memberjunction/external-change-detection": "5.1.0",
64
- "@memberjunction/global": "5.1.0",
65
- "@memberjunction/graphql-dataprovider": "5.1.0",
66
- "@memberjunction/interactive-component-types": "5.1.0",
67
- "@memberjunction/notifications": "5.1.0",
68
- "@memberjunction/queue": "5.1.0",
69
- "@memberjunction/scheduling-actions": "5.1.0",
70
- "@memberjunction/scheduling-base-types": "5.1.0",
71
- "@memberjunction/scheduling-engine": "5.1.0",
72
- "@memberjunction/scheduling-engine-base": "5.1.0",
73
- "@memberjunction/skip-types": "5.1.0",
74
- "@memberjunction/sqlserver-dataprovider": "5.1.0",
75
- "@memberjunction/storage": "5.1.0",
76
- "@memberjunction/templates": "5.1.0",
77
- "@memberjunction/testing-engine": "5.1.0",
78
- "@memberjunction/testing-engine-base": "5.1.0",
79
- "@memberjunction/version-history": "5.1.0",
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 EntityPermissionsEntity_Server extends MJEntityPermissionEntity {
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) EntityPermissionsEntity_Server.AddToQueue(this.EntityID);
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) EntityPermissionsEntity_Server.AddToQueue(this.EntityID);
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, UserViewEntityExtended } from '@memberjunction/core-entities';
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<UserViewEntityExtended>({
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<UserViewEntityExtended>('MJ: User Views', contextUser);
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: UserViewEntityExtended = {
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 UserViewEntityExtended; // only providing a few bits of data here, but it's enough to get the view to run
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: UserViewEntityExtended | null = null;
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<UserViewEntityExtended>('MJ: User Views', contextUser);
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 UserViewEntityExtended;
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: UserViewEntityExtended,
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 { UserViewEntityExtended } from '@memberjunction/core-entities';
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<UserViewEntityExtended>(await super.findBy<UserViewEntityExtended>(provider, "MJ: User Views", { Name: input.ViewName }, userPayload.userRecord));
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<UserViewEntityExtended>(await super.findBy<UserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
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<UserViewEntityExtended>(await super.findBy<UserViewEntityExtended>(provider, "MJ: User Views", { ID: input.ViewID }, userPayload.userRecord));
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/entityPermissions.server.js';
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 { QueryEntityExtended } from '@memberjunction/core-entities-server';
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 QueryEntityExtended which handles AI processing
429
- const record = await provider.GetEntityObject<QueryEntityExtended>("MJ: Queries", context.userPayload.userRecord);
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 QueryEntityExtended
641
+ // Load the existing query using MJQueryEntityServer
642
642
  const provider = GetReadWriteProvider(context.providers);
643
- const queryEntity = await provider.GetEntityObject<QueryEntityExtended>('MJ: Queries', context.userPayload.userRecord);
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 { AIAgentEntityExtended, AIAgentRunEntityExtended, ExecuteAgentResult, ConversationUtility, AttachmentData } from '@memberjunction/ai-core-plus';
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<AIAgentEntityExtended> {
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: AIAgentEntityExtended) => a.ID === agentId);
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: AIAgentRunEntityExtended,
633
+ agentRun: MJAIAgentRunEntityExtended,
634
634
  artifactInfo: { artifactId: string; versionId: string; versionNumber: number },
635
635
  conversationDetailId: string,
636
636
  contextUser: UserInfo,