serverless-simple-middleware 0.0.69 → 0.0.70

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.
@@ -0,0 +1,6 @@
1
+ export declare class OncePromise<T> {
2
+ private promise?;
3
+ private factory?;
4
+ constructor(factory?: () => Promise<T>);
5
+ run(factory?: () => Promise<T>): Promise<T>;
6
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OncePromise = void 0;
4
+ class OncePromise {
5
+ promise;
6
+ factory;
7
+ constructor(factory) {
8
+ this.factory = factory;
9
+ }
10
+ async run(factory) {
11
+ if (!this.promise) {
12
+ const f = factory || this.factory;
13
+ if (!f) {
14
+ throw new Error('OncePromise requires a factory');
15
+ }
16
+ this.promise = f();
17
+ try {
18
+ return await this.promise;
19
+ }
20
+ catch (err) {
21
+ this.promise = undefined;
22
+ throw err;
23
+ }
24
+ }
25
+ return this.promise;
26
+ }
27
+ }
28
+ exports.OncePromise = OncePromise;
@@ -1,10 +1,14 @@
1
1
  import { MySQLPluginOptions } from '../mysql';
2
2
  export declare class ConnectionProxy {
3
- private pluginConfig;
3
+ private readonly options;
4
4
  private connection?;
5
+ private connectionConfig;
6
+ private secretsCache;
7
+ private configInitOnce;
8
+ private connectionInitOnce;
5
9
  private initialized;
6
10
  private dbName?;
7
- constructor(config: MySQLPluginOptions);
11
+ constructor(options: MySQLPluginOptions);
8
12
  query: <T>(sql: string, params?: any[]) => Promise<T | undefined>;
9
13
  fetch: <T>(sql: string, params?: any[]) => Promise<T[]>;
10
14
  fetchOne: <T>(sql: string, params?: any[], defaultValue?: T) => Promise<T>;
@@ -19,6 +23,7 @@ export declare class ConnectionProxy {
19
23
  destroyConnection: () => void;
20
24
  onPluginCreated: () => Promise<void>;
21
25
  private prepareConnection;
26
+ private ensureConnectionConfig;
22
27
  private changeDatabase;
23
28
  private tryToInitializeSchema;
24
29
  }
@@ -1,28 +1,35 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectionProxy = void 0;
4
- const mysql_1 = require("mysql");
4
+ const mysql2_1 = require("mysql2");
5
+ const oncePromise_1 = require("../../internal/oncePromise");
5
6
  const utils_1 = require("../../utils");
7
+ const secretsManager_1 = require("../../utils/secretsManager");
6
8
  const logger = (0, utils_1.getLogger)(__filename);
7
9
  class ConnectionProxy {
8
- pluginConfig;
10
+ options;
9
11
  connection;
12
+ connectionConfig;
13
+ secretsCache;
14
+ configInitOnce = new oncePromise_1.OncePromise();
15
+ connectionInitOnce = new oncePromise_1.OncePromise();
10
16
  initialized;
11
17
  dbName;
12
- constructor(config) {
13
- this.pluginConfig = config;
14
- if (config.schema && config.schema.database) {
15
- this.dbName = config.config.database;
16
- config.config.database = undefined;
18
+ constructor(options) {
19
+ this.options = options;
20
+ if (options.schema && options.schema.database) {
21
+ this.dbName = options.config.database;
22
+ options.config.database = undefined;
17
23
  }
24
+ this.secretsCache = secretsManager_1.SecretsManagerCache.getInstance();
18
25
  }
19
26
  query = (sql, params) => new Promise(async (resolve, reject) => {
20
- const connection = this.prepareConnection();
27
+ const connection = await this.prepareConnection();
21
28
  await this.tryToInitializeSchema(false);
22
29
  if (process.env.NODE_ENV !== 'test') {
23
30
  logger.silly(`Execute query[${sql}] with params[${params}]`);
24
31
  }
25
- connection.query(sql, params, (err, result) => {
32
+ connection.query(sql, params, (err, result, _fields) => {
26
33
  if (err) {
27
34
  logger.error(`error occurred in database query=${sql}, error=${err}`);
28
35
  reject(err);
@@ -44,7 +51,7 @@ class ConnectionProxy {
44
51
  return res[0];
45
52
  });
46
53
  beginTransaction = () => new Promise(async (resolve, reject) => {
47
- const connection = this.prepareConnection();
54
+ const connection = await this.prepareConnection();
48
55
  await this.tryToInitializeSchema(false);
49
56
  connection.beginTransaction((err) => {
50
57
  if (err) {
@@ -55,7 +62,7 @@ class ConnectionProxy {
55
62
  });
56
63
  });
57
64
  commit = () => new Promise(async (resolve, reject) => {
58
- const connection = this.prepareConnection();
65
+ const connection = await this.prepareConnection();
59
66
  await this.tryToInitializeSchema(false);
60
67
  connection.commit((err) => {
61
68
  if (err) {
@@ -66,7 +73,7 @@ class ConnectionProxy {
66
73
  });
67
74
  });
68
75
  rollback = () => new Promise(async (resolve, reject) => {
69
- const connection = this.prepareConnection();
76
+ const connection = await this.prepareConnection();
70
77
  await this.tryToInitializeSchema(false);
71
78
  connection.rollback((err) => {
72
79
  if (err) {
@@ -95,19 +102,43 @@ class ConnectionProxy {
95
102
  }
96
103
  };
97
104
  onPluginCreated = async () => this.tryToInitializeSchema(true);
98
- prepareConnection = () => {
105
+ prepareConnection = async () => {
99
106
  if (this.connection) {
100
107
  return this.connection;
101
108
  }
102
- this.connection = (0, mysql_1.createConnection)(this.pluginConfig.config);
103
- this.connection.connect();
104
- return this.connection;
109
+ return await this.connectionInitOnce.run(async () => {
110
+ await this.ensureConnectionConfig();
111
+ const conn = (0, mysql2_1.createConnection)(this.connectionConfig);
112
+ conn.connect();
113
+ this.connection = conn;
114
+ return this.connection;
115
+ });
116
+ };
117
+ ensureConnectionConfig = async () => {
118
+ if (this.connectionConfig) {
119
+ return;
120
+ }
121
+ await this.configInitOnce.run(async () => {
122
+ const baseConfig = this.options.config;
123
+ if (!this.options.secretId) {
124
+ this.connectionConfig = baseConfig;
125
+ return;
126
+ }
127
+ const credentials = await this.secretsCache.getDatabaseCredentials(this.options.secretId);
128
+ this.connectionConfig = {
129
+ ...baseConfig,
130
+ user: credentials.username,
131
+ password: credentials.password,
132
+ };
133
+ });
105
134
  };
106
- changeDatabase = (dbName) => new Promise((resolve, reject) => this.prepareConnection().changeUser({
135
+ changeDatabase = async (dbName) => new Promise((resolve, reject) => this.prepareConnection()
136
+ .then((connection) => connection.changeUser({
107
137
  database: dbName,
108
- }, (err) => (err ? reject(err) : resolve(undefined))));
138
+ }, (err) => (err ? reject(err) : resolve(undefined))))
139
+ .catch(reject));
109
140
  tryToInitializeSchema = async (initial) => {
110
- const { eager = false, ignoreError = false, database = '', tables = {}, } = this.pluginConfig.schema || {};
141
+ const { eager = false, ignoreError = false, database = '', tables = {}, } = this.options.schema || {};
111
142
  if (initial && !eager) {
112
143
  return;
113
144
  }
@@ -1,8 +1,8 @@
1
1
  import { Kysely } from 'kysely';
2
- import { type ConnectionOptions } from 'mysql2';
2
+ import { MySQLPluginOptions } from '../mysql';
3
3
  export declare class SQLClient<T = unknown> extends Kysely<T> {
4
4
  private pool;
5
- constructor(config: ConnectionOptions);
5
+ constructor(config: MySQLPluginOptions);
6
6
  clearConnection: () => Promise<void>;
7
7
  /**
8
8
  * Destroy the connection socket immediately. No further events or callbacks will be triggered.
@@ -3,27 +3,61 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sql = exports.expressionBuilder = exports.SQLClient = void 0;
4
4
  const kysely_1 = require("kysely");
5
5
  const mysql2_1 = require("mysql2");
6
+ const oncePromise_1 = require("../../internal/oncePromise");
7
+ const secretsManager_1 = require("../../utils/secretsManager");
6
8
  class LazyConnectionPool {
7
- config;
9
+ options;
8
10
  connection = null;
9
- constructor(config) {
10
- this.config = config;
11
+ connectionConfig;
12
+ secretsCache;
13
+ configInitOnce = new oncePromise_1.OncePromise();
14
+ connectionInitOnce = new oncePromise_1.OncePromise();
15
+ constructor(options) {
16
+ this.options = options;
17
+ this.secretsCache = secretsManager_1.SecretsManagerCache.getInstance();
11
18
  }
12
- getConnection = (callback) => {
13
- if (this.connection) {
14
- callback(null, this.connection);
19
+ ensureConnectionConfig = async () => {
20
+ if (this.connectionConfig) {
15
21
  return;
16
22
  }
17
- const conn = (0, mysql2_1.createConnection)(this.config);
18
- conn.connect((err) => {
19
- if (err) {
20
- callback(err, {});
23
+ await this.configInitOnce.run(async () => {
24
+ const baseConfig = this.options.config;
25
+ if (!this.options.secretId) {
26
+ this.connectionConfig = baseConfig;
21
27
  return;
22
28
  }
23
- this.connection = this._addRelease(conn);
24
- callback(null, this.connection);
29
+ const credentials = await this.secretsCache.getDatabaseCredentials(this.options.secretId);
30
+ this.connectionConfig = {
31
+ ...baseConfig,
32
+ user: credentials.username,
33
+ password: credentials.password,
34
+ };
25
35
  });
26
36
  };
37
+ getConnection = (callback) => {
38
+ if (this.connection) {
39
+ callback(null, this.connection);
40
+ return;
41
+ }
42
+ this.connectionInitOnce
43
+ .run(async () => {
44
+ await this.ensureConnectionConfig();
45
+ const conn = (0, mysql2_1.createConnection)(this.connectionConfig);
46
+ return await new Promise((resolve, reject) => {
47
+ conn.connect((err) => {
48
+ if (err) {
49
+ reject(err);
50
+ return;
51
+ }
52
+ const wrapped = this._addRelease(conn);
53
+ this.connection = wrapped;
54
+ resolve(wrapped);
55
+ });
56
+ });
57
+ })
58
+ .then((conn) => callback(null, conn))
59
+ .catch((err) => callback(err, {}));
60
+ };
27
61
  end = (callback) => {
28
62
  if (this.connection) {
29
63
  this.connection.end((err) => {
@@ -49,7 +83,7 @@ class SQLClient extends kysely_1.Kysely {
49
83
  pool;
50
84
  constructor(config) {
51
85
  const pool = new LazyConnectionPool(config);
52
- super({
86
+ const kyselyConfig = {
53
87
  dialect: new kysely_1.MysqlDialect({
54
88
  pool,
55
89
  }),
@@ -58,7 +92,8 @@ class SQLClient extends kysely_1.Kysely {
58
92
  strategy: kysely_1.replaceWithNoncontingentExpression,
59
93
  }),
60
94
  ],
61
- });
95
+ };
96
+ super(kyselyConfig);
62
97
  this.pool = pool;
63
98
  }
64
99
  clearConnection = () => new Promise((resolve) => {
@@ -1,10 +1,17 @@
1
- import type { ConnectionConfig } from 'mysql';
2
- import type { PoolOptions } from 'mysql2';
1
+ import type { ConnectionOptions, PoolOptions } from 'mysql2';
3
2
  import { HandlerAuxBase, HandlerPluginBase } from './base';
4
3
  import { ConnectionProxy } from './database/connectionProxy';
5
4
  import { SQLClient } from './database/sqlClient';
5
+ export interface DatabaseCredentials {
6
+ username: string;
7
+ password: string;
8
+ }
6
9
  export interface MySQLPluginOptions {
7
- config: ConnectionConfig & PoolOptions;
10
+ config: ConnectionOptions & PoolOptions;
11
+ /**
12
+ * AWS Secrets Manager secret ID containing {@link DatabaseCredentials}
13
+ */
14
+ secretId?: string;
8
15
  schema?: {
9
16
  eager?: boolean;
10
17
  ignoreError?: boolean;
@@ -10,7 +10,7 @@ class MySQLPlugin extends base_1.HandlerPluginBase {
10
10
  constructor(options) {
11
11
  super();
12
12
  this.proxy = new connectionProxy_1.ConnectionProxy(options);
13
- this.sqlClient = new sqlClient_1.SQLClient(options.config);
13
+ this.sqlClient = new sqlClient_1.SQLClient(options);
14
14
  }
15
15
  create = async () => {
16
16
  await this.proxy.onPluginCreated();
@@ -0,0 +1,11 @@
1
+ import type { DatabaseCredentials } from '../middleware/mysql';
2
+ export declare class SecretsManagerCache {
3
+ private static instance;
4
+ private client;
5
+ private cache;
6
+ private constructor();
7
+ static getInstance(): SecretsManagerCache;
8
+ private getClient;
9
+ getSecret<T = any>(secretId: string): Promise<T>;
10
+ getDatabaseCredentials(secretId: string): Promise<DatabaseCredentials>;
11
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SecretsManagerCache = void 0;
4
+ const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
5
+ const logger_1 = require("./logger");
6
+ const misc_1 = require("./misc");
7
+ const logger = (0, logger_1.getLogger)(__filename);
8
+ class SecretsManagerCache {
9
+ static instance;
10
+ client;
11
+ cache = new Map();
12
+ constructor() { }
13
+ static getInstance() {
14
+ if (!SecretsManagerCache.instance) {
15
+ SecretsManagerCache.instance = new SecretsManagerCache();
16
+ }
17
+ return SecretsManagerCache.instance;
18
+ }
19
+ getClient() {
20
+ if (!this.client) {
21
+ this.client = new client_secrets_manager_1.SecretsManagerClient({});
22
+ logger.debug('SecretsManager client initialized');
23
+ }
24
+ return this.client;
25
+ }
26
+ async getSecret(secretId) {
27
+ if (this.cache.has(secretId)) {
28
+ logger.debug(`Secret ${secretId} found in cache`);
29
+ return this.cache.get(secretId);
30
+ }
31
+ try {
32
+ const command = new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretId });
33
+ const response = await this.getClient().send(command);
34
+ if (!response.SecretString) {
35
+ throw new Error(`Secret ${secretId} has no SecretString value`);
36
+ }
37
+ const secretValue = JSON.parse(response.SecretString);
38
+ this.cache.set(secretId, secretValue);
39
+ return secretValue;
40
+ }
41
+ catch (error) {
42
+ logger.error(`Failed to fetch secret ${secretId}: ${(0, misc_1.stringifyError)(error)}`);
43
+ throw error;
44
+ }
45
+ }
46
+ async getDatabaseCredentials(secretId) {
47
+ const secret = await this.getSecret(secretId);
48
+ if (!secret.username || !secret.password) {
49
+ throw new Error(`Secret ${secretId} does not contain required database credentials (username, password)`);
50
+ }
51
+ return secret;
52
+ }
53
+ }
54
+ exports.SecretsManagerCache = SecretsManagerCache;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "serverless-simple-middleware",
3
3
  "description": "Simple middleware to translate the interface of lambda's handler to request => response",
4
- "version": "0.0.69",
4
+ "version": "0.0.70",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "author": "VoyagerX",
@@ -32,19 +32,17 @@
32
32
  "dependencies": {
33
33
  "@aws-sdk/client-dynamodb": "^3.828.0",
34
34
  "@aws-sdk/client-s3": "^3.828.0",
35
+ "@aws-sdk/client-secrets-manager": "3.828.0",
35
36
  "@aws-sdk/client-sqs": "^3.828.0",
36
37
  "@aws-sdk/cloudfront-signer": "^3.821.0",
37
38
  "@aws-sdk/lib-dynamodb": "^3.828.0",
38
39
  "@aws-sdk/lib-storage": "^3.828.0",
39
40
  "@aws-sdk/s3-request-presigner": "^3.828.0",
40
41
  "@types/aws-lambda": "8",
41
- "@types/mysql": "^2.15.5",
42
42
  "cross-fetch": "^2.2.2",
43
43
  "kysely": "^0.28.2",
44
- "mysql": "^2.16.0",
45
44
  "mysql2": "^3.14.1",
46
45
  "nanoid": "4.0.2",
47
- "p-limit": "^2.0.0",
48
46
  "simple-staging": "^0.0.12",
49
47
  "ts-enum-util": "^3.1.0",
50
48
  "uuid": "^3.3.2"
@@ -0,0 +1,25 @@
1
+ export class OncePromise<T> {
2
+ private promise?: Promise<T>;
3
+ private factory?: () => Promise<T>;
4
+
5
+ constructor(factory?: () => Promise<T>) {
6
+ this.factory = factory;
7
+ }
8
+
9
+ public async run(factory?: () => Promise<T>): Promise<T> {
10
+ if (!this.promise) {
11
+ const f = factory || this.factory;
12
+ if (!f) {
13
+ throw new Error('OncePromise requires a factory');
14
+ }
15
+ this.promise = f();
16
+ try {
17
+ return await this.promise;
18
+ } catch (err) {
19
+ this.promise = undefined;
20
+ throw err;
21
+ }
22
+ }
23
+ return this.promise;
24
+ }
25
+ }
@@ -1,43 +1,61 @@
1
- import { createConnection, type Connection, type MysqlError } from 'mysql';
1
+ import {
2
+ createConnection,
3
+ type Connection,
4
+ type ConnectionOptions,
5
+ type FieldPacket,
6
+ type QueryError,
7
+ type QueryResult,
8
+ } from 'mysql2';
9
+ import { OncePromise } from '../../internal/oncePromise';
2
10
  import { getLogger } from '../../utils';
11
+ import { SecretsManagerCache } from '../../utils/secretsManager';
3
12
  import { MySQLPluginOptions } from '../mysql';
4
13
 
5
14
  const logger = getLogger(__filename);
6
15
 
7
16
  export class ConnectionProxy {
8
- private pluginConfig: MySQLPluginOptions;
9
17
  private connection?: Connection;
18
+ private connectionConfig: ConnectionOptions;
19
+ private secretsCache: SecretsManagerCache;
20
+ private configInitOnce = new OncePromise<void>();
21
+ private connectionInitOnce = new OncePromise<Connection>();
10
22
 
11
23
  private initialized: boolean;
12
24
  private dbName?: string;
13
25
 
14
- public constructor(config: MySQLPluginOptions) {
15
- this.pluginConfig = config;
16
- if (config.schema && config.schema.database) {
17
- this.dbName = config.config.database;
18
- config.config.database = undefined;
26
+ public constructor(private readonly options: MySQLPluginOptions) {
27
+ if (options.schema && options.schema.database) {
28
+ this.dbName = options.config.database;
29
+ options.config.database = undefined;
19
30
  }
31
+ this.secretsCache = SecretsManagerCache.getInstance();
20
32
  }
21
33
 
22
34
  public query = <T>(sql: string, params?: any[]) =>
23
35
  new Promise<T | undefined>(async (resolve, reject) => {
24
- const connection = this.prepareConnection();
36
+ const connection = await this.prepareConnection();
25
37
  await this.tryToInitializeSchema(false);
26
38
 
27
39
  if (process.env.NODE_ENV !== 'test') {
28
40
  logger.silly(`Execute query[${sql}] with params[${params}]`);
29
41
  }
30
- connection.query(sql, params, (err: MysqlError, result?: T) => {
31
- if (err) {
32
- logger.error(`error occurred in database query=${sql}, error=${err}`);
33
- reject(err);
34
- } else {
35
- resolve(result);
36
- if (process.env.NODE_ENV !== 'test') {
37
- logger.silly(`DB result is ${JSON.stringify(result)}`);
42
+ connection.query(
43
+ sql,
44
+ params,
45
+ (err: QueryError, result: QueryResult, _fields?: FieldPacket[]) => {
46
+ if (err) {
47
+ logger.error(
48
+ `error occurred in database query=${sql}, error=${err}`,
49
+ );
50
+ reject(err);
51
+ } else {
52
+ resolve(result as T);
53
+ if (process.env.NODE_ENV !== 'test') {
54
+ logger.silly(`DB result is ${JSON.stringify(result)}`);
55
+ }
38
56
  }
39
- }
40
- });
57
+ },
58
+ );
41
59
  });
42
60
 
43
61
  public fetch = <T>(sql: string, params?: any[]) =>
@@ -54,10 +72,10 @@ export class ConnectionProxy {
54
72
 
55
73
  public beginTransaction = () =>
56
74
  new Promise<void>(async (resolve, reject) => {
57
- const connection = this.prepareConnection();
75
+ const connection = await this.prepareConnection();
58
76
  await this.tryToInitializeSchema(false);
59
77
 
60
- connection.beginTransaction((err: MysqlError) => {
78
+ connection.beginTransaction((err: QueryError) => {
61
79
  if (err) {
62
80
  reject(err);
63
81
  return;
@@ -68,10 +86,10 @@ export class ConnectionProxy {
68
86
 
69
87
  public commit = () =>
70
88
  new Promise<void>(async (resolve, reject) => {
71
- const connection = this.prepareConnection();
89
+ const connection = await this.prepareConnection();
72
90
  await this.tryToInitializeSchema(false);
73
91
 
74
- connection.commit((err: MysqlError) => {
92
+ connection.commit((err: QueryError) => {
75
93
  if (err) {
76
94
  reject(err);
77
95
  return;
@@ -82,10 +100,10 @@ export class ConnectionProxy {
82
100
 
83
101
  public rollback = () =>
84
102
  new Promise<void>(async (resolve, reject) => {
85
- const connection = this.prepareConnection();
103
+ const connection = await this.prepareConnection();
86
104
  await this.tryToInitializeSchema(false);
87
105
 
88
- connection.rollback((err: MysqlError) => {
106
+ connection.rollback((err: QueryError) => {
89
107
  if (err) {
90
108
  reject(err);
91
109
  return;
@@ -116,23 +134,54 @@ export class ConnectionProxy {
116
134
 
117
135
  public onPluginCreated = async () => this.tryToInitializeSchema(true);
118
136
 
119
- private prepareConnection = () => {
137
+ private prepareConnection = async (): Promise<Connection> => {
120
138
  if (this.connection) {
121
139
  return this.connection;
122
140
  }
123
- this.connection = createConnection(this.pluginConfig.config);
124
- this.connection.connect();
125
- return this.connection;
141
+
142
+ return await this.connectionInitOnce.run(async () => {
143
+ await this.ensureConnectionConfig();
144
+ const conn = createConnection(this.connectionConfig);
145
+ conn.connect();
146
+ this.connection = conn;
147
+ return this.connection;
148
+ });
149
+ };
150
+
151
+ private ensureConnectionConfig = async (): Promise<void> => {
152
+ if (this.connectionConfig) {
153
+ return;
154
+ }
155
+
156
+ await this.configInitOnce.run(async () => {
157
+ const baseConfig = this.options.config;
158
+ if (!this.options.secretId) {
159
+ this.connectionConfig = baseConfig;
160
+ return;
161
+ }
162
+ const credentials = await this.secretsCache.getDatabaseCredentials(
163
+ this.options.secretId,
164
+ );
165
+ this.connectionConfig = {
166
+ ...baseConfig,
167
+ user: credentials.username,
168
+ password: credentials.password,
169
+ };
170
+ });
126
171
  };
127
172
 
128
- private changeDatabase = (dbName: string) =>
173
+ private changeDatabase = async (dbName: string) =>
129
174
  new Promise<void>((resolve, reject) =>
130
- this.prepareConnection().changeUser(
131
- {
132
- database: dbName,
133
- },
134
- (err) => (err ? reject(err) : resolve(undefined)),
135
- ),
175
+ this.prepareConnection()
176
+ .then((connection) =>
177
+ connection.changeUser(
178
+ {
179
+ database: dbName,
180
+ },
181
+ (err) => (err ? reject(err) : resolve(undefined)),
182
+ ),
183
+ )
184
+ .catch(reject),
136
185
  );
137
186
 
138
187
  private tryToInitializeSchema = async (initial: boolean) => {
@@ -141,7 +190,7 @@ export class ConnectionProxy {
141
190
  ignoreError = false,
142
191
  database = '',
143
192
  tables = {},
144
- } = this.pluginConfig.schema || {};
193
+ } = this.options.schema || {};
145
194
  if (initial && !eager) {
146
195
  return;
147
196
  }
@@ -1,16 +1,20 @@
1
1
  import {
2
2
  HandleEmptyInListsPlugin,
3
3
  Kysely,
4
+ type KyselyConfig,
4
5
  MysqlDialect,
5
- MysqlPool,
6
+ type MysqlPool,
6
7
  replaceWithNoncontingentExpression,
7
8
  } from 'kysely';
8
9
  import {
9
- createConnection,
10
10
  type Connection,
11
11
  type ConnectionOptions,
12
+ createConnection,
12
13
  type QueryError,
13
14
  } from 'mysql2';
15
+ import { OncePromise } from '../../internal/oncePromise';
16
+ import { SecretsManagerCache } from '../../utils/secretsManager';
17
+ import { MySQLPluginOptions } from '../mysql';
14
18
 
15
19
  interface LazyMysqlPoolConnection extends Connection {
16
20
  release: () => void;
@@ -18,8 +22,37 @@ interface LazyMysqlPoolConnection extends Connection {
18
22
 
19
23
  class LazyConnectionPool implements MysqlPool {
20
24
  private connection: LazyMysqlPoolConnection | null = null;
25
+ private connectionConfig: ConnectionOptions;
26
+ private secretsCache: SecretsManagerCache;
27
+ private configInitOnce = new OncePromise<void>();
28
+ private connectionInitOnce = new OncePromise<LazyMysqlPoolConnection>();
29
+
30
+ constructor(private readonly options: MySQLPluginOptions) {
31
+ this.secretsCache = SecretsManagerCache.getInstance();
32
+ }
33
+
34
+ private ensureConnectionConfig = async (): Promise<void> => {
35
+ if (this.connectionConfig) {
36
+ return;
37
+ }
38
+
39
+ await this.configInitOnce.run(async () => {
40
+ const baseConfig = this.options.config;
41
+ if (!this.options.secretId) {
42
+ this.connectionConfig = baseConfig;
43
+ return;
44
+ }
45
+ const credentials = await this.secretsCache.getDatabaseCredentials(
46
+ this.options.secretId,
47
+ );
21
48
 
22
- constructor(private config: ConnectionOptions) {}
49
+ this.connectionConfig = {
50
+ ...baseConfig,
51
+ user: credentials.username,
52
+ password: credentials.password,
53
+ };
54
+ });
55
+ };
23
56
 
24
57
  public getConnection = (
25
58
  callback: (error: unknown, connection: LazyMysqlPoolConnection) => void,
@@ -28,15 +61,25 @@ class LazyConnectionPool implements MysqlPool {
28
61
  callback(null, this.connection);
29
62
  return;
30
63
  }
31
- const conn = createConnection(this.config);
32
- conn.connect((err: QueryError) => {
33
- if (err) {
34
- callback(err, {} as LazyMysqlPoolConnection);
35
- return;
36
- }
37
- this.connection = this._addRelease(conn);
38
- callback(null, this.connection);
39
- });
64
+
65
+ this.connectionInitOnce
66
+ .run(async () => {
67
+ await this.ensureConnectionConfig();
68
+ const conn = createConnection(this.connectionConfig);
69
+ return await new Promise<LazyMysqlPoolConnection>((resolve, reject) => {
70
+ conn.connect((err: QueryError) => {
71
+ if (err) {
72
+ reject(err);
73
+ return;
74
+ }
75
+ const wrapped = this._addRelease(conn);
76
+ this.connection = wrapped;
77
+ resolve(wrapped);
78
+ });
79
+ });
80
+ })
81
+ .then((conn) => callback(null, conn))
82
+ .catch((err) => callback(err, {} as LazyMysqlPoolConnection));
40
83
  };
41
84
 
42
85
  public end = (callback: (error: unknown) => void): void => {
@@ -66,9 +109,9 @@ class LazyConnectionPool implements MysqlPool {
66
109
  export class SQLClient<T = unknown> extends Kysely<T> {
67
110
  private pool: LazyConnectionPool;
68
111
 
69
- constructor(config: ConnectionOptions) {
112
+ constructor(config: MySQLPluginOptions) {
70
113
  const pool = new LazyConnectionPool(config);
71
- super({
114
+ const kyselyConfig: KyselyConfig = {
72
115
  dialect: new MysqlDialect({
73
116
  pool,
74
117
  }),
@@ -77,7 +120,8 @@ export class SQLClient<T = unknown> extends Kysely<T> {
77
120
  strategy: replaceWithNoncontingentExpression,
78
121
  }),
79
122
  ],
80
- });
123
+ };
124
+ super(kyselyConfig);
81
125
  this.pool = pool;
82
126
  }
83
127
 
@@ -1,11 +1,19 @@
1
- import type { ConnectionConfig } from 'mysql';
2
- import type { PoolOptions } from 'mysql2';
1
+ import type { ConnectionOptions, PoolOptions } from 'mysql2';
3
2
  import { HandlerAuxBase, HandlerPluginBase } from './base';
4
3
  import { ConnectionProxy } from './database/connectionProxy';
5
4
  import { SQLClient } from './database/sqlClient';
6
5
 
6
+ export interface DatabaseCredentials {
7
+ username: string;
8
+ password: string;
9
+ }
10
+
7
11
  export interface MySQLPluginOptions {
8
- config: ConnectionConfig & PoolOptions;
12
+ config: ConnectionOptions & PoolOptions;
13
+ /**
14
+ * AWS Secrets Manager secret ID containing {@link DatabaseCredentials}
15
+ */
16
+ secretId?: string;
9
17
  schema?: {
10
18
  eager?: boolean;
11
19
  ignoreError?: boolean;
@@ -30,7 +38,7 @@ export class MySQLPlugin<T = unknown> extends HandlerPluginBase<
30
38
  constructor(options: MySQLPluginOptions) {
31
39
  super();
32
40
  this.proxy = new ConnectionProxy(options);
33
- this.sqlClient = new SQLClient(options.config);
41
+ this.sqlClient = new SQLClient(options);
34
42
  }
35
43
 
36
44
  public create = async () => {
@@ -0,0 +1,73 @@
1
+ import {
2
+ GetSecretValueCommand,
3
+ SecretsManagerClient,
4
+ } from '@aws-sdk/client-secrets-manager';
5
+ import type { DatabaseCredentials } from '../middleware/mysql';
6
+ import { getLogger } from './logger';
7
+ import { stringifyError } from './misc';
8
+
9
+ const logger = getLogger(__filename);
10
+
11
+ export class SecretsManagerCache {
12
+ private static instance: SecretsManagerCache;
13
+ private client: SecretsManagerClient | undefined;
14
+ private cache = new Map<string, any>();
15
+
16
+ private constructor() {}
17
+
18
+ public static getInstance(): SecretsManagerCache {
19
+ if (!SecretsManagerCache.instance) {
20
+ SecretsManagerCache.instance = new SecretsManagerCache();
21
+ }
22
+ return SecretsManagerCache.instance;
23
+ }
24
+
25
+ private getClient(): SecretsManagerClient {
26
+ if (!this.client) {
27
+ this.client = new SecretsManagerClient({});
28
+ logger.debug('SecretsManager client initialized');
29
+ }
30
+ return this.client;
31
+ }
32
+
33
+ public async getSecret<T = any>(secretId: string): Promise<T> {
34
+ if (this.cache.has(secretId)) {
35
+ logger.debug(`Secret ${secretId} found in cache`);
36
+ return this.cache.get(secretId);
37
+ }
38
+
39
+ try {
40
+ const command = new GetSecretValueCommand({ SecretId: secretId });
41
+ const response = await this.getClient().send(command);
42
+
43
+ if (!response.SecretString) {
44
+ throw new Error(`Secret ${secretId} has no SecretString value`);
45
+ }
46
+
47
+ const secretValue = JSON.parse(response.SecretString);
48
+
49
+ this.cache.set(secretId, secretValue);
50
+
51
+ return secretValue;
52
+ } catch (error) {
53
+ logger.error(
54
+ `Failed to fetch secret ${secretId}: ${stringifyError(error)}`,
55
+ );
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ public async getDatabaseCredentials(
61
+ secretId: string,
62
+ ): Promise<DatabaseCredentials> {
63
+ const secret = await this.getSecret<DatabaseCredentials>(secretId);
64
+
65
+ if (!secret.username || !secret.password) {
66
+ throw new Error(
67
+ `Secret ${secretId} does not contain required database credentials (username, password)`,
68
+ );
69
+ }
70
+
71
+ return secret;
72
+ }
73
+ }