cdk-rds-sql 9.1.1 → 9.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
@@ -11289,6 +11289,6 @@
11289
11289
  "symbolId": "src/sql:SqlProps"
11290
11290
  }
11291
11291
  },
11292
- "version": "9.1.1",
11293
- "fingerprint": "2JVR5sakEHON6uTnca6xJrBAe9ezH/4hFD1HtuE7yxg="
11292
+ "version": "9.1.2",
11293
+ "fingerprint": "DOvwHf5DkaTFsOXeSwFamf8PiOcAMMtjOeFmJ2ALNG4="
11294
11294
  }
package/lib/database.js CHANGED
@@ -14,7 +14,7 @@ class ImportedDatabase extends constructs_1.Construct {
14
14
  }
15
15
  }
16
16
  class Database extends aws_cdk_lib_1.CustomResource {
17
- static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Database", version: "9.1.1" };
17
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Database", version: "9.1.2" };
18
18
  /**
19
19
  * Return a Database based upon name only. Use for importing existing databases.
20
20
  */
@@ -27144,6 +27144,13 @@ var AbstractEngine = class {
27144
27144
  this.log = process.env.LOGGER === "true" ? console.debug : (_message, ..._optionalParams) => {
27145
27145
  };
27146
27146
  }
27147
+ /**
27148
+ * Load connection state required before SQL is rendered.
27149
+ * MySQL literal escaping depends on the session SQL mode, so the handler
27150
+ * calls this before create/update/delete SQL is built.
27151
+ */
27152
+ async prepare(_config) {
27153
+ }
27147
27154
  /**
27148
27155
  * Parse password field from secret. Returns void on error or if no password field exists.
27149
27156
  */
@@ -27414,11 +27421,78 @@ var DsqlEngine = class extends AbstractEngine {
27414
27421
 
27415
27422
  // lambda/engine.mysql.ts
27416
27423
  var fs = __toESM(require("fs"));
27424
+
27425
+ // lambda/mysql-quote.ts
27426
+ var NO_BACKSLASH_ESCAPES = "NO_BACKSLASH_ESCAPES";
27427
+ function quoteMysqlIdentifier(identifier) {
27428
+ return `\`${identifier.replaceAll("`", "``")}\``;
27429
+ }
27430
+ function quoteMysqlString(value, sqlMode) {
27431
+ const noBackslashEscapes = sqlMode.split(",").some((mode) => mode.trim() === NO_BACKSLASH_ESCAPES);
27432
+ let quoted = "";
27433
+ for (const char of value) {
27434
+ if (char === "'") {
27435
+ quoted += "''";
27436
+ continue;
27437
+ }
27438
+ if (!noBackslashEscapes) {
27439
+ const escaped = backslashEscape(char);
27440
+ if (escaped !== void 0) {
27441
+ quoted += escaped;
27442
+ continue;
27443
+ }
27444
+ }
27445
+ quoted += char;
27446
+ }
27447
+ return `'${quoted}'`;
27448
+ }
27449
+ function backslashEscape(char) {
27450
+ switch (char) {
27451
+ case "\\":
27452
+ return "\\\\";
27453
+ case "\0":
27454
+ return "\\0";
27455
+ case "\b":
27456
+ return "\\b";
27457
+ case "\n":
27458
+ return "\\n";
27459
+ case "\r":
27460
+ return "\\r";
27461
+ case " ":
27462
+ return "\\t";
27463
+ case "":
27464
+ return "\\Z";
27465
+ default:
27466
+ return void 0;
27467
+ }
27468
+ }
27469
+
27470
+ // lambda/engine.mysql.ts
27417
27471
  var MysqlEngine = class extends AbstractEngine {
27472
+ /**
27473
+ * Session SQL mode used to quote literals. Empty means MySQL's default
27474
+ * (backslash escapes enabled) so unit tests can render SQL without a
27475
+ * connection. Production replaces this from @@SESSION.sql_mode.
27476
+ */
27477
+ sqlMode = "";
27478
+ /** Visible to tests. Production sets this from the live session in prepare(). */
27479
+ setSqlMode(sqlMode) {
27480
+ this.sqlMode = sqlMode;
27481
+ }
27482
+ async prepare(config) {
27483
+ const connection = await this.openConnection(config);
27484
+ try {
27485
+ const [rows] = await connection.query("SELECT @@SESSION.sql_mode AS sqlMode");
27486
+ this.sqlMode = readSqlMode(rows);
27487
+ this.log(`MySQL session sql_mode is ${this.sqlMode === "" ? "(empty)" : this.sqlMode}`);
27488
+ } finally {
27489
+ await connection.end();
27490
+ }
27491
+ }
27418
27492
  createDatabase(resourceId, props) {
27419
- const sql = [`CREATE DATABASE IF NOT EXISTS \`${resourceId}\``];
27493
+ const sql = [`CREATE DATABASE IF NOT EXISTS ${quoteMysqlIdentifier(resourceId)}`];
27420
27494
  if (props.Owner) {
27421
- sql.push(`GRANT ALL PRIVILEGES ON \`${resourceId}\`.* TO '${props.Owner}'@'%'`);
27495
+ sql.push(this.grantDatabase(resourceId, props.Owner));
27422
27496
  sql.push("FLUSH PRIVILEGES");
27423
27497
  }
27424
27498
  return sql;
@@ -27427,24 +27501,22 @@ var MysqlEngine = class extends AbstractEngine {
27427
27501
  throw new Error("Renaming database is not supported in MySQL.");
27428
27502
  }
27429
27503
  deleteDatabase(resourceId, _masterUser) {
27430
- return [`DROP DATABASE IF EXISTS \`${resourceId}\``];
27504
+ return [`DROP DATABASE IF EXISTS ${quoteMysqlIdentifier(resourceId)}`];
27431
27505
  }
27432
27506
  async createRole(resourceId, props) {
27433
27507
  const sql = [];
27434
27508
  if (props.EnableIamAuth) {
27435
- sql.push(
27436
- `CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'`
27437
- );
27509
+ sql.push(this.iamUser(resourceId, true));
27438
27510
  } else {
27439
27511
  if (!props.PasswordArn) throw new Error("No PasswordArn provided");
27440
27512
  const password = await this.getPassword(props.PasswordArn);
27441
27513
  if (!password) throw `Cannot parse password from ${props.PasswordArn}`;
27442
- sql.push(`CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY '${password}'`);
27514
+ sql.push(this.passwordUser(resourceId, password, true));
27443
27515
  }
27444
27516
  if (props.DatabaseName) {
27445
- sql.push(`GRANT ALL PRIVILEGES ON \`${props.DatabaseName}\`.* TO '${resourceId}'@'%'`);
27517
+ sql.push(this.grantDatabase(props.DatabaseName, resourceId));
27446
27518
  }
27447
- appendMaxUserConnections(sql, resourceId, props.MaxUserConnections);
27519
+ appendMaxUserConnections(sql, this.account(resourceId), props.MaxUserConnections);
27448
27520
  sql.push("FLUSH PRIVILEGES");
27449
27521
  return sql;
27450
27522
  }
@@ -27452,42 +27524,40 @@ var MysqlEngine = class extends AbstractEngine {
27452
27524
  const sql = [];
27453
27525
  if (oldResourceId !== resourceId) {
27454
27526
  if (props.EnableIamAuth) {
27455
- sql.push(
27456
- `CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'`
27457
- );
27527
+ sql.push(this.iamUser(resourceId, true));
27458
27528
  } else if (props?.PasswordArn) {
27459
27529
  const password = await this.getPassword(props.PasswordArn);
27460
27530
  if (!password) throw `Cannot parse password from ${props.PasswordArn}`;
27461
- sql.push(`CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY '${password}'`);
27531
+ sql.push(this.passwordUser(resourceId, password, true));
27462
27532
  } else {
27463
- sql.push(`CREATE USER IF NOT EXISTS '${resourceId}'@'%' IDENTIFIED BY UUID()`);
27464
- sql.push(`ALTER USER '${resourceId}'@'%' PASSWORD EXPIRE`);
27533
+ sql.push(`CREATE USER IF NOT EXISTS ${this.account(resourceId)} IDENTIFIED BY UUID()`);
27534
+ sql.push(`ALTER USER ${this.account(resourceId)} PASSWORD EXPIRE`);
27465
27535
  }
27466
- sql.push(`DROP USER IF EXISTS '${oldResourceId}'@'%'`);
27536
+ sql.push(`DROP USER IF EXISTS ${this.account(oldResourceId)}`);
27467
27537
  } else {
27468
27538
  if (props?.EnableIamAuth && !oldProps?.EnableIamAuth) {
27469
- sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`);
27470
- sql.push(`CREATE USER '${resourceId}'@'%' IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'`);
27539
+ sql.push(`DROP USER IF EXISTS ${this.account(resourceId)}`);
27540
+ sql.push(this.iamUser(resourceId, false));
27471
27541
  } else if (!props?.EnableIamAuth && oldProps?.EnableIamAuth) {
27472
- sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`);
27542
+ sql.push(`DROP USER IF EXISTS ${this.account(resourceId)}`);
27473
27543
  if (props?.PasswordArn) {
27474
27544
  const password = await this.getPassword(props.PasswordArn);
27475
27545
  if (!password) throw `Cannot parse password from ${props.PasswordArn}`;
27476
- sql.push(`CREATE USER '${resourceId}'@'%' IDENTIFIED BY '${password}'`);
27546
+ sql.push(this.passwordUser(resourceId, password, false));
27477
27547
  }
27478
27548
  } else if (!props?.EnableIamAuth && props?.PasswordArn) {
27479
27549
  const password = await this.getPassword(props.PasswordArn);
27480
27550
  if (!password) throw `Cannot parse password from ${props.PasswordArn}`;
27481
- sql.push(`ALTER USER '${resourceId}'@'%' IDENTIFIED BY '${password}'`);
27551
+ sql.push(`ALTER USER ${this.account(resourceId)} IDENTIFIED BY ${this.literal(password)}`);
27482
27552
  }
27483
27553
  }
27484
27554
  if (oldProps?.DatabaseName && props?.DatabaseName && oldProps.DatabaseName !== props.DatabaseName) {
27485
- sql.push(`REVOKE ALL PRIVILEGES ON \`${oldProps.DatabaseName}\`.* FROM '${resourceId}'@'%'`);
27555
+ sql.push(this.revokeDatabase(oldProps.DatabaseName, resourceId));
27486
27556
  }
27487
27557
  if (props?.DatabaseName) {
27488
- sql.push(`GRANT ALL PRIVILEGES ON \`${props.DatabaseName}\`.* TO '${resourceId}'@'%'`);
27558
+ sql.push(this.grantDatabase(props.DatabaseName, resourceId));
27489
27559
  }
27490
- appendMaxUserConnections(sql, resourceId, props.MaxUserConnections);
27560
+ appendMaxUserConnections(sql, this.account(resourceId), props.MaxUserConnections);
27491
27561
  if (sql.length > 0) {
27492
27562
  sql.push("FLUSH PRIVILEGES");
27493
27563
  }
@@ -27496,9 +27566,9 @@ var MysqlEngine = class extends AbstractEngine {
27496
27566
  async deleteRole(resourceId, props) {
27497
27567
  const sql = [];
27498
27568
  if (props?.DatabaseName) {
27499
- sql.push(`REVOKE ALL PRIVILEGES ON \`${props.DatabaseName}\`.* FROM '${resourceId}'@'%'`);
27569
+ sql.push(this.revokeDatabase(props.DatabaseName, resourceId));
27500
27570
  }
27501
- sql.push(`DROP USER IF EXISTS '${resourceId}'@'%'`);
27571
+ sql.push(`DROP USER IF EXISTS ${this.account(resourceId)}`);
27502
27572
  sql.push("FLUSH PRIVILEGES");
27503
27573
  return sql;
27504
27574
  }
@@ -27530,6 +27600,48 @@ var MysqlEngine = class extends AbstractEngine {
27530
27600
  throw new Error("IAM grants are only supported with DSQL clusters");
27531
27601
  }
27532
27602
  async executeSQL(sql, config) {
27603
+ const isSslEnabled = process.env.SSL ? JSON.parse(process.env.SSL) : true;
27604
+ const connection = await this.openConnection(config);
27605
+ this.log(
27606
+ `Connecting to MySQL/MariaDB host ${config.host}:${config.port}${isSslEnabled ? " using a secure connection" : ""}, database ${config.database} as ${config.user}`
27607
+ );
27608
+ this.log("Executing SQL", sql);
27609
+ try {
27610
+ if (typeof sql === "string") {
27611
+ return await connection.query(sql);
27612
+ } else if (sql) {
27613
+ return await Promise.all(sql.map((statement) => connection.query(statement)));
27614
+ }
27615
+ } finally {
27616
+ await connection.end();
27617
+ }
27618
+ }
27619
+ literal(value) {
27620
+ return quoteMysqlString(value, this.sqlMode);
27621
+ }
27622
+ /** Account name as a string literal. The host is the fixed VPC wildcard. */
27623
+ account(user) {
27624
+ return `${this.literal(user)}@'%'`;
27625
+ }
27626
+ iamUser(user, ifNotExists) {
27627
+ const exists = ifNotExists ? "IF NOT EXISTS " : "";
27628
+ const account = this.account(user);
27629
+ return `CREATE USER ${exists}${account} IDENTIFIED WITH AWSAuthenticationPlugin as 'RDS'`;
27630
+ }
27631
+ passwordUser(user, password, ifNotExists) {
27632
+ const exists = ifNotExists ? "IF NOT EXISTS " : "";
27633
+ const account = this.account(user);
27634
+ return `CREATE USER ${exists}${account} IDENTIFIED BY ${this.literal(password)}`;
27635
+ }
27636
+ grantDatabase(databaseName, user) {
27637
+ const database = quoteMysqlIdentifier(databaseName);
27638
+ return `GRANT ALL PRIVILEGES ON ${database}.* TO ${this.account(user)}`;
27639
+ }
27640
+ revokeDatabase(databaseName, user) {
27641
+ const database = quoteMysqlIdentifier(databaseName);
27642
+ return `REVOKE ALL PRIVILEGES ON ${database}.* FROM ${this.account(user)}`;
27643
+ }
27644
+ async openConnection(config) {
27533
27645
  const { createConnection } = await Promise.resolve().then(() => __toESM(require_promise()));
27534
27646
  const isSslEnabled = process.env.SSL ? JSON.parse(process.env.SSL) : true;
27535
27647
  const sslOptions = isSslEnabled ? {
@@ -27538,7 +27650,7 @@ var MysqlEngine = class extends AbstractEngine {
27538
27650
  rejectUnauthorized: true
27539
27651
  }
27540
27652
  } : {};
27541
- const connectionConfig = {
27653
+ return createConnection({
27542
27654
  host: config.host,
27543
27655
  port: config.port,
27544
27656
  user: config.user,
@@ -27547,31 +27659,30 @@ var MysqlEngine = class extends AbstractEngine {
27547
27659
  connectTimeout: 3e4,
27548
27660
  multipleStatements: true,
27549
27661
  ...sslOptions
27550
- };
27551
- this.log(
27552
- `Connecting to MySQL/MariaDB host ${connectionConfig.host}:${connectionConfig.port}${isSslEnabled ? " using a secure connection" : ""}, database ${connectionConfig.database} as ${connectionConfig.user}`
27553
- );
27554
- this.log("Executing SQL", sql);
27555
- const connection = await createConnection(connectionConfig);
27556
- try {
27557
- if (typeof sql === "string") {
27558
- return await connection.query(sql);
27559
- } else if (sql) {
27560
- return await Promise.all(sql.map((statement) => connection.query(statement)));
27561
- }
27562
- } finally {
27563
- await connection.end();
27564
- }
27662
+ });
27565
27663
  }
27566
27664
  };
27567
- function appendMaxUserConnections(sql, resourceId, maxUserConnections) {
27665
+ function appendMaxUserConnections(sql, account, maxUserConnections) {
27568
27666
  if (maxUserConnections === void 0) {
27569
27667
  return;
27570
27668
  }
27571
27669
  if (!Number.isInteger(maxUserConnections) || maxUserConnections < 0) {
27572
27670
  throw new Error(`MaxUserConnections must be a non-negative integer, got ${maxUserConnections}`);
27573
27671
  }
27574
- sql.push(`ALTER USER '${resourceId}'@'%' WITH MAX_USER_CONNECTIONS ${maxUserConnections}`);
27672
+ sql.push(`ALTER USER ${account} WITH MAX_USER_CONNECTIONS ${maxUserConnections}`);
27673
+ }
27674
+ function readSqlMode(rows) {
27675
+ if (!Array.isArray(rows) || rows.length !== 1) {
27676
+ throw new Error("MySQL did not return a session sql_mode");
27677
+ }
27678
+ const row = rows[0];
27679
+ if (!isSqlModeRow(row)) {
27680
+ throw new Error("MySQL did not return a session sql_mode");
27681
+ }
27682
+ return row.sqlMode;
27683
+ }
27684
+ function isSqlModeRow(row) {
27685
+ return typeof row === "object" && row !== null && "sqlMode" in row && typeof row.sqlMode === "string";
27575
27686
  }
27576
27687
 
27577
27688
  // lambda/engine.postgresql.ts
@@ -27942,6 +28053,30 @@ var handler = async (event) => {
27942
28053
  engine = secretValues.engine || "postgresql";
27943
28054
  }
27944
28055
  const dbEngine = EngineFactory.createEngine(engine);
28056
+ let database;
28057
+ if (engine === "dsql") {
28058
+ database = "postgres";
28059
+ } else if (resource === "role" /* ROLE */) {
28060
+ database = secretValues.dbname;
28061
+ } else {
28062
+ database = databaseName ?? secretValues.dbname;
28063
+ }
28064
+ const config = {
28065
+ host: secretValues.host,
28066
+ port: secretValues.port,
28067
+ user: secretValues.username,
28068
+ password: secretValues.password || "",
28069
+ // DSQL will generate token in engine
28070
+ database
28071
+ };
28072
+ if (resource !== "parameter_password" /* PARAMETER_PASSWORD */) {
28073
+ try {
28074
+ await dbEngine.prepare(config);
28075
+ } catch (error) {
28076
+ log("Error preparing database connection: %o", error);
28077
+ throw error;
28078
+ }
28079
+ }
27945
28080
  let sql;
27946
28081
  switch (requestType) {
27947
28082
  case "Create": {
@@ -28050,22 +28185,6 @@ var handler = async (event) => {
28050
28185
  }
28051
28186
  }
28052
28187
  if (sql) {
28053
- let database;
28054
- if (engine === "dsql") {
28055
- database = "postgres";
28056
- } else if (resource === "role" /* ROLE */) {
28057
- database = secretValues.dbname;
28058
- } else {
28059
- database = databaseName ?? secretValues.dbname;
28060
- }
28061
- const config = {
28062
- host: secretValues.host,
28063
- port: secretValues.port,
28064
- user: secretValues.username,
28065
- password: secretValues.password || "",
28066
- // DSQL will generate token in engine
28067
- database
28068
- };
28069
28188
  try {
28070
28189
  await (0, import_exponential_backoff.backOff)(
28071
28190
  async () => {
package/lib/iam-grant.js CHANGED
@@ -6,7 +6,7 @@ const aws_cdk_lib_1 = require("aws-cdk-lib");
6
6
  const enum_1 = require("./enum");
7
7
  const provider_1 = require("./provider");
8
8
  class IamGrant extends aws_cdk_lib_1.CustomResource {
9
- static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.IamGrant", version: "9.1.1" };
9
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.IamGrant", version: "9.1.2" };
10
10
  constructor(scope, id, props) {
11
11
  // IAM grants are only supported on DSQL
12
12
  if (props.provider.engine !== provider_1.DatabaseEngine.DSQL) {
package/lib/provider.js CHANGED
@@ -60,7 +60,7 @@ var DatabaseEngine;
60
60
  DatabaseEngine["DSQL"] = "dsql";
61
61
  })(DatabaseEngine || (exports.DatabaseEngine = DatabaseEngine = {}));
62
62
  class Provider extends constructs_1.Construct {
63
- static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Provider", version: "9.1.1" };
63
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Provider", version: "9.1.2" };
64
64
  /**
65
65
  * Import an existing provider Lambda function
66
66
  */
package/lib/role.js CHANGED
@@ -83,7 +83,7 @@ class Parameters extends constructs_1.Construct {
83
83
  }
84
84
  }
85
85
  class Role extends constructs_1.Construct {
86
- static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Role", version: "9.1.1" };
86
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Role", version: "9.1.2" };
87
87
  /**
88
88
  * The role name.
89
89
  */
package/lib/schema.js CHANGED
@@ -5,7 +5,7 @@ const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti");
5
5
  const aws_cdk_lib_1 = require("aws-cdk-lib");
6
6
  const enum_1 = require("./enum");
7
7
  class Schema extends aws_cdk_lib_1.CustomResource {
8
- static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Schema", version: "9.1.1" };
8
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Schema", version: "9.1.2" };
9
9
  schemaName;
10
10
  constructor(scope, id, props) {
11
11
  super(scope, id, {
package/lib/sql.js CHANGED
@@ -5,7 +5,7 @@ const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti");
5
5
  const aws_cdk_lib_1 = require("aws-cdk-lib");
6
6
  const enum_1 = require("./enum");
7
7
  class Sql extends aws_cdk_lib_1.CustomResource {
8
- static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Sql", version: "9.1.1" };
8
+ static [JSII_RTTI_SYMBOL_1] = { fqn: "cdk-rds-sql.Sql", version: "9.1.2" };
9
9
  constructor(scope, id, props) {
10
10
  super(scope, id, {
11
11
  serviceToken: props.provider.serviceToken,
package/package.json CHANGED
@@ -75,7 +75,7 @@
75
75
  "publishConfig": {
76
76
  "access": "public"
77
77
  },
78
- "version": "9.1.1",
78
+ "version": "9.1.2",
79
79
  "jest": {
80
80
  "coverageProvider": "v8",
81
81
  "testMatch": [