@friggframework/devtools 2.0.0--canary.627.ce32cdc.0 → 2.0.0--canary.640.b31eb4a.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/infrastructure/__tests__/aurora-scale-to-zero-connectivity.test.js +255 -0
- package/infrastructure/domains/admin-scripts/admin-script-builder.js +272 -50
- package/infrastructure/domains/admin-scripts/admin-script-builder.test.js +313 -0
- package/infrastructure/domains/database/aurora-builder.js +232 -39
- package/infrastructure/domains/networking/vpc-builder.js +65 -4
- package/infrastructure/domains/shared/types/app-definition.js +18 -2
- package/infrastructure/domains/shared/utilities/base-definition-factory.js +1 -10
- package/package.json +7 -7
|
@@ -620,6 +620,319 @@ describe('AdminScriptBuilder', () => {
|
|
|
620
620
|
});
|
|
621
621
|
});
|
|
622
622
|
|
|
623
|
+
describe('reports (ReportQueue + reportExecutor)', () => {
|
|
624
|
+
it('creates ReportQueue + reportExecutor and wires REPORT_QUEUE_URL when reports are present', async () => {
|
|
625
|
+
const appDefinition = {
|
|
626
|
+
reports: [{ Definition: { name: 'my-report', version: '1.0.0' } }],
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
630
|
+
|
|
631
|
+
// Queue
|
|
632
|
+
expect(result.resources.ReportQueue).toBeDefined();
|
|
633
|
+
expect(result.resources.ReportQueue.Type).toBe('AWS::SQS::Queue');
|
|
634
|
+
expect(
|
|
635
|
+
result.resources.ReportQueue.Properties.MessageRetentionPeriod
|
|
636
|
+
).toBe(86400);
|
|
637
|
+
expect(
|
|
638
|
+
result.resources.ReportQueue.Properties.VisibilityTimeout
|
|
639
|
+
).toBe(900);
|
|
640
|
+
expect(
|
|
641
|
+
result.resources.ReportQueue.Properties.RedrivePolicy
|
|
642
|
+
).toEqual({
|
|
643
|
+
maxReceiveCount: 3,
|
|
644
|
+
deadLetterTargetArn: {
|
|
645
|
+
'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Executor function
|
|
650
|
+
expect(result.functions.reportExecutor).toBeDefined();
|
|
651
|
+
expect(result.functions.reportExecutor.handler).toBe(
|
|
652
|
+
'node_modules/@friggframework/admin-scripts/src/infrastructure/report-executor-handler.handler'
|
|
653
|
+
);
|
|
654
|
+
expect(result.functions.reportExecutor.timeout).toBe(900);
|
|
655
|
+
expect(result.functions.reportExecutor.memorySize).toBe(1024);
|
|
656
|
+
expect(result.functions.reportExecutor.events).toEqual([
|
|
657
|
+
{
|
|
658
|
+
sqs: {
|
|
659
|
+
arn: { 'Fn::GetAtt': ['ReportQueue', 'Arn'] },
|
|
660
|
+
batchSize: 1,
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
]);
|
|
664
|
+
expect(result.functions.reportExecutor.skipEsbuild).toBe(true);
|
|
665
|
+
expect(result.functions.reportExecutor.layers).toEqual([
|
|
666
|
+
{ Ref: 'PrismaLambdaLayer' },
|
|
667
|
+
]);
|
|
668
|
+
|
|
669
|
+
// Env wired app-wide (scoped flag off)
|
|
670
|
+
expect(result.environment.REPORT_QUEUE_URL).toEqual({
|
|
671
|
+
Ref: 'ReportQueue',
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// IAM SendMessage grant on the queue Arn
|
|
675
|
+
const grant = result.iamStatements.find(
|
|
676
|
+
(s) =>
|
|
677
|
+
Array.isArray(s.Action) &&
|
|
678
|
+
s.Action.includes('sqs:SendMessage') &&
|
|
679
|
+
s.Resource &&
|
|
680
|
+
s.Resource['Fn::GetAtt'] &&
|
|
681
|
+
s.Resource['Fn::GetAtt'][0] === 'ReportQueue'
|
|
682
|
+
);
|
|
683
|
+
expect(grant).toBeDefined();
|
|
684
|
+
expect(grant.Action).toContain('sqs:SendMessageBatch');
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('scopes REPORT_QUEUE_URL to reportRouter + reportExecutor when scopedEnvironment is on', async () => {
|
|
688
|
+
const appDefinition = {
|
|
689
|
+
lambda: { scopedEnvironment: true },
|
|
690
|
+
reports: [{ Definition: { name: 'my-report', version: '1.0.0' } }],
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
694
|
+
|
|
695
|
+
expect(result.environment.REPORT_QUEUE_URL).toBeUndefined();
|
|
696
|
+
for (const fnName of ['reportRouter', 'reportExecutor']) {
|
|
697
|
+
expect(
|
|
698
|
+
result.functionEnvironments[fnName].REPORT_QUEUE_URL
|
|
699
|
+
).toEqual({ Ref: 'ReportQueue' });
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('creates ReportQueue + reportExecutor when only builtin reports are enabled', async () => {
|
|
704
|
+
const appDefinition = {
|
|
705
|
+
admin: { includeBuiltinReports: true },
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
709
|
+
|
|
710
|
+
expect(result.resources.ReportQueue).toBeDefined();
|
|
711
|
+
expect(result.functions.reportExecutor).toBeDefined();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it('does NOT create ReportQueue or reportExecutor when only adminScripts are present', async () => {
|
|
715
|
+
const appDefinition = {
|
|
716
|
+
adminScripts: [{ Definition: { name: 'test-script' } }],
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
720
|
+
|
|
721
|
+
expect(result.resources.ReportQueue).toBeUndefined();
|
|
722
|
+
expect(result.functions.reportExecutor).toBeUndefined();
|
|
723
|
+
expect(result.functions.reportRouter).toBeUndefined();
|
|
724
|
+
expect(result.environment.REPORT_QUEUE_URL).toBeUndefined();
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
describe('report artifacts (ReportArtifactBucket for non-JSON output)', () => {
|
|
729
|
+
it('provisions a private encrypted bucket + IAM + env when a report emits non-JSON', async () => {
|
|
730
|
+
const appDefinition = {
|
|
731
|
+
reports: [
|
|
732
|
+
{
|
|
733
|
+
Definition: {
|
|
734
|
+
name: 'sales-csv',
|
|
735
|
+
version: '1.0.0',
|
|
736
|
+
output: { format: 'csv' },
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
743
|
+
|
|
744
|
+
const bucket = result.resources.ReportArtifactBucket;
|
|
745
|
+
expect(bucket).toBeDefined();
|
|
746
|
+
expect(bucket.Type).toBe('AWS::S3::Bucket');
|
|
747
|
+
expect(
|
|
748
|
+
bucket.Properties.BucketEncryption
|
|
749
|
+
.ServerSideEncryptionConfiguration[0]
|
|
750
|
+
.ServerSideEncryptionByDefault.SSEAlgorithm
|
|
751
|
+
).toBe('AES256');
|
|
752
|
+
expect(bucket.Properties.PublicAccessBlockConfiguration).toEqual({
|
|
753
|
+
BlockPublicAcls: true,
|
|
754
|
+
BlockPublicPolicy: true,
|
|
755
|
+
IgnorePublicAcls: true,
|
|
756
|
+
RestrictPublicBuckets: true,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Env wired app-wide (scoped flag off).
|
|
760
|
+
expect(result.environment.REPORT_ARTIFACT_BUCKET).toEqual({
|
|
761
|
+
Ref: 'ReportArtifactBucket',
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// IAM: object-level Put/Get scoped to the bucket keys.
|
|
765
|
+
const grant = result.iamStatements.find(
|
|
766
|
+
(s) =>
|
|
767
|
+
Array.isArray(s.Action) &&
|
|
768
|
+
s.Action.includes('s3:PutObject')
|
|
769
|
+
);
|
|
770
|
+
expect(grant).toBeDefined();
|
|
771
|
+
expect(grant.Action).toContain('s3:GetObject');
|
|
772
|
+
expect(grant.Resource['Fn::Sub'][0]).toBe('${BucketArn}/*');
|
|
773
|
+
expect(grant.Resource['Fn::Sub'][1]).toEqual({
|
|
774
|
+
BucketArn: { 'Fn::GetAtt': ['ReportArtifactBucket', 'Arn'] },
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('does NOT provision the bucket for JSON-only reports', async () => {
|
|
779
|
+
const appDefinition = {
|
|
780
|
+
reports: [
|
|
781
|
+
{
|
|
782
|
+
Definition: {
|
|
783
|
+
name: 'json-report',
|
|
784
|
+
version: '1.0.0',
|
|
785
|
+
output: { format: 'json' },
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
// No output field defaults to JSON.
|
|
789
|
+
{ Definition: { name: 'plain', version: '1.0.0' } },
|
|
790
|
+
],
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
794
|
+
|
|
795
|
+
expect(result.resources.ReportArtifactBucket).toBeUndefined();
|
|
796
|
+
expect(result.environment.REPORT_ARTIFACT_BUCKET).toBeUndefined();
|
|
797
|
+
const grant = result.iamStatements.find(
|
|
798
|
+
(s) =>
|
|
799
|
+
Array.isArray(s.Action) &&
|
|
800
|
+
s.Action.includes('s3:PutObject')
|
|
801
|
+
);
|
|
802
|
+
expect(grant).toBeUndefined();
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('scopes REPORT_ARTIFACT_BUCKET to report functions when scopedEnvironment is on', async () => {
|
|
806
|
+
const appDefinition = {
|
|
807
|
+
lambda: { scopedEnvironment: true },
|
|
808
|
+
reports: [
|
|
809
|
+
{
|
|
810
|
+
Definition: {
|
|
811
|
+
name: 'sales-csv',
|
|
812
|
+
version: '1.0.0',
|
|
813
|
+
output: { format: 'csv' },
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
],
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
820
|
+
|
|
821
|
+
expect(result.environment.REPORT_ARTIFACT_BUCKET).toBeUndefined();
|
|
822
|
+
for (const fnName of ['reportRouter', 'reportExecutor']) {
|
|
823
|
+
expect(
|
|
824
|
+
result.functionEnvironments[fnName].REPORT_ARTIFACT_BUCKET
|
|
825
|
+
).toEqual({ Ref: 'ReportArtifactBucket' });
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
describe('report scheduling (enableScheduling && reports)', () => {
|
|
831
|
+
it('wires the report scheduler env onto reportRouter targeting the report executor', async () => {
|
|
832
|
+
const appDefinition = {
|
|
833
|
+
reports: [{ Definition: { name: 'my-report', version: '1.0.0' } }],
|
|
834
|
+
admin: { enableScheduling: true },
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
838
|
+
|
|
839
|
+
// Shared scheduler role + group are created even without scripts.
|
|
840
|
+
expect(result.resources.AdminScriptSchedulerRole).toBeDefined();
|
|
841
|
+
expect(result.resources.AdminScriptScheduleGroup).toBeDefined();
|
|
842
|
+
|
|
843
|
+
const routerEnv = result.functions.reportRouter.environment;
|
|
844
|
+
expect(routerEnv.SCHEDULER_PROVIDER).toBe('aws');
|
|
845
|
+
expect(routerEnv.SCHEDULER_ROLE_ARN).toEqual({
|
|
846
|
+
'Fn::GetAtt': ['AdminScriptSchedulerRole', 'Arn'],
|
|
847
|
+
});
|
|
848
|
+
expect(routerEnv.REPORT_SCHEDULE_GROUP).toEqual({
|
|
849
|
+
Ref: 'AdminScriptScheduleGroup',
|
|
850
|
+
});
|
|
851
|
+
// Constructed ARN (Fn::Sub), not Fn::GetAtt — avoids a circular dep.
|
|
852
|
+
expect(routerEnv.REPORT_EXECUTOR_LAMBDA_ARN).toEqual({
|
|
853
|
+
'Fn::Sub':
|
|
854
|
+
'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${self:service}-${self:provider.stage}-reportExecutor',
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
// Must NOT leak onto the shared provider env.
|
|
858
|
+
expect(result.environment.SCHEDULER_PROVIDER).toBeUndefined();
|
|
859
|
+
expect(result.environment.REPORT_EXECUTOR_LAMBDA_ARN).toBeUndefined();
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('grants the scheduler role invoke on the report executor (reports-only)', async () => {
|
|
863
|
+
const appDefinition = {
|
|
864
|
+
reports: [{ Definition: { name: 'my-report', version: '1.0.0' } }],
|
|
865
|
+
admin: { enableScheduling: true },
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
869
|
+
|
|
870
|
+
const statement =
|
|
871
|
+
result.resources.AdminScriptSchedulerRole.Properties.Policies[0]
|
|
872
|
+
.PolicyDocument.Statement[0];
|
|
873
|
+
// Only reports present -> single Resource, the report executor ARN.
|
|
874
|
+
expect(statement.Resource).toEqual({
|
|
875
|
+
'Fn::Sub':
|
|
876
|
+
'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${self:service}-${self:provider.stage}-reportExecutor',
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// scheduler:* + iam:PassRole grants present.
|
|
880
|
+
const schedulerGrant = result.iamStatements.find(
|
|
881
|
+
(s) =>
|
|
882
|
+
Array.isArray(s.Action) &&
|
|
883
|
+
s.Action.includes('scheduler:CreateSchedule')
|
|
884
|
+
);
|
|
885
|
+
expect(schedulerGrant).toBeDefined();
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it('lets the scheduler role invoke BOTH executors when scripts and reports coexist', async () => {
|
|
889
|
+
const appDefinition = {
|
|
890
|
+
adminScripts: [{ Definition: { name: 'test-script' } }],
|
|
891
|
+
reports: [{ Definition: { name: 'my-report', version: '1.0.0' } }],
|
|
892
|
+
admin: { enableScheduling: true },
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
896
|
+
|
|
897
|
+
const statement =
|
|
898
|
+
result.resources.AdminScriptSchedulerRole.Properties.Policies[0]
|
|
899
|
+
.PolicyDocument.Statement[0];
|
|
900
|
+
expect(statement.Resource).toEqual([
|
|
901
|
+
{
|
|
902
|
+
'Fn::Sub':
|
|
903
|
+
'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${self:service}-${self:provider.stage}-adminScriptExecutor',
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
'Fn::Sub':
|
|
907
|
+
'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${self:service}-${self:provider.stage}-reportExecutor',
|
|
908
|
+
},
|
|
909
|
+
]);
|
|
910
|
+
|
|
911
|
+
// Both routers get their own scheduler env.
|
|
912
|
+
expect(
|
|
913
|
+
result.functions.adminScriptRouter.environment
|
|
914
|
+
.ADMIN_SCRIPT_EXECUTOR_LAMBDA_ARN
|
|
915
|
+
).toBeDefined();
|
|
916
|
+
expect(
|
|
917
|
+
result.functions.reportRouter.environment
|
|
918
|
+
.REPORT_EXECUTOR_LAMBDA_ARN
|
|
919
|
+
).toBeDefined();
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('does NOT wire report scheduler env when enableScheduling is off', async () => {
|
|
923
|
+
const appDefinition = {
|
|
924
|
+
reports: [{ Definition: { name: 'my-report', version: '1.0.0' } }],
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const result = await adminScriptBuilder.build(appDefinition, {});
|
|
928
|
+
|
|
929
|
+
expect(result.resources.AdminScriptSchedulerRole).toBeUndefined();
|
|
930
|
+
expect(
|
|
931
|
+
result.functions.reportRouter.environment
|
|
932
|
+
).toBeUndefined();
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
623
936
|
describe('getName()', () => {
|
|
624
937
|
it('should return AdminScriptBuilder', () => {
|
|
625
938
|
expect(adminScriptBuilder.getName()).toBe('AdminScriptBuilder');
|