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.
- package/dist/internal/oncePromise.d.ts +6 -0
- package/dist/internal/oncePromise.js +28 -0
- package/dist/middleware/database/connectionProxy.d.ts +7 -2
- package/dist/middleware/database/connectionProxy.js +50 -19
- package/dist/middleware/database/sqlClient.d.ts +2 -2
- package/dist/middleware/database/sqlClient.js +49 -14
- package/dist/middleware/mysql.d.ts +10 -3
- package/dist/middleware/mysql.js +1 -1
- package/dist/utils/secretsManager.d.ts +11 -0
- package/dist/utils/secretsManager.js +54 -0
- package/package.json +2 -4
- package/src/internal/oncePromise.ts +25 -0
- package/src/middleware/database/connectionProxy.ts +85 -36
- package/src/middleware/database/sqlClient.ts +59 -15
- package/src/middleware/mysql.ts +12 -4
- package/src/utils/secretsManager.ts +73 -0
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
13
|
-
this.
|
|
14
|
-
if (
|
|
15
|
-
this.dbName =
|
|
16
|
-
|
|
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.
|
|
103
|
-
|
|
104
|
-
|
|
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()
|
|
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.
|
|
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 {
|
|
2
|
+
import { MySQLPluginOptions } from '../mysql';
|
|
3
3
|
export declare class SQLClient<T = unknown> extends Kysely<T> {
|
|
4
4
|
private pool;
|
|
5
|
-
constructor(config:
|
|
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
|
-
|
|
9
|
+
options;
|
|
8
10
|
connection = null;
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
if (this.
|
|
14
|
-
callback(null, this.connection);
|
|
19
|
+
ensureConnectionConfig = async () => {
|
|
20
|
+
if (this.connectionConfig) {
|
|
15
21
|
return;
|
|
16
22
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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 {
|
|
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:
|
|
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;
|
package/dist/middleware/mysql.js
CHANGED
|
@@ -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
|
|
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.
|
|
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 {
|
|
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(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
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()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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:
|
|
112
|
+
constructor(config: MySQLPluginOptions) {
|
|
70
113
|
const pool = new LazyConnectionPool(config);
|
|
71
|
-
|
|
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
|
|
package/src/middleware/mysql.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
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
|
|
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
|
+
}
|