@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.
Files changed (127) hide show
  1. package/README.md +106 -1
  2. package/dist/auth/APIKeyScopeAuth.d.ts +51 -0
  3. package/dist/auth/APIKeyScopeAuth.d.ts.map +1 -0
  4. package/dist/auth/APIKeyScopeAuth.js +163 -0
  5. package/dist/auth/APIKeyScopeAuth.js.map +1 -0
  6. package/dist/auth/BaseAuthProvider.d.ts +1 -0
  7. package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
  8. package/dist/auth/BaseAuthProvider.js +2 -0
  9. package/dist/auth/BaseAuthProvider.js.map +1 -1
  10. package/dist/auth/IAuthProvider.d.ts +1 -0
  11. package/dist/auth/IAuthProvider.d.ts.map +1 -1
  12. package/dist/auth/index.d.ts +1 -0
  13. package/dist/auth/index.d.ts.map +1 -1
  14. package/dist/auth/index.js +1 -0
  15. package/dist/auth/index.js.map +1 -1
  16. package/dist/config.js +2 -2
  17. package/dist/config.js.map +1 -1
  18. package/dist/context.d.ts +8 -1
  19. package/dist/context.d.ts.map +1 -1
  20. package/dist/context.js +44 -7
  21. package/dist/context.js.map +1 -1
  22. package/dist/generated/generated.d.ts +681 -2
  23. package/dist/generated/generated.d.ts.map +1 -1
  24. package/dist/generated/generated.js +10627 -6409
  25. package/dist/generated/generated.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +3 -2
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +52 -4
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +29 -1
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +143 -0
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +4 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/resolvers/APIKeyResolver.d.ts +24 -0
  39. package/dist/resolvers/APIKeyResolver.d.ts.map +1 -0
  40. package/dist/resolvers/APIKeyResolver.js +194 -0
  41. package/dist/resolvers/APIKeyResolver.js.map +1 -0
  42. package/dist/resolvers/ActionResolver.d.ts +2 -1
  43. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  44. package/dist/resolvers/ActionResolver.js +4 -1
  45. package/dist/resolvers/ActionResolver.js.map +1 -1
  46. package/dist/resolvers/DatasetResolver.d.ts +5 -4
  47. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  48. package/dist/resolvers/DatasetResolver.js +7 -4
  49. package/dist/resolvers/DatasetResolver.js.map +1 -1
  50. package/dist/resolvers/EntityCommunicationsResolver.d.ts +2 -1
  51. package/dist/resolvers/EntityCommunicationsResolver.d.ts.map +1 -1
  52. package/dist/resolvers/EntityCommunicationsResolver.js +3 -1
  53. package/dist/resolvers/EntityCommunicationsResolver.js.map +1 -1
  54. package/dist/resolvers/GetDataContextDataResolver.d.ts +2 -1
  55. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  56. package/dist/resolvers/GetDataContextDataResolver.js +10 -3
  57. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  58. package/dist/resolvers/MCPResolver.d.ts +37 -0
  59. package/dist/resolvers/MCPResolver.d.ts.map +1 -0
  60. package/dist/resolvers/MCPResolver.js +363 -0
  61. package/dist/resolvers/MCPResolver.js.map +1 -0
  62. package/dist/resolvers/MergeRecordsResolver.d.ts +2 -1
  63. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  64. package/dist/resolvers/MergeRecordsResolver.js +3 -1
  65. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  66. package/dist/resolvers/QueryResolver.d.ts +2 -1
  67. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  68. package/dist/resolvers/QueryResolver.js +6 -1
  69. package/dist/resolvers/QueryResolver.js.map +1 -1
  70. package/dist/resolvers/ReportResolver.d.ts +2 -1
  71. package/dist/resolvers/ReportResolver.d.ts.map +1 -1
  72. package/dist/resolvers/ReportResolver.js +4 -1
  73. package/dist/resolvers/ReportResolver.js.map +1 -1
  74. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  75. package/dist/resolvers/RunAIAgentResolver.js +3 -1
  76. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  77. package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
  78. package/dist/resolvers/RunAIPromptResolver.js +3 -0
  79. package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
  80. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  81. package/dist/resolvers/RunTemplateResolver.js +1 -0
  82. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  83. package/dist/resolvers/TaskResolver.d.ts.map +1 -1
  84. package/dist/resolvers/TaskResolver.js +1 -0
  85. package/dist/resolvers/TaskResolver.js.map +1 -1
  86. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  87. package/dist/resolvers/UserResolver.js +35 -1
  88. package/dist/resolvers/UserResolver.js.map +1 -1
  89. package/dist/types.d.ts +4 -1
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/types.js.map +1 -1
  92. package/package.json +47 -45
  93. package/src/auth/APIKeyScopeAuth.ts +366 -0
  94. package/src/auth/BaseAuthProvider.ts +3 -0
  95. package/src/auth/IAuthProvider.ts +5 -0
  96. package/src/auth/index.ts +1 -0
  97. package/src/config.ts +2 -2
  98. package/src/context.ts +91 -9
  99. package/src/generated/generated.ts +6327 -3668
  100. package/src/generic/ResolverBase.ts +127 -8
  101. package/src/generic/RunViewResolver.ts +132 -5
  102. package/src/index.ts +12 -2
  103. package/src/resolvers/APIKeyResolver.ts +241 -0
  104. package/src/resolvers/ActionResolver.ts +8 -1
  105. package/src/resolvers/DatasetResolver.ts +11 -4
  106. package/src/resolvers/EntityCommunicationsResolver.ts +5 -1
  107. package/src/resolvers/GetDataContextDataResolver.ts +14 -6
  108. package/src/resolvers/MCPResolver.ts +480 -0
  109. package/src/resolvers/MergeRecordsResolver.ts +5 -1
  110. package/src/resolvers/QueryResolver.ts +17 -3
  111. package/src/resolvers/ReportResolver.ts +8 -1
  112. package/src/resolvers/RunAIAgentResolver.ts +7 -1
  113. package/src/resolvers/RunAIPromptResolver.ts +10 -1
  114. package/src/resolvers/RunTemplateResolver.ts +4 -1
  115. package/src/resolvers/TaskResolver.ts +3 -0
  116. package/src/resolvers/UserResolver.ts +52 -4
  117. package/src/types.ts +7 -2
  118. package/dist/resolvers/AskSkipResolver.d.ts +0 -123
  119. package/dist/resolvers/AskSkipResolver.d.ts.map +0 -1
  120. package/dist/resolvers/AskSkipResolver.js +0 -1788
  121. package/dist/resolvers/AskSkipResolver.js.map +0 -1
  122. package/dist/scheduler/LearningCycleScheduler.d.ts +0 -4
  123. package/dist/scheduler/LearningCycleScheduler.d.ts.map +0 -1
  124. package/dist/scheduler/LearningCycleScheduler.js +0 -4
  125. package/dist/scheduler/LearningCycleScheduler.js.map +0 -1
  126. package/src/resolvers/AskSkipResolver.ts +0 -3446
  127. 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
- const rawData: RunViewGenericResult[] = await super.RunViewsGeneric(input, provider, userPayload);
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: RunViewGenericResult[] = await super.RunViewsGeneric(input, provider, userPayload);
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 { TokenExpiredError, getSystemUser } from './auth/index.js';
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';