@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.
- package/LICENSE +180 -4
- package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.js +69 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
- package/dist/auth/AuthProviderEngine.d.ts +96 -0
- package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
- package/dist/auth/AuthProviderEngine.js +245 -0
- package/dist/auth/AuthProviderEngine.js.map +1 -0
- package/dist/auth/index.d.ts +11 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +56 -13
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.d.ts +64 -1
- package/dist/auth/initializeProviders.d.ts.map +1 -1
- package/dist/auth/initializeProviders.js +117 -5
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/config.d.ts +89 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +52 -35
- package/dist/config.js.map +1 -1
- package/dist/generated/generated.d.ts +501 -1597
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2711 -17021
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +7 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +31 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -14
- package/dist/index.js.map +1 -1
- package/dist/integration/RSUProgressBridge.d.ts +83 -0
- package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
- package/dist/integration/RSUProgressBridge.js +230 -0
- package/dist/integration/RSUProgressBridge.js.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
- package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
- package/dist/integration/SchemaRefreshLaunch.js +50 -0
- package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
- package/dist/providerConfigUnits.d.ts +20 -0
- package/dist/providerConfigUnits.d.ts.map +1 -0
- package/dist/providerConfigUnits.js +22 -0
- package/dist/providerConfigUnits.js.map +1 -0
- package/dist/resolvers/FileResolver.d.ts +34 -4
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +218 -28
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +76 -3
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +299 -81
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +5 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +33 -14
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -7
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts +19 -17
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +66 -67
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/currentUserRoles.d.ts +19 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
- package/dist/resolvers/currentUserRoles.js +36 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +17 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
- package/dist/rest/MediaAccessKeys.js +42 -1
- package/dist/rest/MediaAccessKeys.js.map +1 -1
- package/dist/rest/MediaStreamHandler.d.ts +3 -2
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
- package/dist/rest/MediaStreamHandler.js +83 -2
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
- package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
- package/dist/rest/OAuthCallbackHandler.js +129 -43
- package/dist/rest/OAuthCallbackHandler.js.map +1 -1
- package/dist/rest/UploadTokenManager.d.ts +90 -0
- package/dist/rest/UploadTokenManager.d.ts.map +1 -0
- package/dist/rest/UploadTokenManager.js +156 -0
- package/dist/rest/UploadTokenManager.js.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
- package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
- package/dist/services/IntegrationSyncWorkerService.js +132 -0
- package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
- package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
- package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +21 -1
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +7 -0
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +23 -0
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +91 -91
- package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
- package/src/__tests__/AuthProviderEngine.test.ts +278 -0
- package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
- package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
- package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
- package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
- package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
- package/src/__tests__/RSUProgressBridge.test.ts +250 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
- package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
- package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
- package/src/__tests__/config-units.test.ts +74 -0
- package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
- package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
- package/src/__tests__/newUsers.test.ts +726 -0
- package/src/__tests__/uploadTokenManager.test.ts +159 -0
- package/src/auth/AuthProviderCatalogRouter.ts +76 -0
- package/src/auth/AuthProviderEngine.ts +253 -0
- package/src/auth/index.ts +59 -15
- package/src/auth/initializeProviders.ts +128 -7
- package/src/config.ts +55 -36
- package/src/generated/generated.ts +1911 -10551
- package/src/generic/ResolverBase.ts +11 -4
- package/src/generic/RunViewResolver.ts +31 -0
- package/src/index.ts +108 -15
- package/src/integration/RSUProgressBridge.ts +248 -0
- package/src/integration/SchemaRefreshLaunch.ts +77 -0
- package/src/providerConfigUnits.ts +21 -0
- package/src/resolvers/FileResolver.ts +200 -40
- package/src/resolvers/IntegrationDiscoveryResolver.ts +316 -68
- package/src/resolvers/QueryResolver.ts +25 -11
- package/src/resolvers/QuerySystemUserResolver.ts +28 -13
- package/src/resolvers/RunAIAgentResolver.ts +21 -6
- package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
- package/src/resolvers/UserResolver.ts +99 -60
- package/src/resolvers/currentUserRoles.ts +47 -0
- package/src/rest/MediaAccessKeys.ts +47 -1
- package/src/rest/MediaStreamHandler.ts +98 -3
- package/src/rest/OAuthCallbackHandler.ts +149 -41
- package/src/rest/UploadTokenManager.ts +211 -0
- package/src/services/IntegrationSyncWorkerService.ts +141 -0
- package/src/services/StartTaskGraphDispatcher.ts +21 -1
- package/src/services/TaskGraphAgentRunner.ts +23 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
186
|
-
*
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|