@intentius/chant-lexicon-aws 0.1.0 → 0.1.4

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 (39) hide show
  1. package/dist/integrity.json +25 -19
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +734 -435
  4. package/dist/rules/waw032.ts +52 -0
  5. package/dist/rules/waw033.ts +86 -0
  6. package/dist/rules/waw034.ts +63 -0
  7. package/dist/rules/waw035.ts +71 -0
  8. package/dist/rules/waw036.ts +88 -0
  9. package/dist/rules/waw037.ts +81 -0
  10. package/dist/types/index.d.ts +991 -59
  11. package/package.json +2 -2
  12. package/src/codegen/docs.ts +9 -1
  13. package/src/composites/composites.test.ts +65 -0
  14. package/src/composites/ec2-instance-role.ts +39 -0
  15. package/src/composites/efs-with-access-point.ts +90 -0
  16. package/src/composites/fargate-service.ts +102 -2
  17. package/src/composites/index.ts +8 -0
  18. package/src/composites/lambda-dynamodb.ts +66 -17
  19. package/src/composites/lambda-function.ts +2 -1
  20. package/src/composites/lambda-s3.ts +66 -20
  21. package/src/composites/minimal-vpc.ts +71 -0
  22. package/src/composites/solr-fargate-service.ts +42 -0
  23. package/src/generated/index.d.ts +991 -59
  24. package/src/generated/index.ts +34 -5
  25. package/src/generated/lexicon-aws.json +734 -435
  26. package/src/index.ts +4 -0
  27. package/src/lint/post-synth/waw032.test.ts +83 -0
  28. package/src/lint/post-synth/waw032.ts +52 -0
  29. package/src/lint/post-synth/waw033.test.ts +68 -0
  30. package/src/lint/post-synth/waw033.ts +86 -0
  31. package/src/lint/post-synth/waw034.test.ts +54 -0
  32. package/src/lint/post-synth/waw034.ts +63 -0
  33. package/src/lint/post-synth/waw035.test.ts +74 -0
  34. package/src/lint/post-synth/waw035.ts +71 -0
  35. package/src/lint/post-synth/waw036.test.ts +217 -0
  36. package/src/lint/post-synth/waw036.ts +88 -0
  37. package/src/lint/post-synth/waw037.test.ts +155 -0
  38. package/src/lint/post-synth/waw037.ts +81 -0
  39. package/src/serializer.ts +1 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -47,7 +47,7 @@
47
47
  "js-yaml": "^4.1.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@intentius/chant": "0.1.0",
50
+ "@intentius/chant": "0.1.4",
51
51
  "typescript": "^5.9.3"
52
52
  },
53
53
  "peerDependencies": {
@@ -169,7 +169,9 @@ Reference parameters with \`Ref\`:
169
169
 
170
170
  ## Outputs
171
171
 
172
- Use \`output()\` to create explicit stack outputs. Cross-resource \`AttrRef\` usage is also auto-detected and promoted to outputs when needed.
172
+ Use \`output()\` to create explicit stack outputs. Accepts an \`AttrRef\` (resource attribute)
173
+ or any intrinsic (e.g. \`Sub\`, \`Join\`) for computed values. Cross-resource \`AttrRef\` usage
174
+ is also auto-detected and promoted to outputs when needed.
173
175
 
174
176
  {{file:docs-snippets/src/output-explicit.ts}}
175
177
 
@@ -183,6 +185,12 @@ Produces:
183
185
  }
184
186
  \`\`\`
185
187
 
188
+ Use \`Sub\` to export a computed value like a constructed URL:
189
+
190
+ \`\`\`typescript
191
+ export const solrUrl = output(Sub\`http://\${Ref(albDnsName)}/solr\`, "solrUrl");
192
+ \`\`\`
193
+
186
194
  ## Pseudo-parameters
187
195
 
188
196
  Runtime context values available in every template, accessed via the \`AWS\` namespace:
@@ -14,6 +14,8 @@ import { FargateAlb } from "./fargate-alb";
14
14
  import { AlbShared } from "./alb-shared";
15
15
  import { FargateService } from "./fargate-service";
16
16
  import { RdsInstance } from "./rds-instance";
17
+ import { Ec2InstanceRole } from "./ec2-instance-role";
18
+ import { MinimalVpc } from "./minimal-vpc";
17
19
 
18
20
  const baseProps = {
19
21
  name: "TestFunc",
@@ -865,3 +867,66 @@ describe("per-member defaults", () => {
865
867
  expect(funcPropsA.Timeout).toBe(funcPropsB.Timeout);
866
868
  });
867
869
  });
870
+
871
+ describe("Ec2InstanceRole", () => {
872
+ test("returns role and instanceProfile members", () => {
873
+ const instance = Ec2InstanceRole({});
874
+ expect(instance.role).toBeDefined();
875
+ expect(instance.instanceProfile).toBeDefined();
876
+ expect(Object.keys(instance.members)).toEqual(["role", "instanceProfile"]);
877
+ });
878
+
879
+ test("role has EC2 trust policy", () => {
880
+ const instance = Ec2InstanceRole({});
881
+ const roleProps = (instance.role as any).props;
882
+ expect(roleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe("ec2.amazonaws.com");
883
+ });
884
+
885
+ test("ManagedPolicyArns are passed through", () => {
886
+ const arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore";
887
+ const instance = Ec2InstanceRole({ ManagedPolicyArns: [arn] });
888
+ const roleProps = (instance.role as any).props;
889
+ expect(roleProps.ManagedPolicyArns).toContain(arn);
890
+ });
891
+
892
+ test("expandComposite produces 2 entries", () => {
893
+ const expanded = expandComposite("myRole", Ec2InstanceRole({}));
894
+ expect(expanded.size).toBe(2);
895
+ expect(expanded.has("myRoleRole")).toBe(true);
896
+ expect(expanded.has("myRoleInstanceProfile")).toBe(true);
897
+ });
898
+ });
899
+
900
+ describe("MinimalVpc", () => {
901
+ test("returns 8 members", () => {
902
+ const instance = MinimalVpc({});
903
+ expect(Object.keys(instance.members)).toEqual([
904
+ "vpc", "subnet", "igw", "igwAttachment", "routeTable", "defaultRoute", "subnetRta", "securityGroup",
905
+ ]);
906
+ });
907
+
908
+ test("subnet uses Select/GetAZs for AZ (not a plain string)", () => {
909
+ const instance = MinimalVpc({});
910
+ const subnetProps = (instance.subnet as any).props;
911
+ expect(typeof subnetProps.AvailabilityZone).not.toBe("string");
912
+ });
913
+
914
+ test("vpc cidr defaults to 10.0.0.0/24", () => {
915
+ const instance = MinimalVpc({});
916
+ const vpcProps = (instance.vpc as any).props;
917
+ expect(vpcProps.CidrBlock).toBe("10.0.0.0/24");
918
+ });
919
+
920
+ test("custom cidr is respected", () => {
921
+ const instance = MinimalVpc({ cidr: "192.168.0.0/16", subnetCidr: "192.168.1.0/24" });
922
+ const vpcProps = (instance.vpc as any).props;
923
+ const subnetProps = (instance.subnet as any).props;
924
+ expect(vpcProps.CidrBlock).toBe("192.168.0.0/16");
925
+ expect(subnetProps.CidrBlock).toBe("192.168.1.0/24");
926
+ });
927
+
928
+ test("expandComposite produces 8 entries", () => {
929
+ const expanded = expandComposite("net", MinimalVpc({}));
930
+ expect(expanded.size).toBe(8);
931
+ });
932
+ });
@@ -0,0 +1,39 @@
1
+ import { Composite, mergeDefaults } from "@intentius/chant";
2
+ import { Role, InstanceProfile } from "../generated";
3
+ import { Ref } from "../intrinsics";
4
+
5
+ export interface Ec2InstanceRoleProps {
6
+ ManagedPolicyArns?: string[];
7
+ Policies?: ConstructorParameters<typeof Role>[0]["Policies"];
8
+ defaults?: {
9
+ role?: Partial<ConstructorParameters<typeof Role>[0]>;
10
+ instanceProfile?: Partial<ConstructorParameters<typeof InstanceProfile>[0]>;
11
+ };
12
+ }
13
+
14
+ const EC2_ASSUME_ROLE = {
15
+ Version: "2012-10-17" as const,
16
+ Statement: [
17
+ {
18
+ Effect: "Allow" as const,
19
+ Principal: { Service: "ec2.amazonaws.com" },
20
+ Action: "sts:AssumeRole",
21
+ },
22
+ ],
23
+ };
24
+
25
+ export const Ec2InstanceRole = Composite<Ec2InstanceRoleProps>((props) => {
26
+ const { defaults } = props;
27
+
28
+ const role = new Role(mergeDefaults({
29
+ AssumeRolePolicyDocument: EC2_ASSUME_ROLE,
30
+ ManagedPolicyArns: props.ManagedPolicyArns ?? [],
31
+ Policies: props.Policies ?? [],
32
+ }, defaults?.role));
33
+
34
+ const instanceProfile = new InstanceProfile(mergeDefaults({
35
+ Roles: [Ref(role)],
36
+ }, defaults?.instanceProfile));
37
+
38
+ return { role, instanceProfile };
39
+ }, "Ec2InstanceRole");
@@ -0,0 +1,90 @@
1
+ import { Composite } from "@intentius/chant";
2
+ import {
3
+ EFSFileSystem,
4
+ EFSFileSystem_ElasticFileSystemTag,
5
+ EFSAccessPoint,
6
+ EFSAccessPoint_PosixUser,
7
+ EFSAccessPoint_RootDirectory,
8
+ EFSAccessPoint_CreationInfo,
9
+ EFSAccessPoint_AccessPointTag,
10
+ SecurityGroup,
11
+ SecurityGroup_Ingress,
12
+ } from "../generated";
13
+
14
+ export interface EfsWithAccessPointProps {
15
+ name: string;
16
+ vpcId: string;
17
+ uid: string;
18
+ gid: string;
19
+ rootPath: string;
20
+ permissions?: string;
21
+ /** CIDR allowed inbound on NFS port 2049. Mutually exclusive with sourceSecurityGroupId.
22
+ * When neither ingressCidr nor sourceSecurityGroupId is set, no inline ingress rule is created
23
+ * (add a cross-stack EC2SecurityGroupIngress separately). */
24
+ ingressCidr?: string;
25
+ /** Security group ID allowed inbound on NFS port 2049. When provided, overrides ingressCidr. */
26
+ sourceSecurityGroupId?: string;
27
+ performanceMode?: "generalPurpose" | "maxIO";
28
+ throughputMode?: "bursting" | "provisioned" | "elastic";
29
+ }
30
+
31
+ export const EfsWithAccessPoint = Composite<EfsWithAccessPointProps>((props) => {
32
+ const ingressRules: SecurityGroup_Ingress[] = [];
33
+ if (props.sourceSecurityGroupId) {
34
+ ingressRules.push(new SecurityGroup_Ingress({
35
+ IpProtocol: "tcp",
36
+ FromPort: 2049,
37
+ ToPort: 2049,
38
+ SourceSecurityGroupId: props.sourceSecurityGroupId,
39
+ }));
40
+ } else if (props.ingressCidr) {
41
+ ingressRules.push(new SecurityGroup_Ingress({
42
+ IpProtocol: "tcp",
43
+ FromPort: 2049,
44
+ ToPort: 2049,
45
+ CidrIp: props.ingressCidr,
46
+ }));
47
+ }
48
+
49
+ const securityGroup = new SecurityGroup({
50
+ GroupDescription: "EFS mount target SG",
51
+ VpcId: props.vpcId,
52
+ ...(ingressRules.length > 0 ? { SecurityGroupIngress: ingressRules } : {}),
53
+ });
54
+
55
+ const nameTag = new EFSFileSystem_ElasticFileSystemTag({
56
+ Key: "Name",
57
+ Value: props.name,
58
+ });
59
+
60
+ const fs = new EFSFileSystem({
61
+ Encrypted: true,
62
+ PerformanceMode: props.performanceMode ?? "generalPurpose",
63
+ ThroughputMode: props.throughputMode ?? "bursting",
64
+ FileSystemTags: [nameTag],
65
+ });
66
+
67
+ const posixUser = new EFSAccessPoint_PosixUser({ Uid: props.uid, Gid: props.gid });
68
+
69
+ const creationInfo = new EFSAccessPoint_CreationInfo({
70
+ OwnerUid: props.uid,
71
+ OwnerGid: props.gid,
72
+ Permissions: props.permissions ?? "755",
73
+ });
74
+
75
+ const rootDirectory = new EFSAccessPoint_RootDirectory({
76
+ Path: props.rootPath,
77
+ CreationInfo: creationInfo,
78
+ });
79
+
80
+ const apNameTag = new EFSAccessPoint_AccessPointTag({ Key: "Name", Value: props.name });
81
+
82
+ const accessPoint = new EFSAccessPoint({
83
+ FileSystemId: fs.FileSystemId,
84
+ PosixUser: posixUser,
85
+ RootDirectory: rootDirectory,
86
+ AccessPointTags: [apNameTag],
87
+ });
88
+
89
+ return { securityGroup, fs, accessPoint };
90
+ }, "EfsWithAccessPoint");
@@ -6,9 +6,13 @@ import {
6
6
  EcsService_AwsVpcConfiguration,
7
7
  TaskDefinition,
8
8
  TaskDefinition_ContainerDefinition,
9
+ TaskDefinition_MountPoint,
9
10
  TaskDefinition_PortMapping,
10
11
  TaskDefinition_LogConfiguration,
11
12
  TaskDefinition_KeyValuePair,
13
+ TaskDefinition_EFSVolumeConfiguration,
14
+ TaskDefinition_Volume,
15
+ TaskDefinition_Ulimit,
12
16
  TargetGroup,
13
17
  ListenerRule,
14
18
  ListenerRule_Action,
@@ -20,8 +24,12 @@ import {
20
24
  LogGroup,
21
25
  Role,
22
26
  Role_Policy,
27
+ ScalableTarget,
28
+ ApplicationAutoScalingScalingPolicy,
29
+ ApplicationAutoScalingScalingPolicy_TargetTrackingScalingPolicyConfiguration,
30
+ ApplicationAutoScalingScalingPolicy_PredefinedMetricSpecification,
23
31
  } from "../generated";
24
- import { Sub } from "../intrinsics";
32
+ import { Sub, Join, Select, Split } from "../intrinsics";
25
33
  import { ecsTrustPolicy } from "./ecs-trust-policy";
26
34
 
27
35
  export interface FargateServiceProps {
@@ -42,14 +50,34 @@ export interface FargateServiceProps {
42
50
  cpu?: string;
43
51
  memory?: string;
44
52
  desiredCount?: number;
53
+
54
+ // Autoscaling
55
+ autoscaling?: {
56
+ minCapacity?: number;
57
+ maxCapacity: number;
58
+ cpuTarget?: number;
59
+ scaleInCooldown?: number;
60
+ scaleOutCooldown?: number;
61
+ };
45
62
  environment?: Record<string, string>;
46
63
  command?: string[];
64
+ mountPoints?: TaskDefinition_MountPoint[];
65
+ efsMounts?: Array<{
66
+ fileSystemId: string;
67
+ accessPointId?: string;
68
+ containerPath: string;
69
+ volumeName?: string;
70
+ transitEncryption?: "ENABLED" | "DISABLED";
71
+ }>;
47
72
 
48
73
  // Networking
49
74
  vpcId: string;
50
75
  privateSubnetIds: string[];
51
76
  healthCheckPath?: string;
52
77
 
78
+ // Ulimits (container-level)
79
+ ulimits?: Array<{ name: string; softLimit: number; hardLimit: number }>;
80
+
53
81
  // IAM
54
82
  ManagedPolicyArns?: string[];
55
83
  Policies?: InstanceType<typeof Role_Policy>[];
@@ -78,10 +106,17 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
78
106
  const logRetentionDays = props.logRetentionDays ?? 30;
79
107
  const { defaults: defs } = props;
80
108
 
109
+ // Auto-inject EFS managed policy when efsMounts are present
110
+ const EFS_POLICY = "arn:aws:iam::aws:policy/AmazonElasticFileSystemClientReadWriteAccess";
111
+ const managedPolicies = props.ManagedPolicyArns ? [...props.ManagedPolicyArns] : [];
112
+ if (props.efsMounts?.length && !managedPolicies.includes(EFS_POLICY)) {
113
+ managedPolicies.push(EFS_POLICY);
114
+ }
115
+
81
116
  // Task role — app permissions
82
117
  const taskRole = new Role(mergeDefaults({
83
118
  AssumeRolePolicyDocument: ecsTrustPolicy,
84
- ManagedPolicyArns: props.ManagedPolicyArns,
119
+ ManagedPolicyArns: managedPolicies.length > 0 ? managedPolicies : undefined,
85
120
  Policies: props.Policies,
86
121
  }, defs?.taskRole));
87
122
 
@@ -114,6 +149,27 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
114
149
  }
115
150
  }
116
151
 
152
+ // EFS volumes and mount points
153
+ const efsVolumes = (props.efsMounts ?? []).map((m, i) =>
154
+ new TaskDefinition_Volume({
155
+ Name: m.volumeName ?? `efs-${i}`,
156
+ EFSVolumeConfiguration: new TaskDefinition_EFSVolumeConfiguration({
157
+ FileSystemId: m.fileSystemId,
158
+ ...(m.accessPointId && { AuthorizationConfig: { AccessPointId: m.accessPointId } }),
159
+ TransitEncryption: m.transitEncryption ?? "ENABLED",
160
+ }),
161
+ }),
162
+ );
163
+
164
+ const efsMountPoints = (props.efsMounts ?? []).map((m, i) =>
165
+ new TaskDefinition_MountPoint({
166
+ ContainerPath: m.containerPath,
167
+ SourceVolume: m.volumeName ?? `efs-${i}`,
168
+ }),
169
+ );
170
+
171
+ const allMountPoints = [...efsMountPoints, ...(props.mountPoints ?? [])];
172
+
117
173
  const container = new TaskDefinition_ContainerDefinition({
118
174
  Name: "app",
119
175
  Image: props.image,
@@ -122,6 +178,12 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
122
178
  LogConfiguration: logConfiguration,
123
179
  Environment: environmentVars.length > 0 ? environmentVars : undefined,
124
180
  Command: props.command,
181
+ MountPoints: allMountPoints.length > 0 ? allMountPoints : undefined,
182
+ Ulimits: props.ulimits?.map(u => new TaskDefinition_Ulimit({
183
+ Name: u.name,
184
+ SoftLimit: u.softLimit,
185
+ HardLimit: u.hardLimit,
186
+ })),
125
187
  });
126
188
 
127
189
  // Task definition
@@ -133,6 +195,7 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
133
195
  ExecutionRoleArn: props.executionRoleArn,
134
196
  TaskRoleArn: taskRole.Arn,
135
197
  ContainerDefinitions: [container],
198
+ ...(efsVolumes.length > 0 && { Volumes: efsVolumes }),
136
199
  }, defs?.taskDef));
137
200
 
138
201
  // Task security group — ingress on container port from ALB SG
@@ -228,6 +291,41 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
228
291
  { DependsOn: [rule] },
229
292
  );
230
293
 
294
+ let scalableTarget: InstanceType<typeof ScalableTarget> | undefined;
295
+ let scalingPolicy: InstanceType<typeof ApplicationAutoScalingScalingPolicy> | undefined;
296
+
297
+ if (props.autoscaling) {
298
+ const { minCapacity = 1, maxCapacity, cpuTarget = 60, scaleInCooldown, scaleOutCooldown } = props.autoscaling;
299
+
300
+ const resourceId = Join("/", ["service", Select(1, Split("/", props.clusterArn)), service.Name]);
301
+
302
+ scalableTarget = new ScalableTarget({
303
+ ServiceNamespace: "ecs",
304
+ ScalableDimension: "ecs:service:DesiredCount",
305
+ ResourceId: resourceId,
306
+ MinCapacity: minCapacity,
307
+ MaxCapacity: maxCapacity,
308
+ });
309
+
310
+ const trackingConfig = new ApplicationAutoScalingScalingPolicy_TargetTrackingScalingPolicyConfiguration({
311
+ TargetValue: cpuTarget,
312
+ PredefinedMetricSpecification: new ApplicationAutoScalingScalingPolicy_PredefinedMetricSpecification({
313
+ PredefinedMetricType: "ECSServiceAverageCPUUtilization",
314
+ }),
315
+ ...(scaleInCooldown !== undefined && { ScaleInCooldown: scaleInCooldown }),
316
+ ...(scaleOutCooldown !== undefined && { ScaleOutCooldown: scaleOutCooldown }),
317
+ });
318
+
319
+ scalingPolicy = new ApplicationAutoScalingScalingPolicy({
320
+ PolicyName: Sub`\${AWS::StackName}-cpu`,
321
+ PolicyType: "TargetTrackingScaling",
322
+ ServiceNamespace: "ecs",
323
+ ScalableDimension: "ecs:service:DesiredCount",
324
+ ResourceId: resourceId,
325
+ TargetTrackingScalingPolicyConfiguration: trackingConfig,
326
+ });
327
+ }
328
+
231
329
  return {
232
330
  taskRole,
233
331
  logGroup,
@@ -236,5 +334,7 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
236
334
  targetGroup,
237
335
  rule,
238
336
  service,
337
+ ...(scalableTarget ? { scalableTarget } : {}),
338
+ ...(scalingPolicy ? { scalingPolicy } : {}),
239
339
  };
240
340
  }, "FargateService");
@@ -24,3 +24,11 @@ export { FargateService } from "./fargate-service";
24
24
  export type { FargateServiceProps } from "./fargate-service";
25
25
  export { RdsInstance, RdsInstance as RdsPostgres } from "./rds-instance";
26
26
  export type { RdsInstanceProps, RdsInstanceProps as RdsPostgresProps } from "./rds-instance";
27
+ export { EfsWithAccessPoint } from "./efs-with-access-point";
28
+ export type { EfsWithAccessPointProps } from "./efs-with-access-point";
29
+ export { SolrFargateService } from "./solr-fargate-service";
30
+ export type { SolrFargateServiceProps } from "./solr-fargate-service";
31
+ export { Ec2InstanceRole } from "./ec2-instance-role";
32
+ export type { Ec2InstanceRoleProps } from "./ec2-instance-role";
33
+ export { MinimalVpc } from "./minimal-vpc";
34
+ export type { MinimalVpcProps } from "./minimal-vpc";
@@ -1,5 +1,12 @@
1
1
  import { Composite, mergeDefaults } from "@intentius/chant";
2
- import { Table, Table_AttributeDefinition, Table_KeySchema, Role_Policy } from "../generated";
2
+ import {
3
+ Table,
4
+ Table_AttributeDefinition,
5
+ Table_KeySchema,
6
+ Table_StreamSpecification,
7
+ Role_Policy,
8
+ EventSourceMapping,
9
+ } from "../generated";
3
10
  import { DynamoDBActions } from "../actions/dynamodb";
4
11
  import { LambdaFunction, type LambdaFunctionProps } from "./lambda-function";
5
12
 
@@ -7,9 +14,16 @@ export interface LambdaDynamoDBProps extends LambdaFunctionProps {
7
14
  tableName?: string;
8
15
  partitionKey: string;
9
16
  sortKey?: string;
10
- access?: "ReadOnly" | "ReadWrite" | "Full";
17
+ access?: "ReadOnly" | "ReadWrite" | "Full" | "None";
18
+ streams?: {
19
+ viewType?: "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | "KEYS_ONLY";
20
+ batchSize?: number;
21
+ startingPosition?: "TRIM_HORIZON" | "LATEST";
22
+ bisectOnFunctionError?: boolean;
23
+ };
11
24
  defaults?: LambdaFunctionProps["defaults"] & {
12
25
  table?: Partial<ConstructorParameters<typeof Table>[0]>;
26
+ eventSourceMapping?: Partial<ConstructorParameters<typeof EventSourceMapping>[0]>;
13
27
  };
14
28
  }
15
29
 
@@ -37,26 +51,49 @@ export const LambdaDynamoDB = Composite<LambdaDynamoDBProps>((props) => {
37
51
  BillingMode: "PAY_PER_REQUEST",
38
52
  AttributeDefinitions: attributeDefinitions,
39
53
  KeySchema: keySchema,
54
+ ...(props.streams && {
55
+ StreamSpecification: new Table_StreamSpecification({
56
+ StreamViewType: props.streams.viewType ?? "NEW_AND_OLD_IMAGES",
57
+ }),
58
+ }),
40
59
  }, defaults?.table));
41
60
 
42
61
  const access = props.access ?? "ReadWrite";
43
- const dynamoPolicyDocument = {
44
- Version: "2012-10-17",
45
- Statement: [
46
- {
47
- Effect: "Allow",
48
- Action: DynamoDBActions[access],
49
- Resource: table.Arn,
62
+ const policies: InstanceType<typeof Role_Policy>[] = [];
63
+
64
+ if (access !== "None") {
65
+ policies.push(new Role_Policy({
66
+ PolicyName: `DynamoDB${access}`,
67
+ PolicyDocument: {
68
+ Version: "2012-10-17",
69
+ Statement: [{ Effect: "Allow", Action: DynamoDBActions[access], Resource: table.Arn }],
50
70
  },
51
- ],
52
- };
71
+ }));
72
+ }
53
73
 
54
- const dynamoPolicy = new Role_Policy({
55
- PolicyName: `DynamoDB${access}`,
56
- PolicyDocument: dynamoPolicyDocument,
57
- });
74
+ if (props.streams) {
75
+ policies.push(new Role_Policy({
76
+ PolicyName: "DynamoDBStreamRead",
77
+ PolicyDocument: {
78
+ Version: "2012-10-17",
79
+ Statement: [{
80
+ Effect: "Allow",
81
+ Action: [
82
+ "dynamodb:GetRecords",
83
+ "dynamodb:GetShardIterator",
84
+ "dynamodb:DescribeStream",
85
+ "dynamodb:ListStreams",
86
+ ],
87
+ Resource: table.StreamArn,
88
+ }],
89
+ },
90
+ }));
91
+ }
92
+
93
+ if (props.Policies) {
94
+ policies.push(...props.Policies);
95
+ }
58
96
 
59
- const policies = props.Policies ? [dynamoPolicy, ...props.Policies] : [dynamoPolicy];
60
97
  const env = props.Environment ?? { Variables: {} };
61
98
  const variables = { ...((env as any).Variables ?? {}), TABLE_NAME: table.Ref };
62
99
  const { role, func } = LambdaFunction({
@@ -65,5 +102,17 @@ export const LambdaDynamoDB = Composite<LambdaDynamoDBProps>((props) => {
65
102
  Environment: { Variables: variables },
66
103
  });
67
104
 
68
- return { table, role, func };
105
+ let eventSourceMapping: InstanceType<typeof EventSourceMapping> | undefined;
106
+ if (props.streams) {
107
+ const { startingPosition = "TRIM_HORIZON", batchSize, bisectOnFunctionError } = props.streams;
108
+ eventSourceMapping = new EventSourceMapping(mergeDefaults({
109
+ FunctionName: func.Arn,
110
+ EventSourceArn: table.StreamArn,
111
+ StartingPosition: startingPosition,
112
+ ...(batchSize !== undefined && { BatchSize: batchSize }),
113
+ ...(bisectOnFunctionError !== undefined && { BisectBatchOnFunctionError: bisectOnFunctionError }),
114
+ }, defaults?.eventSourceMapping));
115
+ }
116
+
117
+ return { table, role, func, ...(eventSourceMapping ? { eventSourceMapping } : {}) };
69
118
  }, "LambdaDynamoDB");
@@ -1,5 +1,6 @@
1
1
  import { Composite, withDefaults, mergeDefaults } from "@intentius/chant";
2
2
  import { Role, Function, Function_VpcConfig, Role_Policy } from "../generated";
3
+ import { Sub } from "../intrinsics";
3
4
 
4
5
  const lambdaTrustPolicy = {
5
6
  Version: "2012-10-17" as const,
@@ -18,7 +19,7 @@ const VPC_ACCESS_ARN =
18
19
  "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole";
19
20
 
20
21
  export interface LambdaFunctionProps {
21
- name: string;
22
+ name: string | ReturnType<typeof Sub>;
22
23
  Runtime: string;
23
24
  Handler: string;
24
25
  Code: ConstructorParameters<typeof Function>[0]["Code"];