@memberjunction/server 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -1
- package/dist/auth/APIKeyScopeAuth.d.ts +51 -0
- package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -0
- package/dist/auth/APIKeyScopeAuth.js +163 -0
- package/dist/auth/APIKeyScopeAuth.js.map +1 -0
- package/dist/auth/BaseAuthProvider.d.ts +1 -0
- package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/BaseAuthProvider.js +2 -0
- package/dist/auth/BaseAuthProvider.js.map +1 -1
- package/dist/auth/IAuthProvider.d.ts +1 -0
- package/dist/auth/IAuthProvider.d.ts.map +1 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +44 -7
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +681 -2
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +10627 -6409
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +3 -2
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +52 -4
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +29 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +143 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/resolvers/APIKeyResolver.d.ts +24 -0
- package/dist/resolvers/APIKeyResolver.d.ts.map +1 -0
- package/dist/resolvers/APIKeyResolver.js +194 -0
- package/dist/resolvers/APIKeyResolver.js.map +1 -0
- package/dist/resolvers/ActionResolver.d.ts +2 -1
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +4 -1
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +5 -4
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +7 -4
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts +2 -1
- package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityCommunicationsResolver.js +3 -1
- package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +2 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +10 -3
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/MCPResolver.d.ts +37 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -0
- package/dist/resolvers/MCPResolver.js +363 -0
- package/dist/resolvers/MCPResolver.js.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts +2 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +3 -1
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +2 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +6 -1
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/ReportResolver.d.ts +2 -1
- package/dist/resolvers/ReportResolver.d.ts.map +1 -1
- package/dist/resolvers/ReportResolver.js +4 -1
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +3 -1
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +3 -0
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +1 -0
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/TaskResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskResolver.js +1 -0
- package/dist/resolvers/TaskResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +35 -1
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +47 -45
- package/src/auth/APIKeyScopeAuth.ts +366 -0
- package/src/auth/BaseAuthProvider.ts +3 -0
- package/src/auth/IAuthProvider.ts +5 -0
- package/src/auth/index.ts +1 -0
- package/src/config.ts +2 -2
- package/src/context.ts +91 -9
- package/src/generated/generated.ts +6327 -3668
- package/src/generic/ResolverBase.ts +127 -8
- package/src/generic/RunViewResolver.ts +132 -5
- package/src/index.ts +12 -2
- package/src/resolvers/APIKeyResolver.ts +241 -0
- package/src/resolvers/ActionResolver.ts +8 -1
- package/src/resolvers/DatasetResolver.ts +11 -4
- package/src/resolvers/EntityCommunicationsResolver.ts +5 -1
- package/src/resolvers/GetDataContextDataResolver.ts +14 -6
- package/src/resolvers/MCPResolver.ts +480 -0
- package/src/resolvers/MergeRecordsResolver.ts +5 -1
- package/src/resolvers/QueryResolver.ts +17 -3
- package/src/resolvers/ReportResolver.ts +8 -1
- package/src/resolvers/RunAIAgentResolver.ts +7 -1
- package/src/resolvers/RunAIPromptResolver.ts +10 -1
- package/src/resolvers/RunTemplateResolver.ts +4 -1
- package/src/resolvers/TaskResolver.ts +3 -0
- package/src/resolvers/UserResolver.ts +52 -4
- package/src/types.ts +7 -2
- package/dist/resolvers/AskSkipResolver.d.ts +0 -123
- package/dist/resolvers/AskSkipResolver.d.ts.map +0 -1
- package/dist/resolvers/AskSkipResolver.js +0 -1788
- package/dist/resolvers/AskSkipResolver.js.map +0 -1
- package/dist/scheduler/LearningCycleScheduler.d.ts +0 -4
- package/dist/scheduler/LearningCycleScheduler.d.ts.map +0 -1
- package/dist/scheduler/LearningCycleScheduler.js +0 -4
- package/dist/scheduler/LearningCycleScheduler.js.map +0 -1
- package/src/resolvers/AskSkipResolver.ts +0 -3446
- package/src/scheduler/LearningCycleScheduler.ts +0 -320
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AggregateExpression,
|
|
2
3
|
BaseEntity,
|
|
3
4
|
BaseEntityEvent,
|
|
4
5
|
CompositeKey,
|
|
@@ -18,8 +19,9 @@ import {
|
|
|
18
19
|
} from '@memberjunction/core';
|
|
19
20
|
import { AuditLogEntity, ErrorLogEntity, UserViewEntityExtended } from '@memberjunction/core-entities';
|
|
20
21
|
import { SQLServerDataProvider, UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
21
|
-
import { PubSubEngine } from 'type-graphql';
|
|
22
|
+
import { PubSubEngine, AuthorizationError } from 'type-graphql';
|
|
22
23
|
import { GraphQLError } from 'graphql';
|
|
24
|
+
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
23
25
|
import sql from 'mssql';
|
|
24
26
|
import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
|
|
25
27
|
|
|
@@ -266,6 +268,11 @@ export class ResolverBase {
|
|
|
266
268
|
|
|
267
269
|
async RunViewByNameGeneric(viewInput: RunViewByNameInput, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
268
270
|
try {
|
|
271
|
+
// Log aggregate input for debugging
|
|
272
|
+
if (viewInput.Aggregates?.length) {
|
|
273
|
+
LogStatus(`[ResolverBase] RunViewByNameGeneric received aggregates: viewName=${viewInput.ViewName}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
269
276
|
const rv = provider as any as IRunViewProvider;
|
|
270
277
|
const result = await rv.RunView<UserViewEntityExtended>({
|
|
271
278
|
EntityName: 'User Views',
|
|
@@ -290,7 +297,8 @@ export class ResolverBase {
|
|
|
290
297
|
viewInput.ResultType,
|
|
291
298
|
userPayload,
|
|
292
299
|
viewInput.MaxRows,
|
|
293
|
-
viewInput.StartRow
|
|
300
|
+
viewInput.StartRow,
|
|
301
|
+
viewInput.Aggregates
|
|
294
302
|
);
|
|
295
303
|
}
|
|
296
304
|
else {
|
|
@@ -305,6 +313,11 @@ export class ResolverBase {
|
|
|
305
313
|
|
|
306
314
|
async RunViewByIDGeneric(viewInput: RunViewByIDInput, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
307
315
|
try {
|
|
316
|
+
// Log aggregate input for debugging
|
|
317
|
+
if (viewInput.Aggregates?.length) {
|
|
318
|
+
LogStatus(`[ResolverBase] RunViewByIDGeneric received aggregates: viewID=${viewInput.ViewID}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
308
321
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
309
322
|
const viewInfo = await provider.GetEntityObject<UserViewEntityExtended>('User Views', contextUser);
|
|
310
323
|
await viewInfo.Load(viewInput.ViewID);
|
|
@@ -325,7 +338,8 @@ export class ResolverBase {
|
|
|
325
338
|
viewInput.ResultType,
|
|
326
339
|
userPayload,
|
|
327
340
|
viewInput.MaxRows,
|
|
328
|
-
viewInput.StartRow
|
|
341
|
+
viewInput.StartRow,
|
|
342
|
+
viewInput.Aggregates
|
|
329
343
|
);
|
|
330
344
|
} catch (err) {
|
|
331
345
|
console.log(err);
|
|
@@ -335,6 +349,11 @@ export class ResolverBase {
|
|
|
335
349
|
|
|
336
350
|
async RunDynamicViewGeneric(viewInput: RunDynamicViewInput, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
337
351
|
try {
|
|
352
|
+
// Log aggregate input for debugging
|
|
353
|
+
if (viewInput.Aggregates?.length) {
|
|
354
|
+
LogStatus(`[ResolverBase] RunDynamicViewGeneric received aggregates: entityName=${viewInput.EntityName}, aggregateCount=${viewInput.Aggregates.length}, aggregates=${JSON.stringify(viewInput.Aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
338
357
|
const md = provider;
|
|
339
358
|
const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
|
|
340
359
|
if (!entity) throw new Error(`Entity ${viewInput.EntityName} not found in metadata`);
|
|
@@ -363,7 +382,8 @@ export class ResolverBase {
|
|
|
363
382
|
viewInput.ResultType,
|
|
364
383
|
userPayload,
|
|
365
384
|
viewInput.MaxRows,
|
|
366
|
-
viewInput.StartRow
|
|
385
|
+
viewInput.StartRow,
|
|
386
|
+
viewInput.Aggregates
|
|
367
387
|
);
|
|
368
388
|
} catch (err) {
|
|
369
389
|
console.log(err);
|
|
@@ -422,7 +442,8 @@ export class ResolverBase {
|
|
|
422
442
|
forceAuditLog: viewInput.ForceAuditLog,
|
|
423
443
|
auditLogDescription: viewInput.AuditLogDescription,
|
|
424
444
|
resultType: viewInput.ResultType,
|
|
425
|
-
userPayload,
|
|
445
|
+
userPayload,
|
|
446
|
+
aggregates: viewInput.Aggregates,
|
|
426
447
|
});
|
|
427
448
|
} catch (err) {
|
|
428
449
|
LogError(err);
|
|
@@ -484,7 +505,7 @@ export class ResolverBase {
|
|
|
484
505
|
if (!userPayload) {
|
|
485
506
|
throw new Error(`userPayload is null`);
|
|
486
507
|
}
|
|
487
|
-
|
|
508
|
+
|
|
488
509
|
// first check permissions, the logged in user must have read permissions on the entity to run the view
|
|
489
510
|
if (entityInfo) {
|
|
490
511
|
const userInfo = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload.email.toLowerCase().trim()); // get the user record from MD so we have ROLES attached, don't use the one from payload directly
|
|
@@ -496,12 +517,85 @@ export class ResolverBase {
|
|
|
496
517
|
if (!userPermissions.CanRead) {
|
|
497
518
|
throw new Error(`User ${userPayload.email} does not have read permissions on ${entityInfo.Name}`);
|
|
498
519
|
}
|
|
499
|
-
}
|
|
520
|
+
}
|
|
500
521
|
else {
|
|
501
522
|
throw new Error(`Entity not found in metadata`);
|
|
502
523
|
}
|
|
503
524
|
}
|
|
504
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Checks API key scope authorization. Only performs check if request
|
|
528
|
+
* was authenticated via API key (apiKeyHash present in userPayload).
|
|
529
|
+
* For OAuth/JWT auth, this is a no-op.
|
|
530
|
+
*
|
|
531
|
+
* @param scopePath - The scope path (e.g., 'entity:read', 'agent:execute')
|
|
532
|
+
* @param resource - The resource name (e.g., entity name, agent name)
|
|
533
|
+
* @param userPayload - The user payload from context
|
|
534
|
+
* @throws AuthorizationError if API key lacks required scope
|
|
535
|
+
*/
|
|
536
|
+
protected async CheckAPIKeyScopeAuthorization(
|
|
537
|
+
scopePath: string,
|
|
538
|
+
resource: string,
|
|
539
|
+
userPayload: UserPayload
|
|
540
|
+
): Promise<void> {
|
|
541
|
+
// Skip scope check for OAuth/JWT auth (no API key)
|
|
542
|
+
if (!userPayload.apiKeyHash) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Get system user for authorization call
|
|
547
|
+
// NOTE: We use system user here because Authorize() needs to run internal
|
|
548
|
+
// database queries (loading scope rules, logging decisions). The system user
|
|
549
|
+
// ensures these queries work regardless of what permissions the API key's
|
|
550
|
+
// user has. The API key's associated user (in userPayload.userRecord) is
|
|
551
|
+
// used later when the actual operation executes - their permissions are
|
|
552
|
+
// the ultimate ceiling that scopes can only narrow, never expand.
|
|
553
|
+
const systemUser = UserCache.Instance.Users.find(u => u.Type === 'System');
|
|
554
|
+
if (!systemUser) {
|
|
555
|
+
throw new Error('System user not found');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const apiKeyEngine = GetAPIKeyEngine();
|
|
559
|
+
|
|
560
|
+
// Check for full_access scope first (god power - bypasses all other checks)
|
|
561
|
+
const fullAccessResult = await apiKeyEngine.Authorize(
|
|
562
|
+
userPayload.apiKeyHash,
|
|
563
|
+
'MJAPI',
|
|
564
|
+
'full_access',
|
|
565
|
+
'*',
|
|
566
|
+
systemUser,
|
|
567
|
+
{ endpoint: '/graphql', method: 'POST' }
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
if (fullAccessResult.Allowed) {
|
|
571
|
+
// full_access granted - skip specific scope check
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Check specific scope
|
|
576
|
+
const result = await apiKeyEngine.Authorize(
|
|
577
|
+
userPayload.apiKeyHash,
|
|
578
|
+
'MJAPI',
|
|
579
|
+
scopePath,
|
|
580
|
+
resource,
|
|
581
|
+
systemUser,
|
|
582
|
+
{
|
|
583
|
+
endpoint: '/graphql',
|
|
584
|
+
method: 'POST'
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
if (!result.Allowed) {
|
|
589
|
+
// Provide specific, actionable error message
|
|
590
|
+
throw new AuthorizationError(
|
|
591
|
+
`Access denied. This API key requires the '${scopePath}' scope ` +
|
|
592
|
+
`for resource '${resource}' to perform this operation. ` +
|
|
593
|
+
`Please update the API key's scopes or use an API key with appropriate permissions. ` +
|
|
594
|
+
`Denial reason: ${result.Reason}`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
505
599
|
/**
|
|
506
600
|
* Optimized RunViewGenericInternal implementation with:
|
|
507
601
|
* - Field filtering at source (Fix #7)
|
|
@@ -524,11 +618,15 @@ export class ResolverBase {
|
|
|
524
618
|
resultType: string | undefined,
|
|
525
619
|
userPayload: UserPayload | null,
|
|
526
620
|
maxRows: number | undefined,
|
|
527
|
-
startRow: number | undefined
|
|
621
|
+
startRow: number | undefined,
|
|
622
|
+
aggregates?: AggregateExpression[]
|
|
528
623
|
) {
|
|
529
624
|
try {
|
|
530
625
|
if (!viewInfo || !userPayload) return null;
|
|
531
626
|
|
|
627
|
+
// Check API key scope authorization for view operations
|
|
628
|
+
await this.CheckAPIKeyScopeAuthorization('view:run', viewInfo.Entity, userPayload);
|
|
629
|
+
|
|
532
630
|
const md = provider
|
|
533
631
|
const user = UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
534
632
|
if (!user) throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
@@ -559,6 +657,11 @@ export class ResolverBase {
|
|
|
559
657
|
}
|
|
560
658
|
}
|
|
561
659
|
|
|
660
|
+
// Log aggregate request for debugging
|
|
661
|
+
if (aggregates?.length) {
|
|
662
|
+
LogStatus(`[ResolverBase] RunViewGenericInternal with aggregates: entityName=${viewInfo.Entity}, viewName=${viewInfo.Name}, aggregateCount=${aggregates.length}, aggregates=${JSON.stringify(aggregates.map(a => ({ expression: a.expression, alias: a.alias })))}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
562
665
|
const result = await rv.RunView(
|
|
563
666
|
{
|
|
564
667
|
ViewID: viewInfo.ID,
|
|
@@ -578,10 +681,16 @@ export class ResolverBase {
|
|
|
578
681
|
ForceAuditLog: forceAuditLog,
|
|
579
682
|
AuditLogDescription: auditLogDescription,
|
|
580
683
|
ResultType: rt,
|
|
684
|
+
Aggregates: aggregates,
|
|
581
685
|
},
|
|
582
686
|
user
|
|
583
687
|
);
|
|
584
688
|
|
|
689
|
+
// Log aggregate results for debugging
|
|
690
|
+
if (aggregates?.length) {
|
|
691
|
+
LogStatus(`[ResolverBase] RunView result aggregate info: entityName=${viewInfo.Entity}, hasAggregateResults=${!!result?.AggregateResults}, aggregateResultCount=${result?.AggregateResults?.length || 0}, aggregateExecutionTime=${result?.AggregateExecutionTime}, aggregateResults=${JSON.stringify(result?.AggregateResults)}`);
|
|
692
|
+
}
|
|
693
|
+
|
|
585
694
|
// Process results for GraphQL transport
|
|
586
695
|
const mapper = new FieldMapper();
|
|
587
696
|
if (result?.Success && result.Results?.length) {
|
|
@@ -682,6 +791,7 @@ export class ResolverBase {
|
|
|
682
791
|
ForceAuditLog: param.forceAuditLog,
|
|
683
792
|
AuditLogDescription: param.auditLogDescription,
|
|
684
793
|
ResultType: rt,
|
|
794
|
+
Aggregates: param.aggregates,
|
|
685
795
|
});
|
|
686
796
|
}
|
|
687
797
|
|
|
@@ -875,6 +985,9 @@ export class ResolverBase {
|
|
|
875
985
|
}
|
|
876
986
|
|
|
877
987
|
protected async CreateRecord(entityName: string, input: any, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
988
|
+
// Check API key scope authorization for entity create operations
|
|
989
|
+
await this.CheckAPIKeyScopeAuthorization('entity:create', entityName, userPayload);
|
|
990
|
+
|
|
878
991
|
if (await this.BeforeCreate(provider, input)) {
|
|
879
992
|
// fire event and proceed if it wasn't cancelled
|
|
880
993
|
const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
|
|
@@ -907,6 +1020,9 @@ export class ResolverBase {
|
|
|
907
1020
|
protected async AfterCreate(provider: DatabaseProviderBase, input: any) {}
|
|
908
1021
|
|
|
909
1022
|
protected async UpdateRecord(entityName: string, input: any, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
1023
|
+
// Check API key scope authorization for entity update operations
|
|
1024
|
+
await this.CheckAPIKeyScopeAuthorization('entity:update', entityName, userPayload);
|
|
1025
|
+
|
|
910
1026
|
if (await this.BeforeUpdate(provider, input)) {
|
|
911
1027
|
// fire event and proceed if it wasn't cancelled
|
|
912
1028
|
const userInfo = this.GetUserFromPayload(userPayload);
|
|
@@ -1169,6 +1285,9 @@ export class ResolverBase {
|
|
|
1169
1285
|
userPayload: UserPayload,
|
|
1170
1286
|
pubSub: PubSubEngine
|
|
1171
1287
|
) {
|
|
1288
|
+
// Check API key scope authorization for entity delete operations
|
|
1289
|
+
await this.CheckAPIKeyScopeAuthorization('entity:delete', entityName, userPayload);
|
|
1290
|
+
|
|
1172
1291
|
if (await this.BeforeDelete(provider, key)) {
|
|
1173
1292
|
// fire event and proceed if it wasn't cancelled
|
|
1174
1293
|
const entityObject = await provider.GetEntityObject(entityName, this.GetUserFromPayload(userPayload));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, Int, ObjectType, PubSubEngine, Query, Resolver } from 'type-graphql';
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
3
|
import { ResolverBase } from './ResolverBase.js';
|
|
4
|
-
import { LogError, LogStatus, EntityInfo, RunViewWithCacheCheckResult, RunViewsWithCacheCheckResponse, RunViewWithCacheCheckParams } from '@memberjunction/core';
|
|
4
|
+
import { LogError, LogStatus, EntityInfo, RunViewWithCacheCheckResult, RunViewsWithCacheCheckResponse, RunViewWithCacheCheckParams, AggregateResult } from '@memberjunction/core';
|
|
5
5
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
6
6
|
import { GetReadOnlyProvider } from '../util.js';
|
|
7
7
|
import { UserViewEntityExtended } from '@memberjunction/core-entities';
|
|
@@ -15,6 +15,52 @@ import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
|
15
15
|
* back the results, and have your own type checking in place, this resolver can be used.
|
|
16
16
|
*
|
|
17
17
|
*/
|
|
18
|
+
|
|
19
|
+
//****************************************************************************
|
|
20
|
+
// INPUT/OUTPUT TYPE for Aggregates
|
|
21
|
+
//****************************************************************************
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Input type for a single aggregate expression
|
|
25
|
+
*/
|
|
26
|
+
@InputType()
|
|
27
|
+
export class AggregateExpressionInput {
|
|
28
|
+
@Field(() => String, {
|
|
29
|
+
description: 'SQL expression for the aggregate (e.g., "SUM(OrderTotal)", "COUNT(*)", "AVG(Price)")'
|
|
30
|
+
})
|
|
31
|
+
expression: string;
|
|
32
|
+
|
|
33
|
+
@Field(() => String, {
|
|
34
|
+
nullable: true,
|
|
35
|
+
description: 'Optional alias for the result (used in error messages and debugging)'
|
|
36
|
+
})
|
|
37
|
+
alias?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Output type for a single aggregate result
|
|
42
|
+
*/
|
|
43
|
+
@ObjectType()
|
|
44
|
+
export class AggregateResultOutput {
|
|
45
|
+
@Field(() => String, { description: 'The expression that was calculated' })
|
|
46
|
+
expression: string;
|
|
47
|
+
|
|
48
|
+
@Field(() => String, { description: 'The alias (or expression if no alias provided)' })
|
|
49
|
+
alias: string;
|
|
50
|
+
|
|
51
|
+
@Field(() => String, {
|
|
52
|
+
nullable: true,
|
|
53
|
+
description: 'The calculated value as a JSON string (preserves type information)'
|
|
54
|
+
})
|
|
55
|
+
value?: string;
|
|
56
|
+
|
|
57
|
+
@Field(() => String, {
|
|
58
|
+
nullable: true,
|
|
59
|
+
description: 'Error message if calculation failed'
|
|
60
|
+
})
|
|
61
|
+
error?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
18
64
|
//****************************************************************************
|
|
19
65
|
// INPUT TYPE for Running Views
|
|
20
66
|
//****************************************************************************
|
|
@@ -111,6 +157,12 @@ export class RunViewByIDInput {
|
|
|
111
157
|
description: 'If a value > 0 is provided, this value will be used to offset the rows returned.',
|
|
112
158
|
})
|
|
113
159
|
StartRow?: number;
|
|
160
|
+
|
|
161
|
+
@Field(() => [AggregateExpressionInput], {
|
|
162
|
+
nullable: true,
|
|
163
|
+
description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
|
|
164
|
+
})
|
|
165
|
+
Aggregates?: AggregateExpressionInput[];
|
|
114
166
|
}
|
|
115
167
|
|
|
116
168
|
@InputType()
|
|
@@ -206,6 +258,12 @@ export class RunViewByNameInput {
|
|
|
206
258
|
description: 'If a value > 0 is provided, this value will be used to offset the rows returned.',
|
|
207
259
|
})
|
|
208
260
|
StartRow?: number;
|
|
261
|
+
|
|
262
|
+
@Field(() => [AggregateExpressionInput], {
|
|
263
|
+
nullable: true,
|
|
264
|
+
description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
|
|
265
|
+
})
|
|
266
|
+
Aggregates?: AggregateExpressionInput[];
|
|
209
267
|
}
|
|
210
268
|
|
|
211
269
|
@InputType()
|
|
@@ -287,6 +345,12 @@ export class RunDynamicViewInput {
|
|
|
287
345
|
description: 'If a value > 0 is provided, this value will be used to offset the rows returned.',
|
|
288
346
|
})
|
|
289
347
|
StartRow?: number;
|
|
348
|
+
|
|
349
|
+
@Field(() => [AggregateExpressionInput], {
|
|
350
|
+
nullable: true,
|
|
351
|
+
description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
|
|
352
|
+
})
|
|
353
|
+
Aggregates?: AggregateExpressionInput[];
|
|
290
354
|
}
|
|
291
355
|
|
|
292
356
|
@InputType()
|
|
@@ -382,6 +446,12 @@ export class RunViewGenericInput {
|
|
|
382
446
|
description: 'If a value > 0 is provided, this value will be used to offset the rows returned.',
|
|
383
447
|
})
|
|
384
448
|
StartRow?: number;
|
|
449
|
+
|
|
450
|
+
@Field(() => [AggregateExpressionInput], {
|
|
451
|
+
nullable: true,
|
|
452
|
+
description: 'Optional aggregate expressions to calculate on the full result set (e.g., SUM, COUNT, AVG). Results are returned in AggregateResults.',
|
|
453
|
+
})
|
|
454
|
+
Aggregates?: AggregateExpressionInput[];
|
|
385
455
|
}
|
|
386
456
|
|
|
387
457
|
//****************************************************************************
|
|
@@ -518,6 +588,18 @@ export class RunViewResult {
|
|
|
518
588
|
|
|
519
589
|
@Field(() => Boolean, { nullable: false })
|
|
520
590
|
Success: boolean;
|
|
591
|
+
|
|
592
|
+
@Field(() => [AggregateResultOutput], {
|
|
593
|
+
nullable: true,
|
|
594
|
+
description: 'Results of aggregate calculations, in same order as input Aggregates array. Only present if Aggregates were requested.'
|
|
595
|
+
})
|
|
596
|
+
AggregateResults?: AggregateResultOutput[];
|
|
597
|
+
|
|
598
|
+
@Field(() => Int, {
|
|
599
|
+
nullable: true,
|
|
600
|
+
description: 'Execution time for aggregate query specifically (in milliseconds). Only present if Aggregates were requested.'
|
|
601
|
+
})
|
|
602
|
+
AggregateExecutionTime?: number;
|
|
521
603
|
}
|
|
522
604
|
|
|
523
605
|
@ObjectType()
|
|
@@ -542,6 +624,18 @@ export class RunViewGenericResult {
|
|
|
542
624
|
|
|
543
625
|
@Field(() => Boolean, { nullable: false })
|
|
544
626
|
Success: boolean;
|
|
627
|
+
|
|
628
|
+
@Field(() => [AggregateResultOutput], {
|
|
629
|
+
nullable: true,
|
|
630
|
+
description: 'Results of aggregate calculations, in same order as input Aggregates array. Only present if Aggregates were requested.'
|
|
631
|
+
})
|
|
632
|
+
AggregateResults?: AggregateResultOutput[];
|
|
633
|
+
|
|
634
|
+
@Field(() => Int, {
|
|
635
|
+
nullable: true,
|
|
636
|
+
description: 'Execution time for aggregate query specifically (in milliseconds). Only present if Aggregates were requested.'
|
|
637
|
+
})
|
|
638
|
+
AggregateExecutionTime?: number;
|
|
545
639
|
}
|
|
546
640
|
|
|
547
641
|
@Resolver(RunViewResultRow)
|
|
@@ -567,6 +661,9 @@ export class RunViewResolver extends ResolverBase {
|
|
|
567
661
|
RowCount: rawData?.RowCount,
|
|
568
662
|
TotalRowCount: rawData?.TotalRowCount,
|
|
569
663
|
ExecutionTime: rawData?.ExecutionTime,
|
|
664
|
+
Success: rawData?.Success,
|
|
665
|
+
AggregateResults: this.processAggregateResults(rawData?.AggregateResults),
|
|
666
|
+
AggregateExecutionTime: rawData?.AggregateExecutionTime,
|
|
570
667
|
};
|
|
571
668
|
} catch (err) {
|
|
572
669
|
console.log(err);
|
|
@@ -595,6 +692,9 @@ export class RunViewResolver extends ResolverBase {
|
|
|
595
692
|
RowCount: rawData?.RowCount,
|
|
596
693
|
TotalRowCount: rawData?.TotalRowCount,
|
|
597
694
|
ExecutionTime: rawData?.ExecutionTime,
|
|
695
|
+
Success: rawData?.Success,
|
|
696
|
+
AggregateResults: this.processAggregateResults(rawData?.AggregateResults),
|
|
697
|
+
AggregateExecutionTime: rawData?.AggregateExecutionTime,
|
|
598
698
|
};
|
|
599
699
|
} catch (err) {
|
|
600
700
|
console.log(err);
|
|
@@ -621,6 +721,9 @@ export class RunViewResolver extends ResolverBase {
|
|
|
621
721
|
RowCount: rawData?.RowCount,
|
|
622
722
|
TotalRowCount: rawData?.TotalRowCount,
|
|
623
723
|
ExecutionTime: rawData?.ExecutionTime,
|
|
724
|
+
Success: rawData?.Success,
|
|
725
|
+
AggregateResults: this.processAggregateResults(rawData?.AggregateResults),
|
|
726
|
+
AggregateExecutionTime: rawData?.AggregateExecutionTime,
|
|
624
727
|
};
|
|
625
728
|
} catch (err) {
|
|
626
729
|
console.log(err);
|
|
@@ -636,7 +739,8 @@ export class RunViewResolver extends ResolverBase {
|
|
|
636
739
|
) {
|
|
637
740
|
try {
|
|
638
741
|
const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
|
|
639
|
-
|
|
742
|
+
// Note: RunViewsGeneric returns the core RunViewResult type, not the GraphQL type
|
|
743
|
+
const rawData = await super.RunViewsGeneric(input, provider, userPayload);
|
|
640
744
|
if (!rawData) {
|
|
641
745
|
return null;
|
|
642
746
|
}
|
|
@@ -653,6 +757,8 @@ export class RunViewResolver extends ResolverBase {
|
|
|
653
757
|
TotalRowCount: data?.TotalRowCount,
|
|
654
758
|
ExecutionTime: data?.ExecutionTime,
|
|
655
759
|
Success: data?.Success,
|
|
760
|
+
AggregateResults: this.processAggregateResults(data?.AggregateResults),
|
|
761
|
+
AggregateExecutionTime: data?.AggregateExecutionTime,
|
|
656
762
|
});
|
|
657
763
|
}
|
|
658
764
|
|
|
@@ -824,7 +930,7 @@ export class RunViewResolver extends ResolverBase {
|
|
|
824
930
|
) {
|
|
825
931
|
try {
|
|
826
932
|
const provider = GetReadOnlyProvider(providers, { allowFallbackToReadWrite: true });
|
|
827
|
-
const rawData
|
|
933
|
+
const rawData = await super.RunViewsGeneric(input, provider, userPayload);
|
|
828
934
|
if (!rawData) {
|
|
829
935
|
return null;
|
|
830
936
|
}
|
|
@@ -846,6 +952,8 @@ export class RunViewResolver extends ResolverBase {
|
|
|
846
952
|
ExecutionTime: data?.ExecutionTime,
|
|
847
953
|
Success: data?.Success,
|
|
848
954
|
ErrorMessage: data?.ErrorMessage,
|
|
955
|
+
AggregateResults: this.processAggregateResults(data?.AggregateResults),
|
|
956
|
+
AggregateExecutionTime: data?.AggregateExecutionTime,
|
|
849
957
|
});
|
|
850
958
|
}
|
|
851
959
|
|
|
@@ -979,13 +1087,13 @@ export class RunViewResolver extends ResolverBase {
|
|
|
979
1087
|
const returnResult = [];
|
|
980
1088
|
for (let i = 0; i < rawData.length; i++) {
|
|
981
1089
|
const row = rawData[i];
|
|
982
|
-
|
|
1090
|
+
|
|
983
1091
|
// Build the primary key array from the entity's primary key fields
|
|
984
1092
|
const primaryKey: KeyValuePairOutputType[] = entityInfo.PrimaryKeys.map(pk => ({
|
|
985
1093
|
FieldName: pk.Name,
|
|
986
1094
|
Value: row[pk.Name]?.toString() || ''
|
|
987
1095
|
}));
|
|
988
|
-
|
|
1096
|
+
|
|
989
1097
|
returnResult.push({
|
|
990
1098
|
PrimaryKey: primaryKey,
|
|
991
1099
|
EntityID: entityId,
|
|
@@ -994,4 +1102,23 @@ export class RunViewResolver extends ResolverBase {
|
|
|
994
1102
|
}
|
|
995
1103
|
return returnResult;
|
|
996
1104
|
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Transform core AggregateResult[] to GraphQL AggregateResultOutput[].
|
|
1108
|
+
* Converts the value to a JSON string to preserve type information across GraphQL.
|
|
1109
|
+
*/
|
|
1110
|
+
protected processAggregateResults(aggregateResults: AggregateResult[] | undefined): AggregateResultOutput[] | undefined {
|
|
1111
|
+
if (!aggregateResults || aggregateResults.length === 0) {
|
|
1112
|
+
return undefined;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return aggregateResults.map(result => ({
|
|
1116
|
+
expression: result.expression,
|
|
1117
|
+
alias: result.alias,
|
|
1118
|
+
value: result.value !== null && result.value !== undefined
|
|
1119
|
+
? JSON.stringify(result.value)
|
|
1120
|
+
: undefined,
|
|
1121
|
+
error: result.error
|
|
1122
|
+
}));
|
|
1123
|
+
}
|
|
997
1124
|
}
|
package/src/index.ts
CHANGED
|
@@ -81,7 +81,16 @@ export { configInfo, DEFAULT_SERVER_CONFIG } from './config.js';
|
|
|
81
81
|
export * from './directives/index.js';
|
|
82
82
|
export * from './entitySubclasses/entityPermissions.server.js';
|
|
83
83
|
export * from './types.js';
|
|
84
|
-
export {
|
|
84
|
+
export {
|
|
85
|
+
TokenExpiredError,
|
|
86
|
+
getSystemUser,
|
|
87
|
+
getSigningKeys,
|
|
88
|
+
extractUserInfoFromPayload,
|
|
89
|
+
verifyUserRecord,
|
|
90
|
+
AuthProviderFactory,
|
|
91
|
+
IAuthProvider,
|
|
92
|
+
} from './auth/index.js';
|
|
93
|
+
export * from './auth/APIKeyScopeAuth.js';
|
|
85
94
|
|
|
86
95
|
export * from './generic/PushStatusResolver.js';
|
|
87
96
|
export * from './generic/ResolverBase.js';
|
|
@@ -97,7 +106,6 @@ export * from './generic/DeleteOptionsInput.js';
|
|
|
97
106
|
export * from './agents/skip-agent.js';
|
|
98
107
|
export * from './agents/skip-sdk.js';
|
|
99
108
|
|
|
100
|
-
export * from './resolvers/AskSkipResolver.js';
|
|
101
109
|
export * from './resolvers/ColorResolver.js';
|
|
102
110
|
export * from './resolvers/ComponentRegistryResolver.js';
|
|
103
111
|
export * from './resolvers/DatasetResolver.js';
|
|
@@ -113,6 +121,8 @@ export * from './resolvers/GetDataContextDataResolver.js';
|
|
|
113
121
|
export * from './resolvers/TransactionGroupResolver.js';
|
|
114
122
|
export * from './resolvers/CreateQueryResolver.js';
|
|
115
123
|
export * from './resolvers/TelemetryResolver.js';
|
|
124
|
+
export * from './resolvers/APIKeyResolver.js';
|
|
125
|
+
export * from './resolvers/MCPResolver.js';
|
|
116
126
|
export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
|
|
117
127
|
|
|
118
128
|
export * from './generated/generated.js';
|