cdk-rds-sql 6.1.1 → 6.1.3

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.
@@ -1,26 +1,13 @@
1
1
  import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
2
2
  import { createConnection } from "mysql2/promise"
3
3
  import { GenericContainer, StartedTestContainer } from "testcontainers"
4
- import { createRequest, updateRequest, deleteRequest } from "./util"
5
4
  import { handler } from "./handler"
5
+ import { createRequest, updateRequest, deleteRequest } from "./util"
6
6
 
7
7
  jest.mock("@aws-sdk/client-secrets-manager")
8
8
  const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
9
9
  typeof SecretsManagerClient
10
10
  >
11
- SecretsManagerClientMock.prototype.send.mockImplementation(() => {
12
- return {
13
- SecretString: JSON.stringify({
14
- host: mysqlHost,
15
- port: mysqlPort,
16
- username: DB_MASTER_USERNAME,
17
- password: DB_MASTER_PASSWORD,
18
- dbname: DB_DEFAULT_DB,
19
- engine: "mysql",
20
- dbClusterIdentifier: "dummy",
21
- }),
22
- }
23
- })
24
11
 
25
12
  const DB_PORT = 3306
26
13
  const DB_MASTER_USERNAME = "root"
@@ -31,7 +18,7 @@ let mysqlContainer: StartedTestContainer
31
18
  let mysqlHost: string
32
19
  let mysqlPort: number
33
20
 
34
- beforeEach(async () => {
21
+ beforeAll(async () => {
35
22
  process.env.LOGGER = "true"
36
23
  process.env.SSL = "false"
37
24
  process.env.CONNECTION_TIMEOUT = "5000"
@@ -45,26 +32,70 @@ beforeEach(async () => {
45
32
  .start()
46
33
  mysqlHost = mysqlContainer.getHost()
47
34
  mysqlPort = mysqlContainer.getMappedPort(DB_PORT)
48
- }, 30000)
49
35
 
50
- afterEach(async () => {
51
- jest.clearAllMocks()
36
+ // Set up the mock after we have the host and port values
37
+ SecretsManagerClientMock.prototype.send.mockImplementation(() => {
38
+ return {
39
+ SecretString: JSON.stringify({
40
+ host: mysqlHost,
41
+ port: mysqlPort,
42
+ username: DB_MASTER_USERNAME,
43
+ password: DB_MASTER_PASSWORD,
44
+ dbname: DB_DEFAULT_DB,
45
+ engine: "mysql",
46
+ dbClusterIdentifier: "dummy",
47
+ }),
48
+ }
49
+ })
50
+ }, 60000)
51
+
52
+ afterAll(async () => {
52
53
  if (mysqlContainer) {
53
54
  await mysqlContainer.stop()
54
55
  }
55
56
  })
56
57
 
58
+ beforeEach(async () => {
59
+ jest.clearAllMocks()
60
+
61
+ // Clean up databases and users created by tests
62
+ const connection = await newConnection()
63
+
64
+ try {
65
+ // Drop all databases except system ones and default
66
+ const [databases] = await connection.query(
67
+ "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys', ?)",
68
+ [DB_DEFAULT_DB]
69
+ )
70
+
71
+ for (const db of databases) {
72
+ await connection.query(`DROP DATABASE IF EXISTS \`${db.SCHEMA_NAME}\``)
73
+ }
74
+
75
+ // Drop all users except system ones
76
+ const [users] = await connection.query(
77
+ "SELECT User FROM mysql.user WHERE User NOT IN ('mysql.sys', 'mysql.session', 'mysql.infoschema', 'root')"
78
+ )
79
+
80
+ for (const user of users) {
81
+ await connection.query(`DROP USER IF EXISTS '${user.User}'@'%'`)
82
+ }
83
+ } finally {
84
+ await connection.end()
85
+ }
86
+ })
87
+
57
88
  // Helper functions for MySQL tests
58
89
  async function databaseExists(connection: any, dbName: string): Promise<boolean> {
59
90
  const [rows] = await connection.query(
60
- `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`,
91
+ "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?",
61
92
  [dbName]
62
93
  )
63
94
  return rows.length > 0
64
95
  }
65
96
 
66
97
  async function userExists(connection: any, username: string): Promise<boolean> {
67
- const [rows] = await connection.query(`SELECT User FROM mysql.user WHERE User = ?`, [
98
+ const [rows] = await connection.query("SELECT User FROM mysql.user WHERE User = ?", [
68
99
  username,
69
100
  ])
70
101
  return rows.length > 0
@@ -76,7 +107,7 @@ async function tableExists(
76
107
  tableName: string
77
108
  ): Promise<boolean> {
78
109
  const [rows] = await connection.query(
79
- `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
110
+ "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?",
80
111
  [dbName, tableName]
81
112
  )
82
113
  return rows.length > 0
@@ -103,7 +134,7 @@ test("database", async () => {
103
134
  const newDatabaseName = "mydb2"
104
135
  const create = createRequest("database", oldDatabaseName)
105
136
  await handler(create)
106
- expect(SecretsManagerClientMock).toHaveBeenCalledTimes(1)
137
+ expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
107
138
  const connection = await newConnection()
108
139
  try {
109
140
  expect(await databaseExists(connection, oldDatabaseName)).toEqual(true)
@@ -188,7 +219,7 @@ describe("User creation", () => {
188
219
  DatabaseName: DB_DEFAULT_DB,
189
220
  })
190
221
  await handler(create)
191
- expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
222
+ expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
192
223
 
193
224
  const connection = await newConnection()
194
225
  try {
@@ -227,7 +258,7 @@ describe("User creation", () => {
227
258
  DatabaseName: DB_DEFAULT_DB,
228
259
  })
229
260
  await handler(create)
230
- expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
261
+ expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
231
262
 
232
263
  const connection = await newConnection()
233
264
  try {
@@ -1,6 +1,7 @@
1
1
  import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
2
2
  import { Client, ClientConfig } from "pg"
3
3
  import { GenericContainer, StartedTestContainer } from "testcontainers"
4
+ import { handler } from "./handler"
4
5
  import {
5
6
  createRequest,
6
7
  updateRequest,
@@ -12,7 +13,6 @@ import {
12
13
  rowCount,
13
14
  roleGrantedForSchema,
14
15
  } from "./util"
15
- import { handler } from "./handler"
16
16
 
17
17
  jest.mock("@aws-sdk/client-secrets-manager")
18
18
  const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
@@ -41,7 +41,7 @@ let pgContainer: StartedTestContainer
41
41
  let pgHost: string
42
42
  let pgPort: number
43
43
 
44
- beforeEach(async () => {
44
+ beforeAll(async () => {
45
45
  process.env.LOGGER = "true"
46
46
  process.env.SSL = "false"
47
47
  process.env.CONNECTION_TIMEOUT = "5000"
@@ -55,15 +55,62 @@ beforeEach(async () => {
55
55
  .start()
56
56
  pgHost = pgContainer.getHost()
57
57
  pgPort = pgContainer.getMappedPort(DB_PORT)
58
- }, 30000)
58
+ }, 60000)
59
59
 
60
- afterEach(async () => {
61
- jest.clearAllMocks()
60
+ afterAll(async () => {
62
61
  if (pgContainer) {
63
62
  await pgContainer.stop()
64
63
  }
65
64
  })
66
65
 
66
+ beforeEach(async () => {
67
+ jest.clearAllMocks()
68
+
69
+ // Clean up databases, schemas, and roles created by tests
70
+ const client = new Client({
71
+ host: pgHost,
72
+ port: pgPort,
73
+ database: DB_DEFAULT_DB,
74
+ user: DB_MASTER_USERNAME,
75
+ password: DB_MASTER_PASSWORD,
76
+ })
77
+
78
+ await client.connect()
79
+
80
+ try {
81
+ // Drop all databases except system ones and default
82
+ const databases = await client.query(
83
+ "SELECT datname FROM pg_database WHERE datistemplate = false AND datname != $1",
84
+ [DB_DEFAULT_DB]
85
+ )
86
+
87
+ for (const db of databases.rows) {
88
+ await client.query(`DROP DATABASE IF EXISTS "${db.datname}"`)
89
+ }
90
+
91
+ // Drop all schemas except system ones
92
+ const schemas = await client.query(
93
+ "SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast', 'public')"
94
+ )
95
+
96
+ for (const schema of schemas.rows) {
97
+ await client.query(`DROP SCHEMA IF EXISTS "${schema.schema_name}" CASCADE`)
98
+ }
99
+
100
+ // Drop all roles except system ones
101
+ const roles = await client.query(
102
+ "SELECT rolname FROM pg_roles WHERE rolname NOT LIKE 'pg_%' AND rolname != $1",
103
+ [DB_MASTER_USERNAME]
104
+ )
105
+
106
+ for (const role of roles.rows) {
107
+ await client.query(`DROP ROLE IF EXISTS "${role.rolname}"`)
108
+ }
109
+ } finally {
110
+ await client.end()
111
+ }
112
+ })
113
+
67
114
  //jest.setTimeout(ms("15s"))
68
115
 
69
116
  test("schema", async () => {
@@ -71,7 +118,7 @@ test("schema", async () => {
71
118
  const newSchemaName = "test2"
72
119
  const create = createRequest("schema", oldSchemaName)
73
120
  await handler(create)
74
- expect(SecretsManagerClientMock).toHaveBeenCalledTimes(1)
121
+ expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
75
122
 
76
123
  const client = await newClient()
77
124
  try {
@@ -128,7 +175,7 @@ test("role with existing database", async () => {
128
175
  DatabaseName: "postgres",
129
176
  })
130
177
  await handler(create)
131
- expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
178
+ expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
132
179
  const client = await newClient()
133
180
  try {
134
181
  expect(await roleExists(client, oldRoleName)).toEqual(true)
@@ -165,7 +212,7 @@ test("role without database", async () => {
165
212
  PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
166
213
  })
167
214
  await handler(create)
168
- expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
215
+ expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
169
216
  const client = await newClient()
170
217
  try {
171
218
  expect(await roleExists(client, roleName)).toEqual(true)
@@ -180,7 +227,7 @@ test("change role password", async () => {
180
227
  PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
181
228
  })
182
229
  await handler(create)
183
- expect(SecretsManagerClientMock).toHaveBeenCalledTimes(2)
230
+ expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(2)
184
231
  const client = await newClient()
185
232
  try {
186
233
  expect(await roleExists(client, roleName)).toEqual(true)
@@ -198,7 +245,7 @@ test("database", async () => {
198
245
  const newDatabaseName = "mydb2"
199
246
  const create = createRequest("database", oldDatabaseName)
200
247
  await handler(create)
201
- expect(SecretsManagerClientMock).toHaveBeenCalledTimes(1)
248
+ expect(SecretsManagerClientMock.prototype.send).toHaveBeenCalledTimes(1)
202
249
  const client = await newClient()
203
250
  try {
204
251
  expect(await databaseExists(client, oldDatabaseName)).toEqual(true)
@@ -0,0 +1,164 @@
1
+ import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"
2
+ import { MysqlEngine } from "./engine.mysql"
3
+ import { PostgresqlEngine } from "./engine.postgresql"
4
+ import { handler } from "./handler"
5
+ import { createRequest } from "./util"
6
+
7
+ // Mock the secrets manager client
8
+ jest.mock("@aws-sdk/client-secrets-manager")
9
+ const SecretsManagerClientMock = SecretsManagerClient as jest.MockedClass<
10
+ typeof SecretsManagerClient
11
+ >
12
+
13
+ // Mock the database engines
14
+ jest.mock("./engine.mysql")
15
+ jest.mock("./engine.postgresql")
16
+
17
+ const MockedMysqlEngine = MysqlEngine as jest.MockedClass<typeof MysqlEngine>
18
+ const MockedPostgresqlEngine = PostgresqlEngine as jest.MockedClass<
19
+ typeof PostgresqlEngine
20
+ >
21
+
22
+ describe("Handler CloudFormation Property Conversion", () => {
23
+ let mockMysqlEngine: jest.Mocked<MysqlEngine>
24
+ let mockPostgresqlEngine: jest.Mocked<PostgresqlEngine>
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks()
28
+
29
+ // Mock the engine instances
30
+ mockMysqlEngine = {
31
+ createRole: jest.fn(),
32
+ executeSQL: jest.fn(),
33
+ } as any
34
+
35
+ mockPostgresqlEngine = {
36
+ createRole: jest.fn(),
37
+ executeSQL: jest.fn(),
38
+ } as any
39
+
40
+ MockedMysqlEngine.mockImplementation(() => mockMysqlEngine)
41
+ MockedPostgresqlEngine.mockImplementation(() => mockPostgresqlEngine)
42
+
43
+ // Mock secrets manager to return MySQL engine
44
+ SecretsManagerClientMock.prototype.send.mockImplementation(() => {
45
+ return Promise.resolve({
46
+ SecretString: JSON.stringify({
47
+ host: "localhost",
48
+ port: 3306,
49
+ username: "root",
50
+ password: "password",
51
+ dbname: "test",
52
+ engine: "mysql",
53
+ }),
54
+ })
55
+ })
56
+ })
57
+
58
+ describe("EnableIamAuth property conversion", () => {
59
+ it("should convert string 'false' to boolean false", async () => {
60
+ mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
61
+ mockMysqlEngine.executeSQL.mockResolvedValue({})
62
+
63
+ // Create a request with EnableIamAuth as string 'false' (simulating CloudFormation)
64
+ const request = createRequest("role", "testuser", {
65
+ EnableIamAuth: "false",
66
+ PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
67
+ })
68
+
69
+ await handler(request)
70
+
71
+ // Verify that createRole was called with boolean false, not string 'false'
72
+ expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
73
+ "testuser",
74
+ expect.objectContaining({
75
+ EnableIamAuth: false, // Should be converted to boolean false
76
+ PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
77
+ })
78
+ )
79
+ })
80
+
81
+ it("should convert string 'true' to boolean true", async () => {
82
+ mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
83
+ mockMysqlEngine.executeSQL.mockResolvedValue({})
84
+
85
+ // Create a request with EnableIamAuth as string 'true' (simulating CloudFormation)
86
+ const request = createRequest("role", "testuser", {
87
+ EnableIamAuth: "true", // CloudFormation passes boolean as string
88
+ })
89
+
90
+ await handler(request)
91
+
92
+ // Verify that createRole was called with boolean true, not string 'true'
93
+ expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
94
+ "testuser",
95
+ expect.objectContaining({
96
+ EnableIamAuth: true, // Should be converted to boolean true
97
+ })
98
+ )
99
+ })
100
+
101
+ it("should handle number 0 correctly", async () => {
102
+ mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
103
+ mockMysqlEngine.executeSQL.mockResolvedValue({})
104
+
105
+ // Create a request with EnableIamAuth as actual boolean false
106
+ const request = createRequest("role", "testuser", {
107
+ EnableIamAuth: "0",
108
+ PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
109
+ })
110
+
111
+ await handler(request)
112
+
113
+ // Verify that createRole was called with boolean false
114
+ expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
115
+ "testuser",
116
+ expect.objectContaining({
117
+ EnableIamAuth: false,
118
+ PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
119
+ })
120
+ )
121
+ })
122
+
123
+ it("should handle number 1 correctly", async () => {
124
+ mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
125
+ mockMysqlEngine.executeSQL.mockResolvedValue({})
126
+
127
+ // Create a request with EnableIamAuth as actual boolean true
128
+ const request = createRequest("role", "testuser", {
129
+ EnableIamAuth: "1",
130
+ })
131
+
132
+ await handler(request)
133
+
134
+ // Verify that createRole was called with boolean true
135
+ expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
136
+ "testuser",
137
+ expect.objectContaining({
138
+ EnableIamAuth: true,
139
+ })
140
+ )
141
+ })
142
+
143
+ it("should handle undefined EnableIamAuth correctly", async () => {
144
+ mockMysqlEngine.createRole.mockResolvedValue(["CREATE USER test"])
145
+ mockMysqlEngine.executeSQL.mockResolvedValue({})
146
+
147
+ // Create a request without EnableIamAuth property
148
+ const request = createRequest("role", "testuser", {
149
+ PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
150
+ })
151
+
152
+ await handler(request)
153
+
154
+ // Verify that createRole was called without EnableIamAuth
155
+ expect(mockMysqlEngine.createRole).toHaveBeenCalledWith(
156
+ "testuser",
157
+ expect.objectContaining({
158
+ PasswordArn: "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
159
+ EnableIamAuth: false,
160
+ })
161
+ )
162
+ })
163
+ })
164
+ })
package/lambda/handler.ts CHANGED
@@ -15,10 +15,17 @@ import {
15
15
  CloudFormationCustomResourceDeleteEvent,
16
16
  } from "aws-lambda"
17
17
  import { backOff } from "exponential-backoff"
18
- import { EngineFactory } from "./engine.factory"
19
18
  import { EngineConnectionConfig } from "./engine.abstract"
19
+ import { EngineFactory } from "./engine.factory"
20
+ import {
21
+ ResourceProperties,
22
+ CustomResourceResponse,
23
+ EngineDatabaseProperties,
24
+ EngineRoleProperties,
25
+ EngineSchemaProperties,
26
+ EngineSqlProperties,
27
+ } from "./types"
20
28
  import { RdsSqlResource } from "../src/enum"
21
- import { ResourceProperties, CustomResourceResponse } from "./types"
22
29
 
23
30
  const maxAttempts = 20
24
31
 
@@ -29,6 +36,40 @@ const log =
29
36
 
30
37
  const secrets_client = new SecretsManagerClient({})
31
38
 
39
+ // Helper functions to extract engine properties from CloudFormation properties
40
+ // These functions throw if the resource type doesn't match the expected type
41
+ function toDatabaseEngineProps(props: ResourceProperties): EngineDatabaseProperties {
42
+ if (props.Resource !== RdsSqlResource.DATABASE) {
43
+ throw new Error(`Expected DATABASE resource, got ${props.Resource}`)
44
+ }
45
+ return props
46
+ }
47
+
48
+ function toRoleEngineProps(props: ResourceProperties): EngineRoleProperties {
49
+ if (props.Resource !== RdsSqlResource.ROLE) {
50
+ throw new Error(`Expected ROLE resource, got ${props.Resource}`)
51
+ }
52
+ // Convert string 'true'/'false' to boolean for EnableIamAuth
53
+ return {
54
+ ...props,
55
+ EnableIamAuth: props.EnableIamAuth === "true" || props.EnableIamAuth === "1",
56
+ }
57
+ }
58
+
59
+ function toSchemaEngineProps(props: ResourceProperties): EngineSchemaProperties {
60
+ if (props.Resource !== RdsSqlResource.SCHEMA) {
61
+ throw new Error(`Expected SCHEMA resource, got ${props.Resource}`)
62
+ }
63
+ return props
64
+ }
65
+
66
+ function toSqlEngineProps(props: ResourceProperties): EngineSqlProperties {
67
+ if (props.Resource !== RdsSqlResource.SQL) {
68
+ throw new Error(`Expected SQL resource, got ${props.Resource}`)
69
+ }
70
+ return props
71
+ }
72
+
32
73
  export const handler = async (
33
74
  event:
34
75
  | CloudFormationCustomResourceCreateEvent<ResourceProperties>
@@ -82,16 +123,25 @@ export const handler = async (
82
123
  case "Create": {
83
124
  switch (resource) {
84
125
  case RdsSqlResource.DATABASE:
85
- sql = dbEngine.createDatabase(resourceId, event.ResourceProperties)
126
+ sql = dbEngine.createDatabase(
127
+ resourceId,
128
+ toDatabaseEngineProps(event.ResourceProperties)
129
+ )
86
130
  break
87
131
  case RdsSqlResource.ROLE:
88
- sql = await dbEngine.createRole(resourceId, event.ResourceProperties)
132
+ sql = await dbEngine.createRole(
133
+ resourceId,
134
+ toRoleEngineProps(event.ResourceProperties)
135
+ )
89
136
  break
90
137
  case RdsSqlResource.SCHEMA:
91
- sql = dbEngine.createSchema(resourceId, event.ResourceProperties)
138
+ sql = dbEngine.createSchema(
139
+ resourceId,
140
+ toSchemaEngineProps(event.ResourceProperties)
141
+ )
92
142
  break
93
143
  case RdsSqlResource.SQL:
94
- sql = dbEngine.createSql(resourceId, event.ResourceProperties)
144
+ sql = dbEngine.createSql(resourceId, toSqlEngineProps(event.ResourceProperties))
95
145
  break
96
146
  case RdsSqlResource.PARAMETER_PASSWORD:
97
147
  await handleParameterPassword(event.ResourceProperties)
@@ -104,24 +154,35 @@ export const handler = async (
104
154
  .PhysicalResourceId
105
155
  switch (resource) {
106
156
  case RdsSqlResource.DATABASE:
157
+ const dbProps = toDatabaseEngineProps(event.ResourceProperties)
107
158
  sql = dbEngine.updateDatabase(resourceId, oldResourceId, {
108
- ...event.ResourceProperties,
159
+ ...dbProps,
109
160
  MasterOwner: secretValues.username,
110
161
  })
111
162
  break
112
163
  case RdsSqlResource.ROLE:
164
+ const updateEvent =
165
+ event as CloudFormationCustomResourceUpdateEvent<ResourceProperties>
113
166
  sql = await dbEngine.updateRole(
114
167
  resourceId,
115
168
  oldResourceId,
116
- event.ResourceProperties,
117
- (event as CloudFormationCustomResourceUpdateEvent).OldResourceProperties
169
+ toRoleEngineProps(event.ResourceProperties),
170
+ toRoleEngineProps(updateEvent.OldResourceProperties)
118
171
  )
119
172
  break
120
173
  case RdsSqlResource.SCHEMA:
121
- sql = dbEngine.updateSchema(resourceId, oldResourceId, event.ResourceProperties)
174
+ sql = dbEngine.updateSchema(
175
+ resourceId,
176
+ oldResourceId,
177
+ toSchemaEngineProps(event.ResourceProperties)
178
+ )
122
179
  break
123
180
  case RdsSqlResource.SQL:
124
- sql = dbEngine.updateSql(resourceId, oldResourceId, event.ResourceProperties)
181
+ sql = dbEngine.updateSql(
182
+ resourceId,
183
+ oldResourceId,
184
+ toSqlEngineProps(event.ResourceProperties)
185
+ )
125
186
  break
126
187
  case RdsSqlResource.PARAMETER_PASSWORD:
127
188
  await handleParameterPassword(event.ResourceProperties)
@@ -136,13 +197,19 @@ export const handler = async (
136
197
  sql = dbEngine.deleteDatabase(resourceId, secretValues.username)
137
198
  break
138
199
  case RdsSqlResource.ROLE:
139
- sql = dbEngine.deleteRole(resourceId, event.ResourceProperties)
200
+ sql = dbEngine.deleteRole(
201
+ resourceId,
202
+ toRoleEngineProps(event.ResourceProperties)
203
+ )
140
204
  break
141
205
  case RdsSqlResource.SCHEMA:
142
- sql = dbEngine.deleteSchema(resourceId, event.ResourceProperties)
206
+ sql = dbEngine.deleteSchema(
207
+ resourceId,
208
+ toSchemaEngineProps(event.ResourceProperties)
209
+ )
143
210
  break
144
211
  case RdsSqlResource.SQL:
145
- sql = dbEngine.deleteSql(resourceId, event.ResourceProperties)
212
+ sql = dbEngine.deleteSql(resourceId, toSqlEngineProps(event.ResourceProperties))
146
213
  break
147
214
  case RdsSqlResource.PARAMETER_PASSWORD:
148
215
  await deleteParameterPassword(event.ResourceProperties)
package/lambda/types.ts CHANGED
@@ -1,7 +1,30 @@
1
1
  import { CloudFormationCustomResourceResourcePropertiesCommon } from "aws-lambda"
2
2
  import { RdsSqlResource } from "../src/enum"
3
3
 
4
- // Common properties shared by all resources
4
+ // Engine-specific interfaces (business logic properties only)
5
+ export interface EngineDatabaseProperties {
6
+ readonly Owner?: string
7
+ readonly MasterOwner?: string
8
+ }
9
+
10
+ export interface EngineRoleProperties {
11
+ readonly PasswordArn?: string
12
+ readonly DatabaseName?: string
13
+ readonly EnableIamAuth?: boolean
14
+ }
15
+
16
+ export interface EngineSchemaProperties {
17
+ readonly DatabaseName?: string
18
+ readonly RoleName?: string
19
+ }
20
+
21
+ export interface EngineSqlProperties {
22
+ readonly DatabaseName?: string
23
+ readonly Statement?: string
24
+ readonly Rollback?: string
25
+ }
26
+
27
+ // Common CloudFormation properties shared by all resources
5
28
  export interface CommonProperties
6
29
  extends CloudFormationCustomResourceResourcePropertiesCommon {
7
30
  readonly Resource: RdsSqlResource
@@ -9,32 +32,24 @@ export interface CommonProperties
9
32
  readonly SecretArn: string
10
33
  }
11
34
 
12
- // Database specific properties
13
- export interface DatabaseProperties extends CommonProperties {
35
+ // CloudFormation-specific properties (engine properties + CloudFormation metadata)
36
+ export interface DatabaseProperties extends CommonProperties, EngineDatabaseProperties {
14
37
  readonly Resource: RdsSqlResource.DATABASE
15
- readonly Owner?: string
16
38
  }
17
39
 
18
- // Role specific properties
19
40
  export interface RoleProperties extends CommonProperties {
20
41
  readonly Resource: RdsSqlResource.ROLE
21
42
  readonly PasswordArn?: string
22
43
  readonly DatabaseName?: string
44
+ readonly EnableIamAuth?: string // CloudFormation passes boolean as string
23
45
  }
24
46
 
25
- // Schema specific properties
26
- export interface SchemaProperties extends CommonProperties {
47
+ export interface SchemaProperties extends CommonProperties, EngineSchemaProperties {
27
48
  readonly Resource: RdsSqlResource.SCHEMA
28
- readonly DatabaseName?: string
29
- readonly RoleName?: string
30
49
  }
31
50
 
32
- // SQL specific properties
33
- export interface SqlProperties extends CommonProperties {
51
+ export interface SqlProperties extends CommonProperties, EngineSqlProperties {
34
52
  readonly Resource: RdsSqlResource.SQL
35
- readonly DatabaseName?: string
36
- readonly Statement?: string
37
- readonly Rollback?: string
38
53
  }
39
54
 
40
55
  // Parameter password specific properties
package/lambda/util.ts CHANGED
@@ -25,7 +25,7 @@ export interface ResourcePropertiesInput {
25
25
  export const createRequest = (
26
26
  resource: string,
27
27
  resourceId: string,
28
- props?: ResourcePropertiesInput
28
+ props?: Partial<ResourceProperties>
29
29
  ): CloudFormationCustomResourceCreateEvent<ResourceProperties> => {
30
30
  return {
31
31
  ServiceToken: "",
@@ -41,7 +41,7 @@ export const createRequest = (
41
41
  SecretArn:
42
42
  props?.SecretArn || "arn:aws:secretsmanager:us-east-1:123456789:secret:dummy",
43
43
  ...props,
44
- } as unknown as ResourceProperties,
44
+ },
45
45
  RequestType: "Create",
46
46
  }
47
47
  }