@memberjunction/server 3.2.0 → 3.3.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 +47 -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/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/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 +252 -2
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +1754 -209
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +2 -2
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +22 -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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/resolvers/APIKeyResolver.d.ts +23 -0
- package/dist/resolvers/APIKeyResolver.d.ts.map +1 -0
- package/dist/resolvers/APIKeyResolver.js +191 -0
- package/dist/resolvers/APIKeyResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.js +1 -1
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +31 -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 +46 -45
- package/src/auth/APIKeyScopeAuth.ts +366 -0
- package/src/auth/index.ts +1 -0
- package/src/context.ts +91 -9
- package/src/generated/generated.ts +987 -14
- package/src/generic/ResolverBase.ts +38 -5
- package/src/generic/RunViewResolver.ts +132 -5
- package/src/index.ts +2 -0
- package/src/resolvers/APIKeyResolver.ts +234 -0
- package/src/resolvers/RunAIAgentResolver.ts +1 -1
- package/src/resolvers/UserResolver.ts +37 -1
- package/src/types.ts +7 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AggregateExpression,
|
|
2
3
|
BaseEntity,
|
|
3
4
|
BaseEntityEvent,
|
|
4
5
|
CompositeKey,
|
|
@@ -266,6 +267,11 @@ export class ResolverBase {
|
|
|
266
267
|
|
|
267
268
|
async RunViewByNameGeneric(viewInput: RunViewByNameInput, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
268
269
|
try {
|
|
270
|
+
// Log aggregate input for debugging
|
|
271
|
+
if (viewInput.Aggregates?.length) {
|
|
272
|
+
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 })))}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
269
275
|
const rv = provider as any as IRunViewProvider;
|
|
270
276
|
const result = await rv.RunView<UserViewEntityExtended>({
|
|
271
277
|
EntityName: 'User Views',
|
|
@@ -290,7 +296,8 @@ export class ResolverBase {
|
|
|
290
296
|
viewInput.ResultType,
|
|
291
297
|
userPayload,
|
|
292
298
|
viewInput.MaxRows,
|
|
293
|
-
viewInput.StartRow
|
|
299
|
+
viewInput.StartRow,
|
|
300
|
+
viewInput.Aggregates
|
|
294
301
|
);
|
|
295
302
|
}
|
|
296
303
|
else {
|
|
@@ -305,6 +312,11 @@ export class ResolverBase {
|
|
|
305
312
|
|
|
306
313
|
async RunViewByIDGeneric(viewInput: RunViewByIDInput, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
307
314
|
try {
|
|
315
|
+
// Log aggregate input for debugging
|
|
316
|
+
if (viewInput.Aggregates?.length) {
|
|
317
|
+
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 })))}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
308
320
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
309
321
|
const viewInfo = await provider.GetEntityObject<UserViewEntityExtended>('User Views', contextUser);
|
|
310
322
|
await viewInfo.Load(viewInput.ViewID);
|
|
@@ -325,7 +337,8 @@ export class ResolverBase {
|
|
|
325
337
|
viewInput.ResultType,
|
|
326
338
|
userPayload,
|
|
327
339
|
viewInput.MaxRows,
|
|
328
|
-
viewInput.StartRow
|
|
340
|
+
viewInput.StartRow,
|
|
341
|
+
viewInput.Aggregates
|
|
329
342
|
);
|
|
330
343
|
} catch (err) {
|
|
331
344
|
console.log(err);
|
|
@@ -335,6 +348,11 @@ export class ResolverBase {
|
|
|
335
348
|
|
|
336
349
|
async RunDynamicViewGeneric(viewInput: RunDynamicViewInput, provider: DatabaseProviderBase, userPayload: UserPayload, pubSub: PubSubEngine) {
|
|
337
350
|
try {
|
|
351
|
+
// Log aggregate input for debugging
|
|
352
|
+
if (viewInput.Aggregates?.length) {
|
|
353
|
+
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 })))}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
338
356
|
const md = provider;
|
|
339
357
|
const entity = md.Entities.find((e) => e.Name === viewInput.EntityName);
|
|
340
358
|
if (!entity) throw new Error(`Entity ${viewInput.EntityName} not found in metadata`);
|
|
@@ -363,7 +381,8 @@ export class ResolverBase {
|
|
|
363
381
|
viewInput.ResultType,
|
|
364
382
|
userPayload,
|
|
365
383
|
viewInput.MaxRows,
|
|
366
|
-
viewInput.StartRow
|
|
384
|
+
viewInput.StartRow,
|
|
385
|
+
viewInput.Aggregates
|
|
367
386
|
);
|
|
368
387
|
} catch (err) {
|
|
369
388
|
console.log(err);
|
|
@@ -422,7 +441,8 @@ export class ResolverBase {
|
|
|
422
441
|
forceAuditLog: viewInput.ForceAuditLog,
|
|
423
442
|
auditLogDescription: viewInput.AuditLogDescription,
|
|
424
443
|
resultType: viewInput.ResultType,
|
|
425
|
-
userPayload,
|
|
444
|
+
userPayload,
|
|
445
|
+
aggregates: viewInput.Aggregates,
|
|
426
446
|
});
|
|
427
447
|
} catch (err) {
|
|
428
448
|
LogError(err);
|
|
@@ -524,7 +544,8 @@ export class ResolverBase {
|
|
|
524
544
|
resultType: string | undefined,
|
|
525
545
|
userPayload: UserPayload | null,
|
|
526
546
|
maxRows: number | undefined,
|
|
527
|
-
startRow: number | undefined
|
|
547
|
+
startRow: number | undefined,
|
|
548
|
+
aggregates?: AggregateExpression[]
|
|
528
549
|
) {
|
|
529
550
|
try {
|
|
530
551
|
if (!viewInfo || !userPayload) return null;
|
|
@@ -559,6 +580,11 @@ export class ResolverBase {
|
|
|
559
580
|
}
|
|
560
581
|
}
|
|
561
582
|
|
|
583
|
+
// Log aggregate request for debugging
|
|
584
|
+
if (aggregates?.length) {
|
|
585
|
+
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 })))}`);
|
|
586
|
+
}
|
|
587
|
+
|
|
562
588
|
const result = await rv.RunView(
|
|
563
589
|
{
|
|
564
590
|
ViewID: viewInfo.ID,
|
|
@@ -578,10 +604,16 @@ export class ResolverBase {
|
|
|
578
604
|
ForceAuditLog: forceAuditLog,
|
|
579
605
|
AuditLogDescription: auditLogDescription,
|
|
580
606
|
ResultType: rt,
|
|
607
|
+
Aggregates: aggregates,
|
|
581
608
|
},
|
|
582
609
|
user
|
|
583
610
|
);
|
|
584
611
|
|
|
612
|
+
// Log aggregate results for debugging
|
|
613
|
+
if (aggregates?.length) {
|
|
614
|
+
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)}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
585
617
|
// Process results for GraphQL transport
|
|
586
618
|
const mapper = new FieldMapper();
|
|
587
619
|
if (result?.Success && result.Results?.length) {
|
|
@@ -682,6 +714,7 @@ export class ResolverBase {
|
|
|
682
714
|
ForceAuditLog: param.forceAuditLog,
|
|
683
715
|
AuditLogDescription: param.auditLogDescription,
|
|
684
716
|
ResultType: rt,
|
|
717
|
+
Aggregates: param.aggregates,
|
|
685
718
|
});
|
|
686
719
|
}
|
|
687
720
|
|
|
@@ -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
|
@@ -82,6 +82,7 @@ export * from './directives/index.js';
|
|
|
82
82
|
export * from './entitySubclasses/entityPermissions.server.js';
|
|
83
83
|
export * from './types.js';
|
|
84
84
|
export { TokenExpiredError, getSystemUser } from './auth/index.js';
|
|
85
|
+
export * from './auth/APIKeyScopeAuth.js';
|
|
85
86
|
|
|
86
87
|
export * from './generic/PushStatusResolver.js';
|
|
87
88
|
export * from './generic/ResolverBase.js';
|
|
@@ -113,6 +114,7 @@ export * from './resolvers/GetDataContextDataResolver.js';
|
|
|
113
114
|
export * from './resolvers/TransactionGroupResolver.js';
|
|
114
115
|
export * from './resolvers/CreateQueryResolver.js';
|
|
115
116
|
export * from './resolvers/TelemetryResolver.js';
|
|
117
|
+
export * from './resolvers/APIKeyResolver.js';
|
|
116
118
|
export { GetReadOnlyDataSource, GetReadWriteDataSource, GetReadWriteProvider, GetReadOnlyProvider } from './util.js';
|
|
117
119
|
|
|
118
120
|
export * from './generated/generated.js';
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Resolver, Mutation, Arg, Ctx } from "type-graphql";
|
|
2
|
+
import { Field, InputType, ObjectType } from "type-graphql";
|
|
3
|
+
import { LogError, Metadata } from "@memberjunction/core";
|
|
4
|
+
import { APIKeyScopeEntity } from "@memberjunction/core-entities";
|
|
5
|
+
import { GetAPIKeyEngine } from "@memberjunction/api-keys";
|
|
6
|
+
import { AppContext } from "../types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Input type for creating a new API key
|
|
10
|
+
*/
|
|
11
|
+
@InputType()
|
|
12
|
+
export class CreateAPIKeyInput {
|
|
13
|
+
/**
|
|
14
|
+
* Human-readable label for the API key
|
|
15
|
+
*/
|
|
16
|
+
@Field()
|
|
17
|
+
Label: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Optional description of what the key is used for
|
|
21
|
+
*/
|
|
22
|
+
@Field(() => String, { nullable: true })
|
|
23
|
+
Description?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional expiration date for the key
|
|
27
|
+
*/
|
|
28
|
+
@Field(() => Date, { nullable: true })
|
|
29
|
+
ExpiresAt?: Date;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Optional array of scope IDs to assign to this key
|
|
33
|
+
*/
|
|
34
|
+
@Field(() => [String], { nullable: true })
|
|
35
|
+
ScopeIDs?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Result type for API key creation
|
|
40
|
+
* Returns the raw key ONCE - it cannot be recovered after this
|
|
41
|
+
*/
|
|
42
|
+
@ObjectType()
|
|
43
|
+
export class CreateAPIKeyResult {
|
|
44
|
+
/**
|
|
45
|
+
* Whether the key was created successfully
|
|
46
|
+
*/
|
|
47
|
+
@Field()
|
|
48
|
+
Success: boolean;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The raw API key - show this to the user ONCE
|
|
52
|
+
* This cannot be recovered after the initial response
|
|
53
|
+
*/
|
|
54
|
+
@Field(() => String, { nullable: true })
|
|
55
|
+
RawKey?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The database ID of the created API key
|
|
59
|
+
*/
|
|
60
|
+
@Field(() => String, { nullable: true })
|
|
61
|
+
APIKeyID?: string;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Error message if creation failed
|
|
65
|
+
*/
|
|
66
|
+
@Field(() => String, { nullable: true })
|
|
67
|
+
Error?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Result type for API key revocation
|
|
72
|
+
*/
|
|
73
|
+
@ObjectType()
|
|
74
|
+
export class RevokeAPIKeyResult {
|
|
75
|
+
/**
|
|
76
|
+
* Whether the key was revoked successfully
|
|
77
|
+
*/
|
|
78
|
+
@Field()
|
|
79
|
+
Success: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Error message if revocation failed
|
|
83
|
+
*/
|
|
84
|
+
@Field(() => String, { nullable: true })
|
|
85
|
+
Error?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolver for API key operations
|
|
90
|
+
* Handles secure server-side API key generation
|
|
91
|
+
*/
|
|
92
|
+
@Resolver()
|
|
93
|
+
export class APIKeyResolver {
|
|
94
|
+
/**
|
|
95
|
+
* Creates a new API key with proper server-side cryptographic hashing.
|
|
96
|
+
*
|
|
97
|
+
* This mutation:
|
|
98
|
+
* 1. Generates a cryptographically secure API key using APIKeyEngine
|
|
99
|
+
* 2. Stores only the SHA-256 hash in the database (never the raw key)
|
|
100
|
+
* 3. Returns the raw key ONCE - it cannot be recovered after this call
|
|
101
|
+
* 4. Optionally assigns scope permissions to the key
|
|
102
|
+
*
|
|
103
|
+
* @param input The creation parameters
|
|
104
|
+
* @param ctx The GraphQL context with authenticated user
|
|
105
|
+
* @returns The raw key (show once!) and database ID
|
|
106
|
+
*/
|
|
107
|
+
@Mutation(() => CreateAPIKeyResult)
|
|
108
|
+
async CreateAPIKey(
|
|
109
|
+
@Arg("input") input: CreateAPIKeyInput,
|
|
110
|
+
@Ctx() ctx: AppContext
|
|
111
|
+
): Promise<CreateAPIKeyResult> {
|
|
112
|
+
try {
|
|
113
|
+
// Get the authenticated user
|
|
114
|
+
const user = ctx.userPayload.userRecord;
|
|
115
|
+
if (!user) {
|
|
116
|
+
return {
|
|
117
|
+
Success: false,
|
|
118
|
+
Error: "User is not authenticated"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Use APIKeyEngine to create the API key with proper server-side crypto
|
|
123
|
+
const apiKeyEngine = GetAPIKeyEngine();
|
|
124
|
+
const result = await apiKeyEngine.CreateAPIKey(
|
|
125
|
+
{
|
|
126
|
+
UserId: user.ID,
|
|
127
|
+
Label: input.Label,
|
|
128
|
+
Description: input.Description,
|
|
129
|
+
ExpiresAt: input.ExpiresAt
|
|
130
|
+
},
|
|
131
|
+
user
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!result.Success) {
|
|
135
|
+
return {
|
|
136
|
+
Success: false,
|
|
137
|
+
Error: result.Error || "Failed to create API key"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Save scope associations if provided
|
|
142
|
+
if (input.ScopeIDs && input.ScopeIDs.length > 0 && result.APIKeyId) {
|
|
143
|
+
await this.saveScopeAssociations(result.APIKeyId, input.ScopeIDs, user);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
Success: true,
|
|
148
|
+
RawKey: result.RawKey,
|
|
149
|
+
APIKeyID: result.APIKeyId
|
|
150
|
+
};
|
|
151
|
+
} catch (e) {
|
|
152
|
+
const error = e as Error;
|
|
153
|
+
LogError(`Error in CreateAPIKey resolver: ${error.message}`);
|
|
154
|
+
return {
|
|
155
|
+
Success: false,
|
|
156
|
+
Error: `Error creating API key: ${error.message}`
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Revokes an API key, permanently disabling it.
|
|
163
|
+
*
|
|
164
|
+
* Once revoked, an API key cannot be reactivated. Users must create a new key.
|
|
165
|
+
* This uses APIKeyEngine.RevokeAPIKey() for consistency.
|
|
166
|
+
*
|
|
167
|
+
* @param apiKeyId The database ID of the API key to revoke
|
|
168
|
+
* @param ctx The GraphQL context with authenticated user
|
|
169
|
+
* @returns Success status
|
|
170
|
+
*/
|
|
171
|
+
@Mutation(() => RevokeAPIKeyResult)
|
|
172
|
+
async RevokeAPIKey(
|
|
173
|
+
@Arg("apiKeyId") apiKeyId: string,
|
|
174
|
+
@Ctx() ctx: AppContext
|
|
175
|
+
): Promise<RevokeAPIKeyResult> {
|
|
176
|
+
try {
|
|
177
|
+
const user = ctx.userPayload.userRecord;
|
|
178
|
+
if (!user) {
|
|
179
|
+
return {
|
|
180
|
+
Success: false,
|
|
181
|
+
Error: "User is not authenticated"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const apiKeyEngine = GetAPIKeyEngine();
|
|
186
|
+
const result = await apiKeyEngine.RevokeAPIKey(apiKeyId, user);
|
|
187
|
+
|
|
188
|
+
if (result) {
|
|
189
|
+
return { Success: true };
|
|
190
|
+
} else {
|
|
191
|
+
return {
|
|
192
|
+
Success: false,
|
|
193
|
+
Error: "Failed to revoke API key. It may not exist or you may not have permission."
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {
|
|
197
|
+
const error = e as Error;
|
|
198
|
+
LogError(`Error in RevokeAPIKey resolver: ${error.message}`);
|
|
199
|
+
return {
|
|
200
|
+
Success: false,
|
|
201
|
+
Error: `Error revoking API key: ${error.message}`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Saves scope associations for the newly created API key
|
|
208
|
+
* @param apiKeyId The ID of the created API key
|
|
209
|
+
* @param scopeIds Array of scope IDs to associate
|
|
210
|
+
* @param user The context user
|
|
211
|
+
*/
|
|
212
|
+
private async saveScopeAssociations(
|
|
213
|
+
apiKeyId: string,
|
|
214
|
+
scopeIds: string[],
|
|
215
|
+
user: any
|
|
216
|
+
): Promise<void> {
|
|
217
|
+
const md = new Metadata();
|
|
218
|
+
|
|
219
|
+
for (const scopeId of scopeIds) {
|
|
220
|
+
try {
|
|
221
|
+
const keyScope = await md.GetEntityObject<APIKeyScopeEntity>(
|
|
222
|
+
'MJ: API Key Scopes',
|
|
223
|
+
user
|
|
224
|
+
);
|
|
225
|
+
keyScope.APIKeyID = apiKeyId;
|
|
226
|
+
keyScope.ScopeID = scopeId;
|
|
227
|
+
await keyScope.Save();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
LogError(`Error saving scope association for API key ${apiKeyId}, scope ${scopeId}: ${error}`);
|
|
230
|
+
// Continue with other scopes even if one fails
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -896,7 +896,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
896
896
|
private mapDetailRoleToMessageRole(role: string): 'user' | 'assistant' | 'system' {
|
|
897
897
|
const roleLower = (role || '').toLowerCase();
|
|
898
898
|
if (roleLower === 'user') return 'user';
|
|
899
|
-
if (roleLower === 'assistant' || roleLower === 'agent') return 'assistant';
|
|
899
|
+
if (roleLower === 'assistant' || roleLower === 'agent' || roleLower === 'ai') return 'assistant';
|
|
900
900
|
if (roleLower === 'system') return 'system';
|
|
901
901
|
return 'user'; // Default to user
|
|
902
902
|
}
|
|
@@ -6,8 +6,44 @@ import { GetReadOnlyProvider } from '../util.js';
|
|
|
6
6
|
export class UserResolver extends MJUserResolverBase {
|
|
7
7
|
@Query(() => MJUser_)
|
|
8
8
|
async CurrentUser(@Ctx() context: AppContext) {
|
|
9
|
+
// If userRecord is already available (e.g., from API key auth or system auth),
|
|
10
|
+
// use it directly instead of looking up by email again
|
|
11
|
+
if (context.userPayload.userRecord) {
|
|
12
|
+
// Convert UserInfo to the GraphQL response format
|
|
13
|
+
const userRecord = context.userPayload.userRecord;
|
|
14
|
+
const userData = {
|
|
15
|
+
ID: userRecord.ID,
|
|
16
|
+
Name: userRecord.Name,
|
|
17
|
+
FirstName: userRecord.FirstName,
|
|
18
|
+
LastName: userRecord.LastName,
|
|
19
|
+
Title: userRecord.Title,
|
|
20
|
+
Email: userRecord.Email,
|
|
21
|
+
Type: userRecord.Type,
|
|
22
|
+
IsActive: userRecord.IsActive,
|
|
23
|
+
LinkedRecordType: userRecord.LinkedRecordType,
|
|
24
|
+
EmployeeID: userRecord.EmployeeID,
|
|
25
|
+
LinkedEntityID: userRecord.LinkedEntityID,
|
|
26
|
+
LinkedEntityRecordID: userRecord.LinkedEntityRecordID,
|
|
27
|
+
__mj_CreatedAt: userRecord.__mj_CreatedAt,
|
|
28
|
+
__mj_UpdatedAt: userRecord.__mj_UpdatedAt,
|
|
29
|
+
// Include the UserRoles sub-array for the GraphQL response
|
|
30
|
+
UserRoles_UserIDArray: userRecord.UserRoles?.map((r: { ID: string; UserID: string; RoleID: string; RoleName?: string; __mj_CreatedAt?: Date; __mj_UpdatedAt?: Date }) => ({
|
|
31
|
+
ID: r.ID,
|
|
32
|
+
UserID: r.UserID,
|
|
33
|
+
RoleID: r.RoleID,
|
|
34
|
+
User: userRecord.Name,
|
|
35
|
+
Role: r.RoleName,
|
|
36
|
+
__mj_CreatedAt: r.__mj_CreatedAt,
|
|
37
|
+
__mj_UpdatedAt: r.__mj_UpdatedAt,
|
|
38
|
+
})) || [],
|
|
39
|
+
};
|
|
40
|
+
console.log('CurrentUser (from userRecord)', userData.Email);
|
|
41
|
+
return this.MapFieldNamesToCodeNames('Users', userData);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Fall back to email lookup for JWT-based auth
|
|
9
45
|
const result = await this.UserByEmail(context.userPayload.email, context);
|
|
10
|
-
console.log('CurrentUser', result);
|
|
46
|
+
console.log('CurrentUser (from email lookup)', result?.Email);
|
|
11
47
|
return result;
|
|
12
48
|
}
|
|
13
49
|
|