@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.
@@ -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');