serverless-simple-middleware 0.0.69 → 0.0.71
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 +7 -0
- package/dist/internal/oncePromise.js +31 -0
- package/dist/middleware/database/connectionProxy.d.ts +7 -2
- package/dist/middleware/database/connectionProxy.js +52 -19
- package/dist/middleware/database/sqlClient.d.ts +2 -2
- package/dist/middleware/database/sqlClient.js +51 -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 +29 -0
- package/src/middleware/database/connectionProxy.ts +87 -36
- package/src/middleware/database/sqlClient.ts +61 -15
- package/src/middleware/mysql.ts +12 -4
- package/src/utils/secretsManager.ts +73 -0
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
reset() {
|
|
28
|
+
this.promise = undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
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) {
|
|
@@ -80,6 +87,7 @@ class ConnectionProxy {
|
|
|
80
87
|
if (this.connection) {
|
|
81
88
|
this.connection.end();
|
|
82
89
|
this.connection = undefined;
|
|
90
|
+
this.connectionInitOnce.reset();
|
|
83
91
|
logger.verbose('Connection is end');
|
|
84
92
|
}
|
|
85
93
|
};
|
|
@@ -91,23 +99,48 @@ class ConnectionProxy {
|
|
|
91
99
|
if (this.connection) {
|
|
92
100
|
this.connection.destroy();
|
|
93
101
|
this.connection = undefined;
|
|
102
|
+
this.connectionInitOnce.reset();
|
|
94
103
|
logger.verbose('Connection is destroyed');
|
|
95
104
|
}
|
|
96
105
|
};
|
|
97
106
|
onPluginCreated = async () => this.tryToInitializeSchema(true);
|
|
98
|
-
prepareConnection = () => {
|
|
107
|
+
prepareConnection = async () => {
|
|
99
108
|
if (this.connection) {
|
|
100
109
|
return this.connection;
|
|
101
110
|
}
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
return await this.connectionInitOnce.run(async () => {
|
|
112
|
+
await this.ensureConnectionConfig();
|
|
113
|
+
const conn = (0, mysql2_1.createConnection)(this.connectionConfig);
|
|
114
|
+
conn.connect();
|
|
115
|
+
this.connection = conn;
|
|
116
|
+
return this.connection;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
ensureConnectionConfig = async () => {
|
|
120
|
+
if (this.connectionConfig) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await this.configInitOnce.run(async () => {
|
|
124
|
+
const baseConfig = this.options.config;
|
|
125
|
+
if (!this.options.secretId) {
|
|
126
|
+
this.connectionConfig = baseConfig;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const credentials = await this.secretsCache.getDatabaseCredentials(this.options.secretId);
|
|
130
|
+
this.connectionConfig = {
|
|
131
|
+
...baseConfig,
|
|
132
|
+
user: credentials.username,
|
|
133
|
+
password: credentials.password,
|
|
134
|
+
};
|
|
135
|
+
});
|
|
105
136
|
};
|
|
106
|
-
changeDatabase = (dbName) => new Promise((resolve, reject) => this.prepareConnection()
|
|
137
|
+
changeDatabase = async (dbName) => new Promise((resolve, reject) => this.prepareConnection()
|
|
138
|
+
.then((connection) => connection.changeUser({
|
|
107
139
|
database: dbName,
|
|
108
|
-
}, (err) => (err ? reject(err) : resolve(undefined))))
|
|
140
|
+
}, (err) => (err ? reject(err) : resolve(undefined))))
|
|
141
|
+
.catch(reject));
|
|
109
142
|
tryToInitializeSchema = async (initial) => {
|
|
110
|
-
const { eager = false, ignoreError = false, database = '', tables = {}, } = this.
|
|
143
|
+
const { eager = false, ignoreError = false, database = '', tables = {}, } = this.options.schema || {};
|
|
111
144
|
if (initial && !eager) {
|
|
112
145
|
return;
|
|
113
146
|
}
|
|
@@ -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,31 +3,66 @@ 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) => {
|
|
30
64
|
this.connection = null;
|
|
65
|
+
this.connectionInitOnce.reset();
|
|
31
66
|
callback(err);
|
|
32
67
|
});
|
|
33
68
|
}
|
|
@@ -39,6 +74,7 @@ class LazyConnectionPool {
|
|
|
39
74
|
if (this.connection) {
|
|
40
75
|
this.connection.destroy();
|
|
41
76
|
this.connection = null;
|
|
77
|
+
this.connectionInitOnce.reset();
|
|
42
78
|
}
|
|
43
79
|
};
|
|
44
80
|
_addRelease = (connection) => Object.assign(connection, {
|
|
@@ -49,7 +85,7 @@ class SQLClient extends kysely_1.Kysely {
|
|
|
49
85
|
pool;
|
|
50
86
|
constructor(config) {
|
|
51
87
|
const pool = new LazyConnectionPool(config);
|
|
52
|
-
|
|
88
|
+
const kyselyConfig = {
|
|
53
89
|
dialect: new kysely_1.MysqlDialect({
|
|
54
90
|
pool,
|
|
55
91
|
}),
|
|
@@ -58,7 +94,8 @@ class SQLClient extends kysely_1.Kysely {
|
|
|
58
94
|
strategy: kysely_1.replaceWithNoncontingentExpression,
|
|
59
95
|
}),
|
|
60
96
|
],
|
|
61
|
-
}
|
|
97
|
+
};
|
|
98
|
+
super(kyselyConfig);
|
|
62
99
|
this.pool = pool;
|
|
63
100
|
}
|
|
64
101
|
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.71",
|
|
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,29 @@
|
|
|
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
|
+
|
|
26
|
+
public reset(): void {
|
|
27
|
+
this.promise = undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -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;
|
|
@@ -98,6 +116,7 @@ export class ConnectionProxy {
|
|
|
98
116
|
if (this.connection) {
|
|
99
117
|
this.connection.end();
|
|
100
118
|
this.connection = undefined;
|
|
119
|
+
this.connectionInitOnce.reset();
|
|
101
120
|
logger.verbose('Connection is end');
|
|
102
121
|
}
|
|
103
122
|
};
|
|
@@ -110,29 +129,61 @@ export class ConnectionProxy {
|
|
|
110
129
|
if (this.connection) {
|
|
111
130
|
this.connection.destroy();
|
|
112
131
|
this.connection = undefined;
|
|
132
|
+
this.connectionInitOnce.reset();
|
|
113
133
|
logger.verbose('Connection is destroyed');
|
|
114
134
|
}
|
|
115
135
|
};
|
|
116
136
|
|
|
117
137
|
public onPluginCreated = async () => this.tryToInitializeSchema(true);
|
|
118
138
|
|
|
119
|
-
private prepareConnection = () => {
|
|
139
|
+
private prepareConnection = async (): Promise<Connection> => {
|
|
120
140
|
if (this.connection) {
|
|
121
141
|
return this.connection;
|
|
122
142
|
}
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
143
|
+
|
|
144
|
+
return await this.connectionInitOnce.run(async () => {
|
|
145
|
+
await this.ensureConnectionConfig();
|
|
146
|
+
const conn = createConnection(this.connectionConfig);
|
|
147
|
+
conn.connect();
|
|
148
|
+
this.connection = conn;
|
|
149
|
+
return this.connection;
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
private ensureConnectionConfig = async (): Promise<void> => {
|
|
154
|
+
if (this.connectionConfig) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await this.configInitOnce.run(async () => {
|
|
159
|
+
const baseConfig = this.options.config;
|
|
160
|
+
if (!this.options.secretId) {
|
|
161
|
+
this.connectionConfig = baseConfig;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const credentials = await this.secretsCache.getDatabaseCredentials(
|
|
165
|
+
this.options.secretId,
|
|
166
|
+
);
|
|
167
|
+
this.connectionConfig = {
|
|
168
|
+
...baseConfig,
|
|
169
|
+
user: credentials.username,
|
|
170
|
+
password: credentials.password,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
126
173
|
};
|
|
127
174
|
|
|
128
|
-
private changeDatabase = (dbName: string) =>
|
|
175
|
+
private changeDatabase = async (dbName: string) =>
|
|
129
176
|
new Promise<void>((resolve, reject) =>
|
|
130
|
-
this.prepareConnection()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
177
|
+
this.prepareConnection()
|
|
178
|
+
.then((connection) =>
|
|
179
|
+
connection.changeUser(
|
|
180
|
+
{
|
|
181
|
+
database: dbName,
|
|
182
|
+
},
|
|
183
|
+
(err) => (err ? reject(err) : resolve(undefined)),
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
.catch(reject),
|
|
136
187
|
);
|
|
137
188
|
|
|
138
189
|
private tryToInitializeSchema = async (initial: boolean) => {
|
|
@@ -141,7 +192,7 @@ export class ConnectionProxy {
|
|
|
141
192
|
ignoreError = false,
|
|
142
193
|
database = '',
|
|
143
194
|
tables = {},
|
|
144
|
-
} = this.
|
|
195
|
+
} = this.options.schema || {};
|
|
145
196
|
if (initial && !eager) {
|
|
146
197
|
return;
|
|
147
198
|
}
|
|
@@ -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,21 +61,32 @@ 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 => {
|
|
43
86
|
if (this.connection) {
|
|
44
87
|
this.connection.end((err: QueryError) => {
|
|
45
88
|
this.connection = null;
|
|
89
|
+
this.connectionInitOnce.reset();
|
|
46
90
|
callback(err);
|
|
47
91
|
});
|
|
48
92
|
} else {
|
|
@@ -54,6 +98,7 @@ class LazyConnectionPool implements MysqlPool {
|
|
|
54
98
|
if (this.connection) {
|
|
55
99
|
this.connection.destroy();
|
|
56
100
|
this.connection = null;
|
|
101
|
+
this.connectionInitOnce.reset();
|
|
57
102
|
}
|
|
58
103
|
};
|
|
59
104
|
|
|
@@ -66,9 +111,9 @@ class LazyConnectionPool implements MysqlPool {
|
|
|
66
111
|
export class SQLClient<T = unknown> extends Kysely<T> {
|
|
67
112
|
private pool: LazyConnectionPool;
|
|
68
113
|
|
|
69
|
-
constructor(config:
|
|
114
|
+
constructor(config: MySQLPluginOptions) {
|
|
70
115
|
const pool = new LazyConnectionPool(config);
|
|
71
|
-
|
|
116
|
+
const kyselyConfig: KyselyConfig = {
|
|
72
117
|
dialect: new MysqlDialect({
|
|
73
118
|
pool,
|
|
74
119
|
}),
|
|
@@ -77,7 +122,8 @@ export class SQLClient<T = unknown> extends Kysely<T> {
|
|
|
77
122
|
strategy: replaceWithNoncontingentExpression,
|
|
78
123
|
}),
|
|
79
124
|
],
|
|
80
|
-
}
|
|
125
|
+
};
|
|
126
|
+
super(kyselyConfig);
|
|
81
127
|
this.pool = pool;
|
|
82
128
|
}
|
|
83
129
|
|
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
|
+
}
|