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.
@@ -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,13 +78,18 @@ class ConnectionProxy {
82
78
  }
83
79
  resolve();
84
80
  });
85
- });
81
+ })));
86
82
  clearConnection = () => {
87
- if (this.connection) {
88
- this.connection.end();
89
- this.connection = undefined;
90
- this.connectionInitOnce.reset();
91
- logger.verbose('Connection is end');
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
- if (this.connection) {
100
- this.connection.destroy();
101
- this.connection = undefined;
102
- this.connectionInitOnce.reset();
103
- logger.verbose('Connection is destroyed');
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
- const conn = (0, mysql2_1.createConnection)(this.connectionConfig);
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, } from 'kysely';
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
- const conn = (0, mysql2_1.createConnection)(this.connectionConfig);
46
- return await new Promise((resolve, reject) => {
47
- conn.connect((err) => {
48
- 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.');
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
- if (this.connection) {
63
- this.connection.end((err) => {
64
- this.connection = null;
65
- this.connectionInitOnce.reset();
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
- if (this.connection) {
75
- this.connection.destroy();
76
- this.connection = null;
77
- this.connectionInitOnce.reset();
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.72",
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,53 +78,64 @@ 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
- if (this.connection) {
117
- this.connection.end();
118
- this.connection = undefined;
119
- this.connectionInitOnce.reset();
120
- logger.verbose('Connection is end');
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
- if (this.connection) {
130
- this.connection.destroy();
131
- this.connection = undefined;
132
- this.connectionInitOnce.reset();
133
- logger.verbose('Connection is destroyed');
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
- const conn = createConnection(this.connectionConfig);
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
- 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
- });
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
- if (this.connection) {
87
- this.connection.end((err: QueryError) => {
88
- this.connection = null;
89
- this.connectionInitOnce.reset();
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
- if (this.connection) {
99
- this.connection.destroy();
100
- this.connection = null;
101
- this.connectionInitOnce.reset();
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
+
@@ -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
  }