@intentius/chant-lexicon-aws 0.0.24 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.0.24",
3
+ "version": "0.1.1",
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",
@@ -43,11 +43,14 @@
43
43
  "prepack": "bun run generate && bun run bundle && bun run validate"
44
44
  },
45
45
  "dependencies": {
46
- "@intentius/chant": "0.0.22",
47
46
  "fflate": "^0.8.2",
48
47
  "js-yaml": "^4.1.0"
49
48
  },
50
49
  "devDependencies": {
50
+ "@intentius/chant": "0.1.1",
51
51
  "typescript": "^5.9.3"
52
+ },
53
+ "peerDependencies": {
54
+ "@intentius/chant": "^0.1.0"
52
55
  }
53
56
  }
@@ -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:
@@ -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,12 @@ 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,
12
15
  TargetGroup,
13
16
  ListenerRule,
14
17
  ListenerRule_Action,
@@ -20,8 +23,12 @@ import {
20
23
  LogGroup,
21
24
  Role,
22
25
  Role_Policy,
26
+ ScalableTarget,
27
+ ApplicationAutoScalingScalingPolicy,
28
+ ApplicationAutoScalingScalingPolicy_TargetTrackingScalingPolicyConfiguration,
29
+ ApplicationAutoScalingScalingPolicy_PredefinedMetricSpecification,
23
30
  } from "../generated";
24
- import { Sub } from "../intrinsics";
31
+ import { Sub, Join, Select, Split } from "../intrinsics";
25
32
  import { ecsTrustPolicy } from "./ecs-trust-policy";
26
33
 
27
34
  export interface FargateServiceProps {
@@ -42,8 +49,25 @@ export interface FargateServiceProps {
42
49
  cpu?: string;
43
50
  memory?: string;
44
51
  desiredCount?: number;
52
+
53
+ // Autoscaling
54
+ autoscaling?: {
55
+ minCapacity?: number;
56
+ maxCapacity: number;
57
+ cpuTarget?: number;
58
+ scaleInCooldown?: number;
59
+ scaleOutCooldown?: number;
60
+ };
45
61
  environment?: Record<string, string>;
46
62
  command?: string[];
63
+ mountPoints?: TaskDefinition_MountPoint[];
64
+ efsMounts?: Array<{
65
+ fileSystemId: string;
66
+ accessPointId?: string;
67
+ containerPath: string;
68
+ volumeName?: string;
69
+ transitEncryption?: "ENABLED" | "DISABLED";
70
+ }>;
47
71
 
48
72
  // Networking
49
73
  vpcId: string;
@@ -78,10 +102,17 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
78
102
  const logRetentionDays = props.logRetentionDays ?? 30;
79
103
  const { defaults: defs } = props;
80
104
 
105
+ // Auto-inject EFS managed policy when efsMounts are present
106
+ const EFS_POLICY = "arn:aws:iam::aws:policy/AmazonElasticFileSystemClientReadWriteAccess";
107
+ const managedPolicies = props.ManagedPolicyArns ? [...props.ManagedPolicyArns] : [];
108
+ if (props.efsMounts?.length && !managedPolicies.includes(EFS_POLICY)) {
109
+ managedPolicies.push(EFS_POLICY);
110
+ }
111
+
81
112
  // Task role — app permissions
82
113
  const taskRole = new Role(mergeDefaults({
83
114
  AssumeRolePolicyDocument: ecsTrustPolicy,
84
- ManagedPolicyArns: props.ManagedPolicyArns,
115
+ ManagedPolicyArns: managedPolicies.length > 0 ? managedPolicies : undefined,
85
116
  Policies: props.Policies,
86
117
  }, defs?.taskRole));
87
118
 
@@ -114,6 +145,27 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
114
145
  }
115
146
  }
116
147
 
148
+ // EFS volumes and mount points
149
+ const efsVolumes = (props.efsMounts ?? []).map((m, i) =>
150
+ new TaskDefinition_Volume({
151
+ Name: m.volumeName ?? `efs-${i}`,
152
+ EFSVolumeConfiguration: new TaskDefinition_EFSVolumeConfiguration({
153
+ FileSystemId: m.fileSystemId,
154
+ ...(m.accessPointId && { AuthorizationConfig: { AccessPointId: m.accessPointId } }),
155
+ TransitEncryption: m.transitEncryption ?? "ENABLED",
156
+ }),
157
+ }),
158
+ );
159
+
160
+ const efsMountPoints = (props.efsMounts ?? []).map((m, i) =>
161
+ new TaskDefinition_MountPoint({
162
+ ContainerPath: m.containerPath,
163
+ SourceVolume: m.volumeName ?? `efs-${i}`,
164
+ }),
165
+ );
166
+
167
+ const allMountPoints = [...efsMountPoints, ...(props.mountPoints ?? [])];
168
+
117
169
  const container = new TaskDefinition_ContainerDefinition({
118
170
  Name: "app",
119
171
  Image: props.image,
@@ -122,6 +174,7 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
122
174
  LogConfiguration: logConfiguration,
123
175
  Environment: environmentVars.length > 0 ? environmentVars : undefined,
124
176
  Command: props.command,
177
+ MountPoints: allMountPoints.length > 0 ? allMountPoints : undefined,
125
178
  });
126
179
 
127
180
  // Task definition
@@ -133,6 +186,7 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
133
186
  ExecutionRoleArn: props.executionRoleArn,
134
187
  TaskRoleArn: taskRole.Arn,
135
188
  ContainerDefinitions: [container],
189
+ ...(efsVolumes.length > 0 && { Volumes: efsVolumes }),
136
190
  }, defs?.taskDef));
137
191
 
138
192
  // Task security group — ingress on container port from ALB SG
@@ -228,6 +282,41 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
228
282
  { DependsOn: [rule] },
229
283
  );
230
284
 
285
+ let scalableTarget: InstanceType<typeof ScalableTarget> | undefined;
286
+ let scalingPolicy: InstanceType<typeof ApplicationAutoScalingScalingPolicy> | undefined;
287
+
288
+ if (props.autoscaling) {
289
+ const { minCapacity = 1, maxCapacity, cpuTarget = 60, scaleInCooldown, scaleOutCooldown } = props.autoscaling;
290
+
291
+ const resourceId = Join("/", ["service", Select(1, Split("/", props.clusterArn)), service.Name]);
292
+
293
+ scalableTarget = new ScalableTarget({
294
+ ServiceNamespace: "ecs",
295
+ ScalableDimension: "ecs:service:DesiredCount",
296
+ ResourceId: resourceId,
297
+ MinCapacity: minCapacity,
298
+ MaxCapacity: maxCapacity,
299
+ });
300
+
301
+ const trackingConfig = new ApplicationAutoScalingScalingPolicy_TargetTrackingScalingPolicyConfiguration({
302
+ TargetValue: cpuTarget,
303
+ PredefinedMetricSpecification: new ApplicationAutoScalingScalingPolicy_PredefinedMetricSpecification({
304
+ PredefinedMetricType: "ECSServiceAverageCPUUtilization",
305
+ }),
306
+ ...(scaleInCooldown !== undefined && { ScaleInCooldown: scaleInCooldown }),
307
+ ...(scaleOutCooldown !== undefined && { ScaleOutCooldown: scaleOutCooldown }),
308
+ });
309
+
310
+ scalingPolicy = new ApplicationAutoScalingScalingPolicy({
311
+ PolicyName: Sub`\${AWS::StackName}-cpu`,
312
+ PolicyType: "TargetTrackingScaling",
313
+ ServiceNamespace: "ecs",
314
+ ScalableDimension: "ecs:service:DesiredCount",
315
+ ResourceId: resourceId,
316
+ TargetTrackingScalingPolicyConfiguration: trackingConfig,
317
+ });
318
+ }
319
+
231
320
  return {
232
321
  taskRole,
233
322
  logGroup,
@@ -236,5 +325,7 @@ export const FargateService = Composite<FargateServiceProps>((props) => {
236
325
  targetGroup,
237
326
  rule,
238
327
  service,
328
+ ...(scalableTarget ? { scalableTarget } : {}),
329
+ ...(scalingPolicy ? { scalingPolicy } : {}),
239
330
  };
240
331
  }, "FargateService");
@@ -24,3 +24,5 @@ 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";
@@ -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");
@@ -5,33 +5,36 @@ import {
5
5
  Bucket_ServerSideEncryptionRule,
6
6
  Bucket_ServerSideEncryptionByDefault,
7
7
  Bucket_PublicAccessBlockConfiguration,
8
+ Bucket_NotificationConfiguration,
9
+ Bucket_LambdaConfiguration,
10
+ Permission,
8
11
  Role_Policy,
9
12
  } from "../generated";
10
- import { Sub } from "../intrinsics";
13
+ import { Sub, Join } from "../intrinsics";
11
14
  import { S3Actions } from "../actions/s3";
12
15
  import { LambdaFunction, type LambdaFunctionProps } from "./lambda-function";
13
16
 
14
17
  export interface LambdaS3Props extends LambdaFunctionProps {
15
18
  bucketName?: string;
16
19
  access?: "ReadOnly" | "ReadWrite" | "Full";
20
+ trigger?: {
21
+ events?: string[];
22
+ prefix?: string;
23
+ suffix?: string;
24
+ };
17
25
  defaults?: LambdaFunctionProps["defaults"] & {
18
26
  bucket?: Partial<ConstructorParameters<typeof Bucket>[0]>;
19
27
  };
20
28
  }
21
29
 
22
30
  export const LambdaS3 = Composite<LambdaS3Props>((props) => {
23
- const encryptionDefault = new Bucket_ServerSideEncryptionByDefault({
24
- SSEAlgorithm: "AES256",
25
- });
26
-
31
+ const encryptionDefault = new Bucket_ServerSideEncryptionByDefault({ SSEAlgorithm: "AES256" });
27
32
  const encryptionRule = new Bucket_ServerSideEncryptionRule({
28
33
  ServerSideEncryptionByDefault: encryptionDefault,
29
34
  });
30
-
31
35
  const bucketEncryption = new Bucket_BucketEncryption({
32
36
  ServerSideEncryptionConfiguration: [encryptionRule],
33
37
  });
34
-
35
38
  const publicAccessBlock = new Bucket_PublicAccessBlockConfiguration({
36
39
  BlockPublicAcls: true,
37
40
  BlockPublicPolicy: true,
@@ -40,30 +43,73 @@ export const LambdaS3 = Composite<LambdaS3Props>((props) => {
40
43
  });
41
44
 
42
45
  const { defaults } = props;
46
+ const access = props.access ?? "ReadWrite";
47
+
48
+ if (props.trigger) {
49
+ // Create Lambda first to break the circular CFN dependency.
50
+ // Compute bucket ARN from name (no GetAtt on bucket → no resource dep).
51
+ const name = props.bucketName ?? Sub`\${AWS::StackName}-bucket`;
52
+ const bucketArnBase = Join("", ["arn:aws:s3:::", name]);
53
+ const bucketArnWildcard = Join("", ["arn:aws:s3:::", name, "/*"]);
54
+
55
+ const s3Policy = new Role_Policy({
56
+ PolicyName: `S3${access}`,
57
+ PolicyDocument: {
58
+ Version: "2012-10-17",
59
+ Statement: [{ Effect: "Allow", Action: S3Actions[access], Resource: [bucketArnBase, bucketArnWildcard] }],
60
+ },
61
+ });
62
+
63
+ const policies = props.Policies ? [s3Policy, ...props.Policies] : [s3Policy];
64
+ const env = props.Environment ?? { Variables: {} };
65
+ const variables = { ...((env as any).Variables ?? {}), BUCKET_NAME: name };
66
+ const { role, func } = LambdaFunction({ ...props, Policies: policies, Environment: { Variables: variables } });
43
67
 
68
+ const permission = new Permission({
69
+ FunctionName: func.Arn,
70
+ Action: "lambda:InvokeFunction",
71
+ Principal: "s3.amazonaws.com",
72
+ SourceArn: bucketArnBase,
73
+ });
74
+
75
+ const { events = ["s3:ObjectCreated:*"], prefix, suffix } = props.trigger;
76
+ const rules: Array<{ Name: string; Value: string }> = [];
77
+ if (prefix) rules.push({ Name: "prefix", Value: prefix });
78
+ if (suffix) rules.push({ Name: "suffix", Value: suffix });
79
+
80
+ const lambdaConfigs = events.map((event) => new Bucket_LambdaConfiguration({
81
+ Event: event,
82
+ Function: func.Arn,
83
+ ...(rules.length > 0 && { Filter: { S3Key: { Rules: rules } } }),
84
+ }));
85
+
86
+ const notificationConfig = new Bucket_NotificationConfiguration({
87
+ LambdaConfigurations: lambdaConfigs,
88
+ });
89
+
90
+ const bucket = new Bucket(mergeDefaults({
91
+ BucketName: props.bucketName,
92
+ BucketEncryption: bucketEncryption,
93
+ PublicAccessBlockConfiguration: publicAccessBlock,
94
+ NotificationConfiguration: notificationConfig,
95
+ }, defaults?.bucket), { DependsOn: [permission] });
96
+
97
+ return { bucket, role, func, permission };
98
+ }
99
+
100
+ // Non-trigger path: original flow
44
101
  const bucket = new Bucket(mergeDefaults({
45
102
  BucketName: props.bucketName,
46
103
  BucketEncryption: bucketEncryption,
47
104
  PublicAccessBlockConfiguration: publicAccessBlock,
48
105
  }, defaults?.bucket));
49
106
 
50
- const access = props.access ?? "ReadWrite";
51
107
  const s3PolicyDocument = {
52
108
  Version: "2012-10-17",
53
- Statement: [
54
- {
55
- Effect: "Allow",
56
- Action: S3Actions[access],
57
- Resource: [bucket.Arn, Sub`${bucket.Arn}/*`],
58
- },
59
- ],
109
+ Statement: [{ Effect: "Allow", Action: S3Actions[access], Resource: [bucket.Arn, Sub`${bucket.Arn}/*`] }],
60
110
  };
61
111
 
62
- const s3Policy = new Role_Policy({
63
- PolicyName: `S3${access}`,
64
- PolicyDocument: s3PolicyDocument,
65
- });
66
-
112
+ const s3Policy = new Role_Policy({ PolicyName: `S3${access}`, PolicyDocument: s3PolicyDocument });
67
113
  const policies = props.Policies ? [s3Policy, ...props.Policies] : [s3Policy];
68
114
  const env = props.Environment ?? { Variables: {} };
69
115
  const variables = { ...((env as any).Variables ?? {}), BUCKET_NAME: bucket.Ref };