@intentius/chant-lexicon-aws 0.46.0 → 0.50.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 (183) hide show
  1. package/dist/api/read-client.d.ts +10 -2
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/codegen/generate-lexicon.d.ts +5 -0
  4. package/dist/codegen/generate-lexicon.d.ts.map +1 -1
  5. package/dist/components/host-delivery.d.ts +4 -3
  6. package/dist/components/host-delivery.d.ts.map +1 -1
  7. package/dist/composites/agentcore-agent.d.ts +26 -1
  8. package/dist/composites/agentcore-agent.d.ts.map +1 -1
  9. package/dist/composites/bucket-deployment.d.ts +82 -0
  10. package/dist/composites/bucket-deployment.d.ts.map +1 -0
  11. package/dist/composites/dynamodb-table.d.ts +45 -0
  12. package/dist/composites/dynamodb-table.d.ts.map +1 -0
  13. package/dist/composites/ec2-instance-bundle.d.ts +61 -0
  14. package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
  15. package/dist/composites/ecr-repository.d.ts +49 -0
  16. package/dist/composites/ecr-repository.d.ts.map +1 -0
  17. package/dist/composites/fargate-service.d.ts +16 -0
  18. package/dist/composites/fargate-service.d.ts.map +1 -1
  19. package/dist/composites/index.d.ts +16 -2
  20. package/dist/composites/index.d.ts.map +1 -1
  21. package/dist/composites/monitoring-stack.d.ts +99 -0
  22. package/dist/composites/monitoring-stack.d.ts.map +1 -0
  23. package/dist/composites/nlb-service.d.ts +48 -0
  24. package/dist/composites/nlb-service.d.ts.map +1 -0
  25. package/dist/composites/step-functions-workflow.d.ts +36 -0
  26. package/dist/composites/step-functions-workflow.d.ts.map +1 -0
  27. package/dist/deep-observe.d.ts +45 -0
  28. package/dist/deep-observe.d.ts.map +1 -1
  29. package/dist/deep-topology.d.ts +104 -0
  30. package/dist/deep-topology.d.ts.map +1 -0
  31. package/dist/effect-receipt-row.d.ts +92 -0
  32. package/dist/effect-receipt-row.d.ts.map +1 -0
  33. package/dist/index.d.ts +7 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/integrity.json +14 -6
  36. package/dist/lint/audit-catalog.d.ts.map +1 -1
  37. package/dist/lint/post-synth/cf-refs.d.ts +17 -0
  38. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  39. package/dist/lint/post-synth/index.d.ts.map +1 -1
  40. package/dist/lint/post-synth/waw016.d.ts +10 -2
  41. package/dist/lint/post-synth/waw016.d.ts.map +1 -1
  42. package/dist/lint/post-synth/waw061.d.ts +19 -0
  43. package/dist/lint/post-synth/waw061.d.ts.map +1 -0
  44. package/dist/lint/post-synth/waw062.d.ts +19 -0
  45. package/dist/lint/post-synth/waw062.d.ts.map +1 -0
  46. package/dist/lint/post-synth/waw063.d.ts +35 -0
  47. package/dist/lint/post-synth/waw063.d.ts.map +1 -0
  48. package/dist/lint/post-synth/waw064.d.ts +17 -0
  49. package/dist/lint/post-synth/waw064.d.ts.map +1 -0
  50. package/dist/lint/post-synth/waw065.d.ts +23 -0
  51. package/dist/lint/post-synth/waw065.d.ts.map +1 -0
  52. package/dist/lint/post-synth/waw066.d.ts +25 -0
  53. package/dist/lint/post-synth/waw066.d.ts.map +1 -0
  54. package/dist/lint/post-synth/waw067.d.ts +24 -0
  55. package/dist/lint/post-synth/waw067.d.ts.map +1 -0
  56. package/dist/lint/post-synth/waw068.d.ts +20 -0
  57. package/dist/lint/post-synth/waw068.d.ts.map +1 -0
  58. package/dist/manifest.json +1 -1
  59. package/dist/meta.json +34 -0
  60. package/dist/okf/index.md +8 -0
  61. package/dist/okf/rules/WAW061.md +17 -0
  62. package/dist/okf/rules/WAW062.md +27 -0
  63. package/dist/okf/rules/WAW063.md +19 -0
  64. package/dist/okf/rules/WAW064.md +18 -0
  65. package/dist/okf/rules/WAW065.md +25 -0
  66. package/dist/okf/rules/WAW066.md +22 -0
  67. package/dist/okf/rules/WAW067.md +20 -0
  68. package/dist/okf/rules/WAW068.md +19 -0
  69. package/dist/okf/types/Action.md +1 -0
  70. package/dist/okf/types/AutoScalingGroup.md +1 -0
  71. package/dist/okf/types/Bucket.md +1 -0
  72. package/dist/okf/types/EC2Route.md +2 -0
  73. package/dist/okf/types/ECRRepository.md +1 -0
  74. package/dist/okf/types/Function.md +1 -0
  75. package/dist/okf/types/IamPolicy.md +1 -0
  76. package/dist/okf/types/InternetGateway.md +4 -0
  77. package/dist/okf/types/LaunchTemplate.md +1 -0
  78. package/dist/okf/types/LoadBalancer.md +1 -0
  79. package/dist/okf/types/ManagedPolicy.md +2 -0
  80. package/dist/okf/types/Map.md +5 -0
  81. package/dist/okf/types/NatGateway.md +4 -0
  82. package/dist/okf/types/Role.md +2 -0
  83. package/dist/okf/types/RouteTable.md +4 -0
  84. package/dist/okf/types/SecurityGroup.md +1 -0
  85. package/dist/okf/types/StateMachine.md +4 -0
  86. package/dist/okf/types/Subnet.md +6 -0
  87. package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
  88. package/dist/okf/types/Table.md +4 -0
  89. package/dist/okf/types/TransitGateway.md +6 -0
  90. package/dist/okf/types/TransitGatewayAttachment.md +4 -0
  91. package/dist/okf/types/TransitGatewayPeeringAttachment.md +4 -0
  92. package/dist/okf/types/TransitGatewayRoute.md +5 -0
  93. package/dist/okf/types/TransitGatewayRouteTable.md +4 -0
  94. package/dist/okf/types/TransitGatewayRouteTableAssociation.md +4 -0
  95. package/dist/okf/types/TransitGatewayRouteTablePropagation.md +4 -0
  96. package/dist/okf/types/TransitGatewayVpcAttachment.md +4 -0
  97. package/dist/okf/types/Type.md +8 -0
  98. package/dist/okf/types/VPNConnection.md +4 -0
  99. package/dist/okf/types/VPNGateway.md +4 -0
  100. package/dist/okf/types/Vpc.md +1 -0
  101. package/dist/op/activities/index.d.ts +6 -0
  102. package/dist/op/activities/index.d.ts.map +1 -1
  103. package/dist/op/builders.d.ts +47 -0
  104. package/dist/op/builders.d.ts.map +1 -0
  105. package/dist/plugin.d.ts.map +1 -1
  106. package/dist/receipt-store.d.ts +109 -0
  107. package/dist/receipt-store.d.ts.map +1 -0
  108. package/dist/rules/cf-refs.ts +40 -0
  109. package/dist/rules/waw016.ts +37 -16
  110. package/dist/rules/waw061.ts +67 -0
  111. package/dist/rules/waw062.ts +0 -0
  112. package/dist/rules/waw063.ts +0 -0
  113. package/dist/rules/waw064.ts +52 -0
  114. package/dist/rules/waw065.ts +110 -0
  115. package/dist/rules/waw066.ts +155 -0
  116. package/dist/rules/waw067.ts +114 -0
  117. package/dist/rules/waw068.ts +78 -0
  118. package/dist/serializer.d.ts.map +1 -1
  119. package/dist/skills/chant-aws-carve-terraform.md +26 -23
  120. package/dist/spec/parse.d.ts +7 -0
  121. package/dist/spec/parse.d.ts.map +1 -1
  122. package/package.json +3 -3
  123. package/src/api/read-client.test.ts +9 -0
  124. package/src/api/read-client.ts +15 -2
  125. package/src/codegen/fallback.ts +1 -0
  126. package/src/codegen/generate-lexicon.ts +6 -0
  127. package/src/codegen/sam.ts +9 -0
  128. package/src/components/host-delivery.ts +4 -3
  129. package/src/composites/agentcore-agent.ts +35 -13
  130. package/src/composites/bucket-deployment.test.ts +84 -0
  131. package/src/composites/bucket-deployment.ts +153 -0
  132. package/src/composites/composites.test.ts +325 -0
  133. package/src/composites/dynamodb-table.test.ts +154 -0
  134. package/src/composites/dynamodb-table.ts +124 -0
  135. package/src/composites/ec2-instance-bundle.ts +134 -0
  136. package/src/composites/ecr-repository.test.ts +118 -0
  137. package/src/composites/ecr-repository.ts +117 -0
  138. package/src/composites/fargate-service.ts +19 -3
  139. package/src/composites/index.ts +21 -2
  140. package/src/composites/monitoring-stack.test.ts +130 -0
  141. package/src/composites/monitoring-stack.ts +164 -0
  142. package/src/composites/nlb-service.ts +117 -0
  143. package/src/composites/step-functions-workflow.ts +127 -0
  144. package/src/deep-observe.test.ts +201 -1
  145. package/src/deep-observe.ts +204 -0
  146. package/src/deep-topology.test.ts +392 -0
  147. package/src/deep-topology.ts +237 -0
  148. package/src/effect-receipt-row.test.ts +164 -0
  149. package/src/effect-receipt-row.ts +147 -0
  150. package/src/generated/lexicon-aws.json +34 -0
  151. package/src/index.ts +36 -7
  152. package/src/lint/audit-catalog.ts +16 -0
  153. package/src/lint/post-synth/cf-refs.ts +40 -0
  154. package/src/lint/post-synth/index.ts +16 -0
  155. package/src/lint/post-synth/waw016.test.ts +61 -4
  156. package/src/lint/post-synth/waw016.ts +37 -16
  157. package/src/lint/post-synth/waw061.test.ts +99 -0
  158. package/src/lint/post-synth/waw061.ts +67 -0
  159. package/src/lint/post-synth/waw062.test.ts +109 -0
  160. package/src/lint/post-synth/waw062.ts +0 -0
  161. package/src/lint/post-synth/waw063.test.ts +220 -0
  162. package/src/lint/post-synth/waw063.ts +0 -0
  163. package/src/lint/post-synth/waw064.test.ts +79 -0
  164. package/src/lint/post-synth/waw064.ts +52 -0
  165. package/src/lint/post-synth/waw065.test.ts +90 -0
  166. package/src/lint/post-synth/waw065.ts +110 -0
  167. package/src/lint/post-synth/waw066.test.ts +108 -0
  168. package/src/lint/post-synth/waw066.ts +155 -0
  169. package/src/lint/post-synth/waw067.test.ts +79 -0
  170. package/src/lint/post-synth/waw067.ts +114 -0
  171. package/src/lint/post-synth/waw068.test.ts +74 -0
  172. package/src/lint/post-synth/waw068.ts +78 -0
  173. package/src/lsp/hover.ts +17 -2
  174. package/src/op/activities/index.ts +15 -0
  175. package/src/op/builders.test.ts +61 -0
  176. package/src/op/builders.ts +72 -0
  177. package/src/plugin.ts +41 -10
  178. package/src/receipt-store.test.ts +204 -0
  179. package/src/receipt-store.ts +315 -0
  180. package/src/serializer.ts +105 -2
  181. package/src/skills/chant-aws-carve-terraform.md +26 -23
  182. package/src/spec/parse.test.ts +74 -0
  183. package/src/spec/parse.ts +26 -6
@@ -13,9 +13,12 @@ import { VpcDefault } from "./vpc-default";
13
13
  import { FargateAlb } from "./fargate-alb";
14
14
  import { AlbShared } from "./alb-shared";
15
15
  import { FargateService } from "./fargate-service";
16
+ import { NlbService } from "./nlb-service";
16
17
  import { RdsInstance } from "./rds-instance";
17
18
  import { Ec2InstanceRole } from "./ec2-instance-role";
19
+ import { Ec2InstanceBundle } from "./ec2-instance-bundle";
18
20
  import { MinimalVpc } from "./minimal-vpc";
21
+ import { StepFunctionsWorkflow } from "./step-functions-workflow";
19
22
 
20
23
  const baseProps = {
21
24
  name: "TestFunc",
@@ -639,6 +642,112 @@ describe("FargateService", () => {
639
642
  });
640
643
  });
641
644
 
645
+ describe("NlbService", () => {
646
+ const nlbProps = {
647
+ vpcId: "vpc-123",
648
+ subnetIds: ["subnet-pub1", "subnet-pub2"],
649
+ };
650
+
651
+ test("returns 3 members with correct names", () => {
652
+ const instance = NlbService(nlbProps);
653
+ const names = Object.keys(instance.members);
654
+ expect(names).toEqual(["nlb", "targetGroup", "listener"]);
655
+ });
656
+
657
+ test("expandComposite produces correct logical names", () => {
658
+ const expanded = expandComposite("app", NlbService(nlbProps));
659
+ expect(expanded.has("appNlb")).toBe(true);
660
+ expect(expanded.has("appTargetGroup")).toBe(true);
661
+ expect(expanded.has("appListener")).toBe(true);
662
+ expect(expanded.size).toBe(3);
663
+ });
664
+
665
+ test("NLB has Type network and given subnets", () => {
666
+ const instance = NlbService(nlbProps);
667
+ const nlbResourceProps = (instance.nlb as any).props;
668
+ expect(nlbResourceProps.Type).toBe("network");
669
+ expect(nlbResourceProps.Scheme).toBe("internet-facing");
670
+ expect(nlbResourceProps.Subnets).toEqual(["subnet-pub1", "subnet-pub2"]);
671
+ });
672
+
673
+ test("no security group is created", () => {
674
+ const instance = NlbService(nlbProps);
675
+ expect(Object.keys(instance.members)).not.toContain("albSg");
676
+ expect(Object.keys(instance.members)).not.toContain("nlbSg");
677
+ });
678
+
679
+ test("defaults: TCP protocol, port 80, ip target type", () => {
680
+ const instance = NlbService(nlbProps);
681
+ const tgProps = (instance.targetGroup as any).props;
682
+ expect(tgProps.Protocol).toBe("TCP");
683
+ expect(tgProps.Port).toBe(80);
684
+ expect(tgProps.TargetType).toBe("ip");
685
+ expect(tgProps.HealthCheckProtocol).toBe("TCP");
686
+
687
+ const listenerProps = (instance.listener as any).props;
688
+ expect(listenerProps.Protocol).toBe("TCP");
689
+ expect(listenerProps.Port).toBe(80);
690
+ });
691
+
692
+ test("targetPort defaults to listenerPort but can be overridden", () => {
693
+ const instance = NlbService({ ...nlbProps, listenerPort: 443, targetPort: 8443 });
694
+ const tgProps = (instance.targetGroup as any).props;
695
+ expect(tgProps.Port).toBe(8443);
696
+ const listenerProps = (instance.listener as any).props;
697
+ expect(listenerProps.Port).toBe(443);
698
+ });
699
+
700
+ test("listener forwards to the target group", () => {
701
+ const instance = NlbService(nlbProps);
702
+ const listenerProps = (instance.listener as any).props;
703
+ expect(listenerProps.DefaultActions).toHaveLength(1);
704
+ const action = (listenerProps.DefaultActions[0] as any).props;
705
+ expect(action.Type).toBe("forward");
706
+ });
707
+
708
+ test("static targets are registered on the target group", () => {
709
+ const instance = NlbService({
710
+ ...nlbProps,
711
+ targetType: "instance",
712
+ targets: [{ id: "i-0123456789abcdef0", port: 8080 }],
713
+ });
714
+ const tgProps = (instance.targetGroup as any).props;
715
+ expect(tgProps.TargetType).toBe("instance");
716
+ expect(tgProps.Targets).toHaveLength(1);
717
+ const target = (tgProps.Targets[0] as any).props;
718
+ expect(target.Id).toBe("i-0123456789abcdef0");
719
+ expect(target.Port).toBe(8080);
720
+ });
721
+
722
+ test("internal scheme is respected", () => {
723
+ const instance = NlbService({ ...nlbProps, scheme: "internal" });
724
+ const nlbResourceProps = (instance.nlb as any).props;
725
+ expect(nlbResourceProps.Scheme).toBe("internal");
726
+ });
727
+
728
+ test("TLS protocol adds certificate to listener", () => {
729
+ const instance = NlbService({
730
+ ...nlbProps,
731
+ protocol: "TLS",
732
+ certificateArn: "arn:aws:acm:us-east-1:123:certificate/abc",
733
+ });
734
+ const listenerProps = (instance.listener as any).props;
735
+ expect(listenerProps.Protocol).toBe("TLS");
736
+ expect(listenerProps.Certificates).toHaveLength(1);
737
+ const cert = (listenerProps.Certificates[0] as any).props;
738
+ expect(cert.CertificateArn).toBe("arn:aws:acm:us-east-1:123:certificate/abc");
739
+
740
+ const tgProps = (instance.targetGroup as any).props;
741
+ expect(tgProps.Protocol).toBe("TLS");
742
+ });
743
+
744
+ test("throws when protocol is TLS without a certificateArn", () => {
745
+ expect(() =>
746
+ NlbService({ ...nlbProps, protocol: "TLS" }),
747
+ ).toThrow("NlbService requires certificateArn when protocol is TLS");
748
+ });
749
+ });
750
+
642
751
  describe("RdsInstance", () => {
643
752
  const rdsProps = {
644
753
  vpcId: "vpc-123",
@@ -897,6 +1006,140 @@ describe("Ec2InstanceRole", () => {
897
1006
  });
898
1007
  });
899
1008
 
1009
+ describe("Ec2InstanceBundle", () => {
1010
+ const instanceProps = {
1011
+ imageId: "ami-0abcdef1234567890",
1012
+ vpcId: "vpc-123",
1013
+ subnetId: "subnet-456",
1014
+ };
1015
+
1016
+ test("returns role, instanceProfile, sg, instance members", () => {
1017
+ const instance = Ec2InstanceBundle(instanceProps);
1018
+ expect(Object.keys(instance.members)).toEqual(["role", "instanceProfile", "sg", "instance"]);
1019
+ });
1020
+
1021
+ test("expandComposite produces correct logical names", () => {
1022
+ const expanded = expandComposite("web", Ec2InstanceBundle(instanceProps));
1023
+ expect(expanded.has("webRole")).toBe(true);
1024
+ expect(expanded.has("webInstanceProfile")).toBe(true);
1025
+ expect(expanded.has("webSg")).toBe(true);
1026
+ expect(expanded.has("webInstance")).toBe(true);
1027
+ expect(expanded.size).toBe(4);
1028
+ });
1029
+
1030
+ test("role has EC2 trust policy", () => {
1031
+ const instance = Ec2InstanceBundle(instanceProps);
1032
+ const roleProps = (instance.role as any).props;
1033
+ expect(roleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe("ec2.amazonaws.com");
1034
+ });
1035
+
1036
+ test("instanceProfile references the role", () => {
1037
+ const instance = Ec2InstanceBundle(instanceProps);
1038
+ const profileProps = (instance.instanceProfile as any).props;
1039
+ expect(profileProps.Roles).toHaveLength(1);
1040
+ });
1041
+
1042
+ test("ManagedPolicyArns and Policies pass through to the role", () => {
1043
+ const arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore";
1044
+ const instance = Ec2InstanceBundle({
1045
+ ...instanceProps,
1046
+ ManagedPolicyArns: [arn],
1047
+ Policies: [{ PolicyName: "extra", PolicyDocument: { Version: "2012-10-17", Statement: [] } }],
1048
+ });
1049
+ const roleProps = (instance.role as any).props;
1050
+ expect(roleProps.ManagedPolicyArns).toContain(arn);
1051
+ expect(roleProps.Policies).toHaveLength(1);
1052
+ });
1053
+
1054
+ test("instance wires ImageId, InstanceType default, subnet, SG, and instance profile", () => {
1055
+ const instance = Ec2InstanceBundle(instanceProps);
1056
+ const instanceProps_ = (instance.instance as any).props;
1057
+ expect(instanceProps_.ImageId).toBe("ami-0abcdef1234567890");
1058
+ expect(instanceProps_.InstanceType).toBe("t3.micro");
1059
+ expect(instanceProps_.SubnetId).toBe("subnet-456");
1060
+ expect(instanceProps_.SecurityGroupIds).toHaveLength(1);
1061
+ expect(instanceProps_.IamInstanceProfile).toBeDefined();
1062
+ });
1063
+
1064
+ test("custom instanceType is respected", () => {
1065
+ const instance = Ec2InstanceBundle({ ...instanceProps, instanceType: "m5.large" });
1066
+ expect((instance.instance as any).props.InstanceType).toBe("m5.large");
1067
+ });
1068
+
1069
+ test("keyName is passed through when provided", () => {
1070
+ const instance = Ec2InstanceBundle({ ...instanceProps, keyName: "my-key" });
1071
+ expect((instance.instance as any).props.KeyName).toBe("my-key");
1072
+ });
1073
+
1074
+ test("keyName is omitted by default", () => {
1075
+ const instance = Ec2InstanceBundle(instanceProps);
1076
+ expect((instance.instance as any).props.KeyName).toBeUndefined();
1077
+ });
1078
+
1079
+ test("userData is wrapped in Fn::Base64", () => {
1080
+ const instance = Ec2InstanceBundle({ ...instanceProps, userData: "#!/bin/bash\necho hi" });
1081
+ const userData = (instance.instance as any).props.UserData;
1082
+ expect(JSON.stringify(userData)).toContain("Fn::Base64");
1083
+ });
1084
+
1085
+ test("no ingress rules by default", () => {
1086
+ const instance = Ec2InstanceBundle(instanceProps);
1087
+ expect((instance.sg as any).props.SecurityGroupIngress).toBeUndefined();
1088
+ });
1089
+
1090
+ test("ingress with cidr produces a CidrIp rule", () => {
1091
+ const instance = Ec2InstanceBundle({
1092
+ ...instanceProps,
1093
+ ingress: [{ fromPort: 22, cidr: "10.0.0.0/16", description: "ssh" }],
1094
+ });
1095
+ const rules = (instance.sg as any).props.SecurityGroupIngress;
1096
+ expect(rules).toHaveLength(1);
1097
+ const rule = (rules[0] as any).props;
1098
+ expect(rule.CidrIp).toBe("10.0.0.0/16");
1099
+ expect(rule.FromPort).toBe(22);
1100
+ expect(rule.ToPort).toBe(22);
1101
+ expect(rule.IpProtocol).toBe("tcp");
1102
+ expect(rule.Description).toBe("ssh");
1103
+ });
1104
+
1105
+ test("ingress with sourceSecurityGroupId produces a SG-sourced rule", () => {
1106
+ const instance = Ec2InstanceBundle({
1107
+ ...instanceProps,
1108
+ ingress: [{ fromPort: 80, toPort: 8080, sourceSecurityGroupId: "sg-abc123" }],
1109
+ });
1110
+ const rule = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
1111
+ expect(rule.SourceSecurityGroupId).toBe("sg-abc123");
1112
+ expect(rule.FromPort).toBe(80);
1113
+ expect(rule.ToPort).toBe(8080);
1114
+ });
1115
+
1116
+ test("ingress with cidrIpv6 produces an IPv6 rule", () => {
1117
+ const instance = Ec2InstanceBundle({
1118
+ ...instanceProps,
1119
+ ingress: [{ fromPort: 443, cidrIpv6: "::/0" }],
1120
+ });
1121
+ const rule = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
1122
+ expect(rule.CidrIpv6).toBe("::/0");
1123
+ });
1124
+
1125
+ test("ingress rule with no source throws", () => {
1126
+ expect(() =>
1127
+ Ec2InstanceBundle({ ...instanceProps, ingress: [{ fromPort: 22 }] }),
1128
+ ).toThrow(/needs one of cidr, cidrIpv6, or sourceSecurityGroupId/);
1129
+ });
1130
+
1131
+ test("multiple ingress rules are all applied", () => {
1132
+ const instance = Ec2InstanceBundle({
1133
+ ...instanceProps,
1134
+ ingress: [
1135
+ { fromPort: 22, cidr: "10.0.0.0/16" },
1136
+ { fromPort: 443, cidr: "0.0.0.0/0" },
1137
+ ],
1138
+ });
1139
+ expect((instance.sg as any).props.SecurityGroupIngress).toHaveLength(2);
1140
+ });
1141
+ });
1142
+
900
1143
  describe("MinimalVpc", () => {
901
1144
  test("returns 8 members", () => {
902
1145
  const instance = MinimalVpc({});
@@ -930,3 +1173,85 @@ describe("MinimalVpc", () => {
930
1173
  expect(expanded.size).toBe(8);
931
1174
  });
932
1175
  });
1176
+
1177
+ describe("StepFunctionsWorkflow", () => {
1178
+ const passDefinition = { StartAt: "Hello", States: { Hello: { Type: "Pass", End: true } } };
1179
+
1180
+ test("returns role, logGroup, stateMachine members", () => {
1181
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1182
+ expect(Object.keys(instance.members)).toEqual(["role", "logGroup", "stateMachine"]);
1183
+ });
1184
+
1185
+ test("role has states.amazonaws.com trust policy", () => {
1186
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1187
+ const roleProps = (instance.role as any).props;
1188
+ expect(roleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe("states.amazonaws.com");
1189
+ });
1190
+
1191
+ test("role always carries the log-delivery policy", () => {
1192
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1193
+ const roleProps = (instance.role as any).props;
1194
+ const names = roleProps.Policies.map((p: any) => p.props.PolicyName);
1195
+ expect(names).toContain("StepFunctionsLogDelivery");
1196
+ });
1197
+
1198
+ test("lambdaFunctionArns grants InvokeFunction on those ARNs", () => {
1199
+ const instance = StepFunctionsWorkflow({
1200
+ definition: passDefinition,
1201
+ lambdaFunctionArns: ["arn:aws:lambda:us-east-1:123456789012:function:worker"],
1202
+ });
1203
+ const roleProps = (instance.role as any).props;
1204
+ const invokePolicy = roleProps.Policies.find((p: any) => p.props.PolicyName === "InvokeLambdaTasks");
1205
+ expect(invokePolicy).toBeDefined();
1206
+ expect(invokePolicy.props.PolicyDocument.Statement[0].Resource).toEqual([
1207
+ "arn:aws:lambda:us-east-1:123456789012:function:worker",
1208
+ ]);
1209
+ });
1210
+
1211
+ test("without lambdaFunctionArns, no InvokeLambdaTasks policy", () => {
1212
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1213
+ const roleProps = (instance.role as any).props;
1214
+ const names = roleProps.Policies.map((p: any) => p.props.PolicyName);
1215
+ expect(names).not.toContain("InvokeLambdaTasks");
1216
+ });
1217
+
1218
+ test("stateMachine carries the definition object as Definition (not a string)", () => {
1219
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1220
+ const smProps = (instance.stateMachine as any).props;
1221
+ expect(smProps.Definition).toEqual(passDefinition);
1222
+ expect(smProps.DefinitionString).toBeUndefined();
1223
+ });
1224
+
1225
+ test("stateMachine references role.Arn and defaults to STANDARD", () => {
1226
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1227
+ const smProps = (instance.stateMachine as any).props;
1228
+ expect(smProps.RoleArn).toBeInstanceOf(AttrRef);
1229
+ expect(smProps.StateMachineType).toBe("STANDARD");
1230
+ });
1231
+
1232
+ test("logGroup defaults to 14-day retention, overridable", () => {
1233
+ const defaultInstance = StepFunctionsWorkflow({ definition: passDefinition });
1234
+ expect((defaultInstance.logGroup as any).props.RetentionInDays).toBe(14);
1235
+
1236
+ const overridden = StepFunctionsWorkflow({ definition: passDefinition, logRetentionDays: 30 });
1237
+ expect((overridden.logGroup as any).props.RetentionInDays).toBe(30);
1238
+ });
1239
+
1240
+ test("LoggingConfiguration wires the log group and defaults level to ALL", () => {
1241
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1242
+ const smProps = (instance.stateMachine as any).props;
1243
+ const loggingProps = (smProps.LoggingConfiguration as any).props;
1244
+ expect(loggingProps.Level).toBe("ALL");
1245
+ const destProps = (loggingProps.Destinations[0] as any).props;
1246
+ const cwProps = (destProps.CloudWatchLogsLogGroup as any).props;
1247
+ expect(cwProps.LogGroupArn).toBeInstanceOf(AttrRef);
1248
+ });
1249
+
1250
+ test("expandComposite produces 3 entries", () => {
1251
+ const expanded = expandComposite("workflow", StepFunctionsWorkflow({ definition: passDefinition }));
1252
+ expect(expanded.has("workflowRole")).toBe(true);
1253
+ expect(expanded.has("workflowLogGroup")).toBe(true);
1254
+ expect(expanded.has("workflowStateMachine")).toBe(true);
1255
+ expect(expanded.size).toBe(3);
1256
+ });
1257
+ });
@@ -0,0 +1,154 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { expandComposite } from "@intentius/chant";
3
+ import { DynamoDBTable } from "./dynamodb-table";
4
+
5
+ // Generated property-kind instances (Table_KeySchema, Table_Projection, ...) store
6
+ // their data on a non-enumerable `.props`, so `toEqual` against a plain object
7
+ // compares empty own-enumerable-property sets unless each instance is unwrapped
8
+ // first. `p` unwraps one instance; `ps` maps an array of them.
9
+ const p = (x: any) => x.props;
10
+ const ps = (xs: any[]) => xs.map(p);
11
+
12
+ describe("DynamoDBTable", () => {
13
+ test("partition-key-only: PAY_PER_REQUEST by default, single member", () => {
14
+ const instance = DynamoDBTable({ partitionKey: { name: "id" } });
15
+ expect(Object.keys(instance.members)).toEqual(["table"]);
16
+ const props = (instance.table as any).props;
17
+ expect(props.BillingMode).toBe("PAY_PER_REQUEST");
18
+ expect(ps(props.KeySchema)).toEqual([{ AttributeName: "id", KeyType: "HASH" }]);
19
+ expect(ps(props.AttributeDefinitions)).toEqual([{ AttributeName: "id", AttributeType: "S" }]);
20
+ expect(props.ProvisionedThroughput).toBeUndefined();
21
+ });
22
+
23
+ test("expandComposite produces the table's logical name", () => {
24
+ const expanded = expandComposite("orders", DynamoDBTable({ partitionKey: { name: "id" } }));
25
+ expect(expanded.has("ordersTable")).toBe(true);
26
+ expect(expanded.size).toBe(1);
27
+ });
28
+
29
+ test("partition + sort key, non-default types", () => {
30
+ const instance = DynamoDBTable({
31
+ partitionKey: { name: "pk", type: "S" },
32
+ sortKey: { name: "sk", type: "N" },
33
+ });
34
+ const props = (instance.table as any).props;
35
+ expect(ps(props.KeySchema)).toEqual([
36
+ { AttributeName: "pk", KeyType: "HASH" },
37
+ { AttributeName: "sk", KeyType: "RANGE" },
38
+ ]);
39
+ expect(ps(props.AttributeDefinitions)).toEqual([
40
+ { AttributeName: "pk", AttributeType: "S" },
41
+ { AttributeName: "sk", AttributeType: "N" },
42
+ ]);
43
+ });
44
+
45
+ test("no GSIs: GlobalSecondaryIndexes is omitted, not an empty array", () => {
46
+ const props = (DynamoDBTable({ partitionKey: { name: "id" } }).table as any).props;
47
+ expect(props.GlobalSecondaryIndexes).toBeUndefined();
48
+ });
49
+
50
+ test("a GSI defaults to ALL projection and, on-demand billing, carries no throughput", () => {
51
+ const instance = DynamoDBTable({
52
+ partitionKey: { name: "pk" },
53
+ globalSecondaryIndexes: [{ name: "byStatus", partitionKey: { name: "status" } }],
54
+ });
55
+ const props = (instance.table as any).props;
56
+ expect(props.GlobalSecondaryIndexes).toHaveLength(1);
57
+ const gsi = p(props.GlobalSecondaryIndexes[0]);
58
+ expect(gsi.IndexName).toBe("byStatus");
59
+ expect(ps(gsi.KeySchema)).toEqual([{ AttributeName: "status", KeyType: "HASH" }]);
60
+ expect(p(gsi.Projection)).toEqual({ ProjectionType: "ALL" });
61
+ expect(gsi.ProvisionedThroughput).toBeUndefined();
62
+ });
63
+
64
+ test("a GSI reusing the table's partition key does not duplicate its AttributeDefinition", () => {
65
+ const instance = DynamoDBTable({
66
+ partitionKey: { name: "pk" },
67
+ sortKey: { name: "sk" },
68
+ globalSecondaryIndexes: [{ name: "byPk", partitionKey: { name: "pk" }, sortKey: { name: "gsiSort" } }],
69
+ });
70
+ const props = (instance.table as any).props;
71
+ const names = ps(props.AttributeDefinitions).map((a: any) => a.AttributeName);
72
+ expect(names).toEqual(["pk", "sk", "gsiSort"]);
73
+ });
74
+
75
+ test("INCLUDE projection carries nonKeyAttributes through", () => {
76
+ const instance = DynamoDBTable({
77
+ partitionKey: { name: "pk" },
78
+ globalSecondaryIndexes: [{
79
+ name: "byStatus",
80
+ partitionKey: { name: "status" },
81
+ projectionType: "INCLUDE",
82
+ nonKeyAttributes: ["total", "updatedAt"],
83
+ }],
84
+ });
85
+ const gsi = p(((instance.table as any).props.GlobalSecondaryIndexes)[0]);
86
+ expect(p(gsi.Projection)).toEqual({ ProjectionType: "INCLUDE", NonKeyAttributes: ["total", "updatedAt"] });
87
+ });
88
+
89
+ test("ttlAttribute enables TimeToLiveSpecification; omitted, no TTL block at all", () => {
90
+ const withTtl = (DynamoDBTable({ partitionKey: { name: "id" }, ttlAttribute: "expiresAt" }).table as any).props;
91
+ expect(p(withTtl.TimeToLiveSpecification)).toEqual({ Enabled: true, AttributeName: "expiresAt" });
92
+
93
+ const withoutTtl = (DynamoDBTable({ partitionKey: { name: "id" } }).table as any).props;
94
+ expect(withoutTtl.TimeToLiveSpecification).toBeUndefined();
95
+ });
96
+
97
+ test("stream: true defaults to NEW_AND_OLD_IMAGES", () => {
98
+ const props = (DynamoDBTable({ partitionKey: { name: "id" }, stream: true }).table as any).props;
99
+ expect(p(props.StreamSpecification)).toEqual({ StreamViewType: "NEW_AND_OLD_IMAGES" });
100
+ });
101
+
102
+ test("stream view type is overridable; omitted, no stream block at all", () => {
103
+ const withView = (DynamoDBTable({
104
+ partitionKey: { name: "id" },
105
+ stream: { viewType: "KEYS_ONLY" },
106
+ }).table as any).props;
107
+ expect(p(withView.StreamSpecification)).toEqual({ StreamViewType: "KEYS_ONLY" });
108
+
109
+ const withoutStream = (DynamoDBTable({ partitionKey: { name: "id" } }).table as any).props;
110
+ expect(withoutStream.StreamSpecification).toBeUndefined();
111
+ });
112
+
113
+ test("PROVISIONED billing mode defaults capacity to 5/5 on both table and GSI", () => {
114
+ const instance = DynamoDBTable({
115
+ partitionKey: { name: "id" },
116
+ billingMode: "PROVISIONED",
117
+ globalSecondaryIndexes: [{ name: "byStatus", partitionKey: { name: "status" } }],
118
+ });
119
+ const props = (instance.table as any).props;
120
+ expect(p(props.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 5, WriteCapacityUnits: 5 });
121
+ const gsi = p(props.GlobalSecondaryIndexes[0]);
122
+ expect(p(gsi.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 5, WriteCapacityUnits: 5 });
123
+ });
124
+
125
+ test("PROVISIONED billing mode: explicit table capacity flows to a GSI that doesn't set its own", () => {
126
+ const instance = DynamoDBTable({
127
+ partitionKey: { name: "id" },
128
+ billingMode: "PROVISIONED",
129
+ provisionedThroughput: { readCapacity: 20, writeCapacity: 10 },
130
+ globalSecondaryIndexes: [
131
+ { name: "inherits", partitionKey: { name: "status" } },
132
+ { name: "overrides", partitionKey: { name: "kind" }, provisionedThroughput: { readCapacity: 1, writeCapacity: 1 } },
133
+ ],
134
+ });
135
+ const props = (instance.table as any).props;
136
+ expect(p(props.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 20, WriteCapacityUnits: 10 });
137
+ const [inherits, overrides] = ps(props.GlobalSecondaryIndexes);
138
+ expect(p(inherits.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 20, WriteCapacityUnits: 10 });
139
+ expect(p(overrides.ProvisionedThroughput)).toEqual({ ReadCapacityUnits: 1, WriteCapacityUnits: 1 });
140
+ });
141
+
142
+ test("tableName is threaded through when given", () => {
143
+ const props = (DynamoDBTable({ partitionKey: { name: "id" }, tableName: "orders-prod" }).table as any).props;
144
+ expect(props.TableName).toBe("orders-prod");
145
+ });
146
+
147
+ test("defaults escape hatch reaches the table (e.g. deletion protection)", () => {
148
+ const props = (DynamoDBTable({
149
+ partitionKey: { name: "id" },
150
+ defaults: { table: { DeletionProtectionEnabled: true } },
151
+ }).table as any).props;
152
+ expect(props.DeletionProtectionEnabled).toBe(true);
153
+ });
154
+ });
@@ -0,0 +1,124 @@
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
+ import {
3
+ Table,
4
+ Table_AttributeDefinition,
5
+ Table_KeySchema,
6
+ Table_GlobalSecondaryIndex,
7
+ Table_Projection,
8
+ Table_ProvisionedThroughput,
9
+ Table_StreamSpecification,
10
+ Table_TimeToLiveSpecification,
11
+ } from "../generated";
12
+
13
+ export interface DynamoDBKey {
14
+ name: string;
15
+ type?: "S" | "N" | "B";
16
+ }
17
+
18
+ export interface DynamoDBCapacity {
19
+ readCapacity: number;
20
+ writeCapacity: number;
21
+ }
22
+
23
+ export interface DynamoDBGlobalSecondaryIndex {
24
+ name: string;
25
+ partitionKey: DynamoDBKey;
26
+ sortKey?: DynamoDBKey;
27
+ /** Defaults to "ALL" — projecting the whole item is the common case; narrow it deliberately. */
28
+ projectionType?: "ALL" | "KEYS_ONLY" | "INCLUDE";
29
+ /** Only meaningful with projectionType "INCLUDE". */
30
+ nonKeyAttributes?: string[];
31
+ /** PROVISIONED tables only. Falls back to the table's own capacity when a GSI doesn't need its own. */
32
+ provisionedThroughput?: DynamoDBCapacity;
33
+ }
34
+
35
+ export interface DynamoDBTableProps {
36
+ /** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). */
37
+ tableName?: Value<string>;
38
+ partitionKey: DynamoDBKey;
39
+ sortKey?: DynamoDBKey;
40
+ /** Defaults to "PAY_PER_REQUEST" — the recommended default for most workloads (matches LambdaDynamoDB). */
41
+ billingMode?: "PAY_PER_REQUEST" | "PROVISIONED";
42
+ /** Required when billingMode is "PROVISIONED"; ignored otherwise. Falls back to 5/5 (the DynamoDB console default) if omitted. */
43
+ provisionedThroughput?: DynamoDBCapacity;
44
+ globalSecondaryIndexes?: DynamoDBGlobalSecondaryIndex[];
45
+ /** Attribute name that carries the expiry timestamp. Enables TimeToLiveSpecification when set. */
46
+ ttlAttribute?: string;
47
+ /** Enables StreamSpecification. `true` defaults to NEW_AND_OLD_IMAGES (the shape most stream consumers want). */
48
+ stream?: boolean | { viewType?: "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | "KEYS_ONLY" };
49
+ defaults?: {
50
+ table?: Partial<ConstructorParameters<typeof Table>[0]>;
51
+ };
52
+ }
53
+
54
+ const DEFAULT_CAPACITY: DynamoDBCapacity = { readCapacity: 5, writeCapacity: 5 };
55
+
56
+ function toProvisionedThroughput(capacity: DynamoDBCapacity): InstanceType<typeof Table_ProvisionedThroughput> {
57
+ return new Table_ProvisionedThroughput({
58
+ ReadCapacityUnits: capacity.readCapacity,
59
+ WriteCapacityUnits: capacity.writeCapacity,
60
+ });
61
+ }
62
+
63
+ function toKeySchema(partitionKey: DynamoDBKey, sortKey?: DynamoDBKey): InstanceType<typeof Table_KeySchema>[] {
64
+ // A ternary (not an `if`) keeps the `new`s unconditional per EVL002, and
65
+ // returning array literals directly from each branch — no spread — sidesteps
66
+ // EVL004 (outside a Composite factory, EVL004 only traces a spread source to a
67
+ // module-top-level const or a literal, not a local variable).
68
+ const partitionEntry = new Table_KeySchema({ AttributeName: partitionKey.name, KeyType: "HASH" });
69
+ const sortEntry = sortKey ? new Table_KeySchema({ AttributeName: sortKey.name, KeyType: "RANGE" }) : undefined;
70
+ return sortEntry ? [partitionEntry, sortEntry] : [partitionEntry];
71
+ }
72
+
73
+ export const DynamoDBTable = Composite((props: DynamoDBTableProps) => {
74
+ const billingMode = props.billingMode ?? "PAY_PER_REQUEST";
75
+ const { defaults } = props;
76
+
77
+ // Every key attribute used anywhere (table keys + every GSI's keys) needs exactly
78
+ // one AttributeDefinition. A Map dedupes by name so a GSI reusing the table's
79
+ // partition key (a common pattern) doesn't produce a duplicate definition.
80
+ const attributeTypes = new Map<string, "S" | "N" | "B">();
81
+ attributeTypes.set(props.partitionKey.name, props.partitionKey.type ?? "S");
82
+ if (props.sortKey) attributeTypes.set(props.sortKey.name, props.sortKey.type ?? "S");
83
+ for (const gsi of props.globalSecondaryIndexes ?? []) {
84
+ attributeTypes.set(gsi.partitionKey.name, gsi.partitionKey.type ?? "S");
85
+ if (gsi.sortKey) attributeTypes.set(gsi.sortKey.name, gsi.sortKey.type ?? "S");
86
+ }
87
+ const attributeDefinitions = [...attributeTypes.entries()].map(
88
+ ([AttributeName, AttributeType]) => new Table_AttributeDefinition({ AttributeName, AttributeType }),
89
+ );
90
+
91
+ const globalSecondaryIndexes = (props.globalSecondaryIndexes ?? []).map((gsi) => new Table_GlobalSecondaryIndex({
92
+ IndexName: gsi.name,
93
+ KeySchema: toKeySchema(gsi.partitionKey, gsi.sortKey),
94
+ Projection: new Table_Projection({
95
+ ProjectionType: gsi.projectionType ?? "ALL",
96
+ ...(gsi.nonKeyAttributes ? { NonKeyAttributes: gsi.nonKeyAttributes } : {}),
97
+ }),
98
+ ...(billingMode === "PROVISIONED"
99
+ ? { ProvisionedThroughput: toProvisionedThroughput(gsi.provisionedThroughput ?? props.provisionedThroughput ?? DEFAULT_CAPACITY) }
100
+ : {}),
101
+ }));
102
+
103
+ const streamConfig = props.stream === true ? {} : props.stream || undefined;
104
+ const ttlEnabled = props.ttlAttribute !== undefined;
105
+
106
+ const table = new Table(mergeDefaults({
107
+ TableName: props.tableName,
108
+ BillingMode: billingMode,
109
+ AttributeDefinitions: attributeDefinitions,
110
+ KeySchema: toKeySchema(props.partitionKey, props.sortKey),
111
+ ...(globalSecondaryIndexes.length > 0 ? { GlobalSecondaryIndexes: globalSecondaryIndexes } : {}),
112
+ ...(billingMode === "PROVISIONED"
113
+ ? { ProvisionedThroughput: toProvisionedThroughput(props.provisionedThroughput ?? DEFAULT_CAPACITY) }
114
+ : {}),
115
+ ...(streamConfig
116
+ ? { StreamSpecification: new Table_StreamSpecification({ StreamViewType: streamConfig.viewType ?? "NEW_AND_OLD_IMAGES" }) }
117
+ : {}),
118
+ ...(ttlEnabled
119
+ ? { TimeToLiveSpecification: new Table_TimeToLiveSpecification({ Enabled: true, AttributeName: props.ttlAttribute }) }
120
+ : {}),
121
+ }, defaults?.table));
122
+
123
+ return { table };
124
+ }, "DynamoDBTable");