@intentius/chant-lexicon-aws 0.0.12 → 0.0.13

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.13",
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.12",
26
26
  "fflate": "^0.8.2",
27
27
  "js-yaml": "^4.1.0"
28
28
  },
@@ -407,6 +407,7 @@ The AWS lexicon ships ready-to-use composites for common patterns. Import them f
407
407
  | \`FargateAlb\` | \`cluster\`, \`executionRole\`, \`taskRole\`, \`logGroup\`, \`taskDef\`, \`albSg\`, \`taskSg\`, \`alb\`, \`targetGroup\`, \`listener\`, \`service\` | Fargate service behind an ALB. Accepts VPC outputs as props. |
408
408
  | \`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
409
  | \`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. |
410
+ | \`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
411
 
411
412
  All built-in composites accept \`ManagedPolicyArns\` and \`Policies\` for adding IAM permissions to the auto-created role.
412
413
 
@@ -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
+ });