serverless-simple-middleware 0.0.73 → 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.
@@ -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
  }
@@ -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 { Handler, HandlerAuxBase, HandlerPluginBase } from './base';
2
- declare const build: <Aux extends HandlerAuxBase>(plugins: Array<HandlerPluginBase<any>>) => (handler: Handler<Aux>) => (event: any, context: any, callback: any) => void;
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;
@@ -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
- return (handler) => (event, context, callback) => {
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(async (resolve, reject) => {
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(async (resolve, reject) => {
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(async (resolve, reject) => {
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(async (resolve, reject) => {
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,7 +78,7 @@ class ConnectionProxy {
82
78
  }
83
79
  resolve();
84
80
  });
85
- });
81
+ })));
86
82
  clearConnection = () => {
87
83
  const conn = this.connection;
88
84
  this.connection = undefined;
@@ -120,12 +116,40 @@ class ConnectionProxy {
120
116
  }
121
117
  return await this.connectionInitOnce.run(async () => {
122
118
  await this.ensureConnectionConfig();
123
- const conn = (0, mysql2_1.createConnection)(this.connectionConfig);
124
- conn.connect();
125
- this.connection = conn;
119
+ this.connection = await this.createConnection(this.MAX_RETRIES);
126
120
  return this.connection;
127
121
  });
128
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
+ };
129
153
  ensureConnectionConfig = async () => {
130
154
  if (this.connectionConfig) {
131
155
  return;
@@ -14,9 +14,13 @@ class LazyConnectionPool {
14
14
  secretsCache;
15
15
  configInitOnce = new oncePromise_1.OncePromise();
16
16
  connectionInitOnce = new oncePromise_1.OncePromise();
17
+ MAX_RETRIES = 1;
17
18
  constructor(options) {
18
19
  this.options = options;
19
20
  this.secretsCache = secretsManager_1.SecretsManagerCache.getInstance();
21
+ if (options.secretsManagerConfig) {
22
+ this.secretsCache.configure(options.secretsManagerConfig);
23
+ }
20
24
  }
21
25
  ensureConnectionConfig = async () => {
22
26
  if (this.connectionConfig) {
@@ -44,21 +48,42 @@ class LazyConnectionPool {
44
48
  this.connectionInitOnce
45
49
  .run(async () => {
46
50
  await this.ensureConnectionConfig();
47
- const conn = (0, mysql2_1.createConnection)(this.connectionConfig);
48
- return await new Promise((resolve, reject) => {
49
- conn.connect((err) => {
50
- if (err) {
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.');
51
76
  reject(err);
52
- return;
53
77
  }
78
+ }
79
+ else {
80
+ logger.verbose('Database connection established successfully.');
54
81
  const wrapped = this._addRelease(conn);
55
82
  this.connection = wrapped;
56
83
  resolve(wrapped);
57
- });
84
+ }
58
85
  });
59
- })
60
- .then((conn) => callback(null, conn))
61
- .catch((err) => callback(err, {}));
86
+ });
62
87
  };
63
88
  end = (callback) => {
64
89
  const conn = this.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.73",
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",
@@ -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
  }
@@ -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
- return (handler: Handler<Aux>) =>
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
- new Promise<T | undefined>(async (resolve, reject) => {
36
- const connection = await this.prepareConnection();
37
- await this.tryToInitializeSchema(false);
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(`DB result is ${JSON.stringify(result)}`);
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,46 +78,52 @@ export class ConnectionProxy {
71
78
  });
72
79
 
73
80
  public beginTransaction = () =>
74
- new Promise<void>(async (resolve, reject) => {
75
- const connection = await this.prepareConnection();
76
- await this.tryToInitializeSchema(false);
77
-
78
- connection.beginTransaction((err: QueryError) => {
79
- if (err) {
80
- reject(err);
81
- return;
82
- }
83
- resolve();
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
- new Promise<void>(async (resolve, reject) => {
89
- const connection = await this.prepareConnection();
90
- await this.tryToInitializeSchema(false);
91
-
92
- connection.commit((err: QueryError) => {
93
- if (err) {
94
- reject(err);
95
- return;
96
- }
97
- resolve();
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
- new Promise<void>(async (resolve, reject) => {
103
- const connection = await this.prepareConnection();
104
- await this.tryToInitializeSchema(false);
105
-
106
- connection.rollback((err: QueryError) => {
107
- if (err) {
108
- reject(err);
109
- return;
110
- }
111
- resolve();
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
129
  const conn = this.connection;
@@ -153,13 +166,47 @@ export class ConnectionProxy {
153
166
 
154
167
  return await this.connectionInitOnce.run(async () => {
155
168
  await this.ensureConnectionConfig();
156
- const conn = createConnection(this.connectionConfig);
157
- conn.connect();
158
- this.connection = conn;
169
+ this.connection = await this.createConnection(this.MAX_RETRIES);
159
170
  return this.connection;
160
171
  });
161
172
  };
162
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
+
163
210
  private ensureConnectionConfig = async (): Promise<void> => {
164
211
  if (this.connectionConfig) {
165
212
  return;
@@ -30,8 +30,13 @@ class LazyConnectionPool implements MysqlPool {
30
30
  private configInitOnce = new OncePromise<void>();
31
31
  private connectionInitOnce = new OncePromise<LazyMysqlPoolConnection>();
32
32
 
33
+ private readonly MAX_RETRIES: number = 1;
34
+
33
35
  constructor(private readonly options: MySQLPluginOptions) {
34
36
  this.secretsCache = SecretsManagerCache.getInstance();
37
+ if (options.secretsManagerConfig) {
38
+ this.secretsCache.configure(options.secretsManagerConfig);
39
+ }
35
40
  }
36
41
 
37
42
  private ensureConnectionConfig = async (): Promise<void> => {
@@ -68,23 +73,50 @@ class LazyConnectionPool implements MysqlPool {
68
73
  this.connectionInitOnce
69
74
  .run(async () => {
70
75
  await this.ensureConnectionConfig();
71
- const conn = createConnection(this.connectionConfig);
72
- return await new Promise<LazyMysqlPoolConnection>((resolve, reject) => {
73
- conn.connect((err: QueryError) => {
74
- if (err) {
75
- reject(err);
76
- return;
77
- }
78
- const wrapped = this._addRelease(conn);
79
- this.connection = wrapped;
80
- resolve(wrapped);
81
- });
82
- });
76
+ return await this.createConnection(this.MAX_RETRIES);
83
77
  })
84
78
  .then((conn) => callback(null, conn))
85
79
  .catch((err) => callback(err, {} as LazyMysqlPoolConnection));
86
80
  };
87
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
+
88
120
  public end = (callback: (error: unknown) => void): void => {
89
121
  const conn = this.connection;
90
122
  this.connection = null;
@@ -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;
package/tsconfig.json CHANGED
@@ -9,7 +9,8 @@
9
9
  "noImplicitAny": true,
10
10
  "strictNullChecks": true,
11
11
  "noUnusedLocals": true,
12
- "noUnusedParameters": true
12
+ "noUnusedParameters": true,
13
+ "skipLibCheck": true
13
14
  },
14
15
  "exclude": ["node_modules", "dist", "__tests__"]
15
16
  }