@intentius/chant-lexicon-aws 0.0.12 → 0.0.14

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.12",
3
+ "version": "0.0.14",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": ["src/", "dist/"],
@@ -22,7 +22,7 @@
22
22
  "prepack": "bun run bundle && bun run validate"
23
23
  },
24
24
  "dependencies": {
25
- "@intentius/chant": "0.0.11",
25
+ "@intentius/chant": "0.0.13",
26
26
  "fflate": "^0.8.2",
27
27
  "js-yaml": "^4.1.0"
28
28
  },
@@ -149,22 +149,23 @@ When you reference a resource or attribute from another file (e.g. \`dataBucket.
149
149
 
150
150
  CloudFormation parameters let you customize a stack at deploy time. Export a \`Parameter\` to add it to the template's \`Parameters\` section:
151
151
 
152
- {{file:docs-snippets/src/parameter-ref.ts}}
152
+ {{file:docs-snippets/src/parameter-declaration.ts}}
153
153
 
154
154
  Produces:
155
155
 
156
156
  \`\`\`json
157
157
  "Parameters": {
158
- "Name": {
158
+ "Environment": {
159
159
  "Type": "String",
160
- "Description": "Project name used in resource naming"
160
+ "Default": "dev",
161
+ "Description": "Deployment environment"
161
162
  }
162
163
  }
163
164
  \`\`\`
164
165
 
165
166
  Reference parameters with \`Ref\`:
166
167
 
167
- {{file:docs-snippets/src/parameter-ref.ts}}
168
+ {{file:docs-snippets/src/parameter-cross-file-ref.ts}}
168
169
 
169
170
  ## Outputs
170
171
 
@@ -201,7 +202,7 @@ Runtime context values available in every template, accessed via the \`AWS\` nam
201
202
 
202
203
  ## Intrinsic functions
203
204
 
204
- The lexicon provides 8 intrinsic functions (\`Sub\`, \`Ref\`, \`GetAtt\`, \`If\`, \`Join\`, \`Select\`, \`Split\`, \`Base64\`) that map directly to CloudFormation \`Fn::\` calls. See [Intrinsic Functions](../intrinsics/) for full usage examples.
205
+ The lexicon provides 9 intrinsic functions (\`Sub\`, \`Ref\`, \`GetAtt\`, \`If\`, \`Join\`, \`Select\`, \`Split\`, \`Base64\`, \`GetAZs\`) that map directly to CloudFormation \`Fn::\` calls. See [Intrinsic Functions](../intrinsics/) for full usage examples.
205
206
 
206
207
  ## Dependencies
207
208
 
@@ -369,7 +370,13 @@ Splits a string by a delimiter:
369
370
 
370
371
  Encodes a string to Base64, commonly used for EC2 user data:
371
372
 
372
- {{file:docs-snippets/src/intrinsics-detail.ts:23-27}}`,
373
+ {{file:docs-snippets/src/intrinsics-detail.ts:23-27}}
374
+
375
+ ## \`GetAZs\` — availability zones
376
+
377
+ Returns the list of Availability Zones for a region:
378
+
379
+ {{file:docs-snippets/src/intrinsics-detail.ts:29-31}}`,
373
380
  },
374
381
  {
375
382
  slug: "composites",
@@ -407,6 +414,7 @@ The AWS lexicon ships ready-to-use composites for common patterns. Import them f
407
414
  | \`FargateAlb\` | \`cluster\`, \`executionRole\`, \`taskRole\`, \`logGroup\`, \`taskDef\`, \`albSg\`, \`taskSg\`, \`alb\`, \`targetGroup\`, \`listener\`, \`service\` | Fargate service behind an ALB. Accepts VPC outputs as props. |
408
415
  | \`AlbShared\` | \`cluster\`, \`executionRole\`, \`albSg\`, \`alb\`, \`listener\` | Shared ALB infrastructure (ECS cluster, execution role, ALB, listener with 404 default). Created once, consumed by multiple \`FargateService\` instances. |
409
416
  | \`FargateService\` | \`taskRole\`, \`logGroup\`, \`taskDef\`, \`taskSg\`, \`targetGroup\`, \`rule\`, \`service\` | Per-service Fargate resources with listener rule routing. Wire to an \`AlbShared\` instance for multi-service ALB patterns. |
417
+ | \`RdsInstance\` | \`subnetGroup\`, \`sg\`, \`db\` (+ \`parameterGroup\` if configured) | RDS instance (postgres, mysql, mariadb) in private subnets. Creates DB subnet group, security group, and optionally a parameter group. Engine-specific defaults for port, username, and version. Encrypted by default. |
410
418
 
411
419
  All built-in composites accept \`ManagedPolicyArns\` and \`Policies\` for adding IAM permissions to the auto-created role.
412
420
 
@@ -749,6 +757,72 @@ WAW030: API Gateway Deployment "MyDeployment" has no DependsOn on any Method
749
757
  WAW030: ScalableTarget "MyTarget" targets DynamoDB but has no DependsOn on any Table
750
758
  \`\`\`
751
759
 
760
+ ### WAW018 — S3 Bucket Missing Public Access Block
761
+
762
+ **Severity:** error | **Category:** security
763
+
764
+ Flags S3 buckets without a \`PublicAccessBlockConfiguration\`. Without an explicit public access block, the bucket may be publicly accessible. Always set \`BlockPublicAcls\`, \`BlockPublicPolicy\`, \`IgnorePublicAcls\`, and \`RestrictPublicBuckets\` to \`true\`.
765
+
766
+ ### WAW019 — Security Group Unrestricted Ingress on Sensitive Ports
767
+
768
+ **Severity:** error | **Category:** security
769
+
770
+ Flags security group ingress rules that allow unrestricted access (\`0.0.0.0/0\` or \`::/0\`) on sensitive ports (22, 3389, 3306, 5432, 1433, 6379, 27017). Restrict ingress to known CIDR ranges or security groups.
771
+
772
+ ### WAW020 — IAM Policy Uses Wildcard Action
773
+
774
+ **Severity:** warning | **Category:** security
775
+
776
+ Flags IAM policy statements that use wildcard actions (\`"Action": "*"\` or \`"Action": "s3:*"\`). Use specific action names following the principle of least privilege.
777
+
778
+ ### WAW021 — RDS Storage Not Encrypted
779
+
780
+ **Severity:** error | **Category:** security
781
+
782
+ Flags RDS instances without \`StorageEncrypted: true\`. All RDS instances should encrypt data at rest to meet compliance and security requirements.
783
+
784
+ ### WAW022 — Lambda Not in VPC
785
+
786
+ **Severity:** warning | **Category:** security
787
+
788
+ Flags Lambda functions without a \`VpcConfig\`. Functions that access internal resources (databases, caches, internal APIs) should run inside a VPC. Functions that only call public APIs can safely skip VPC configuration.
789
+
790
+ ### WAW023 — CloudFront Without WAF
791
+
792
+ **Severity:** warning | **Category:** security
793
+
794
+ Flags CloudFront distributions without a \`WebACLId\`. Attaching a WAF web ACL protects your distribution from common web exploits and bots.
795
+
796
+ ### WAW024 — ALB Without Access Logging
797
+
798
+ **Severity:** warning | **Category:** best practice
799
+
800
+ Flags Application Load Balancers without access logging enabled. Enable \`access_logs.s3.enabled\` to capture request logs for debugging and compliance.
801
+
802
+ ### WAW025 — SNS Topic Not Encrypted
803
+
804
+ **Severity:** warning | **Category:** security
805
+
806
+ Flags SNS topics without \`KmsMasterKeyId\`. Encrypting topics at rest protects sensitive notification payloads.
807
+
808
+ ### WAW026 — SQS Queue Not Encrypted
809
+
810
+ **Severity:** warning | **Category:** security
811
+
812
+ Flags SQS queues without \`KmsMasterKeyId\` or \`SqsManagedSseEnabled\`. Encrypting queues at rest protects sensitive message payloads.
813
+
814
+ ### WAW027 — DynamoDB Missing Point-in-Time Recovery
815
+
816
+ **Severity:** info | **Category:** best practice
817
+
818
+ Flags DynamoDB tables without \`PointInTimeRecoverySpecification.PointInTimeRecoveryEnabled\` set to \`true\`. Point-in-time recovery provides continuous backups and protects against accidental writes or deletes.
819
+
820
+ ### WAW028 — EBS Volume Not Encrypted
821
+
822
+ **Severity:** warning | **Category:** security
823
+
824
+ Flags EBS volumes without \`Encrypted: true\`. All EBS volumes should encrypt data at rest for compliance and security.
825
+
752
826
  ## Running lint
753
827
 
754
828
  \`\`\`bash
@@ -953,7 +1027,19 @@ src/
953
1027
  - **Composite presets** — \`SecureApi\` (low memory, short timeout) and \`HighMemoryApi\` (high memory, longer timeout)
954
1028
  - **Custom lint rule** — \`api-timeout.ts\` enforces API Gateway's 29-second timeout limit (see [Custom Lint Rules](../custom-rules/))
955
1029
 
956
- The example produces 10 CloudFormation resources: 1 S3 bucket + 3 composites × 3 members each.`,
1030
+ The example produces 10 CloudFormation resources: 1 S3 bucket + 3 composites × 3 members each.
1031
+
1032
+ ## RDS Instance
1033
+
1034
+ \`examples/rds-postgres/\` — production RDS PostgreSQL instance using the \`RdsInstance\` composite with VPC networking and SSM parameter references.
1035
+
1036
+ {{file:rds-postgres/src/params.ts}}
1037
+
1038
+ {{file:rds-postgres/src/network.ts}}
1039
+
1040
+ {{file:rds-postgres/src/database.ts}}
1041
+
1042
+ Produces a complete RDS stack: VPC infrastructure (from \`VpcDefault\`), DB subnet group, security group, and RDS instance with encrypted storage.`,
957
1043
  },
958
1044
  {
959
1045
  slug: "skills",
@@ -78,7 +78,7 @@ export function generateTypeScriptDeclarations(
78
78
  description: p.description,
79
79
  }));
80
80
  const dtsAttrs: DtsAttribute[] = r.resource.attributes.map((a) => ({
81
- name: a.name,
81
+ name: a.name.replace(/\./g, "_").replace(/\*/g, "Item"), // Subscribers.*.Status → Subscribers_Item_Status
82
82
  type: a.tsType,
83
83
  }));
84
84
 
@@ -170,10 +170,11 @@ function generateRuntimeIndex(
170
170
  const tsName = naming.resolve(cfnType);
171
171
  if (!tsName) continue;
172
172
 
173
- // Build attrs map
173
+ // Build attrs map: TS key (underscores) → CF attr name (dots)
174
174
  const attrs: Record<string, string> = {};
175
175
  for (const a of r.resource.attributes) {
176
- attrs[a.name] = a.name;
176
+ const tsKey = a.name.replace(/\./g, "_").replace(/\*/g, "Item"); // Subscribers.*.Status → Subscribers_Item_Status
177
+ attrs[tsKey] = a.name; // maps to "Endpoint.Address" for GetAtt
177
178
  }
178
179
 
179
180
  resourceEntries.push({ tsName, resourceType: cfnType, attrs });
@@ -13,6 +13,7 @@ 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 { RdsInstance } from "./rds-instance";
16
17
 
17
18
  const baseProps = {
18
19
  name: "TestFunc",
@@ -633,3 +634,150 @@ describe("FargateService", () => {
633
634
  expect(svcProps.DesiredCount).toBe(2);
634
635
  });
635
636
  });
637
+
638
+ describe("RdsInstance", () => {
639
+ const rdsProps = {
640
+ vpcId: "vpc-123",
641
+ subnetIds: ["subnet-1", "subnet-2"],
642
+ masterPassword: "secret",
643
+ };
644
+
645
+ test("returns subnetGroup, sg, db members", () => {
646
+ const instance = RdsInstance(rdsProps);
647
+ const names = Object.keys(instance.members);
648
+ expect(names).toContain("subnetGroup");
649
+ expect(names).toContain("sg");
650
+ expect(names).toContain("db");
651
+ expect(names).toHaveLength(3);
652
+ });
653
+
654
+ test("expandComposite produces correct logical names", () => {
655
+ const expanded = expandComposite("myDb", RdsInstance(rdsProps));
656
+ expect(expanded.has("myDbSubnetGroup")).toBe(true);
657
+ expect(expanded.has("myDbSg")).toBe(true);
658
+ expect(expanded.has("myDbDb")).toBe(true);
659
+ expect(expanded.size).toBe(3);
660
+ });
661
+
662
+ test("with parameterGroupFamily, also returns parameterGroup", () => {
663
+ const instance = RdsInstance({
664
+ ...rdsProps,
665
+ parameterGroupFamily: "postgres16",
666
+ parameters: { shared_preload_libraries: "pg_stat_statements" },
667
+ });
668
+ const names = Object.keys(instance.members);
669
+ expect(names).toContain("parameterGroup");
670
+ expect(names).toHaveLength(4);
671
+ });
672
+
673
+ test("expandComposite with parameterGroup produces 4 entries", () => {
674
+ const expanded = expandComposite("pg", RdsInstance({
675
+ ...rdsProps,
676
+ parameterGroupFamily: "postgres16",
677
+ }));
678
+ expect(expanded.has("pgSubnetGroup")).toBe(true);
679
+ expect(expanded.has("pgSg")).toBe(true);
680
+ expect(expanded.has("pgDb")).toBe(true);
681
+ expect(expanded.has("pgParameterGroup")).toBe(true);
682
+ expect(expanded.size).toBe(4);
683
+ });
684
+
685
+ test("ingress from SG produces SourceSecurityGroupId rule", () => {
686
+ const instance = RdsInstance({
687
+ ...rdsProps,
688
+ ingressSourceSG: "sg-app123",
689
+ });
690
+ const sgProps = (instance.sg as any).props;
691
+ expect(sgProps.SecurityGroupIngress).toHaveLength(1);
692
+ const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
693
+ expect(ingress.SourceSecurityGroupId).toBe("sg-app123");
694
+ expect(ingress.FromPort).toBe(5432);
695
+ expect(ingress.ToPort).toBe(5432);
696
+ });
697
+
698
+ test("ingress from CIDR produces CidrIp rule", () => {
699
+ const instance = RdsInstance({
700
+ ...rdsProps,
701
+ ingressCidr: "10.0.0.0/16",
702
+ });
703
+ const sgProps = (instance.sg as any).props;
704
+ expect(sgProps.SecurityGroupIngress).toHaveLength(1);
705
+ const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
706
+ expect(ingress.CidrIp).toBe("10.0.0.0/16");
707
+ expect(ingress.FromPort).toBe(5432);
708
+ });
709
+
710
+ test("no ingress when neither SG nor CIDR provided", () => {
711
+ const instance = RdsInstance(rdsProps);
712
+ const sgProps = (instance.sg as any).props;
713
+ expect(sgProps.SecurityGroupIngress).toBeUndefined();
714
+ });
715
+
716
+ test("default engine is postgres with correct defaults", () => {
717
+ const instance = RdsInstance(rdsProps);
718
+ const dbProps = (instance.db as any).props;
719
+ expect(dbProps.Engine).toBe("postgres");
720
+ expect(dbProps.EngineVersion).toBe("16.6");
721
+ expect(dbProps.DBInstanceClass).toBe("db.t4g.micro");
722
+ expect(dbProps.AllocatedStorage).toBe("20");
723
+ expect(dbProps.StorageType).toBe("gp3");
724
+ expect(dbProps.StorageEncrypted).toBe(true);
725
+ expect(dbProps.MultiAZ).toBe(false);
726
+ expect(dbProps.BackupRetentionPeriod).toBe(7);
727
+ expect(dbProps.CopyTagsToSnapshot).toBe(true);
728
+ expect(dbProps.AutoMinorVersionUpgrade).toBe(true);
729
+ expect(dbProps.PubliclyAccessible).toBe(false);
730
+ expect(dbProps.DeletionProtection).toBe(false);
731
+ expect(dbProps.MasterUsername).toBe("postgres");
732
+ });
733
+
734
+ test("engine: mysql uses mysql-specific defaults", () => {
735
+ const instance = RdsInstance({
736
+ ...rdsProps,
737
+ engine: "mysql",
738
+ ingressCidr: "10.0.0.0/16",
739
+ });
740
+ const dbProps = (instance.db as any).props;
741
+ expect(dbProps.Engine).toBe("mysql");
742
+ expect(dbProps.EngineVersion).toBe("8.0.40");
743
+ expect(dbProps.MasterUsername).toBe("admin");
744
+ expect(dbProps.Port).toBe("3306");
745
+ const ingress = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
746
+ expect(ingress.FromPort).toBe(3306);
747
+ expect(ingress.ToPort).toBe(3306);
748
+ });
749
+
750
+ test("engine: mariadb uses mariadb-specific defaults", () => {
751
+ const instance = RdsInstance({
752
+ ...rdsProps,
753
+ engine: "mariadb",
754
+ });
755
+ const dbProps = (instance.db as any).props;
756
+ expect(dbProps.Engine).toBe("mariadb");
757
+ expect(dbProps.EngineVersion).toBe("11.4.3");
758
+ expect(dbProps.MasterUsername).toBe("admin");
759
+ expect(dbProps.Port).toBe("3306");
760
+ });
761
+
762
+ test("custom port is applied to SG and DB", () => {
763
+ const instance = RdsInstance({
764
+ ...rdsProps,
765
+ port: 3306,
766
+ ingressCidr: "10.0.0.0/8",
767
+ });
768
+ const dbProps = (instance.db as any).props;
769
+ expect(dbProps.Port).toBe("3306");
770
+ const ingress = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
771
+ expect(ingress.FromPort).toBe(3306);
772
+ expect(ingress.ToPort).toBe(3306);
773
+ });
774
+
775
+ test("db references subnet group and security group", () => {
776
+ const instance = RdsInstance(rdsProps);
777
+ const dbProps = (instance.db as any).props;
778
+ // Subnet group is passed as a resource instance (serializer resolves to { Ref: ... })
779
+ expect(dbProps.DBSubnetGroupName).toBe(instance.subnetGroup);
780
+ expect(dbProps.VPCSecurityGroups).toHaveLength(1);
781
+ expect(dbProps.VPCSecurityGroups[0]).toBeInstanceOf(AttrRef);
782
+ });
783
+ });
@@ -22,3 +22,5 @@ export { AlbShared } from "./alb-shared";
22
22
  export type { AlbSharedProps } from "./alb-shared";
23
23
  export { FargateService } from "./fargate-service";
24
24
  export type { FargateServiceProps } from "./fargate-service";
25
+ export { RdsInstance, RdsInstance as RdsPostgres } from "./rds-instance";
26
+ export type { RdsInstanceProps, RdsInstanceProps as RdsPostgresProps } from "./rds-instance";
@@ -0,0 +1,173 @@
1
+ import { Composite } from "@intentius/chant";
2
+ import {
3
+ DbInstance,
4
+ RDSDBSubnetGroup,
5
+ RDSDBParameterGroup,
6
+ SecurityGroup,
7
+ SecurityGroup_Ingress,
8
+ } from "../generated";
9
+
10
+ const ENGINE_DEFAULTS: Record<string, { port: number; username: string; version: string; logExport: string }> = {
11
+ postgres: { port: 5432, username: "postgres", version: "16.6", logExport: "postgresql" },
12
+ mysql: { port: 3306, username: "admin", version: "8.0.40", logExport: "general" },
13
+ mariadb: { port: 3306, username: "admin", version: "11.4.3", logExport: "general" },
14
+ };
15
+
16
+ export interface RdsInstanceProps {
17
+ // ── Engine ──────────────────────────────────────────────────────
18
+ engine?: "postgres" | "mysql" | "mariadb";
19
+
20
+ // ── Networking (required) ─────────────────────────────────────
21
+ vpcId: string;
22
+ subnetIds: string[];
23
+ ingressSourceSG?: string;
24
+ ingressCidr?: string;
25
+ port?: number;
26
+ publiclyAccessible?: boolean;
27
+
28
+ // ── Identity & auth (required) ────────────────────────────────
29
+ masterUsername?: string;
30
+ masterPassword: string;
31
+
32
+ // ── Engine version ──────────────────────────────────────────────
33
+ engineVersion?: string;
34
+ databaseName?: string;
35
+
36
+ // ── Instance sizing ───────────────────────────────────────────
37
+ instanceClass?: string;
38
+ allocatedStorage?: number;
39
+ storageType?: string;
40
+ maxAllocatedStorage?: number;
41
+
42
+ // ── High availability ─────────────────────────────────────────
43
+ multiAZ?: boolean;
44
+
45
+ // ── Encryption ────────────────────────────────────────────────
46
+ storageEncrypted?: boolean;
47
+ kmsKeyId?: string;
48
+
49
+ // ── Backup ────────────────────────────────────────────────────
50
+ backupRetentionPeriod?: number;
51
+ preferredBackupWindow?: string;
52
+ copyTagsToSnapshot?: boolean;
53
+
54
+ // ── Maintenance ───────────────────────────────────────────────
55
+ preferredMaintenanceWindow?: string;
56
+ autoMinorVersionUpgrade?: boolean;
57
+
58
+ // ── Monitoring ────────────────────────────────────────────────
59
+ enableCloudwatchLogs?: boolean;
60
+ enablePerformanceInsights?: boolean;
61
+ performanceInsightsRetentionPeriod?: number;
62
+
63
+ // ── Parameter group ───────────────────────────────────────────
64
+ parameterGroupFamily?: string;
65
+ parameters?: Record<string, string>;
66
+
67
+ // ── Protection ────────────────────────────────────────────────
68
+ deletionProtection?: boolean;
69
+ }
70
+
71
+ export const RdsInstance = Composite<RdsInstanceProps>((props) => {
72
+ const engine = props.engine ?? "postgres";
73
+ const defaults = ENGINE_DEFAULTS[engine];
74
+ const port = props.port ?? defaults.port;
75
+ const masterUsername = props.masterUsername ?? defaults.username;
76
+ const engineVersion = props.engineVersion ?? defaults.version;
77
+ const instanceClass = props.instanceClass ?? "db.t4g.micro";
78
+ const allocatedStorage = props.allocatedStorage ?? 20;
79
+ const storageType = props.storageType ?? "gp3";
80
+ const multiAZ = props.multiAZ ?? false;
81
+ const storageEncrypted = props.storageEncrypted ?? true;
82
+ const backupRetentionPeriod = props.backupRetentionPeriod ?? 7;
83
+ const copyTagsToSnapshot = props.copyTagsToSnapshot ?? true;
84
+ const autoMinorVersionUpgrade = props.autoMinorVersionUpgrade ?? true;
85
+ const publiclyAccessible = props.publiclyAccessible ?? false;
86
+ const deletionProtection = props.deletionProtection ?? false;
87
+
88
+ // DB Subnet Group
89
+ const subnetGroup = new RDSDBSubnetGroup({
90
+ DBSubnetGroupDescription: "Subnet group for RDS instance",
91
+ SubnetIds: props.subnetIds,
92
+ });
93
+
94
+ // Security Group
95
+ const ingressRules: InstanceType<typeof SecurityGroup_Ingress>[] = [];
96
+ if (props.ingressSourceSG) {
97
+ ingressRules.push(
98
+ new SecurityGroup_Ingress({
99
+ IpProtocol: "tcp",
100
+ FromPort: port,
101
+ ToPort: port,
102
+ SourceSecurityGroupId: props.ingressSourceSG,
103
+ }),
104
+ );
105
+ } else if (props.ingressCidr) {
106
+ ingressRules.push(
107
+ new SecurityGroup_Ingress({
108
+ IpProtocol: "tcp",
109
+ FromPort: port,
110
+ ToPort: port,
111
+ CidrIp: props.ingressCidr,
112
+ }),
113
+ );
114
+ }
115
+
116
+ const sg = new SecurityGroup({
117
+ GroupDescription: "Security group for RDS instance",
118
+ VpcId: props.vpcId,
119
+ SecurityGroupIngress: ingressRules.length > 0 ? ingressRules : undefined,
120
+ });
121
+
122
+ // Optional Parameter Group
123
+ let parameterGroup: InstanceType<typeof RDSDBParameterGroup> | undefined;
124
+ if (props.parameterGroupFamily) {
125
+ parameterGroup = new RDSDBParameterGroup({
126
+ Family: props.parameterGroupFamily,
127
+ Description: "Custom parameter group",
128
+ Parameters: props.parameters,
129
+ });
130
+ }
131
+
132
+ // DB Instance
133
+ const dbProps: Record<string, any> = {
134
+ Engine: engine,
135
+ EngineVersion: engineVersion,
136
+ DBInstanceClass: instanceClass,
137
+ MasterUsername: masterUsername,
138
+ MasterUserPassword: props.masterPassword,
139
+ AllocatedStorage: String(allocatedStorage),
140
+ StorageType: storageType,
141
+ DBSubnetGroupName: subnetGroup.Ref,
142
+ VPCSecurityGroups: [sg.GroupId],
143
+ Port: String(port),
144
+ PubliclyAccessible: publiclyAccessible,
145
+ MultiAZ: multiAZ,
146
+ StorageEncrypted: storageEncrypted,
147
+ BackupRetentionPeriod: backupRetentionPeriod,
148
+ CopyTagsToSnapshot: copyTagsToSnapshot,
149
+ AutoMinorVersionUpgrade: autoMinorVersionUpgrade,
150
+ DeletionProtection: deletionProtection,
151
+ };
152
+
153
+ if (props.databaseName) dbProps.DBName = props.databaseName;
154
+ if (props.kmsKeyId) dbProps.KmsKeyId = props.kmsKeyId;
155
+ if (props.maxAllocatedStorage) dbProps.MaxAllocatedStorage = props.maxAllocatedStorage;
156
+ if (props.preferredBackupWindow) dbProps.PreferredBackupWindow = props.preferredBackupWindow;
157
+ if (props.preferredMaintenanceWindow) dbProps.PreferredMaintenanceWindow = props.preferredMaintenanceWindow;
158
+ if (props.enableCloudwatchLogs) dbProps.EnableCloudwatchLogsExports = [defaults.logExport];
159
+ if (props.enablePerformanceInsights) {
160
+ dbProps.EnablePerformanceInsights = true;
161
+ if (props.performanceInsightsRetentionPeriod) {
162
+ dbProps.PerformanceInsightsRetentionPeriod = props.performanceInsightsRetentionPeriod;
163
+ }
164
+ }
165
+ if (parameterGroup) dbProps.DBParameterGroupName = parameterGroup.Ref;
166
+
167
+ const db = new DbInstance(dbProps);
168
+
169
+ const result: Record<string, any> = { subnetGroup, sg, db };
170
+ if (parameterGroup) result.parameterGroup = parameterGroup;
171
+
172
+ return result;
173
+ }, "RdsInstance");
@@ -0,0 +1,31 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { existsSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
7
+ const generatedDir = join(pkgDir, "src", "generated");
8
+ const hasGenerated = existsSync(join(generatedDir, "lexicon-aws.json"));
9
+
10
+ describe("coverage", () => {
11
+ test.skipIf(!hasGenerated)("computeCoverage function exists", async () => {
12
+ const { computeCoverage } = await import("./coverage");
13
+ expect(typeof computeCoverage).toBe("function");
14
+ });
15
+
16
+ test.skipIf(!hasGenerated)("overallPct function exists", async () => {
17
+ const { overallPct } = await import("./coverage");
18
+ expect(typeof overallPct).toBe("function");
19
+ });
20
+
21
+ test("handles missing generated files gracefully", async () => {
22
+ const { computeCoverage } = await import("./coverage");
23
+ if (!hasGenerated) {
24
+ try {
25
+ await computeCoverage(generatedDir);
26
+ } catch {
27
+ // Expected — no generated files
28
+ }
29
+ }
30
+ });
31
+ });