@memberjunction/server 6.1.0-edge.2 → 6.1.0-edge.3

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 (162) hide show
  1. package/LICENSE +180 -4
  2. package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
  3. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
  4. package/dist/auth/AuthProviderCatalogRouter.js +69 -0
  5. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
  6. package/dist/auth/AuthProviderEngine.d.ts +96 -0
  7. package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
  8. package/dist/auth/AuthProviderEngine.js +245 -0
  9. package/dist/auth/AuthProviderEngine.js.map +1 -0
  10. package/dist/auth/index.d.ts +11 -0
  11. package/dist/auth/index.d.ts.map +1 -1
  12. package/dist/auth/index.js +56 -13
  13. package/dist/auth/index.js.map +1 -1
  14. package/dist/auth/initializeProviders.d.ts +64 -1
  15. package/dist/auth/initializeProviders.d.ts.map +1 -1
  16. package/dist/auth/initializeProviders.js +117 -5
  17. package/dist/auth/initializeProviders.js.map +1 -1
  18. package/dist/config.d.ts +89 -0
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +52 -35
  21. package/dist/config.js.map +1 -1
  22. package/dist/generated/generated.d.ts +501 -1597
  23. package/dist/generated/generated.d.ts.map +1 -1
  24. package/dist/generated/generated.js +2711 -17021
  25. package/dist/generated/generated.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +1 -1
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +7 -4
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +4 -0
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +31 -0
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +99 -14
  37. package/dist/index.js.map +1 -1
  38. package/dist/integration/RSUProgressBridge.d.ts +83 -0
  39. package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
  40. package/dist/integration/RSUProgressBridge.js +230 -0
  41. package/dist/integration/RSUProgressBridge.js.map +1 -0
  42. package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
  43. package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
  44. package/dist/integration/SchemaRefreshLaunch.js +50 -0
  45. package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
  46. package/dist/providerConfigUnits.d.ts +20 -0
  47. package/dist/providerConfigUnits.d.ts.map +1 -0
  48. package/dist/providerConfigUnits.js +22 -0
  49. package/dist/providerConfigUnits.js.map +1 -0
  50. package/dist/resolvers/FileResolver.d.ts +34 -4
  51. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  52. package/dist/resolvers/FileResolver.js +218 -28
  53. package/dist/resolvers/FileResolver.js.map +1 -1
  54. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +76 -3
  55. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  56. package/dist/resolvers/IntegrationDiscoveryResolver.js +299 -81
  57. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  58. package/dist/resolvers/QueryResolver.d.ts +5 -4
  59. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  60. package/dist/resolvers/QueryResolver.js +29 -14
  61. package/dist/resolvers/QueryResolver.js.map +1 -1
  62. package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
  63. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  64. package/dist/resolvers/QuerySystemUserResolver.js +33 -14
  65. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  66. package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
  67. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  68. package/dist/resolvers/RunAIAgentResolver.js +20 -7
  69. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  70. package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
  71. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  72. package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
  73. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  74. package/dist/resolvers/UserResolver.d.ts +19 -17
  75. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  76. package/dist/resolvers/UserResolver.js +66 -67
  77. package/dist/resolvers/UserResolver.js.map +1 -1
  78. package/dist/resolvers/currentUserRoles.d.ts +19 -0
  79. package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
  80. package/dist/resolvers/currentUserRoles.js +36 -0
  81. package/dist/resolvers/currentUserRoles.js.map +1 -0
  82. package/dist/rest/MediaAccessKeys.d.ts +17 -0
  83. package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
  84. package/dist/rest/MediaAccessKeys.js +42 -1
  85. package/dist/rest/MediaAccessKeys.js.map +1 -1
  86. package/dist/rest/MediaStreamHandler.d.ts +3 -2
  87. package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
  88. package/dist/rest/MediaStreamHandler.js +83 -2
  89. package/dist/rest/MediaStreamHandler.js.map +1 -1
  90. package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
  91. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  92. package/dist/rest/OAuthCallbackHandler.js +129 -43
  93. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  94. package/dist/rest/UploadTokenManager.d.ts +90 -0
  95. package/dist/rest/UploadTokenManager.d.ts.map +1 -0
  96. package/dist/rest/UploadTokenManager.js +156 -0
  97. package/dist/rest/UploadTokenManager.js.map +1 -0
  98. package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
  99. package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
  100. package/dist/services/IntegrationSyncWorkerService.js +132 -0
  101. package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
  102. package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
  103. package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
  104. package/dist/services/StartTaskGraphDispatcher.js +21 -1
  105. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  106. package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
  107. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  108. package/dist/services/TaskGraphAgentRunner.js +23 -0
  109. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  110. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  111. package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
  112. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  113. package/dist/types.d.ts +6 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/dist/types.js.map +1 -1
  116. package/package.json +91 -91
  117. package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
  118. package/src/__tests__/AuthProviderEngine.test.ts +278 -0
  119. package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
  120. package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
  121. package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
  122. package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
  123. package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
  124. package/src/__tests__/RSUProgressBridge.test.ts +250 -0
  125. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
  126. package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
  127. package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
  128. package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
  129. package/src/__tests__/config-units.test.ts +74 -0
  130. package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
  131. package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
  132. package/src/__tests__/newUsers.test.ts +726 -0
  133. package/src/__tests__/uploadTokenManager.test.ts +159 -0
  134. package/src/auth/AuthProviderCatalogRouter.ts +76 -0
  135. package/src/auth/AuthProviderEngine.ts +253 -0
  136. package/src/auth/index.ts +59 -15
  137. package/src/auth/initializeProviders.ts +128 -7
  138. package/src/config.ts +55 -36
  139. package/src/generated/generated.ts +1911 -10551
  140. package/src/generic/ResolverBase.ts +11 -4
  141. package/src/generic/RunViewResolver.ts +31 -0
  142. package/src/index.ts +108 -15
  143. package/src/integration/RSUProgressBridge.ts +248 -0
  144. package/src/integration/SchemaRefreshLaunch.ts +77 -0
  145. package/src/providerConfigUnits.ts +21 -0
  146. package/src/resolvers/FileResolver.ts +200 -40
  147. package/src/resolvers/IntegrationDiscoveryResolver.ts +316 -68
  148. package/src/resolvers/QueryResolver.ts +25 -11
  149. package/src/resolvers/QuerySystemUserResolver.ts +28 -13
  150. package/src/resolvers/RunAIAgentResolver.ts +21 -6
  151. package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
  152. package/src/resolvers/UserResolver.ts +99 -60
  153. package/src/resolvers/currentUserRoles.ts +47 -0
  154. package/src/rest/MediaAccessKeys.ts +47 -1
  155. package/src/rest/MediaStreamHandler.ts +98 -3
  156. package/src/rest/OAuthCallbackHandler.ts +149 -41
  157. package/src/rest/UploadTokenManager.ts +211 -0
  158. package/src/services/IntegrationSyncWorkerService.ts +141 -0
  159. package/src/services/StartTaskGraphDispatcher.ts +21 -1
  160. package/src/services/TaskGraphAgentRunner.ts +23 -0
  161. package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
  162. package/src/types.ts +6 -0
@@ -43,6 +43,9 @@ export class RunQueryInput {
43
43
 
44
44
  @Field(() => GraphQLJSONObject, { nullable: true, description: 'Optional runtime-only directive to post-process result rows through a registered query result enricher ({ EnricherKey, Config })' })
45
45
  Enrichment?: RunQueryEnrichment;
46
+
47
+ @Field(() => String, { nullable: true, description: "Read source: 'Live' (default) or 'Materialized' (serve from a fresh RowFilterBroad materialization when available; falls back to live otherwise)" })
48
+ DataSource?: 'Live' | 'Materialized';
46
49
  }
47
50
 
48
51
  @ObjectType()
@@ -195,7 +198,8 @@ export class RunQueryResolver extends ResolverBase {
195
198
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
196
199
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
197
200
  @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
198
- @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
201
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment,
202
+ @Arg('DataSource', () => String, {nullable: true, description: "Read source: 'Live' (default) or 'Materialized' (serve from a fresh RowFilterBroad materialization when available; falls back to live otherwise)"}) DataSource?: 'Live' | 'Materialized'): Promise<RunQueryResultType> {
199
203
  // Check API key scope authorization for query execution
200
204
  await this.CheckAPIKeyScopeAuthorization('query:run', QueryID, context.userPayload);
201
205
 
@@ -212,7 +216,8 @@ export class RunQueryResolver extends ResolverBase {
212
216
  StartRow: StartRow,
213
217
  ForceAuditLog: ForceAuditLog,
214
218
  AuditLogDescription: AuditLogDescription,
215
- Enrichment: Enrichment
219
+ Enrichment: Enrichment,
220
+ DataSource: DataSource
216
221
  },
217
222
  context.userPayload.userRecord);
218
223
 
@@ -256,7 +261,8 @@ export class RunQueryResolver extends ResolverBase {
256
261
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
257
262
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
258
263
  @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
259
- @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
264
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment,
265
+ @Arg('DataSource', () => String, {nullable: true, description: "Read source: 'Live' (default) or 'Materialized' (serve from a fresh RowFilterBroad materialization when available; falls back to live otherwise)"}) DataSource?: 'Live' | 'Materialized'): Promise<RunQueryResultType> {
260
266
  // Check API key scope authorization for query execution
261
267
  await this.CheckAPIKeyScopeAuthorization('query:run', QueryName, context.userPayload);
262
268
 
@@ -272,7 +278,8 @@ export class RunQueryResolver extends ResolverBase {
272
278
  StartRow: StartRow,
273
279
  ForceAuditLog: ForceAuditLog,
274
280
  AuditLogDescription: AuditLogDescription,
275
- Enrichment: Enrichment
281
+ Enrichment: Enrichment,
282
+ DataSource: DataSource
276
283
  },
277
284
  context.userPayload.userRecord);
278
285
 
@@ -304,7 +311,8 @@ export class RunQueryResolver extends ResolverBase {
304
311
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
305
312
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
306
313
  @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
307
- @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
314
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment,
315
+ @Arg('DataSource', () => String, {nullable: true, description: "Read source: 'Live' (default) or 'Materialized' (serve from a fresh RowFilterBroad materialization when available; falls back to live otherwise)"}) DataSource?: 'Live' | 'Materialized'): Promise<RunQueryResultType> {
308
316
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
309
317
  const md = provider as unknown as IMetadataProvider;
310
318
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
@@ -319,10 +327,11 @@ export class RunQueryResolver extends ResolverBase {
319
327
  StartRow: StartRow,
320
328
  ForceAuditLog: ForceAuditLog,
321
329
  AuditLogDescription: AuditLogDescription,
322
- Enrichment: Enrichment
330
+ Enrichment: Enrichment,
331
+ DataSource: DataSource
323
332
  },
324
333
  context.userPayload.userRecord);
325
-
334
+
326
335
  // If QueryName is not populated by the provider, use efficient lookup
327
336
  let queryName = result.QueryName;
328
337
  if (!queryName) {
@@ -364,7 +373,8 @@ export class RunQueryResolver extends ResolverBase {
364
373
  @Arg('StartRow', () => Int, {nullable: true}) StartRow?: number,
365
374
  @Arg('ForceAuditLog', () => Boolean, {nullable: true}) ForceAuditLog?: boolean,
366
375
  @Arg('AuditLogDescription', () => String, {nullable: true}) AuditLogDescription?: string,
367
- @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment): Promise<RunQueryResultType> {
376
+ @Arg('Enrichment', () => GraphQLJSONObject, {nullable: true}) Enrichment?: RunQueryEnrichment,
377
+ @Arg('DataSource', () => String, {nullable: true, description: "Read source: 'Live' (default) or 'Materialized' (serve from a fresh RowFilterBroad materialization when available; falls back to live otherwise)"}) DataSource?: 'Live' | 'Materialized'): Promise<RunQueryResultType> {
368
378
  const provider = GetReadOnlyProvider(context.providers, {allowFallbackToReadWrite: true});
369
379
  const rq = new RunQuery(provider as unknown as IRunQueryProvider);
370
380
 
@@ -378,7 +388,8 @@ export class RunQueryResolver extends ResolverBase {
378
388
  StartRow: StartRow,
379
389
  ForceAuditLog: ForceAuditLog,
380
390
  AuditLogDescription: AuditLogDescription,
381
- Enrichment: Enrichment
391
+ Enrichment: Enrichment,
392
+ DataSource: DataSource
382
393
  },
383
394
  context.userPayload.userRecord);
384
395
 
@@ -426,7 +437,8 @@ export class RunQueryResolver extends ResolverBase {
426
437
  StartRow: i.StartRow,
427
438
  ForceAuditLog: i.ForceAuditLog,
428
439
  AuditLogDescription: i.AuditLogDescription,
429
- Enrichment: i.Enrichment
440
+ Enrichment: i.Enrichment,
441
+ DataSource: i.DataSource
430
442
  }));
431
443
 
432
444
  // Execute all queries in parallel using the batch method
@@ -476,7 +488,8 @@ export class RunQueryResolver extends ResolverBase {
476
488
  StartRow: i.StartRow,
477
489
  ForceAuditLog: i.ForceAuditLog,
478
490
  AuditLogDescription: i.AuditLogDescription,
479
- Enrichment: i.Enrichment
491
+ Enrichment: i.Enrichment,
492
+ DataSource: i.DataSource
480
493
  }));
481
494
 
482
495
  // Execute all queries in parallel using the batch method
@@ -540,6 +553,7 @@ export class RunQueryResolver extends ResolverBase {
540
553
  ForceAuditLog: item.params.ForceAuditLog,
541
554
  AuditLogDescription: item.params.AuditLogDescription,
542
555
  Enrichment: item.params.Enrichment,
556
+ DataSource: item.params.DataSource,
543
557
  },
544
558
  cacheStatus: item.cacheStatus ? {
545
559
  maxUpdatedAt: item.cacheStatus.maxUpdatedAt,
@@ -182,8 +182,13 @@ export class UpdateQuerySystemUserInput {
182
182
  * Composes the CodeGen-generated MJQuery_ type so that new entity fields
183
183
  * are automatically available in the GraphQL schema without manual sync.
184
184
  *
185
- * On success, Query contains the full query data (scalars + related entities).
186
- * On failure, Query is null and ErrorMessage describes the problem.
185
+ * Child collections live on this result type (not as generated `*Array`
186
+ * FieldResolvers on MJQuery_) so the mutation can return the graph it already
187
+ * loaded without a per-parent SELECT *.
188
+ *
189
+ * On success, Query contains the query scalars and Fields/Parameters/Entities/
190
+ * Permissions hold the related rows. On failure, Query is null and
191
+ * ErrorMessage describes the problem.
187
192
  */
188
193
  @ObjectType()
189
194
  export class QueryMutationResultType {
@@ -195,6 +200,18 @@ export class QueryMutationResultType {
195
200
 
196
201
  @Field(() => MJQuery_, { nullable: true })
197
202
  Query?: MJQuery_;
203
+
204
+ @Field(() => [MJQueryField_], { nullable: true })
205
+ Fields?: MJQueryField_[];
206
+
207
+ @Field(() => [MJQueryParameter_], { nullable: true })
208
+ Parameters?: MJQueryParameter_[];
209
+
210
+ @Field(() => [MJQueryEntity_], { nullable: true })
211
+ Entities?: MJQueryEntity_[];
212
+
213
+ @Field(() => [MJQueryPermission_], { nullable: true })
214
+ Permissions?: MJQueryPermission_[];
198
215
  }
199
216
 
200
217
  @ObjectType()
@@ -306,7 +323,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
306
323
  }
307
324
 
308
325
 
309
- return this.buildSuccessResult(record);
326
+ return await this.buildSuccessResult(record);
310
327
  }
311
328
  else {
312
329
  // Save failed - check if another request created the same query (race condition)
@@ -319,7 +336,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
319
336
  LogStatus(`[CreateQuery] Unique constraint detected for query '${input.Name}'. Using existing query (ID: ${existingQuery.ID}) created by concurrent request.`);
320
337
  const existingEntity = await provider.GetEntityObject<MJQueryEntityServer>('MJ: Queries', context.userPayload.userRecord);
321
338
  if (await existingEntity.Load(existingQuery.ID)) {
322
- return this.buildSuccessResult(existingEntity);
339
+ return await this.buildSuccessResult(existingEntity);
323
340
  }
324
341
  // Entity load failed after confirming the row exists — extremely rare
325
342
  return {
@@ -350,16 +367,14 @@ export class MJQueryResolverExtended extends MJQueryResolver {
350
367
  * Uses entity.GetAll() for scalar fields so that new CodeGen-generated fields are included
351
368
  * automatically without manual updates. Related entity arrays are mapped explicitly.
352
369
  */
353
- private buildSuccessResult(entity: MJQueryEntityServer): QueryMutationResultType {
370
+ private async buildSuccessResult(entity: MJQueryEntityServer): Promise<QueryMutationResultType> {
354
371
  return {
355
372
  Success: true,
356
- Query: {
357
- ...entity.GetAll(),
358
- MJQueryFields_QueryIDArray: this.mapFields(entity.QueryFields),
359
- MJQueryParameters_QueryIDArray: this.mapParameters(entity.QueryParameters),
360
- MJQueryEntities_QueryIDArray: this.mapEntities(entity.QueryEntities),
361
- MJQueryPermissions_QueryIDArray: this.mapPermissions(entity.QueryPermissions),
362
- } as MJQuery_
373
+ Query: await this.MapFieldNamesToCodeNames('MJ: Queries', entity.GetAll()) as MJQuery_,
374
+ Fields: this.mapFields(entity.QueryFields),
375
+ Parameters: this.mapParameters(entity.QueryParameters),
376
+ Entities: this.mapEntities(entity.QueryEntities),
377
+ Permissions: this.mapPermissions(entity.QueryPermissions),
363
378
  };
364
379
  }
365
380
 
@@ -538,7 +553,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
538
553
  await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
539
554
  }
540
555
 
541
- return this.buildSuccessResult(queryEntity);
556
+ return await this.buildSuccessResult(queryEntity);
542
557
 
543
558
  } catch (err) {
544
559
  LogError(err);
@@ -387,7 +387,9 @@ export class RunAIAgentResolver extends ResolverBase {
387
387
  planMode?: boolean,
388
388
  /** Skill IDs the user requested (via `/skill-name`) — threaded into ExecuteAgentParams.requestedSkillIDs.
389
389
  * The framework intersects them with the agent's accepted skills AND the user's Run permission. */
390
- requestedSkillIDs?: string[]
390
+ requestedSkillIDs?: string[],
391
+ /** JSON `{ paused?: boolean }` — seeds `$.debug` on a submitted task graph at insert. */
392
+ taskGraphDebug?: string
391
393
  ): Promise<AIAgentRunResult> {
392
394
  const startTime = Date.now();
393
395
 
@@ -441,6 +443,7 @@ export class RunAIAgentResolver extends ResolverBase {
441
443
  configurationId: configurationId,
442
444
  planMode: planMode,
443
445
  requestedSkillIDs: requestedSkillIDs,
446
+ taskGraphDebug: parseTaskGraphDebug(taskGraphDebug),
444
447
  data: parsedData,
445
448
  context: {
446
449
  dataSource: dataSource
@@ -632,7 +635,9 @@ export class RunAIAgentResolver extends ResolverBase {
632
635
  @Arg('planMode', { nullable: true }) planMode?: boolean,
633
636
  /** Skill IDs the user requested — symmetric with RunAIAgentFromConversationDetail. Intersected
634
637
  * server-side with the agent's accepted skills AND the user's Run permission. */
635
- @Arg('requestedSkillIDs', () => [String], { nullable: true }) requestedSkillIDs?: string[]
638
+ @Arg('requestedSkillIDs', () => [String], { nullable: true }) requestedSkillIDs?: string[],
639
+ /** JSON `{ paused?: boolean }` — start-paused for a Flow agent that submits a graph. */
640
+ @Arg('taskGraphDebug', { nullable: true }) taskGraphDebug?: string
636
641
  ): Promise<AIAgentRunResult> {
637
642
  // Check API key scope authorization for agent execution
638
643
  await this.CheckAPIKeyScopeAuthorization('agent:execute', agentId, userPayload);
@@ -646,7 +651,7 @@ export class RunAIAgentResolver extends ResolverBase {
646
651
  p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
647
652
  data, payload, lastRunId, autoPopulateLastRunPayload, configurationId,
648
653
  conversationDetailId, createArtifacts || false, createNotification || false,
649
- sourceArtifactId, sourceArtifactVersionId, undefined /*conversationId*/, planMode, requestedSkillIDs
654
+ sourceArtifactId, sourceArtifactVersionId, undefined /*conversationId*/, planMode, requestedSkillIDs, taskGraphDebug
650
655
  );
651
656
 
652
657
  LogStatus(`🔥 Fire-and-forget: Agent ${agentId} execution started in background for session ${sessionId}`);
@@ -680,7 +685,8 @@ export class RunAIAgentResolver extends ResolverBase {
680
685
  undefined, // conversationId (not pre-resolved on this path)
681
686
  undefined, // runRef
682
687
  planMode,
683
- requestedSkillIDs
688
+ requestedSkillIDs,
689
+ taskGraphDebug
684
690
  );
685
691
  }
686
692
 
@@ -1276,7 +1282,8 @@ export class RunAIAgentResolver extends ResolverBase {
1276
1282
  /** Per-request Plan Mode toggle — threaded through to ExecuteAgentParams.planMode. */
1277
1283
  planMode?: boolean,
1278
1284
  /** Skill IDs the user requested — threaded through to ExecuteAgentParams.requestedSkillIDs. */
1279
- requestedSkillIDs?: string[]
1285
+ requestedSkillIDs?: string[],
1286
+ taskGraphDebug?: string
1280
1287
  ): void {
1281
1288
  // Ref the liveness pulse reads to enrich heartbeats once the run is created.
1282
1289
  const runRef: { current: MJAIAgentRunEntityExtended | null } = { current: null };
@@ -1295,7 +1302,7 @@ export class RunAIAgentResolver extends ResolverBase {
1295
1302
  p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
1296
1303
  data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
1297
1304
  configurationId, conversationDetailId, createArtifacts, createNotification,
1298
- sourceArtifactId, sourceArtifactVersionId, conversationId, runRef, planMode, requestedSkillIDs
1305
+ sourceArtifactId, sourceArtifactVersionId, conversationId, runRef, planMode, requestedSkillIDs, taskGraphDebug
1299
1306
  ).catch((error: unknown) => {
1300
1307
  // Background execution failed unexpectedly (executeAIAgent has its own try-catch,
1301
1308
  // so this would only fire for truly unexpected errors).
@@ -1587,4 +1594,12 @@ export class RunAIAgentResolver extends ResolverBase {
1587
1594
  }
1588
1595
  }
1589
1596
 
1597
+ }
1598
+
1599
+ function parseTaskGraphDebug(raw?: string): { paused?: boolean } | undefined {
1600
+ if (!raw) return undefined;
1601
+ const parsed: unknown = SafeJSONParse(raw);
1602
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
1603
+ const paused = (parsed as { paused?: unknown }).paused;
1604
+ return paused === true ? { paused: true } : undefined;
1590
1605
  }
@@ -52,6 +52,58 @@ export class TaskGraphFrameNotification {
52
52
  @Field(() => Number, { nullable: true })
53
53
  totalCount?: number;
54
54
 
55
+ // ── GateDecision ──────────────────────────────────────────────────────────
56
+ @Field(() => ID, { nullable: true })
57
+ edgeId?: string;
58
+
59
+ @Field(() => ID, { nullable: true })
60
+ dependsOnTaskId?: string;
61
+
62
+ /** 'satisfied' | 'notTaken' | 'held' */
63
+ @Field(() => String, { nullable: true })
64
+ verdict?: string;
65
+
66
+ @Field(() => String, { nullable: true })
67
+ conditionText?: string;
68
+
69
+ @Field(() => String, { nullable: true })
70
+ reason?: string;
71
+
72
+ // ── ClaimChanged ──────────────────────────────────────────────────────────
73
+ /** 'claimed' | 'heartbeat-lost' | 'reclaimed' */
74
+ @Field(() => String, { nullable: true })
75
+ claimEvent?: string;
76
+
77
+ @Field(() => String, { nullable: true })
78
+ claimedBy?: string;
79
+
80
+ @Field(() => String, { nullable: true })
81
+ claimExpiresAt?: string;
82
+
83
+ // ── PassCompleted ─────────────────────────────────────────────────────────
84
+ @Field(() => Number, { nullable: true })
85
+ passNumber?: number;
86
+
87
+ @Field(() => Number, { nullable: true })
88
+ eligibleCount?: number;
89
+
90
+ @Field(() => Number, { nullable: true })
91
+ heldCount?: number;
92
+
93
+ @Field(() => Number, { nullable: true })
94
+ claimedCount?: number;
95
+
96
+ /** The dispatcher instance's own load across every graph — not this graph's in-flight count. */
97
+ @Field(() => Number, { nullable: true })
98
+ instanceInFlightCount?: number;
99
+
100
+ // ── NodeProgress ──────────────────────────────────────────────────────────
101
+ @Field(() => String, { nullable: true })
102
+ progressMessage?: string;
103
+
104
+ @Field(() => Number, { nullable: true })
105
+ progressPercent?: number;
106
+
55
107
  @Field(() => Date)
56
108
  date!: Date;
57
109
  }
@@ -154,6 +206,21 @@ export class TaskGraphFrameResolver {
154
206
  assignedUserId: payload.AssignedUserID,
155
207
  completedCount: payload.CompletedCount,
156
208
  totalCount: payload.TotalCount,
209
+ edgeId: payload.EdgeID,
210
+ dependsOnTaskId: payload.DependsOnTaskID,
211
+ verdict: payload.Verdict,
212
+ conditionText: payload.ConditionText,
213
+ reason: payload.Reason,
214
+ claimEvent: payload.ClaimEvent,
215
+ claimedBy: payload.ClaimedBy,
216
+ claimExpiresAt: payload.ClaimExpiresAt,
217
+ passNumber: payload.PassNumber,
218
+ eligibleCount: payload.EligibleCount,
219
+ heldCount: payload.HeldCount,
220
+ claimedCount: payload.ClaimedCount,
221
+ instanceInFlightCount: payload.InstanceInFlightCount,
222
+ progressMessage: payload.ProgressMessage,
223
+ progressPercent: payload.ProgressPercent,
157
224
  date: new Date(),
158
225
  };
159
226
  }
@@ -1,54 +1,46 @@
1
- import { AppContext, Arg, Ctx, FieldResolver, Int, PubSub, PubSubEngine, Query, Resolver, Root } from '@memberjunction/server';
1
+ import { AppContext, Arg, Ctx, Field, Int, ObjectType, PubSub, PubSubEngine, Query, Resolver } from '@memberjunction/server';
2
+ import { IMetadataProvider, RunView, UserInfo } from '@memberjunction/core';
2
3
  import { UUIDsEqual } from '@memberjunction/global';
3
4
  import { MJUser_, MJUserRole_, MJUserResolverBase } from '../generated/generated.js';
4
5
  import { GetReadOnlyProvider } from '../util.js';
6
+ import {
7
+ AssertRoleLoadSucceeded,
8
+ RequireContextUser,
9
+ RequireLoadedUser,
10
+ RequireRoleProvider,
11
+ UserIdExtraFilter,
12
+ } from './currentUserRoles.js';
13
+
14
+ /**
15
+ * CurrentUser payload. Extends the generated user type with a first-class `Roles`
16
+ * field — child-array FieldResolvers (`MJUserRoles_UserIDArray`) are no longer
17
+ * generated. Roles come from the request's `UserInfo` (so magic-link anonymous
18
+ * sessions keep their synthesized roles) or from a RunView on `MJ: User Roles`.
19
+ */
20
+ @ObjectType()
21
+ export class CurrentUserType extends MJUser_ {
22
+ @Field(() => [MJUserRole_])
23
+ Roles: MJUserRole_[];
24
+ }
25
+
26
+ type RoleSource = {
27
+ ID?: string;
28
+ UserID: string;
29
+ RoleID: string;
30
+ RoleName?: string;
31
+ Role?: string;
32
+ __mj_CreatedAt?: Date;
33
+ __mj_UpdatedAt?: Date;
34
+ };
5
35
 
6
36
  @Resolver(MJUser_)
7
37
  export class UserResolver extends MJUserResolverBase {
8
38
 
9
- /**
10
- * Roles for the current user row.
11
- *
12
- * Overrides the generated field resolver to support ANONYMOUS magic-link sessions: those
13
- * resolve to a shared Anonymous principal that holds NO roles in the database (by design —
14
- * persisting them would let anonymous sessions accrete privileges across links). The roles
15
- * are synthesized per-request in {@link buildMagicLinkSessionUser} and live only on the
16
- * request's `UserInfo`. For the session's OWN user row we serve those synthesized roles
17
- * directly, skipping the generated DB query (which returns empty) and its `MJ: User Roles`
18
- * read-permission check (which the restricted anon role wouldn't pass). Every other case —
19
- * named users, named magic-link users, listing other users' roles — falls through to the
20
- * generated resolver unchanged, so there is no behavioral or security change off this path.
21
- */
22
- @FieldResolver(() => [MJUserRole_])
23
- async MJUserRoles_UserIDArray(@Root() mjuser_: MJUser_, @Ctx() context: AppContext, @PubSub() pubSub: PubSubEngine) {
24
- const sessionUser = context.userPayload?.userRecord;
25
- if (sessionUser?.IsMagicLinkAnonymous && UUIDsEqual(mjuser_.ID, sessionUser.ID)) {
26
- const now = new Date();
27
- // Build rows with the entity's RAW field names, then run them through the same
28
- // name→code-name mapping the generated resolver uses (e.g. __mj_CreatedAt →
29
- // _mj__CreatedAt). Returning raw names directly trips GraphQL's non-nullable check.
30
- const rows = (sessionUser.UserRoles ?? []).map((r: { UserID: string; RoleID: string; RoleName?: string; Role?: string }) => ({
31
- ID: sessionUser.ID,
32
- UserID: r.UserID,
33
- RoleID: r.RoleID,
34
- User: sessionUser.Name,
35
- Role: r.RoleName ?? r.Role,
36
- __mj_CreatedAt: now,
37
- __mj_UpdatedAt: now,
38
- }));
39
- return this.ArrayMapFieldNamesToCodeNames('MJ: User Roles', rows, sessionUser);
40
- }
41
- return super.MJUserRoles_UserIDArray(mjuser_, context, pubSub);
42
- }
43
- @Query(() => MJUser_)
44
- async CurrentUser(@Ctx() context: AppContext) {
45
- // Check API key scope authorization for user read (self)
39
+ @Query(() => CurrentUserType)
40
+ async CurrentUser(@Ctx() context: AppContext): Promise<CurrentUserType> {
46
41
  await this.CheckAPIKeyScopeAuthorization('user:read', '*', context.userPayload);
47
42
 
48
- // If userRecord is already available (e.g., from API key auth or system auth),
49
- // use it directly instead of looking up by email again
50
43
  if (context.userPayload.userRecord) {
51
- // Convert UserInfo to the GraphQL response format
52
44
  const userRecord = context.userPayload.userRecord;
53
45
  const userData = {
54
46
  ID: userRecord.ID,
@@ -65,30 +57,29 @@ export class UserResolver extends MJUserResolverBase {
65
57
  LinkedEntityRecordID: userRecord.LinkedEntityRecordID,
66
58
  __mj_CreatedAt: userRecord.__mj_CreatedAt,
67
59
  __mj_UpdatedAt: userRecord.__mj_UpdatedAt,
68
- // Include the UserRoles sub-array for the GraphQL response
69
- MJUserRoles_UserIDArray: userRecord.UserRoles?.map((r: { ID: string; UserID: string; RoleID: string; RoleName?: string; __mj_CreatedAt?: Date; __mj_UpdatedAt?: Date }) => ({
70
- ID: r.ID,
71
- UserID: r.UserID,
72
- RoleID: r.RoleID,
73
- User: userRecord.Name,
74
- Role: r.RoleName,
75
- __mj_CreatedAt: r.__mj_CreatedAt,
76
- __mj_UpdatedAt: r.__mj_UpdatedAt,
77
- })) || [],
78
60
  };
79
61
  console.log('CurrentUser (from userRecord)', userData.Email);
80
- return this.MapFieldNamesToCodeNames('MJ: Users', userData);
62
+ const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
63
+ const mapped = await this.MapFieldNamesToCodeNames('MJ: Users', userData, userRecord, provider) as MJUser_;
64
+ return {
65
+ ...mapped,
66
+ Roles: await this.mapRolesFromUserInfo(userRecord.UserRoles ?? [], userRecord, userRecord, provider),
67
+ };
81
68
  }
82
69
 
83
- // Fall back to email lookup for JWT-based auth
84
- const result = await this.UserByEmail(context.userPayload.email, context);
85
- console.log('CurrentUser (from email lookup)', result?.Email);
86
- return result;
70
+ const result = RequireLoadedUser(
71
+ await this.UserByEmail(context.userPayload.email, context),
72
+ context.userPayload.email,
73
+ );
74
+ console.log('CurrentUser (from email lookup)', result.Email);
75
+ return {
76
+ ...result,
77
+ Roles: await this.loadRolesForUser(result, context),
78
+ };
87
79
  }
88
80
 
89
81
  @Query(() => MJUser_)
90
82
  async UserByID(@Arg('ID', () => Int) ID: number, @Ctx() { providers, userPayload }: AppContext) {
91
- // Check API key scope authorization for user read
92
83
  await this.CheckAPIKeyScopeAuthorization('user:read', ID.toString(), userPayload);
93
84
 
94
85
  const provider = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true})
@@ -98,7 +89,6 @@ export class UserResolver extends MJUserResolverBase {
98
89
 
99
90
  @Query(() => MJUser_)
100
91
  async UserByEmployeeID(@Arg('EmployeeID', () => Int) EmployeeID: number, @Ctx() { providers, userPayload }: AppContext) {
101
- // Check API key scope authorization for user read
102
92
  await this.CheckAPIKeyScopeAuthorization('user:read', EmployeeID.toString(), userPayload);
103
93
 
104
94
  const provider = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true})
@@ -108,14 +98,63 @@ export class UserResolver extends MJUserResolverBase {
108
98
 
109
99
  @Query(() => MJUser_)
110
100
  async UserByEmail(@Arg('Email', () => String) Email: string, @Ctx() { providers, userPayload }: AppContext) {
111
- // Check API key scope authorization for user read
112
101
  await this.CheckAPIKeyScopeAuthorization('user:read', Email, userPayload);
113
102
 
114
- // const searchEmail = userEmailMap[Email] ?? Email;
115
103
  const searchEmail = Email;
116
104
  const provider = GetReadOnlyProvider(providers, {allowFallbackToReadWrite: true})
117
105
  const returnVal = super.safeFirstArrayElement(await this.findBy(provider, 'MJ: Users', { Email: searchEmail }, userPayload.userRecord));
118
106
  return this.MapFieldNamesToCodeNames('MJ: Users', returnVal);
119
107
  }
108
+
109
+ /**
110
+ * Magic-link anonymous sessions hold synthesized roles on UserInfo that are
111
+ * not persisted. Always prefer those when the requested row is the session
112
+ * user. Everyone else loads `MJ: User Roles` via RunView.
113
+ */
114
+ private async loadRolesForUser(user: MJUser_, context: AppContext): Promise<MJUserRole_[]> {
115
+ const contextUser = RequireContextUser(this.GetUserFromPayload(context.userPayload));
116
+
117
+ if (contextUser.IsMagicLinkAnonymous && UUIDsEqual(user.ID, contextUser.ID)) {
118
+ const provider = GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true });
119
+ return await this.mapRolesFromUserInfo(contextUser.UserRoles ?? [], contextUser, contextUser, provider);
120
+ }
121
+
122
+ const provider = RequireRoleProvider(
123
+ GetReadOnlyProvider(context.providers, { allowFallbackToReadWrite: true }),
124
+ );
125
+ const rv = RunView.FromMetadataProvider(provider);
126
+ const result = await rv.RunView<RoleSource>({
127
+ EntityName: 'MJ: User Roles',
128
+ ExtraFilter: UserIdExtraFilter(user.ID),
129
+ ResultType: 'simple',
130
+ }, contextUser);
131
+
132
+ AssertRoleLoadSucceeded(result, user.ID);
133
+ return await this.ArrayMapFieldNamesToCodeNames(
134
+ 'MJ: User Roles',
135
+ result.Results ?? [],
136
+ contextUser,
137
+ provider,
138
+ ) as MJUserRole_[];
139
+ }
140
+
141
+ private async mapRolesFromUserInfo(
142
+ roles: RoleSource[],
143
+ user: { ID: string; Name?: string },
144
+ contextUser?: UserInfo,
145
+ provider?: IMetadataProvider,
146
+ ): Promise<MJUserRole_[]> {
147
+ const now = new Date();
148
+ const rows = roles.map((r) => ({
149
+ ID: r.ID ?? user.ID,
150
+ UserID: r.UserID,
151
+ RoleID: r.RoleID,
152
+ User: user.Name,
153
+ Role: r.RoleName ?? r.Role,
154
+ __mj_CreatedAt: r.__mj_CreatedAt ?? now,
155
+ __mj_UpdatedAt: r.__mj_UpdatedAt ?? now,
156
+ }));
157
+ return await this.ArrayMapFieldNamesToCodeNames('MJ: User Roles', rows, contextUser, provider) as MJUserRole_[];
158
+ }
120
159
  }
121
160
  export default UserResolver;
@@ -0,0 +1,47 @@
1
+ import { LogError } from '@memberjunction/core';
2
+
3
+ /**
4
+ * Guards and filter builders for CurrentUser role loading.
5
+ * Kept out of UserResolver so the login-adjacent failure modes are unit-testable
6
+ * without importing generated GraphQL types or the Apollo config bootstrap.
7
+ */
8
+
9
+ export function RequireLoadedUser<T extends { ID?: string }>(
10
+ user: T | null | undefined,
11
+ email: string | undefined,
12
+ ): T & { ID: string } {
13
+ if (!user?.ID) {
14
+ throw new Error(`CurrentUser: no user found for email '${email ?? ''}'`);
15
+ }
16
+ return user as T & { ID: string };
17
+ }
18
+
19
+ export function RequireContextUser<T>(user: T | null | undefined): T {
20
+ if (!user) {
21
+ throw new Error('CurrentUser: authenticated context user is required to load roles');
22
+ }
23
+ return user;
24
+ }
25
+
26
+ export function RequireRoleProvider<T>(provider: T | null | undefined): T {
27
+ if (!provider) {
28
+ throw new Error('CurrentUser: no data provider available to load roles');
29
+ }
30
+ return provider;
31
+ }
32
+
33
+ /** Quote-escape a server-resolved user id for an ExtraFilter literal. */
34
+ export function UserIdExtraFilter(userId: string): string {
35
+ return `UserID='${userId.replace(/'/g, "''")}'`;
36
+ }
37
+
38
+ export function AssertRoleLoadSucceeded(
39
+ result: { Success: boolean; ErrorMessage?: string | null },
40
+ userId: string,
41
+ ): void {
42
+ if (!result.Success) {
43
+ const message = result.ErrorMessage ?? 'unknown error';
44
+ LogError(`CurrentUser: failed to load roles for user ${userId}: ${message}`);
45
+ throw new Error(`CurrentUser: failed to load roles: ${message}`);
46
+ }
47
+ }