cdk-rds-sql 6.1.0 → 6.1.2

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/.jsii CHANGED
@@ -4172,7 +4172,7 @@
4172
4172
  },
4173
4173
  "locationInModule": {
4174
4174
  "filename": "src/provider.ts",
4175
- "line": 134
4175
+ "line": 133
4176
4176
  },
4177
4177
  "name": "newCustomResourceHandler",
4178
4178
  "parameters": [
@@ -5013,6 +5013,6 @@
5013
5013
  "symbolId": "src/sql:SqlProps"
5014
5014
  }
5015
5015
  },
5016
- "version": "6.1.0",
5017
- "fingerprint": "Jr9mtiVWj8E4ZRwDqRC2WebFyv8Fd/9WJUVkePMxpv8="
5016
+ "version": "6.1.2",
5017
+ "fingerprint": "FQIXsw6jGjZF4hbnqoBdGnn4gH67Zc1OmM9Xs2QqDoQ="
5018
5018
  }
@@ -56,5 +56,5 @@ repos:
56
56
  exclude: .projenrc.ts
57
57
  name: test
58
58
  language: system
59
- entry: sh -c 'npx projen test'
59
+ entry: sh -c 'npx projen test --coverage=false'
60
60
  files: \.ts$
package/CLAUDE.md ADDED
@@ -0,0 +1,117 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ This is a CDK construct library for creating databases, schemas, and roles in Aurora Serverless v2, RDS Database Cluster, or Database Instance. It supports both PostgreSQL and MySQL databases and is designed for enterprise environments with isolated subnets.
8
+
9
+ ## Key Architecture
10
+
11
+ ### Core Components
12
+
13
+ - **Provider**: Main construct that creates a Lambda function handler to execute database operations
14
+ - **Role**: Creates database users/roles with optional IAM authentication
15
+ - **Database**: Creates databases with optional ownership assignment
16
+ - **Schema**: Creates database schemas with role permissions
17
+ - **Sql**: Executes arbitrary SQL statements
18
+
19
+ ### Lambda Handler Architecture
20
+
21
+ - **Engine Factory Pattern**: `lambda/engine.factory.ts` creates database-specific engines
22
+ - **Abstract Engine**: `lambda/engine.abstract.ts` defines common interface
23
+ - **Concrete Engines**: `lambda/engine.postgresql.ts` and `lambda/engine.mysql.ts`
24
+ - **Main Handler**: `lambda/handler.ts` orchestrates CloudFormation custom resource operations
25
+
26
+ The Lambda handler supports both PostgreSQL and MySQL databases, auto-detecting the engine type from the cluster/instance configuration.
27
+
28
+ ## Development Commands
29
+
30
+ ### Build and Test
31
+
32
+ ```bash
33
+ # Build the project
34
+ npm run build
35
+
36
+ # Run all tests
37
+ npm run test
38
+
39
+ # Run tests in watch mode
40
+ npm run test:watch
41
+
42
+ # Run linting
43
+ npm run eslint
44
+
45
+ # Build only the Lambda handler
46
+ npm run build:handler
47
+ ```
48
+
49
+ ### Integration Testing
50
+
51
+ ```bash
52
+ # Deploy PostgreSQL serverless test stack
53
+ npm run integ:deploy:postgresql:serverless
54
+
55
+ # Deploy MySQL serverless test stack
56
+ npm run integ:deploy:mysql:serverless
57
+
58
+ # Destroy test stack
59
+ npm run integ:destroy:serverless
60
+ ```
61
+
62
+ ### Package Management
63
+
64
+ ```bash
65
+ # Package for distribution
66
+ npm run package
67
+
68
+ # Build all packages
69
+ npm run package-all
70
+ ```
71
+
72
+ ## Testing Setup
73
+
74
+ - **Framework**: Jest with TypeScript support
75
+ - **Test Files**: Located in `test/` and `lambda/` directories
76
+ - **Configuration**: Max 2 concurrent workers for database tests
77
+ - **Coverage**: Enabled with multiple reporters (json, lcov, clover, cobertura, text)
78
+
79
+ ## Database Engine Support
80
+
81
+ ### PostgreSQL
82
+
83
+ - Uses `pg` library for connections
84
+ - Supports schemas, roles, and databases
85
+ - IAM authentication supported
86
+
87
+ ### MySQL
88
+
89
+ - Uses `mysql2` library for connections
90
+ - Creates users with '%' host for VPC compatibility
91
+ - IAM authentication supported
92
+
93
+ ## Build Process
94
+
95
+ The project uses projen for build management. Do not change files managed by projen, but instead change `.projenrc.ts`
96
+
97
+ Projen tasks:
98
+
99
+ 1. `pre-compile`: Transpiles Lambda handler using esbuild
100
+ 2. `compile`: TypeScript compilation
101
+ 3. `post-compile`: Copies handler to lib directory
102
+ 4. `eslint`: Runs linter
103
+ 5. `typecheck`: Runs typecheck
104
+
105
+ ## SSL/TLS Configuration
106
+
107
+ - SSL is enabled by default
108
+ - Global RDS certificate bundle is automatically downloaded during build
109
+ - Can be disabled via `ssl: false` in Provider props
110
+
111
+ ## Key Dependencies
112
+
113
+ - **AWS CDK**: v2.171.1
114
+ - **Node.js**: >= 22.14
115
+ - **TypeScript**: ~5.8
116
+ - **Database Drivers**: pg (PostgreSQL), mysql2 (MySQL)
117
+ - **AWS SDKs**: secrets-manager, ssm clients
@@ -3,6 +3,12 @@ import {
3
3
  SecretsManagerClient,
4
4
  GetSecretValueCommand,
5
5
  } from "@aws-sdk/client-secrets-manager"
6
+ import {
7
+ EngineDatabaseProperties,
8
+ EngineRoleProperties,
9
+ EngineSchemaProperties,
10
+ EngineSqlProperties,
11
+ } from "./types"
6
12
 
7
13
  export interface EngineConnectionConfig {
8
14
  host: string
@@ -23,38 +29,50 @@ export abstract class AbstractEngine {
23
29
  : (_message?: any, ..._optionalParams: any[]) => {}
24
30
  }
25
31
 
26
- abstract createDatabase(resourceId: string, props?: any): string | string[]
32
+ abstract createDatabase(
33
+ resourceId: string,
34
+ props: EngineDatabaseProperties
35
+ ): string | string[]
27
36
  abstract updateDatabase(
28
37
  resourceId: string,
29
38
  oldResourceId: string,
30
- props?: any
39
+ props: EngineDatabaseProperties
31
40
  ): string | string[]
32
41
  abstract deleteDatabase(resourceId: string, masterUser: string): string | string[]
33
42
 
34
- abstract createRole(resourceId: string, props?: any): Promise<string | string[]>
43
+ abstract createRole(
44
+ resourceId: string,
45
+ props: EngineRoleProperties
46
+ ): Promise<string | string[]>
35
47
  abstract updateRole(
36
48
  resourceId: string,
37
49
  oldResourceId: string,
38
- props?: any,
39
- oldProps?: any
50
+ props: EngineRoleProperties,
51
+ oldProps: EngineRoleProperties
40
52
  ): Promise<string | string[]>
41
- abstract deleteRole(resourceId: string, props?: any): string | string[]
53
+ abstract deleteRole(resourceId: string, props: EngineRoleProperties): string | string[]
42
54
 
43
- abstract createSchema(resourceId: string, props?: any): string | string[]
55
+ abstract createSchema(
56
+ resourceId: string,
57
+ props: EngineSchemaProperties
58
+ ): string | string[]
44
59
  abstract updateSchema(
45
60
  resourceId: string,
46
61
  oldResourceId: string,
47
- props?: any
62
+ props: EngineSchemaProperties
63
+ ): string | string[]
64
+ abstract deleteSchema(
65
+ resourceId: string,
66
+ props: EngineSchemaProperties
48
67
  ): string | string[]
49
- abstract deleteSchema(resourceId: string, props?: any): string | string[]
50
68
 
51
- abstract createSql(resourceId: string, props?: any): string | string[]
69
+ abstract createSql(resourceId: string, props: EngineSqlProperties): string | string[]
52
70
  abstract updateSql(
53
71
  resourceId: string,
54
72
  oldResourceId: string,
55
- props?: any
73
+ props: EngineSqlProperties
56
74
  ): string | string[]
57
- abstract deleteSql(resourceId: string, props?: any): string | string[]
75
+ abstract deleteSql(resourceId: string, props: EngineSqlProperties): string | string[]
58
76
 
59
77
  abstract executeSQL(
60
78
  sql: string | string[],
@@ -1,6 +1,6 @@
1
1
  import { AbstractEngine } from "./engine.abstract"
2
- import { PostgresqlEngine } from "./engine.postgresql"
3
2
  import { MysqlEngine } from "./engine.mysql"
3
+ import { PostgresqlEngine } from "./engine.postgresql"
4
4
 
5
5
  export class EngineFactory {
6
6
  static createEngine(engine: string): AbstractEngine {
@@ -39,9 +39,14 @@ describe("MySQL Engine", () => {
39
39
  // Mock getPassword implementation
40
40
  jest.spyOn(engine as any, "getPassword").mockResolvedValue("test-password")
41
41
 
42
- const sql = await engine.updateRole("newrole", "oldrole", {
43
- PasswordArn: "arn:aws:secretsmanager:region:account:secret:name",
44
- })
42
+ const sql = await engine.updateRole(
43
+ "newrole",
44
+ "oldrole",
45
+ {
46
+ PasswordArn: "arn:aws:secretsmanager:region:account:secret:name",
47
+ },
48
+ {}
49
+ )
45
50
 
46
51
  expect(Array.isArray(sql)).toBe(true)
47
52
  expect(sql[0]).toContain("CREATE USER IF NOT EXISTS")
@@ -99,7 +104,9 @@ describe("MySQL Engine", () => {
99
104
  const sql = await engine.createRole("iamuser", props)
100
105
 
101
106
  expect(Array.isArray(sql)).toBe(true)
102
- expect(sql[0]).toContain("CREATE USER IF NOT EXISTS 'iamuser'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'")
107
+ expect(sql[0]).toContain(
108
+ "CREATE USER IF NOT EXISTS 'iamuser'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'"
109
+ )
103
110
  expect(sql[1]).toContain("GRANT ALL PRIVILEGES ON `testdb`.* TO 'iamuser'@'%'")
104
111
  expect(sql[2]).toBe("FLUSH PRIVILEGES")
105
112
  })
@@ -116,8 +123,12 @@ describe("MySQL Engine", () => {
116
123
  const sql = await engine.createRole("passworduser", props)
117
124
 
118
125
  expect(Array.isArray(sql)).toBe(true)
119
- expect(sql[0]).toContain("CREATE USER IF NOT EXISTS 'passworduser'@'%' IDENTIFIED BY 'test-password'")
120
- expect(sql[1]).toContain("GRANT ALL PRIVILEGES ON `testdb`.* TO 'passworduser'@'%'")
126
+ expect(sql[0]).toContain(
127
+ "CREATE USER IF NOT EXISTS 'passworduser'@'%' IDENTIFIED BY 'test-password'"
128
+ )
129
+ expect(sql[1]).toContain(
130
+ "GRANT ALL PRIVILEGES ON `testdb`.* TO 'passworduser'@'%'"
131
+ )
121
132
  expect(sql[2]).toBe("FLUSH PRIVILEGES")
122
133
  })
123
134
 
@@ -132,11 +143,18 @@ describe("MySQL Engine", () => {
132
143
  DatabaseName: "testdb",
133
144
  }
134
145
 
135
- const sql = await engine.updateRole("switchuser", "switchuser", newProps, oldProps)
146
+ const sql = await engine.updateRole(
147
+ "switchuser",
148
+ "switchuser",
149
+ newProps,
150
+ oldProps
151
+ )
136
152
 
137
153
  expect(Array.isArray(sql)).toBe(true)
138
154
  expect(sql[0]).toContain("DROP USER IF EXISTS 'switchuser'@'%'")
139
- expect(sql[1]).toContain("CREATE USER 'switchuser'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'")
155
+ expect(sql[1]).toContain(
156
+ "CREATE USER 'switchuser'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'"
157
+ )
140
158
  expect(sql[2]).toContain("GRANT ALL PRIVILEGES ON `testdb`.* TO 'switchuser'@'%'")
141
159
  expect(sql[3]).toBe("FLUSH PRIVILEGES")
142
160
  })
@@ -154,11 +172,18 @@ describe("MySQL Engine", () => {
154
172
  DatabaseName: "testdb",
155
173
  }
156
174
 
157
- const sql = await engine.updateRole("switchuser", "switchuser", newProps, oldProps)
175
+ const sql = await engine.updateRole(
176
+ "switchuser",
177
+ "switchuser",
178
+ newProps,
179
+ oldProps
180
+ )
158
181
 
159
182
  expect(Array.isArray(sql)).toBe(true)
160
183
  expect(sql[0]).toContain("DROP USER IF EXISTS 'switchuser'@'%'")
161
- expect(sql[1]).toContain("CREATE USER 'switchuser'@'%' IDENTIFIED BY 'new-password'")
184
+ expect(sql[1]).toContain(
185
+ "CREATE USER 'switchuser'@'%' IDENTIFIED BY 'new-password'"
186
+ )
162
187
  expect(sql[2]).toContain("GRANT ALL PRIVILEGES ON `testdb`.* TO 'switchuser'@'%'")
163
188
  expect(sql[3]).toBe("FLUSH PRIVILEGES")
164
189
  })
@@ -1,13 +1,19 @@
1
1
  import * as fs from "fs"
2
2
  import { AbstractEngine, EngineConnectionConfig } from "./engine.abstract"
3
+ import {
4
+ EngineDatabaseProperties,
5
+ EngineRoleProperties,
6
+ EngineSchemaProperties,
7
+ EngineSqlProperties,
8
+ } from "./types"
3
9
 
4
10
  export class MysqlEngine extends AbstractEngine {
5
- createDatabase(resourceId: string, props?: any): string[] {
11
+ createDatabase(resourceId: string, props: EngineDatabaseProperties): string[] {
6
12
  const sql = [`CREATE DATABASE IF NOT EXISTS \`${resourceId}\``]
7
13
 
8
- if (props?.Owner) {
14
+ if (props.Owner) {
9
15
  sql.push(`GRANT ALL PRIVILEGES ON \`${resourceId}\`.* TO '${props.Owner}'@'%'`)
10
- sql.push(`FLUSH PRIVILEGES`)
16
+ sql.push("FLUSH PRIVILEGES")
11
17
  }
12
18
 
13
19
  return sql
@@ -21,9 +27,9 @@ export class MysqlEngine extends AbstractEngine {
21
27
  return [`DROP DATABASE IF EXISTS \`${resourceId}\``]
22
28
  }
23
29
 
24
- async createRole(resourceId: string, props?: any): Promise<string[]> {
30
+ async createRole(resourceId: string, props: EngineRoleProperties): Promise<string[]> {
25
31
  const sql: string[] = []
26
-
32
+
27
33
  if (props.EnableIamAuth) {
28
34
  // Create user for IAM authentication
29
35
  sql.push(
@@ -34,7 +40,7 @@ export class MysqlEngine extends AbstractEngine {
34
40
  if (!props.PasswordArn) throw new Error("No PasswordArn provided")
35
41
  const password = await this.getPassword(props.PasswordArn)
36
42
  if (!password) throw `Cannot parse password from ${props.PasswordArn}`
37
-
43
+
38
44
  sql.push(
39
45
  `CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY '${password}'`
40
46
  )
@@ -45,22 +51,22 @@ export class MysqlEngine extends AbstractEngine {
45
51
  `GRANT ALL PRIVILEGES ON \`${props.DatabaseName}\`.* TO '${resourceId}'@'%'`
46
52
  )
47
53
  }
48
-
49
- sql.push(`FLUSH PRIVILEGES`)
54
+
55
+ sql.push("FLUSH PRIVILEGES")
50
56
  return sql
51
57
  }
52
58
 
53
59
  async updateRole(
54
60
  resourceId: string,
55
61
  oldResourceId: string,
56
- props?: any,
57
- oldProps?: any
62
+ props: EngineRoleProperties,
63
+ oldProps: EngineRoleProperties
58
64
  ): Promise<string[]> {
59
65
  const sql: string[] = []
60
66
 
61
67
  if (oldResourceId !== resourceId) {
62
68
  // MySQL doesn't allow renaming users directly, we need to create a new one and drop the old one
63
- if (props?.EnableIamAuth) {
69
+ if (props.EnableIamAuth) {
64
70
  // Create new user with IAM auth
65
71
  sql.push(
66
72
  `CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'`
@@ -124,13 +130,13 @@ export class MysqlEngine extends AbstractEngine {
124
130
  }
125
131
 
126
132
  if (sql.length > 0) {
127
- sql.push(`FLUSH PRIVILEGES`)
133
+ sql.push("FLUSH PRIVILEGES")
128
134
  }
129
135
 
130
136
  return sql
131
137
  }
132
138
 
133
- deleteRole(resourceId: string, props?: any): string[] {
139
+ deleteRole(resourceId: string, props: EngineRoleProperties): string[] {
134
140
  const sql: string[] = []
135
141
 
136
142
  if (props?.DatabaseName) {
@@ -140,33 +146,41 @@ export class MysqlEngine extends AbstractEngine {
140
146
  }
141
147
 
142
148
  sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`)
143
- sql.push(`FLUSH PRIVILEGES`)
149
+ sql.push("FLUSH PRIVILEGES")
144
150
 
145
151
  return sql
146
152
  }
147
153
 
148
- createSchema(_resourceId: string, _props?: any): string[] {
154
+ createSchema(_resourceId: string, _props: EngineSchemaProperties): string[] {
149
155
  throw new Error("Schemas are not supported in MySQL/MariaDB")
150
156
  }
151
157
 
152
- updateSchema(_resourceId: string, _oldResourceId: string, _props?: any): string[] {
158
+ updateSchema(
159
+ _resourceId: string,
160
+ _oldResourceId: string,
161
+ _props: EngineSchemaProperties
162
+ ): string[] {
153
163
  throw new Error("Schemas are not supported in MySQL/MariaDB")
154
164
  }
155
165
 
156
- deleteSchema(_resourceId: string, _props?: any): string[] {
166
+ deleteSchema(_resourceId: string, _props: EngineSchemaProperties): string[] {
157
167
  throw new Error("Schemas are not supported in MySQL/MariaDB")
158
168
  }
159
169
 
160
- createSql(_resourceId: string, props?: any): string {
161
- return props.Statement
170
+ createSql(_resourceId: string, props: EngineSqlProperties): string {
171
+ return props?.Statement || ""
162
172
  }
163
173
 
164
- updateSql(_resourceId: string, _oldResourceId: string, props?: any): string {
165
- return props.Statement
174
+ updateSql(
175
+ _resourceId: string,
176
+ _oldResourceId: string,
177
+ props: EngineSqlProperties
178
+ ): string {
179
+ return props?.Statement || ""
166
180
  }
167
181
 
168
- deleteSql(_resourceId: string, props?: any): string {
169
- return props.Rollback
182
+ deleteSql(_resourceId: string, props: EngineSqlProperties): string {
183
+ return props?.Rollback || ""
170
184
  }
171
185
 
172
186
  async executeSQL(sql: string | string[], config: EngineConnectionConfig): Promise<any> {
@@ -207,9 +221,9 @@ export class MysqlEngine extends AbstractEngine {
207
221
  const connection = await createConnection(connectionConfig)
208
222
  try {
209
223
  if (typeof sql === "string") {
210
- return connection.query(sql)
224
+ return await connection.query(sql)
211
225
  } else if (sql) {
212
- return Promise.all(sql.map((statement) => connection.query(statement)))
226
+ return await Promise.all(sql.map((statement) => connection.query(statement)))
213
227
  }
214
228
  } finally {
215
229
  await connection.end()
@@ -58,8 +58,8 @@ describe("PostgreSQL Engine", () => {
58
58
 
59
59
  expect(Array.isArray(sql)).toBe(true)
60
60
  expect(sql[0]).toBe("start transaction")
61
- expect(sql).toContain('create role iamrole with login')
62
- expect(sql).toContain('grant rds_iam to iamrole')
61
+ expect(sql).toContain("create role iamrole with login")
62
+ expect(sql).toContain("grant rds_iam to iamrole")
63
63
  expect(sql[sql.length - 1]).toBe("commit")
64
64
  })
65
65
 
@@ -76,7 +76,9 @@ describe("PostgreSQL Engine", () => {
76
76
 
77
77
  expect(Array.isArray(sql)).toBe(true)
78
78
  expect(sql[0]).toBe("start transaction")
79
- expect(sql).toContain('create role passwordrole with login password \'test-password\'')
79
+ expect(sql).toContain(
80
+ "create role passwordrole with login password 'test-password'"
81
+ )
80
82
  expect(sql[sql.length - 1]).toBe("commit")
81
83
  })
82
84
 
@@ -90,11 +92,16 @@ describe("PostgreSQL Engine", () => {
90
92
  EnableIamAuth: true,
91
93
  }
92
94
 
93
- const sql = await engine.updateRole("switchrole", "switchrole", newProps, oldProps)
95
+ const sql = await engine.updateRole(
96
+ "switchrole",
97
+ "switchrole",
98
+ newProps,
99
+ oldProps
100
+ )
94
101
 
95
102
  expect(Array.isArray(sql)).toBe(true)
96
103
  expect(sql[0]).toBe("start transaction")
97
- expect(sql).toContain('grant rds_iam to switchrole')
104
+ expect(sql).toContain("grant rds_iam to switchrole")
98
105
  expect(sql[sql.length - 1]).toBe("commit")
99
106
  })
100
107
 
@@ -110,12 +117,17 @@ describe("PostgreSQL Engine", () => {
110
117
  PasswordArn: "arn:aws:secretsmanager:region:account:secret:name",
111
118
  }
112
119
 
113
- const sql = await engine.updateRole("switchrole", "switchrole", newProps, oldProps)
120
+ const sql = await engine.updateRole(
121
+ "switchrole",
122
+ "switchrole",
123
+ newProps,
124
+ oldProps
125
+ )
114
126
 
115
127
  expect(Array.isArray(sql)).toBe(true)
116
128
  expect(sql[0]).toBe("start transaction")
117
- expect(sql).toContain('revoke rds_iam from switchrole')
118
- expect(sql).toContain('alter role switchrole with password \'new-password\'')
129
+ expect(sql).toContain("revoke rds_iam from switchrole")
130
+ expect(sql).toContain("alter role switchrole with password 'new-password'")
119
131
  expect(sql[sql.length - 1]).toBe("commit")
120
132
  })
121
133
  })
@@ -125,7 +137,7 @@ describe("PostgreSQL Engine", () => {
125
137
  describe("Database", () => {
126
138
  it("should generate correct SQL for creating a database", () => {
127
139
  const sql = engine.createDatabase("testdb", {})
128
- expect(sql).toContain('create database testdb')
140
+ expect(sql).toContain("create database testdb")
129
141
  })
130
142
  })
131
143