@memberjunction/server 6.1.5 → 6.1.6

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 (189) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/agentSessions/SessionManager.d.ts +5 -2
  3. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  4. package/dist/agentSessions/SessionManager.js +17 -8
  5. package/dist/agentSessions/SessionManager.js.map +1 -1
  6. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  7. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  8. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  9. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  10. package/dist/agentSessions/index.d.ts +1 -0
  11. package/dist/agentSessions/index.d.ts.map +1 -1
  12. package/dist/agentSessions/index.js +1 -0
  13. package/dist/agentSessions/index.js.map +1 -1
  14. package/dist/auth/actionAuthorization.d.ts +40 -0
  15. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  16. package/dist/auth/actionAuthorization.js +94 -0
  17. package/dist/auth/actionAuthorization.js.map +1 -0
  18. package/dist/auth/exampleNewUserSubClass.js +2 -1
  19. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  20. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  21. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  22. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  23. package/dist/auth/newUsers.d.ts.map +1 -1
  24. package/dist/auth/newUsers.js +15 -11
  25. package/dist/auth/newUsers.js.map +1 -1
  26. package/dist/context.d.ts +6 -0
  27. package/dist/context.d.ts.map +1 -1
  28. package/dist/context.js +13 -11
  29. package/dist/context.js.map +1 -1
  30. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  31. package/dist/generated/graphql-schemas/__mj.js +3417 -3428
  32. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  33. package/dist/generic/CacheInvalidationResolver.d.ts +44 -8
  34. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  35. package/dist/generic/CacheInvalidationResolver.js +59 -19
  36. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  37. package/dist/generic/ResolverBase.d.ts +95 -11
  38. package/dist/generic/ResolverBase.d.ts.map +1 -1
  39. package/dist/generic/ResolverBase.js +186 -76
  40. package/dist/generic/ResolverBase.js.map +1 -1
  41. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  42. package/dist/generic/RunViewResolver.js +12 -0
  43. package/dist/generic/RunViewResolver.js.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +8 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/isolatedProvider.d.ts +20 -0
  49. package/dist/isolatedProvider.d.ts.map +1 -0
  50. package/dist/isolatedProvider.js +22 -0
  51. package/dist/isolatedProvider.js.map +1 -0
  52. package/dist/multiTenancy/index.d.ts.map +1 -1
  53. package/dist/multiTenancy/index.js +22 -2
  54. package/dist/multiTenancy/index.js.map +1 -1
  55. package/dist/realtimeWidget/widgetGuestElevation.d.ts +4 -2
  56. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  57. package/dist/realtimeWidget/widgetGuestElevation.js +4 -2
  58. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  59. package/dist/resolvers/ActionResolver.d.ts +31 -5
  60. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  61. package/dist/resolvers/ActionResolver.js +75 -14
  62. package/dist/resolvers/ActionResolver.js.map +1 -1
  63. package/dist/resolvers/AdhocQueryResolver.d.ts +28 -0
  64. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  65. package/dist/resolvers/AdhocQueryResolver.js +46 -0
  66. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  67. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  68. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  69. package/dist/resolvers/DatasetResolver.js +57 -8
  70. package/dist/resolvers/DatasetResolver.js.map +1 -1
  71. package/dist/resolvers/EntityRecordNameResolver.d.ts +8 -23
  72. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  73. package/dist/resolvers/EntityRecordNameResolver.js +11 -48
  74. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  75. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
  76. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  77. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  78. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  79. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  80. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  81. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  82. package/dist/resolvers/GetDataResolver.js +13 -1
  83. package/dist/resolvers/GetDataResolver.js.map +1 -1
  84. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  85. package/dist/resolvers/QuerySystemUserResolver.js +2 -1
  86. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  87. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +39 -16
  88. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  89. package/dist/resolvers/RealtimeClientSessionResolver.js +84 -38
  90. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  91. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  92. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  93. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  94. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  95. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  96. package/dist/resolvers/RunTemplateResolver.js +12 -11
  97. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  98. package/dist/resolvers/SearchKnowledgeResolver.d.ts +1 -0
  99. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  100. package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
  101. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  102. package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
  103. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
  104. package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
  105. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  106. package/dist/resolvers/SyncDataResolver.js +6 -2
  107. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  108. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  109. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  110. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  111. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  112. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  113. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  114. package/dist/rest/ViewOperationsHandler.js +29 -17
  115. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  116. package/dist/services/TaskGraphActionRunner.d.ts +8 -0
  117. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  118. package/dist/services/TaskGraphActionRunner.js +27 -1
  119. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  120. package/package.json +92 -92
  121. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  122. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +4 -2
  123. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  124. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  125. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  126. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  127. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  128. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  129. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  130. package/src/__tests__/RealtimeClientSessionResolver.test.ts +106 -41
  131. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  132. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +227 -0
  133. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  134. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  135. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  136. package/src/__tests__/ResolverBase.filterEscaping.test.ts +24 -2
  137. package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
  138. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  139. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  140. package/src/__tests__/SessionManager.test.ts +94 -17
  141. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  142. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -0
  143. package/src/__tests__/TaskGraphActionRunner.storableOutputs.test.ts +76 -0
  144. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  145. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  146. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  147. package/src/__tests__/actionAuthorization.test.ts +130 -0
  148. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  149. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  150. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  151. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  152. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  153. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  154. package/src/__tests__/multiTenancy.test.ts +1 -1
  155. package/src/__tests__/newUsers.test.ts +62 -5
  156. package/src/agentSessions/SessionManager.ts +23 -25
  157. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  158. package/src/agentSessions/index.ts +1 -0
  159. package/src/auth/actionAuthorization.ts +119 -0
  160. package/src/auth/exampleNewUserSubClass.ts +2 -2
  161. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  162. package/src/auth/newUsers.ts +15 -11
  163. package/src/context.ts +14 -10
  164. package/src/generated/graphql-schemas/__mj.ts +3417 -3428
  165. package/src/generic/CacheInvalidationResolver.ts +74 -19
  166. package/src/generic/ResolverBase.ts +207 -80
  167. package/src/generic/RunViewResolver.ts +17 -0
  168. package/src/index.ts +9 -1
  169. package/src/isolatedProvider.ts +22 -0
  170. package/src/multiTenancy/index.ts +24 -2
  171. package/src/realtimeWidget/widgetGuestElevation.ts +4 -2
  172. package/src/resolvers/ActionResolver.ts +84 -21
  173. package/src/resolvers/AdhocQueryResolver.ts +49 -1
  174. package/src/resolvers/DatasetResolver.ts +70 -10
  175. package/src/resolvers/EntityRecordNameResolver.ts +11 -52
  176. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
  177. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  178. package/src/resolvers/GetDataResolver.ts +14 -1
  179. package/src/resolvers/QuerySystemUserResolver.ts +2 -1
  180. package/src/resolvers/RealtimeClientSessionResolver.ts +103 -42
  181. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  182. package/src/resolvers/RunTemplateResolver.ts +13 -12
  183. package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
  184. package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
  185. package/src/resolvers/SyncDataResolver.ts +6 -2
  186. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  187. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  188. package/src/rest/ViewOperationsHandler.ts +37 -18
  189. package/src/services/TaskGraphActionRunner.ts +29 -1
@@ -2,7 +2,7 @@ import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
2
2
  import { AppContext, UserPayload } from '../types.js';
3
3
  import { LogError, LogStatus, Metadata, RunView } from '@memberjunction/core';
4
4
  import { MJTemplateContentEntity, MJTemplateEntityExtended } from '@memberjunction/core-entities';
5
- import { TemplateEngineServer } from '@memberjunction/templates';
5
+ import { GetTemplateRunRefusal, TemplateEngineServer } from '@memberjunction/templates';
6
6
  import { ResolverBase } from '../generic/ResolverBase.js';
7
7
  import { GetReadWriteProvider } from '../util.js';
8
8
 
@@ -45,6 +45,18 @@ export class RunTemplateResolver extends ResolverBase {
45
45
  try {
46
46
  LogStatus(`=== RUNNING TEMPLATE FOR ID: ${templateId} ===`);
47
47
 
48
+ // Same rule as the Template.Run remote operation, checked before anything is loaded
49
+ const currentUser = this.GetUserFromPayload(userPayload);
50
+ const p = GetReadWriteProvider(providers);
51
+ const refusal = GetTemplateRunRefusal(currentUser, p);
52
+ if (refusal) {
53
+ return {
54
+ success: false,
55
+ error: refusal,
56
+ executionTimeMs: Date.now() - startTime
57
+ };
58
+ }
59
+
48
60
  // Parse context data (JSON string)
49
61
  let data = {};
50
62
  if (contextData) {
@@ -59,17 +71,6 @@ export class RunTemplateResolver extends ResolverBase {
59
71
  }
60
72
  }
61
73
 
62
- // Get current user from payload
63
- const currentUser = this.GetUserFromPayload(userPayload);
64
- if (!currentUser) {
65
- return {
66
- success: false,
67
- error: 'Unable to determine current user',
68
- executionTimeMs: Date.now() - startTime
69
- };
70
- }
71
-
72
- const p = GetReadWriteProvider(providers);
73
74
  // Load the template entity
74
75
  const templateEntity = await p.GetEntityObject<MJTemplateEntityExtended>('MJ: Templates', currentUser);
75
76
  await templateEntity.Load(templateId);
@@ -32,6 +32,9 @@ export class SearchKnowledgeResultItem {
32
32
  @Field()
33
33
  EntityName: string;
34
34
 
35
+ @Field({ nullable: true })
36
+ EntityDisplayName?: string;
37
+
35
38
  @Field()
36
39
  RecordID: string;
37
40
 
@@ -461,6 +464,7 @@ export class SearchKnowledgeResolver extends ResolverBase {
461
464
  Results: result.Results.map((r: SearchEngineResultItem) => ({
462
465
  ID: r.ID,
463
466
  EntityName: r.EntityName,
467
+ EntityDisplayName: r.EntityDisplayName,
464
468
  RecordID: r.RecordID,
465
469
  SourceType: r.SourceType,
466
470
  ResultType: r.ResultType,
@@ -88,6 +88,7 @@ export class SearchKnowledgeSystemUserResolver extends ResolverBase {
88
88
  Results: result.Results.map((r: SearchEngineResultItem) => ({
89
89
  ID: r.ID,
90
90
  EntityName: r.EntityName,
91
+ EntityDisplayName: r.EntityDisplayName,
91
92
  RecordID: r.RecordID,
92
93
  SourceType: r.SourceType,
93
94
  ResultType: r.ResultType,
@@ -3,6 +3,7 @@ import { AppContext, UserPayload } from '../types.js';
3
3
  import { BaseEntity, CompositeKey, DatabaseProviderBase, EntityDeleteOptions, EntitySaveOptions, IMetadataProvider, LogError, Metadata, RunView, UserInfo } from '@memberjunction/core';
4
4
  import { RequireSystemUser } from '../directives/RequireSystemUser.js';
5
5
  import { GetReadWriteProvider } from '../util.js';
6
+ import { CreateIsolatedProvider } from '../isolatedProvider.js';
6
7
  import { CompositeKeyInputType, CompositeKeyOutputType } from '../generic/KeyInputOutputTypes.js';
7
8
  import { MJDatasetItemEntity } from '@memberjunction/core-entities';
8
9
 
@@ -216,7 +217,11 @@ export class SyncDataResolver {
216
217
  try {
217
218
  // Run the view to find matching records, then delete them all atomically —
218
219
  // any single failure rolls back the entire batch so the dataset stays consistent.
219
- const rv = new RunView();
220
+ // The rows are loaded FROM the transaction's provider so each Delete() runs inside it;
221
+ // with no request provider, an isolated one, never the shared Metadata.Provider, whose
222
+ // transaction would swallow every concurrent caller's writes (see CreateIsolatedProvider).
223
+ const provider = (providerOverride as unknown as DatabaseProviderBase) ?? await CreateIsolatedProvider();
224
+ const rv = RunView.FromMetadataProvider(provider);
220
225
  const data = await rv.RunView<BaseEntity>({
221
226
  EntityName: entityName,
222
227
  ExtraFilter: filter,
@@ -234,7 +239,6 @@ export class SyncDataResolver {
234
239
  return;
235
240
  }
236
241
 
237
- const provider = (providerOverride ?? Metadata.Provider) as unknown as DatabaseProviderBase;
238
242
  await provider.BeginTransaction();
239
243
  try {
240
244
  for (const entityObject of data.Results) {
@@ -1,26 +1,27 @@
1
1
  import { Resolver, Mutation, Arg, Ctx, ObjectType, Field, Float } from 'type-graphql';
2
2
  import { AppContext } from '../types.js';
3
- import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
3
+ import { IMetadataProvider, LogError, LogStatus, UserInfo } from '@memberjunction/core';
4
4
  import { ResolverBase } from '../generic/ResolverBase.js';
5
5
  import { EntityVectorSyncer, VectorizeEntityParams, VectorizeProgressUpdate } from '@memberjunction/ai-vector-sync';
6
6
  import { PubSubManager } from '../generic/PubSubManager.js';
7
7
  import { PipelineProgressNotification } from './PipelineProgressResolver.js';
8
8
  import { v4 as uuidv4 } from 'uuid';
9
+ import { GetReadWriteProvider } from '../util.js';
9
10
 
10
11
  const PIPELINE_PROGRESS_TOPIC = 'PIPELINE_PROGRESS';
11
12
 
12
13
  @ObjectType()
13
14
  export class VectorizeEntityResult {
14
- @Field()
15
+ @Field(() => Boolean)
15
16
  Success: boolean;
16
17
 
17
- @Field({ nullable: true })
18
+ @Field(() => String, { nullable: true })
18
19
  Status?: string;
19
20
 
20
- @Field({ nullable: true })
21
+ @Field(() => String, { nullable: true })
21
22
  ErrorMessage?: string;
22
23
 
23
- @Field({ nullable: true })
24
+ @Field(() => String, { nullable: true })
24
25
  PipelineRunID?: string;
25
26
  }
26
27
 
@@ -28,10 +29,10 @@ export class VectorizeEntityResult {
28
29
  export class VectorizeEntityResolver extends ResolverBase {
29
30
  @Mutation(() => VectorizeEntityResult)
30
31
  async VectorizeEntity(
31
- @Arg('entityDocumentID') entityDocumentID: string,
32
- @Arg('entityID') entityID: string,
32
+ @Arg('entityDocumentID', () => String) entityDocumentID: string,
33
+ @Arg('entityID', () => String) entityID: string,
33
34
  @Arg('batchSize', () => Float, { nullable: true }) batchSize?: number,
34
- @Ctx() { userPayload }: AppContext = {} as AppContext
35
+ @Ctx() { userPayload, providers }: AppContext = {} as AppContext
35
36
  ): Promise<VectorizeEntityResult> {
36
37
  try {
37
38
  const currentUser = this.GetUserFromPayload(userPayload);
@@ -39,12 +40,15 @@ export class VectorizeEntityResolver extends ResolverBase {
39
40
  return { Success: false, Status: 'Error', ErrorMessage: 'Unable to determine current user' };
40
41
  }
41
42
 
43
+ // Capture the request's provider before returning, so the background run writes through the
44
+ // same connection the caller used. It is also the colocated vector host (#4910).
45
+ const provider = GetReadWriteProvider(providers, { allowFallbackToReadOnly: true }) ?? undefined;
42
46
  const pipelineRunID = uuidv4();
43
47
  LogStatus(`VectorizeEntity: starting pipeline ${pipelineRunID} for entity document ${entityDocumentID}`);
44
48
 
45
49
  // Fire-and-forget: start the pipeline in the background and return immediately.
46
50
  // Progress is delivered to the client via the PipelineProgress GraphQL subscription.
47
- this.runPipelineInBackground(pipelineRunID, entityDocumentID, entityID, batchSize, currentUser);
51
+ this.runPipelineInBackground(pipelineRunID, entityDocumentID, entityID, batchSize, currentUser, provider);
48
52
 
49
53
  return {
50
54
  Success: true,
@@ -71,10 +75,11 @@ export class VectorizeEntityResolver extends ResolverBase {
71
75
  entityDocumentID: string,
72
76
  entityID: string,
73
77
  batchSize: number | undefined,
74
- currentUser: UserInfo
78
+ currentUser: UserInfo,
79
+ provider?: IMetadataProvider
75
80
  ): Promise<void> {
76
81
  try {
77
- const syncer = new EntityVectorSyncer();
82
+ const syncer = new EntityVectorSyncer(provider);
78
83
  await syncer.Config(true, currentUser);
79
84
 
80
85
  const params: VectorizeEntityParams = {
@@ -0,0 +1,95 @@
1
+ // type-graphql decorators on the resolver class need the reflect-metadata polyfill.
2
+ import 'reflect-metadata';
3
+
4
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
5
+ import type { MockInstance } from 'vitest';
6
+ import { SQLExpressionValidator } from '@memberjunction/global';
7
+
8
+ const { executedSQL } = vi.hoisted(() => ({ executedSQL: [] as string[] }));
9
+
10
+ // GetData's server bootstrap, provider, auth and driver imports are replaced so the resolver runs
11
+ // offline. The fake driver records every text it is asked to run.
12
+ vi.mock('../../index.js', () => ({ getDbType: () => 'sqlserver' }));
13
+ vi.mock('@memberjunction/generic-database-provider', () => ({
14
+ QueryCompositionEngine: class {
15
+ HasCompositionTokens(): boolean {
16
+ return false;
17
+ }
18
+ },
19
+ }));
20
+ vi.mock('../../util.js', () => ({ GetReadOnlyDataSource: () => ({}), GetReadOnlyProvider: () => null }));
21
+ vi.mock('../../auth/index.js', () => ({ getSystemUser: async () => ({}) }));
22
+ vi.mock('mssql', () => ({
23
+ default: {
24
+ Request: class {
25
+ async query(text: string): Promise<{ recordset: Array<Record<string, unknown>> }> {
26
+ executedSQL.push(text);
27
+ return { recordset: [] };
28
+ }
29
+ },
30
+ },
31
+ }));
32
+
33
+ import { GetDataResolver, GetDataInputType, GetDataOutputType, registerAccessToken } from '../GetDataResolver.js';
34
+ import type { AppContext } from '../../types.js';
35
+
36
+ const CONTEXT = { dataSources: [] } as unknown as AppContext;
37
+
38
+ let tokenSeq = 0;
39
+
40
+ /** Calls GetData with a fresh valid token. */
41
+ async function runGetData(queries: string[]): Promise<GetDataOutputType> {
42
+ const token = `getdata-validation-${Date.now()}-${++tokenSeq}`;
43
+ registerAccessToken(token, 60_000);
44
+ const input = Object.assign(new GetDataInputType(), { Token: token, Queries: queries });
45
+ return new GetDataResolver().GetData(input, CONTEXT);
46
+ }
47
+
48
+ /** The text of every call to the validator's `validate` method. */
49
+ function validatedTexts(spy: MockInstance<SQLExpressionValidator['validate']>): string[] {
50
+ return spy.mock.calls.map(([text]) => text);
51
+ }
52
+
53
+ describe('GetData runs exactly the SQL it validated', () => {
54
+ beforeEach(() => {
55
+ executedSQL.length = 0;
56
+ });
57
+
58
+ it('validates and runs the same text when the query contains backslash sequences', async () => {
59
+ const query = String.raw`SELECT Name FROM __mj.vwFiles WHERE Path = 'C:\new\temp'`;
60
+ const validateSpy = vi.spyOn(SQLExpressionValidator.Instance, 'validate');
61
+
62
+ const output = await runGetData([query]);
63
+
64
+ expect(output.ErrorMessages).toEqual([null]);
65
+ expect(executedSQL).toEqual(validatedTexts(validateSpy));
66
+ expect(executedSQL).toEqual([query]);
67
+ expect(validateSpy).toHaveBeenCalledWith(query, { context: 'full_query', Dialect: 'sqlserver' });
68
+ });
69
+
70
+ it('runs the trimmed text that the validator reads', async () => {
71
+ const validateSpy = vi.spyOn(SQLExpressionValidator.Instance, 'validate');
72
+
73
+ const output = await runGetData(['\n SELECT 1 AS One \n']);
74
+
75
+ expect(output.ErrorMessages).toEqual([null]);
76
+ expect(executedSQL).toEqual(['SELECT 1 AS One']);
77
+ expect(validatedTexts(validateSpy)).toEqual(executedSQL);
78
+ });
79
+
80
+ it('refuses a query whose text, as written, does not open with SELECT or WITH', async () => {
81
+ const output = await runGetData(["-- note\\n SELECT 1\nPRINT 'hello'"]);
82
+
83
+ expect(executedSQL).toEqual([]);
84
+ expect(output.Success).toBe(false);
85
+ expect(output.ErrorMessages[0]).toMatch(/must start with SELECT or WITH/);
86
+ });
87
+
88
+ it('does not run a refused query and still runs the others', async () => {
89
+ const output = await runGetData(['SELECT 1; SELECT 2', 'SELECT 1 AS One']);
90
+
91
+ expect(executedSQL).toEqual(['SELECT 1 AS One']);
92
+ expect(output.ErrorMessages[0]).toMatch(/Semicolons are not allowed/);
93
+ expect(output.ErrorMessages[1]).toBeNull();
94
+ });
95
+ });
@@ -1,7 +1,8 @@
1
1
  import {
2
- LogError, Metadata, RunView, RunViewParams,
3
- RunViewResult, UserInfo
2
+ BaseEntity, LogError, Metadata, RunView, RunViewParams,
3
+ RunViewResult, UserInfo, type IRunViewProvider
4
4
  } from '@memberjunction/core';
5
+ import { ClientClauseScreen } from '@memberjunction/generic-database-provider';
5
6
 
6
7
  /**
7
8
  * View Operations Implementation for REST endpoints
@@ -32,11 +33,11 @@ export class ViewOperationsHandler {
32
33
  };
33
34
  }
34
35
 
35
- // Sanitize and validate parameters
36
- this.sanitizeRunViewParams(params);
36
+ // Sanitize and validate parameters against the provider that runs the view
37
+ const runView = new RunView();
38
+ this.sanitizeRunViewParams(params, md, runView.ProviderToUse, user);
37
39
 
38
40
  // Execute the view
39
- const runView = new RunView();
40
41
  const result = await runView.RunView(params, user);
41
42
 
42
43
  return { success: true, result };
@@ -51,8 +52,9 @@ export class ViewOperationsHandler {
51
52
  */
52
53
  static async runViews(paramsArray: RunViewParams[], user: UserInfo): Promise<{ success: boolean, results?: RunViewResult[], error?: string }> {
53
54
  try {
54
- // Validate and sanitize each set of parameters
55
+ // Validate and sanitize each set of parameters against the provider that runs the views
55
56
  const md = new Metadata(); // global-provider-ok: REST endpoint — no per-request provider injection in REST middleware yet
57
+ const runView = new RunView();
56
58
  for (const params of paramsArray) {
57
59
  // Validate entity exists
58
60
  const entity = md.Entities.find(e => e.Name === params.EntityName);
@@ -73,11 +75,10 @@ export class ViewOperationsHandler {
73
75
  }
74
76
 
75
77
  // Sanitize parameters
76
- this.sanitizeRunViewParams(params);
78
+ this.sanitizeRunViewParams(params, md, runView.ProviderToUse, user);
77
79
  }
78
80
 
79
81
  // Execute the views
80
- const runView = new RunView();
81
82
  const results = await runView.RunViews(paramsArray, user);
82
83
 
83
84
  return { success: true, results };
@@ -104,11 +105,11 @@ export class ViewOperationsHandler {
104
105
  throw new Error(`User ${user.Name} does not have permission to read ${params.EntityName} records`);
105
106
  }
106
107
 
107
- // Sanitize and validate parameters
108
- this.sanitizeRunViewParams(params);
108
+ // Sanitize and validate parameters against the provider that runs the view
109
+ const runView = new RunView();
110
+ this.sanitizeRunViewParams(params, md, runView.ProviderToUse, user);
109
111
 
110
112
  // Execute the view
111
- const runView = new RunView();
112
113
  return await runView.RunView(params, user);
113
114
  } catch (error) {
114
115
  LogError(error);
@@ -173,35 +174,53 @@ export class ViewOperationsHandler {
173
174
  }
174
175
 
175
176
  /**
176
- * Sanitize and validate RunViewParams
177
+ * Sanitize and validate RunViewParams. Client filter and sort text passes the same clause
178
+ * screen as the GraphQL RunView resolvers, parsed in the dialect of `provider`, the provider
179
+ * that runs the view.
177
180
  */
178
- private static sanitizeRunViewParams(params: RunViewParams): void {
181
+ private static sanitizeRunViewParams(params: RunViewParams, md: Metadata, provider: IRunViewProvider, user: UserInfo): void {
179
182
  // Ensure EntityName is provided
180
183
  if (!params.EntityName) {
181
184
  throw new Error('EntityName is required');
182
185
  }
186
+
187
+ // A loaded view entity cannot arrive over JSON; the provider would trust a plain object's
188
+ // fields (its ID is spliced into view SQL). REST callers name a saved view by ViewID or ViewName.
189
+ if (params.ViewEntity != null && !(params.ViewEntity instanceof BaseEntity)) {
190
+ throw new Error('ViewEntity is not accepted over REST; use ViewID or ViewName');
191
+ }
183
192
 
184
193
  // Convert string arrays if they came in as comma-separated strings
185
194
  if (params.Fields && typeof params.Fields === 'string') {
186
195
  params.Fields = (params.Fields as string).split(',');
187
196
  }
188
197
 
189
- // Sanitize numeric values
198
+ // Sanitize numeric values: base-10, integral, and clamped to a sane range so a
199
+ // negative/garbage value can never reach the SQL layer.
190
200
  if (params.MaxRows !== undefined) {
191
- params.MaxRows = typeof params.MaxRows === 'string'
192
- ? parseInt(params.MaxRows as string)
201
+ const parsed = typeof params.MaxRows === 'string'
202
+ ? parseInt(params.MaxRows as string, 10)
193
203
  : params.MaxRows;
204
+ params.MaxRows = Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed as number)) : undefined;
194
205
  }
195
206
 
196
207
  if (params.StartRow !== undefined) {
197
- params.StartRow = typeof params.StartRow === 'string'
198
- ? parseInt(params.StartRow as string)
208
+ const parsed = typeof params.StartRow === 'string'
209
+ ? parseInt(params.StartRow as string, 10)
199
210
  : params.StartRow;
211
+ params.StartRow = Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed as number)) : undefined;
200
212
  }
201
213
 
202
214
  // Default ResultType if not provided
203
215
  if (!params.ResultType) {
204
216
  params.ResultType = 'simple';
205
217
  }
218
+
219
+ ClientClauseScreen.ScreenViewClauses(
220
+ { ExtraFilter: params.ExtraFilter, OrderBy: params.OrderBy, OverrideExcludeFilter: params.OverrideExcludeFilter },
221
+ md.Entities,
222
+ ClientClauseScreen.DialectFor(provider),
223
+ user,
224
+ );
206
225
  }
207
226
  }
@@ -35,9 +35,19 @@ export class TaskGraphActionRunner implements TaskActionRunner {
35
35
  Filters: [],
36
36
  });
37
37
 
38
+ const output = this.buildOutput(result.Params);
39
+ const unstorable = this.findUnstorableOutput(output);
40
+ if (unstorable) {
41
+ const message =
42
+ `Action "${action.Name}" returned output param "${unstorable.Name}", which cannot be stored ` +
43
+ `as the step's result: ${unstorable.Reason}. Output params of a durable step must be plain data.`;
44
+ LogError(`[TaskGraphActionRunner] Task ${params.TaskID}: ${message}`);
45
+ return { Success: false, ErrorMessage: message, ActionLogID: result.LogEntry?.ID };
46
+ }
47
+
38
48
  return {
39
49
  Success: result.Success,
40
- Output: this.buildOutput(result.Params),
50
+ Output: output,
41
51
  ErrorMessage: result.Success ? undefined : result.Message,
42
52
  // The engine already wrote the log; this is the only place its id is in hand. Without
43
53
  // carrying it out, a workflow's action step has no path back to its own execution
@@ -51,6 +61,24 @@ export class TaskGraphActionRunner implements TaskActionRunner {
51
61
  }
52
62
  }
53
63
 
64
+ /**
65
+ * The first output param that cannot be written to the Task row as JSON, or null.
66
+ *
67
+ * Checked here because this is the last place that knows the action's and the param's NAMES: the
68
+ * dispatcher serializes the whole Output later and, for a live object (an entity, an observable),
69
+ * fails with a bare "Converting circular structure to JSON" that names neither.
70
+ */
71
+ private findUnstorableOutput(output: Record<string, unknown>): { Name: string; Reason: string } | null {
72
+ for (const [Name, value] of Object.entries(output)) {
73
+ try {
74
+ JSON.stringify(value);
75
+ } catch (e) {
76
+ return { Name, Reason: e instanceof Error ? e.message.split('\n')[0] : String(e) };
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+
54
82
  /**
55
83
  * Projects the action's parameters onto the flat result a workflow step is expected to produce.
56
84
  *