serverless-simple-middleware 0.0.72 → 0.0.74
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/middleware/base.d.ts +4 -0
- package/dist/middleware/base.js +6 -0
- package/dist/middleware/build.d.ts +30 -2
- package/dist/middleware/build.js +60 -1
- package/dist/middleware/database/connectionProxy.d.ts +2 -0
- package/dist/middleware/database/connectionProxy.js +63 -29
- package/dist/middleware/database/sqlClient.d.ts +1 -1
- package/dist/middleware/database/sqlClient.js +51 -17
- package/dist/middleware/index.d.ts +28 -1
- package/dist/middleware/mysql.d.ts +2 -0
- package/dist/utils/secretsManager.d.ts +3 -0
- package/dist/utils/secretsManager.js +10 -1
- package/package.json +3 -2
- package/src/middleware/base.ts +9 -1
- package/src/middleware/build.ts +102 -2
- package/src/middleware/database/connectionProxy.ts +129 -72
- package/src/middleware/database/sqlClient.ts +65 -21
- package/src/middleware/mysql.ts +2 -0
- package/src/utils/secretsManager.ts +14 -1
- package/tsconfig.json +2 -1
|
@@ -9,12 +9,16 @@ export declare class HandlerRequest {
|
|
|
9
9
|
private lazyBody?;
|
|
10
10
|
constructor(event: any, context: any);
|
|
11
11
|
get body(): any;
|
|
12
|
+
set body(value: any);
|
|
12
13
|
get path(): {
|
|
13
14
|
[key: string]: string | undefined;
|
|
14
15
|
};
|
|
15
16
|
get query(): {
|
|
16
17
|
[key: string]: string | undefined;
|
|
17
18
|
};
|
|
19
|
+
set query(value: {
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
});
|
|
18
22
|
header(key: string): string | undefined;
|
|
19
23
|
records<T, U>(selector?: (each: T) => U): T[] | U[];
|
|
20
24
|
}
|
package/dist/middleware/base.js
CHANGED
|
@@ -29,12 +29,18 @@ class HandlerRequest {
|
|
|
29
29
|
}
|
|
30
30
|
return this.lazyBody || {};
|
|
31
31
|
}
|
|
32
|
+
set body(value) {
|
|
33
|
+
this.lazyBody = value;
|
|
34
|
+
}
|
|
32
35
|
get path() {
|
|
33
36
|
return this.event.pathParameters || {};
|
|
34
37
|
}
|
|
35
38
|
get query() {
|
|
36
39
|
return this.event.queryStringParameters || {};
|
|
37
40
|
}
|
|
41
|
+
set query(value) {
|
|
42
|
+
this.event.queryStringParameters = value;
|
|
43
|
+
}
|
|
38
44
|
header(key) {
|
|
39
45
|
return this.event.headers[key.toLowerCase()];
|
|
40
46
|
}
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { type ZodError, type ZodSchema } from 'zod';
|
|
2
|
+
import { Handler, HandlerAuxBase, HandlerPluginBase, HandlerRequest, HandlerResponse } from './base';
|
|
3
|
+
declare const build: <Aux extends HandlerAuxBase>(plugins: Array<HandlerPluginBase<any>>) => ((handler: Handler<Aux>) => (event: any, context: any, callback: any) => void) & {
|
|
4
|
+
withBody: <S>(schema: ZodSchema<S>, handler: (context: {
|
|
5
|
+
request: Omit<HandlerRequest, "body"> & {
|
|
6
|
+
body: S;
|
|
7
|
+
};
|
|
8
|
+
response: HandlerResponse;
|
|
9
|
+
aux: Aux;
|
|
10
|
+
}) => any, onInvalid?: (error: ZodError) => {
|
|
11
|
+
statusCode: number;
|
|
12
|
+
body: any;
|
|
13
|
+
} | Promise<{
|
|
14
|
+
statusCode: number;
|
|
15
|
+
body: any;
|
|
16
|
+
} | void> | void) => (event: any, context: any, callback: any) => void;
|
|
17
|
+
withQuery: <Q>(schema: ZodSchema<Q>, handler: (context: {
|
|
18
|
+
request: Omit<HandlerRequest, "query"> & {
|
|
19
|
+
query: Q;
|
|
20
|
+
};
|
|
21
|
+
response: HandlerResponse;
|
|
22
|
+
aux: Aux;
|
|
23
|
+
}) => any, onInvalid?: (error: ZodError<Q>) => {
|
|
24
|
+
statusCode: number;
|
|
25
|
+
body: any;
|
|
26
|
+
} | Promise<{
|
|
27
|
+
statusCode: number;
|
|
28
|
+
body: any;
|
|
29
|
+
} | void> | void) => (event: any, context: any, callback: any) => void;
|
|
30
|
+
};
|
|
3
31
|
export default build;
|
package/dist/middleware/build.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const logger_1 = require("../utils/logger");
|
|
4
|
+
const zod_1 = require("zod");
|
|
4
5
|
const utils_1 = require("../utils");
|
|
5
6
|
const base_1 = require("./base");
|
|
6
7
|
const logger = (0, logger_1.getLogger)(__filename);
|
|
@@ -107,8 +108,66 @@ class HandlerProxy {
|
|
|
107
108
|
// It will break type safety because there is no relation between Aux and Plugin.
|
|
108
109
|
const build = (plugins) => {
|
|
109
110
|
const middleware = new HandlerMiddleware(plugins);
|
|
110
|
-
|
|
111
|
+
const invoke = (handler) => (event, context, callback) => {
|
|
111
112
|
new HandlerProxy(event, context, callback).call(middleware, handler);
|
|
112
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* @param schema - Zod schema to validate the request body.
|
|
116
|
+
* @param handler - Handler that receives the validated body.
|
|
117
|
+
* @param onInvalid - Optional callback to customize invalid responses. If it
|
|
118
|
+
* returns `{ statusCode, body }`, that is sent instead of the default zod
|
|
119
|
+
* error payload.
|
|
120
|
+
*/
|
|
121
|
+
const withBody = (schema, handler, onInvalid) => invoke(async ({ request, response, aux }) => {
|
|
122
|
+
const parsed = schema.safeParse(request.body);
|
|
123
|
+
if (!parsed.success) {
|
|
124
|
+
logger.error(`Validation failed: ${(0, utils_1.stringifyError)((0, zod_1.treeifyError)(parsed.error))}`);
|
|
125
|
+
if (onInvalid) {
|
|
126
|
+
const result = await onInvalid(parsed.error);
|
|
127
|
+
if (result) {
|
|
128
|
+
return response.fail(result.body, result.statusCode);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return response.fail((0, zod_1.treeifyError)(parsed.error), 400);
|
|
132
|
+
}
|
|
133
|
+
const typedRequest = request;
|
|
134
|
+
typedRequest.body = parsed.data;
|
|
135
|
+
return handler({
|
|
136
|
+
request: typedRequest,
|
|
137
|
+
response,
|
|
138
|
+
aux,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* @param schema - Zod schema to validate the request query.
|
|
143
|
+
* @param handler - Handler that receives the validated query.
|
|
144
|
+
* @param onInvalid - Optional callback to customize invalid responses. If it
|
|
145
|
+
* returns `{ statusCode, body }`, that is sent instead of the default zod
|
|
146
|
+
* error payload.
|
|
147
|
+
*/
|
|
148
|
+
const withQuery = (schema, handler, onInvalid) => invoke(async ({ request, response, aux }) => {
|
|
149
|
+
const parsed = schema.safeParse(request.query);
|
|
150
|
+
if (!parsed.success) {
|
|
151
|
+
logger.error(`Validation failed: ${(0, utils_1.stringifyError)((0, zod_1.treeifyError)(parsed.error))}`);
|
|
152
|
+
if (onInvalid) {
|
|
153
|
+
const result = await onInvalid(parsed.error);
|
|
154
|
+
if (result) {
|
|
155
|
+
return response.fail(result.body, result.statusCode);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return response.fail((0, zod_1.treeifyError)(parsed.error), 400);
|
|
159
|
+
}
|
|
160
|
+
const typedRequest = request;
|
|
161
|
+
typedRequest.query = parsed.data;
|
|
162
|
+
return handler({
|
|
163
|
+
request: typedRequest,
|
|
164
|
+
response,
|
|
165
|
+
aux,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
return Object.assign(invoke, {
|
|
169
|
+
withBody,
|
|
170
|
+
withQuery,
|
|
171
|
+
});
|
|
113
172
|
};
|
|
114
173
|
exports.default = build;
|
|
@@ -8,6 +8,7 @@ export declare class ConnectionProxy {
|
|
|
8
8
|
private connectionInitOnce;
|
|
9
9
|
private initialized;
|
|
10
10
|
private dbName?;
|
|
11
|
+
private readonly MAX_RETRIES;
|
|
11
12
|
constructor(options: MySQLPluginOptions);
|
|
12
13
|
query: <T>(sql: string, params?: any[]) => Promise<T | undefined>;
|
|
13
14
|
fetch: <T>(sql: string, params?: any[]) => Promise<T[]>;
|
|
@@ -23,6 +24,7 @@ export declare class ConnectionProxy {
|
|
|
23
24
|
destroyConnection: () => void;
|
|
24
25
|
onPluginCreated: () => Promise<void>;
|
|
25
26
|
private prepareConnection;
|
|
27
|
+
private createConnection;
|
|
26
28
|
private ensureConnectionConfig;
|
|
27
29
|
private changeDatabase;
|
|
28
30
|
private tryToInitializeSchema;
|
|
@@ -15,6 +15,7 @@ class ConnectionProxy {
|
|
|
15
15
|
connectionInitOnce = new oncePromise_1.OncePromise();
|
|
16
16
|
initialized;
|
|
17
17
|
dbName;
|
|
18
|
+
MAX_RETRIES = 1;
|
|
18
19
|
constructor(options) {
|
|
19
20
|
this.options = options;
|
|
20
21
|
if (options.schema && options.schema.database) {
|
|
@@ -22,10 +23,11 @@ class ConnectionProxy {
|
|
|
22
23
|
options.config.database = undefined;
|
|
23
24
|
}
|
|
24
25
|
this.secretsCache = secretsManager_1.SecretsManagerCache.getInstance();
|
|
26
|
+
if (options.secretsManagerConfig) {
|
|
27
|
+
this.secretsCache.configure(options.secretsManagerConfig);
|
|
28
|
+
}
|
|
25
29
|
}
|
|
26
|
-
query = (sql, params) => new Promise(
|
|
27
|
-
const connection = await this.prepareConnection();
|
|
28
|
-
await this.tryToInitializeSchema(false);
|
|
30
|
+
query = (sql, params) => this.prepareConnection().then((connection) => this.tryToInitializeSchema(false).then(() => new Promise((resolve, reject) => {
|
|
29
31
|
if (process.env.NODE_ENV !== 'test') {
|
|
30
32
|
logger.silly(`Execute query[${sql}] with params[${params}]`);
|
|
31
33
|
}
|
|
@@ -41,7 +43,7 @@ class ConnectionProxy {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
});
|
|
44
|
-
});
|
|
46
|
+
})));
|
|
45
47
|
fetch = (sql, params) => this.query(sql, params).then((res) => res || []);
|
|
46
48
|
fetchOne = (sql, params, defaultValue) => this.fetch(sql, params).then((res) => {
|
|
47
49
|
if (res === undefined || res[0] === undefined) {
|
|
@@ -50,9 +52,7 @@ class ConnectionProxy {
|
|
|
50
52
|
}
|
|
51
53
|
return res[0];
|
|
52
54
|
});
|
|
53
|
-
beginTransaction = () => new Promise(
|
|
54
|
-
const connection = await this.prepareConnection();
|
|
55
|
-
await this.tryToInitializeSchema(false);
|
|
55
|
+
beginTransaction = () => this.prepareConnection().then((connection) => this.tryToInitializeSchema(false).then(() => new Promise((resolve, reject) => {
|
|
56
56
|
connection.beginTransaction((err) => {
|
|
57
57
|
if (err) {
|
|
58
58
|
reject(err);
|
|
@@ -60,10 +60,8 @@ class ConnectionProxy {
|
|
|
60
60
|
}
|
|
61
61
|
resolve();
|
|
62
62
|
});
|
|
63
|
-
});
|
|
64
|
-
commit = () => new Promise(
|
|
65
|
-
const connection = await this.prepareConnection();
|
|
66
|
-
await this.tryToInitializeSchema(false);
|
|
63
|
+
})));
|
|
64
|
+
commit = () => this.prepareConnection().then((connection) => this.tryToInitializeSchema(false).then(() => new Promise((resolve, reject) => {
|
|
67
65
|
connection.commit((err) => {
|
|
68
66
|
if (err) {
|
|
69
67
|
reject(err);
|
|
@@ -71,10 +69,8 @@ class ConnectionProxy {
|
|
|
71
69
|
}
|
|
72
70
|
resolve();
|
|
73
71
|
});
|
|
74
|
-
});
|
|
75
|
-
rollback = () => new Promise(
|
|
76
|
-
const connection = await this.prepareConnection();
|
|
77
|
-
await this.tryToInitializeSchema(false);
|
|
72
|
+
})));
|
|
73
|
+
rollback = () => this.prepareConnection().then((connection) => this.tryToInitializeSchema(false).then(() => new Promise((resolve, reject) => {
|
|
78
74
|
connection.rollback((err) => {
|
|
79
75
|
if (err) {
|
|
80
76
|
reject(err);
|
|
@@ -82,13 +78,18 @@ class ConnectionProxy {
|
|
|
82
78
|
}
|
|
83
79
|
resolve();
|
|
84
80
|
});
|
|
85
|
-
});
|
|
81
|
+
})));
|
|
86
82
|
clearConnection = () => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
const conn = this.connection;
|
|
84
|
+
this.connection = undefined;
|
|
85
|
+
this.connectionInitOnce.reset();
|
|
86
|
+
if (conn) {
|
|
87
|
+
try {
|
|
88
|
+
conn.end();
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
logger.warn(`Error occurred while ending connection: ${error}`);
|
|
92
|
+
}
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
/**
|
|
@@ -96,11 +97,16 @@ class ConnectionProxy {
|
|
|
96
97
|
* This should be used only for special use cases!
|
|
97
98
|
*/
|
|
98
99
|
destroyConnection = () => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
const conn = this.connection;
|
|
101
|
+
this.connection = undefined;
|
|
102
|
+
this.connectionInitOnce.reset();
|
|
103
|
+
if (conn) {
|
|
104
|
+
try {
|
|
105
|
+
conn.destroy();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
logger.warn(`Error occurred while destroying connection: ${error}`);
|
|
109
|
+
}
|
|
104
110
|
}
|
|
105
111
|
};
|
|
106
112
|
onPluginCreated = async () => this.tryToInitializeSchema(true);
|
|
@@ -110,12 +116,40 @@ class ConnectionProxy {
|
|
|
110
116
|
}
|
|
111
117
|
return await this.connectionInitOnce.run(async () => {
|
|
112
118
|
await this.ensureConnectionConfig();
|
|
113
|
-
|
|
114
|
-
conn.connect();
|
|
115
|
-
this.connection = conn;
|
|
119
|
+
this.connection = await this.createConnection(this.MAX_RETRIES);
|
|
116
120
|
return this.connection;
|
|
117
121
|
});
|
|
118
122
|
};
|
|
123
|
+
createConnection = async (remainingRetries) => {
|
|
124
|
+
const conn = (0, mysql2_1.createConnection)(this.connectionConfig);
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
conn.on('error', (err) => {
|
|
127
|
+
logger.error(`Connection error event: ${err.message}`);
|
|
128
|
+
});
|
|
129
|
+
conn.connect((err) => {
|
|
130
|
+
if (err) {
|
|
131
|
+
logger.error(`Failed to connect to database: ${err.message}`);
|
|
132
|
+
conn.destroy();
|
|
133
|
+
if (remainingRetries > 0) {
|
|
134
|
+
logger.warn(`Retrying database connection... (${remainingRetries} attempt(s) remaining)`);
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
this.createConnection(remainingRetries - 1)
|
|
137
|
+
.then(resolve)
|
|
138
|
+
.catch(reject);
|
|
139
|
+
}, 100);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
logger.error('Database connection failed after all retries. Giving up.');
|
|
143
|
+
reject(err);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
logger.verbose('Database connection established successfully.');
|
|
148
|
+
resolve(conn);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
};
|
|
119
153
|
ensureConnectionConfig = async () => {
|
|
120
154
|
if (this.connectionConfig) {
|
|
121
155
|
return;
|
|
@@ -10,4 +10,4 @@ export declare class SQLClient<T = unknown> extends Kysely<T> {
|
|
|
10
10
|
*/
|
|
11
11
|
destroyConnection: () => void;
|
|
12
12
|
}
|
|
13
|
-
export { expressionBuilder, sql, type DeleteQueryBuilder, type DeleteResult, type Expression, type ExpressionBuilder, type InferResult, type Insertable, type InsertQueryBuilder, type InsertResult, type NotNull, type RawBuilder, type Selectable, type SelectQueryBuilder, type SqlBool, type Transaction, type Updateable, type UpdateQueryBuilder, type UpdateResult
|
|
13
|
+
export { expressionBuilder, sql, type DeleteQueryBuilder, type DeleteResult, type Expression, type ExpressionBuilder, type InferResult, type Insertable, type InsertQueryBuilder, type InsertResult, type NotNull, type RawBuilder, type Selectable, type SelectQueryBuilder, type SqlBool, type Transaction, type Updateable, type UpdateQueryBuilder, type UpdateResult } from 'kysely';
|
|
@@ -4,7 +4,9 @@ exports.sql = exports.expressionBuilder = exports.SQLClient = void 0;
|
|
|
4
4
|
const kysely_1 = require("kysely");
|
|
5
5
|
const mysql2_1 = require("mysql2");
|
|
6
6
|
const oncePromise_1 = require("../../internal/oncePromise");
|
|
7
|
+
const utils_1 = require("../../utils");
|
|
7
8
|
const secretsManager_1 = require("../../utils/secretsManager");
|
|
9
|
+
const logger = (0, utils_1.getLogger)(__filename);
|
|
8
10
|
class LazyConnectionPool {
|
|
9
11
|
options;
|
|
10
12
|
connection = null;
|
|
@@ -12,9 +14,13 @@ class LazyConnectionPool {
|
|
|
12
14
|
secretsCache;
|
|
13
15
|
configInitOnce = new oncePromise_1.OncePromise();
|
|
14
16
|
connectionInitOnce = new oncePromise_1.OncePromise();
|
|
17
|
+
MAX_RETRIES = 1;
|
|
15
18
|
constructor(options) {
|
|
16
19
|
this.options = options;
|
|
17
20
|
this.secretsCache = secretsManager_1.SecretsManagerCache.getInstance();
|
|
21
|
+
if (options.secretsManagerConfig) {
|
|
22
|
+
this.secretsCache.configure(options.secretsManagerConfig);
|
|
23
|
+
}
|
|
18
24
|
}
|
|
19
25
|
ensureConnectionConfig = async () => {
|
|
20
26
|
if (this.connectionConfig) {
|
|
@@ -42,27 +48,49 @@ class LazyConnectionPool {
|
|
|
42
48
|
this.connectionInitOnce
|
|
43
49
|
.run(async () => {
|
|
44
50
|
await this.ensureConnectionConfig();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
return await this.createConnection(this.MAX_RETRIES);
|
|
52
|
+
})
|
|
53
|
+
.then((conn) => callback(null, conn))
|
|
54
|
+
.catch((err) => callback(err, {}));
|
|
55
|
+
};
|
|
56
|
+
createConnection = async (remainingRetries) => {
|
|
57
|
+
const conn = (0, mysql2_1.createConnection)(this.connectionConfig);
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
conn.on('error', (err) => {
|
|
60
|
+
logger.error(`Database connection error occurred: ${err.message}`);
|
|
61
|
+
});
|
|
62
|
+
conn.connect((err) => {
|
|
63
|
+
if (err) {
|
|
64
|
+
logger.error(`Failed to connect to database: ${err.message}`);
|
|
65
|
+
conn.destroy();
|
|
66
|
+
if (remainingRetries > 0) {
|
|
67
|
+
logger.warn(`Retrying database connection... (${remainingRetries} attempt(s) remaining)`);
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
this.createConnection(remainingRetries - 1)
|
|
70
|
+
.then(resolve)
|
|
71
|
+
.catch(reject);
|
|
72
|
+
}, 100);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
logger.error('Database connection failed after all retries. Giving up.');
|
|
49
76
|
reject(err);
|
|
50
|
-
return;
|
|
51
77
|
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
logger.verbose('Database connection established successfully.');
|
|
52
81
|
const wrapped = this._addRelease(conn);
|
|
53
82
|
this.connection = wrapped;
|
|
54
83
|
resolve(wrapped);
|
|
55
|
-
}
|
|
84
|
+
}
|
|
56
85
|
});
|
|
57
|
-
})
|
|
58
|
-
.then((conn) => callback(null, conn))
|
|
59
|
-
.catch((err) => callback(err, {}));
|
|
86
|
+
});
|
|
60
87
|
};
|
|
61
88
|
end = (callback) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
const conn = this.connection;
|
|
90
|
+
this.connection = null;
|
|
91
|
+
this.connectionInitOnce.reset();
|
|
92
|
+
if (conn) {
|
|
93
|
+
conn.end((err) => {
|
|
66
94
|
callback(err);
|
|
67
95
|
});
|
|
68
96
|
}
|
|
@@ -71,10 +99,16 @@ class LazyConnectionPool {
|
|
|
71
99
|
}
|
|
72
100
|
};
|
|
73
101
|
destroy = () => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
102
|
+
const conn = this.connection;
|
|
103
|
+
this.connection = null;
|
|
104
|
+
this.connectionInitOnce.reset();
|
|
105
|
+
if (conn) {
|
|
106
|
+
try {
|
|
107
|
+
conn.destroy();
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
logger.warn(`Error occurred while destroying connection: ${error}`);
|
|
111
|
+
}
|
|
78
112
|
}
|
|
79
113
|
};
|
|
80
114
|
_addRelease = (connection) => Object.assign(connection, {
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
export declare const middleware: {
|
|
2
|
-
build: <Aux extends import("./base").HandlerAuxBase>(plugins: Array<import("./base").HandlerPluginBase<any>>) => (handler: import("./base").Handler<Aux>) => (event: any, context: any, callback: any) => void
|
|
2
|
+
build: <Aux extends import("./base").HandlerAuxBase>(plugins: Array<import("./base").HandlerPluginBase<any>>) => ((handler: import("./base").Handler<Aux>) => (event: any, context: any, callback: any) => void) & {
|
|
3
|
+
withBody: <S>(schema: import("zod").ZodType<S>, handler: (context: {
|
|
4
|
+
request: Omit<import("./base").HandlerRequest, "body"> & {
|
|
5
|
+
body: S;
|
|
6
|
+
};
|
|
7
|
+
response: import("./base").HandlerResponse;
|
|
8
|
+
aux: Aux;
|
|
9
|
+
}) => any, onInvalid?: (error: import("zod").ZodError) => {
|
|
10
|
+
statusCode: number;
|
|
11
|
+
body: any;
|
|
12
|
+
} | Promise<{
|
|
13
|
+
statusCode: number;
|
|
14
|
+
body: any;
|
|
15
|
+
} | void> | void) => (event: any, context: any, callback: any) => void;
|
|
16
|
+
withQuery: <Q>(schema: import("zod").ZodType<Q>, handler: (context: {
|
|
17
|
+
request: Omit<import("./base").HandlerRequest, "query"> & {
|
|
18
|
+
query: Q;
|
|
19
|
+
};
|
|
20
|
+
response: import("./base").HandlerResponse;
|
|
21
|
+
aux: Aux;
|
|
22
|
+
}) => any, onInvalid?: (error: import("zod").ZodError<Q>) => {
|
|
23
|
+
statusCode: number;
|
|
24
|
+
body: any;
|
|
25
|
+
} | Promise<{
|
|
26
|
+
statusCode: number;
|
|
27
|
+
body: any;
|
|
28
|
+
} | void> | void) => (event: any, context: any, callback: any) => void;
|
|
29
|
+
};
|
|
3
30
|
aws: (options?: import("./aws").AWSPluginOptions) => import("./aws").AWSPlugin;
|
|
4
31
|
trace: (options: import("./trace").TracerPluginOptions) => import("./trace").TracerPlugin;
|
|
5
32
|
logger: (options: import("./logger").LoggerPluginOptions) => import("./logger").LoggerPlugin;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SecretsManagerClientConfig } from '@aws-sdk/client-secrets-manager';
|
|
1
2
|
import type { ConnectionOptions, PoolOptions } from 'mysql2';
|
|
2
3
|
import { HandlerAuxBase, HandlerPluginBase } from './base';
|
|
3
4
|
import { ConnectionProxy } from './database/connectionProxy';
|
|
@@ -12,6 +13,7 @@ export interface MySQLPluginOptions {
|
|
|
12
13
|
* AWS Secrets Manager secret ID containing {@link DatabaseCredentials}
|
|
13
14
|
*/
|
|
14
15
|
secretId?: string;
|
|
16
|
+
secretsManagerConfig?: SecretsManagerClientConfig;
|
|
15
17
|
schema?: {
|
|
16
18
|
eager?: boolean;
|
|
17
19
|
ignoreError?: boolean;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { SecretsManagerClientConfig } from '@aws-sdk/client-secrets-manager';
|
|
1
2
|
import type { DatabaseCredentials } from '../middleware/mysql';
|
|
2
3
|
export declare class SecretsManagerCache {
|
|
3
4
|
private static instance;
|
|
4
5
|
private client;
|
|
6
|
+
private clientConfig;
|
|
5
7
|
private cache;
|
|
6
8
|
private constructor();
|
|
7
9
|
static getInstance(): SecretsManagerCache;
|
|
10
|
+
configure(config: SecretsManagerClientConfig): void;
|
|
8
11
|
private getClient;
|
|
9
12
|
getSecret<T = any>(secretId: string): Promise<T>;
|
|
10
13
|
getDatabaseCredentials(secretId: string): Promise<DatabaseCredentials>;
|
|
@@ -8,6 +8,7 @@ const logger = (0, logger_1.getLogger)(__filename);
|
|
|
8
8
|
class SecretsManagerCache {
|
|
9
9
|
static instance;
|
|
10
10
|
client;
|
|
11
|
+
clientConfig;
|
|
11
12
|
cache = new Map();
|
|
12
13
|
constructor() { }
|
|
13
14
|
static getInstance() {
|
|
@@ -16,9 +17,17 @@ class SecretsManagerCache {
|
|
|
16
17
|
}
|
|
17
18
|
return SecretsManagerCache.instance;
|
|
18
19
|
}
|
|
20
|
+
configure(config) {
|
|
21
|
+
if (this.client) {
|
|
22
|
+
logger.warn('SecretsManager client already initialized. Reconfiguring with new config.');
|
|
23
|
+
this.client = undefined;
|
|
24
|
+
}
|
|
25
|
+
this.clientConfig = config;
|
|
26
|
+
logger.debug('SecretsManager client config updated');
|
|
27
|
+
}
|
|
19
28
|
getClient() {
|
|
20
29
|
if (!this.client) {
|
|
21
|
-
this.client = new client_secrets_manager_1.SecretsManagerClient({});
|
|
30
|
+
this.client = new client_secrets_manager_1.SecretsManagerClient(this.clientConfig ?? {});
|
|
22
31
|
logger.debug('SecretsManager client initialized');
|
|
23
32
|
}
|
|
24
33
|
return this.client;
|
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.74",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"author": "VoyagerX",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"nanoid": "4.0.2",
|
|
46
46
|
"simple-staging": "^0.0.12",
|
|
47
47
|
"ts-enum-util": "^3.1.0",
|
|
48
|
-
"uuid": "^3.3.2"
|
|
48
|
+
"uuid": "^3.3.2",
|
|
49
|
+
"zod": "^4.3.5"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@types/jest": "^23.3.1",
|
package/src/middleware/base.ts
CHANGED
|
@@ -40,6 +40,10 @@ export class HandlerRequest {
|
|
|
40
40
|
return this.lazyBody || {};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
set body(value: any) {
|
|
44
|
+
this.lazyBody = value;
|
|
45
|
+
}
|
|
46
|
+
|
|
43
47
|
get path(): { [key: string]: string | undefined } {
|
|
44
48
|
return this.event.pathParameters || {};
|
|
45
49
|
}
|
|
@@ -48,6 +52,10 @@ export class HandlerRequest {
|
|
|
48
52
|
return this.event.queryStringParameters || {};
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
set query(value: { [key: string]: any }) {
|
|
56
|
+
this.event.queryStringParameters = value;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
public header(key: string): string | undefined {
|
|
52
60
|
return this.event.headers[key.toLowerCase()];
|
|
53
61
|
}
|
|
@@ -93,7 +101,7 @@ export class HandlerResponse {
|
|
|
93
101
|
if (this.crossOrigin) {
|
|
94
102
|
headers['Access-Control-Allow-Origin'] = this.crossOrigin;
|
|
95
103
|
}
|
|
96
|
-
let multiValueHeaders = undefined;
|
|
104
|
+
let multiValueHeaders: any = undefined;
|
|
97
105
|
if (this.cookies.length > 0) {
|
|
98
106
|
multiValueHeaders = { 'Set-Cookie': this.cookies };
|
|
99
107
|
}
|
package/src/middleware/build.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getLogger } from '../utils/logger';
|
|
2
2
|
|
|
3
|
+
import { treeifyError, type ZodError, type ZodSchema } from 'zod';
|
|
3
4
|
import { stringifyError } from '../utils';
|
|
4
5
|
import {
|
|
5
6
|
Handler,
|
|
@@ -165,9 +166,108 @@ const build = <Aux extends HandlerAuxBase>(
|
|
|
165
166
|
plugins: Array<HandlerPluginBase<any>>,
|
|
166
167
|
) => {
|
|
167
168
|
const middleware = new HandlerMiddleware<Aux>(plugins);
|
|
168
|
-
|
|
169
|
-
(event: any, context: any, callback: any) => {
|
|
169
|
+
const invoke =
|
|
170
|
+
(handler: Handler<Aux>) => (event: any, context: any, callback: any) => {
|
|
170
171
|
new HandlerProxy<Aux>(event, context, callback).call(middleware, handler);
|
|
171
172
|
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param schema - Zod schema to validate the request body.
|
|
176
|
+
* @param handler - Handler that receives the validated body.
|
|
177
|
+
* @param onInvalid - Optional callback to customize invalid responses. If it
|
|
178
|
+
* returns `{ statusCode, body }`, that is sent instead of the default zod
|
|
179
|
+
* error payload.
|
|
180
|
+
*/
|
|
181
|
+
const withBody = <S>(
|
|
182
|
+
schema: ZodSchema<S>,
|
|
183
|
+
handler: (context: {
|
|
184
|
+
request: Omit<HandlerRequest, 'body'> & { body: S };
|
|
185
|
+
response: HandlerResponse;
|
|
186
|
+
aux: Aux;
|
|
187
|
+
}) => any,
|
|
188
|
+
onInvalid?: (
|
|
189
|
+
error: ZodError,
|
|
190
|
+
) =>
|
|
191
|
+
| { statusCode: number; body: any }
|
|
192
|
+
| Promise<{ statusCode: number; body: any } | void>
|
|
193
|
+
| void,
|
|
194
|
+
) =>
|
|
195
|
+
invoke(async ({ request, response, aux }) => {
|
|
196
|
+
const parsed = schema.safeParse(request.body);
|
|
197
|
+
if (!parsed.success) {
|
|
198
|
+
logger.error(
|
|
199
|
+
`Validation failed: ${stringifyError(treeifyError(parsed.error))}`,
|
|
200
|
+
);
|
|
201
|
+
if (onInvalid) {
|
|
202
|
+
const result = await onInvalid(parsed.error);
|
|
203
|
+
if (result) {
|
|
204
|
+
return response.fail(result.body, result.statusCode);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return response.fail(treeifyError(parsed.error), 400);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const typedRequest = request as Omit<HandlerRequest, 'body'> & {
|
|
211
|
+
body: S;
|
|
212
|
+
};
|
|
213
|
+
typedRequest.body = parsed.data;
|
|
214
|
+
return handler({
|
|
215
|
+
request: typedRequest,
|
|
216
|
+
response,
|
|
217
|
+
aux,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param schema - Zod schema to validate the request query.
|
|
223
|
+
* @param handler - Handler that receives the validated query.
|
|
224
|
+
* @param onInvalid - Optional callback to customize invalid responses. If it
|
|
225
|
+
* returns `{ statusCode, body }`, that is sent instead of the default zod
|
|
226
|
+
* error payload.
|
|
227
|
+
*/
|
|
228
|
+
const withQuery = <Q>(
|
|
229
|
+
schema: ZodSchema<Q>,
|
|
230
|
+
handler: (context: {
|
|
231
|
+
request: Omit<HandlerRequest, 'query'> & { query: Q };
|
|
232
|
+
response: HandlerResponse;
|
|
233
|
+
aux: Aux;
|
|
234
|
+
}) => any,
|
|
235
|
+
onInvalid?: (
|
|
236
|
+
error: ZodError<Q>,
|
|
237
|
+
) =>
|
|
238
|
+
| { statusCode: number; body: any }
|
|
239
|
+
| Promise<{ statusCode: number; body: any } | void>
|
|
240
|
+
| void,
|
|
241
|
+
) =>
|
|
242
|
+
invoke(async ({ request, response, aux }) => {
|
|
243
|
+
const parsed = schema.safeParse(request.query);
|
|
244
|
+
if (!parsed.success) {
|
|
245
|
+
logger.error(
|
|
246
|
+
`Validation failed: ${stringifyError(treeifyError(parsed.error))}`,
|
|
247
|
+
);
|
|
248
|
+
if (onInvalid) {
|
|
249
|
+
const result = await onInvalid(parsed.error);
|
|
250
|
+
if (result) {
|
|
251
|
+
return response.fail(result.body, result.statusCode);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return response.fail(treeifyError(parsed.error), 400);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const typedRequest = request as Omit<HandlerRequest, 'query'> & {
|
|
258
|
+
query: Q;
|
|
259
|
+
};
|
|
260
|
+
typedRequest.query = parsed.data;
|
|
261
|
+
return handler({
|
|
262
|
+
request: typedRequest,
|
|
263
|
+
response,
|
|
264
|
+
aux,
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return Object.assign(invoke, {
|
|
269
|
+
withBody,
|
|
270
|
+
withQuery,
|
|
271
|
+
});
|
|
172
272
|
};
|
|
173
273
|
export default build;
|
|
@@ -23,40 +23,47 @@ export class ConnectionProxy {
|
|
|
23
23
|
private initialized: boolean;
|
|
24
24
|
private dbName?: string;
|
|
25
25
|
|
|
26
|
+
private readonly MAX_RETRIES: number = 1;
|
|
27
|
+
|
|
26
28
|
public constructor(private readonly options: MySQLPluginOptions) {
|
|
27
29
|
if (options.schema && options.schema.database) {
|
|
28
30
|
this.dbName = options.config.database;
|
|
29
31
|
options.config.database = undefined;
|
|
30
32
|
}
|
|
31
33
|
this.secretsCache = SecretsManagerCache.getInstance();
|
|
34
|
+
if (options.secretsManagerConfig) {
|
|
35
|
+
this.secretsCache.configure(options.secretsManagerConfig);
|
|
36
|
+
}
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
public query = <T>(sql: string, params?: any[]) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (process.env.NODE_ENV !== 'test') {
|
|
40
|
-
logger.silly(`Execute query[${sql}] with params[${params}]`);
|
|
41
|
-
}
|
|
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);
|
|
40
|
+
this.prepareConnection().then((connection) =>
|
|
41
|
+
this.tryToInitializeSchema(false).then(
|
|
42
|
+
() =>
|
|
43
|
+
new Promise<T | undefined>((resolve, reject) => {
|
|
53
44
|
if (process.env.NODE_ENV !== 'test') {
|
|
54
|
-
logger.silly(`
|
|
45
|
+
logger.silly(`Execute query[${sql}] with params[${params}]`);
|
|
55
46
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
connection.query(
|
|
48
|
+
sql,
|
|
49
|
+
params,
|
|
50
|
+
(err: QueryError, result: QueryResult, _fields?: FieldPacket[]) => {
|
|
51
|
+
if (err) {
|
|
52
|
+
logger.error(
|
|
53
|
+
`error occurred in database query=${sql}, error=${err}`,
|
|
54
|
+
);
|
|
55
|
+
reject(err);
|
|
56
|
+
} else {
|
|
57
|
+
resolve(result as T);
|
|
58
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
59
|
+
logger.silly(`DB result is ${JSON.stringify(result)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
60
67
|
|
|
61
68
|
public fetch = <T>(sql: string, params?: any[]) =>
|
|
62
69
|
this.query<T[]>(sql, params).then((res) => res || []);
|
|
@@ -71,53 +78,64 @@ export class ConnectionProxy {
|
|
|
71
78
|
});
|
|
72
79
|
|
|
73
80
|
public beginTransaction = () =>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
this.prepareConnection().then((connection) =>
|
|
82
|
+
this.tryToInitializeSchema(false).then(
|
|
83
|
+
() =>
|
|
84
|
+
new Promise<void>((resolve, reject) => {
|
|
85
|
+
connection.beginTransaction((err: QueryError) => {
|
|
86
|
+
if (err) {
|
|
87
|
+
reject(err);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
}),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
86
95
|
|
|
87
96
|
public commit = () =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
this.prepareConnection().then((connection) =>
|
|
98
|
+
this.tryToInitializeSchema(false).then(
|
|
99
|
+
() =>
|
|
100
|
+
new Promise<void>((resolve, reject) => {
|
|
101
|
+
connection.commit((err: QueryError) => {
|
|
102
|
+
if (err) {
|
|
103
|
+
reject(err);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
);
|
|
100
111
|
|
|
101
112
|
public rollback = () =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
this.prepareConnection().then((connection) =>
|
|
114
|
+
this.tryToInitializeSchema(false).then(
|
|
115
|
+
() =>
|
|
116
|
+
new Promise<void>((resolve, reject) => {
|
|
117
|
+
connection.rollback((err: QueryError) => {
|
|
118
|
+
if (err) {
|
|
119
|
+
reject(err);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
}),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
114
127
|
|
|
115
128
|
public clearConnection = () => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
const conn = this.connection;
|
|
130
|
+
this.connection = undefined;
|
|
131
|
+
this.connectionInitOnce.reset();
|
|
132
|
+
|
|
133
|
+
if (conn) {
|
|
134
|
+
try {
|
|
135
|
+
conn.end();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logger.warn(`Error occurred while ending connection: ${error}`);
|
|
138
|
+
}
|
|
121
139
|
}
|
|
122
140
|
};
|
|
123
141
|
|
|
@@ -126,11 +144,16 @@ export class ConnectionProxy {
|
|
|
126
144
|
* This should be used only for special use cases!
|
|
127
145
|
*/
|
|
128
146
|
public destroyConnection = () => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
147
|
+
const conn = this.connection;
|
|
148
|
+
this.connection = undefined;
|
|
149
|
+
this.connectionInitOnce.reset();
|
|
150
|
+
|
|
151
|
+
if (conn) {
|
|
152
|
+
try {
|
|
153
|
+
conn.destroy();
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.warn(`Error occurred while destroying connection: ${error}`);
|
|
156
|
+
}
|
|
134
157
|
}
|
|
135
158
|
};
|
|
136
159
|
|
|
@@ -143,13 +166,47 @@ export class ConnectionProxy {
|
|
|
143
166
|
|
|
144
167
|
return await this.connectionInitOnce.run(async () => {
|
|
145
168
|
await this.ensureConnectionConfig();
|
|
146
|
-
|
|
147
|
-
conn.connect();
|
|
148
|
-
this.connection = conn;
|
|
169
|
+
this.connection = await this.createConnection(this.MAX_RETRIES);
|
|
149
170
|
return this.connection;
|
|
150
171
|
});
|
|
151
172
|
};
|
|
152
173
|
|
|
174
|
+
private createConnection = async (
|
|
175
|
+
remainingRetries: number,
|
|
176
|
+
): Promise<Connection> => {
|
|
177
|
+
const conn = createConnection(this.connectionConfig);
|
|
178
|
+
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
conn.on('error', (err) => {
|
|
181
|
+
logger.error(`Connection error event: ${err.message}`);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
conn.connect((err) => {
|
|
185
|
+
if (err) {
|
|
186
|
+
logger.error(`Failed to connect to database: ${err.message}`);
|
|
187
|
+
conn.destroy();
|
|
188
|
+
|
|
189
|
+
if (remainingRetries > 0) {
|
|
190
|
+
logger.warn(
|
|
191
|
+
`Retrying database connection... (${remainingRetries} attempt(s) remaining)`,
|
|
192
|
+
);
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
this.createConnection(remainingRetries - 1)
|
|
195
|
+
.then(resolve)
|
|
196
|
+
.catch(reject);
|
|
197
|
+
}, 100);
|
|
198
|
+
} else {
|
|
199
|
+
logger.error('Database connection failed after all retries. Giving up.');
|
|
200
|
+
reject(err);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
logger.verbose('Database connection established successfully.');
|
|
204
|
+
resolve(conn);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
153
210
|
private ensureConnectionConfig = async (): Promise<void> => {
|
|
154
211
|
if (this.connectionConfig) {
|
|
155
212
|
return;
|
|
@@ -13,9 +13,12 @@ import {
|
|
|
13
13
|
type QueryError,
|
|
14
14
|
} from 'mysql2';
|
|
15
15
|
import { OncePromise } from '../../internal/oncePromise';
|
|
16
|
+
import { getLogger } from '../../utils';
|
|
16
17
|
import { SecretsManagerCache } from '../../utils/secretsManager';
|
|
17
18
|
import { MySQLPluginOptions } from '../mysql';
|
|
18
19
|
|
|
20
|
+
const logger = getLogger(__filename);
|
|
21
|
+
|
|
19
22
|
interface LazyMysqlPoolConnection extends Connection {
|
|
20
23
|
release: () => void;
|
|
21
24
|
}
|
|
@@ -27,8 +30,13 @@ class LazyConnectionPool implements MysqlPool {
|
|
|
27
30
|
private configInitOnce = new OncePromise<void>();
|
|
28
31
|
private connectionInitOnce = new OncePromise<LazyMysqlPoolConnection>();
|
|
29
32
|
|
|
33
|
+
private readonly MAX_RETRIES: number = 1;
|
|
34
|
+
|
|
30
35
|
constructor(private readonly options: MySQLPluginOptions) {
|
|
31
36
|
this.secretsCache = SecretsManagerCache.getInstance();
|
|
37
|
+
if (options.secretsManagerConfig) {
|
|
38
|
+
this.secretsCache.configure(options.secretsManagerConfig);
|
|
39
|
+
}
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
private ensureConnectionConfig = async (): Promise<void> => {
|
|
@@ -65,28 +73,57 @@ class LazyConnectionPool implements MysqlPool {
|
|
|
65
73
|
this.connectionInitOnce
|
|
66
74
|
.run(async () => {
|
|
67
75
|
await this.ensureConnectionConfig();
|
|
68
|
-
|
|
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
|
-
});
|
|
76
|
+
return await this.createConnection(this.MAX_RETRIES);
|
|
80
77
|
})
|
|
81
78
|
.then((conn) => callback(null, conn))
|
|
82
79
|
.catch((err) => callback(err, {} as LazyMysqlPoolConnection));
|
|
83
80
|
};
|
|
84
81
|
|
|
82
|
+
private createConnection = async (
|
|
83
|
+
remainingRetries: number,
|
|
84
|
+
): Promise<LazyMysqlPoolConnection> => {
|
|
85
|
+
const conn = createConnection(this.connectionConfig);
|
|
86
|
+
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
conn.on('error', (err) => {
|
|
89
|
+
logger.error(`Database connection error occurred: ${err.message}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
conn.connect((err: QueryError) => {
|
|
93
|
+
if (err) {
|
|
94
|
+
logger.error(`Failed to connect to database: ${err.message}`);
|
|
95
|
+
conn.destroy();
|
|
96
|
+
|
|
97
|
+
if (remainingRetries > 0) {
|
|
98
|
+
logger.warn(
|
|
99
|
+
`Retrying database connection... (${remainingRetries} attempt(s) remaining)`,
|
|
100
|
+
);
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
this.createConnection(remainingRetries - 1)
|
|
103
|
+
.then(resolve)
|
|
104
|
+
.catch(reject);
|
|
105
|
+
}, 100);
|
|
106
|
+
} else {
|
|
107
|
+
logger.error('Database connection failed after all retries. Giving up.');
|
|
108
|
+
reject(err);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
logger.verbose('Database connection established successfully.');
|
|
112
|
+
const wrapped = this._addRelease(conn);
|
|
113
|
+
this.connection = wrapped;
|
|
114
|
+
resolve(wrapped);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
85
120
|
public end = (callback: (error: unknown) => void): void => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
const conn = this.connection;
|
|
122
|
+
this.connection = null;
|
|
123
|
+
this.connectionInitOnce.reset();
|
|
124
|
+
|
|
125
|
+
if (conn) {
|
|
126
|
+
conn.end((err: QueryError) => {
|
|
90
127
|
callback(err);
|
|
91
128
|
});
|
|
92
129
|
} else {
|
|
@@ -95,10 +132,16 @@ class LazyConnectionPool implements MysqlPool {
|
|
|
95
132
|
};
|
|
96
133
|
|
|
97
134
|
public destroy = (): void => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
const conn = this.connection;
|
|
136
|
+
this.connection = null;
|
|
137
|
+
this.connectionInitOnce.reset();
|
|
138
|
+
|
|
139
|
+
if (conn) {
|
|
140
|
+
try {
|
|
141
|
+
conn.destroy();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger.warn(`Error occurred while destroying connection: ${error}`);
|
|
144
|
+
}
|
|
102
145
|
}
|
|
103
146
|
};
|
|
104
147
|
|
|
@@ -160,5 +203,6 @@ export {
|
|
|
160
203
|
type Transaction,
|
|
161
204
|
type Updateable,
|
|
162
205
|
type UpdateQueryBuilder,
|
|
163
|
-
type UpdateResult
|
|
206
|
+
type UpdateResult
|
|
164
207
|
} from 'kysely';
|
|
208
|
+
|
package/src/middleware/mysql.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SecretsManagerClientConfig } from '@aws-sdk/client-secrets-manager';
|
|
1
2
|
import type { ConnectionOptions, PoolOptions } from 'mysql2';
|
|
2
3
|
import { HandlerAuxBase, HandlerPluginBase } from './base';
|
|
3
4
|
import { ConnectionProxy } from './database/connectionProxy';
|
|
@@ -14,6 +15,7 @@ export interface MySQLPluginOptions {
|
|
|
14
15
|
* AWS Secrets Manager secret ID containing {@link DatabaseCredentials}
|
|
15
16
|
*/
|
|
16
17
|
secretId?: string;
|
|
18
|
+
secretsManagerConfig?: SecretsManagerClientConfig;
|
|
17
19
|
schema?: {
|
|
18
20
|
eager?: boolean;
|
|
19
21
|
ignoreError?: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GetSecretValueCommand,
|
|
3
3
|
SecretsManagerClient,
|
|
4
|
+
SecretsManagerClientConfig,
|
|
4
5
|
} from '@aws-sdk/client-secrets-manager';
|
|
5
6
|
import type { DatabaseCredentials } from '../middleware/mysql';
|
|
6
7
|
import { getLogger } from './logger';
|
|
@@ -11,6 +12,7 @@ const logger = getLogger(__filename);
|
|
|
11
12
|
export class SecretsManagerCache {
|
|
12
13
|
private static instance: SecretsManagerCache;
|
|
13
14
|
private client: SecretsManagerClient | undefined;
|
|
15
|
+
private clientConfig: SecretsManagerClientConfig | undefined;
|
|
14
16
|
private cache = new Map<string, any>();
|
|
15
17
|
|
|
16
18
|
private constructor() {}
|
|
@@ -22,9 +24,20 @@ export class SecretsManagerCache {
|
|
|
22
24
|
return SecretsManagerCache.instance;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
public configure(config: SecretsManagerClientConfig): void {
|
|
28
|
+
if (this.client) {
|
|
29
|
+
logger.warn(
|
|
30
|
+
'SecretsManager client already initialized. Reconfiguring with new config.',
|
|
31
|
+
);
|
|
32
|
+
this.client = undefined;
|
|
33
|
+
}
|
|
34
|
+
this.clientConfig = config;
|
|
35
|
+
logger.debug('SecretsManager client config updated');
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
private getClient(): SecretsManagerClient {
|
|
26
39
|
if (!this.client) {
|
|
27
|
-
this.client = new SecretsManagerClient({});
|
|
40
|
+
this.client = new SecretsManagerClient(this.clientConfig ?? {});
|
|
28
41
|
logger.debug('SecretsManager client initialized');
|
|
29
42
|
}
|
|
30
43
|
return this.client;
|