@nestjs-kitchen/connextion-postgres 2.0.6 → 2.1.1

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/README.md CHANGED
@@ -9,6 +9,13 @@ A flexible module to provide [node-postgres](https://node-postgres.com/) interfa
9
9
 
10
10
  ---
11
11
 
12
+ ## Feature
13
+
14
+ - ✅ Transaction support
15
+ - ✅ High availability (HA) support
16
+ - ✅ Type intelligent
17
+
18
+
12
19
  ## Install
13
20
 
14
21
  ```bash
@@ -174,6 +181,40 @@ class SampleService {
174
181
  }
175
182
  ```
176
183
 
184
+ ### High availability (HA)
185
+
186
+ Register postgres connection instance with multiple host.
187
+
188
+ When enabled, `instance1` will attempt to connect db with each hosts/ports in sequence until a connection is successfully established.
189
+
190
+ **Note: This is a temporary workaround and will change once `node-postgres` internally supports multiple hosts.**
191
+
192
+ ```typescript
193
+ @Module({
194
+ imports: [
195
+ PostgresModule.register({
196
+ connections: [
197
+ {
198
+ name: 'instance1',
199
+ hosts: [
200
+ {
201
+ host: 'instance_1_host_1',
202
+ port: 1
203
+ },
204
+ {
205
+ host: 'instance_1_host_2',
206
+ port: 2
207
+ }
208
+ ]
209
+ }
210
+ ]
211
+ })
212
+ ],
213
+ providers: [SampleService]
214
+ })
215
+ export class SampleModule {}
216
+ ```
217
+
177
218
  ## License
178
219
 
179
220
  MIT License
@@ -1,8 +1,7 @@
1
- import { type AsyncModuleOptions, type ConnectionOptionName, type ConnextionInstance, type ModuleOptions } from '@nestjs-kitchen/connextion';
2
1
  import type { DynamicModule, Type } from '@nestjs/common';
2
+ import { type AsyncModuleOptions, type ConnectionOptionName, type ConnextionInstance, type ModuleOptions } from '@nestjs-kitchen/connextion';
3
3
  import { DEFAULT_INSTANCE_NAME } from './constants';
4
4
  import { PostgresInstance } from './postgres.instance';
5
- import type { PostgresInstanceOptions } from './types';
6
5
  /**
7
6
  * Creates a set of Postgres services, modules, and their associated Transaction decorator.
8
7
  */
@@ -22,8 +21,8 @@ export declare const definePostgres: <T extends string = typeof DEFAULT_INSTANCE
22
21
  */
23
22
  PostgresModule: {
24
23
  new (): {};
25
- register(options: ModuleOptions<T, PostgresInstanceOptions>): DynamicModule;
26
- registerAsync(options: AsyncModuleOptions<T, PostgresInstanceOptions>): DynamicModule;
24
+ register(options: ModuleOptions<T, import("./types").Options>): DynamicModule;
25
+ registerAsync(options: AsyncModuleOptions<T, import("./types").Options>): DynamicModule;
27
26
  };
28
27
  /**
29
28
  * A decorator that automatically enables transactions for the specific Postgres
@@ -1,19 +1,21 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { ConnextionInstance } from '@nestjs-kitchen/connextion';
3
- import { type PoolClient, type QueryArrayConfig, type QueryArrayResult, type QueryConfig, type QueryConfigValues, type QueryResult, type QueryResultRow, type Submittable } from 'pg';
3
+ import { Pool, type PoolClient, type QueryArrayConfig, type QueryArrayResult, type QueryConfig, type QueryConfigValues, type QueryResult, type QueryResultRow, type Submittable } from 'pg';
4
4
  import { ALS, GET_CLIENT } from './constants';
5
- import type { ALSType, PostgresInstanceOptions } from './types';
5
+ import type { ALSType, Options, PostgresInstanceOptions } from './types';
6
6
  export declare class PostgresInstance extends ConnextionInstance<PostgresInstanceOptions> {
7
- private pool;
7
+ private pools;
8
8
  private logger;
9
- private debug;
9
+ private optionsArray;
10
10
  [ALS]: AsyncLocalStorage<ALSType>;
11
11
  private listener1;
12
12
  private listener2;
13
13
  constructor(name: string, options?: PostgresInstanceOptions);
14
14
  private end;
15
- dispose(): Promise<void> | undefined;
16
- create(options: PostgresInstanceOptions): void;
15
+ dispose(): Promise<void>;
16
+ createPool(options: Options): Pool | undefined;
17
+ create(options: PostgresInstanceOptions): Promise<void>;
18
+ private getAndTestClient;
17
19
  [GET_CLIENT](): Promise<PoolClient>;
18
20
  query<T extends Submittable>(queryStream: T): Promise<T>;
19
21
  query<R extends any[] = any[], I = any[]>(queryConfig: QueryArrayConfig<I>, values?: QueryConfigValues<I>): Promise<QueryArrayResult<R>>;
@@ -3,8 +3,8 @@ var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.PostgresInstance = void 0;
5
5
  const node_async_hooks_1 = require("node:async_hooks");
6
- const connextion_1 = require("@nestjs-kitchen/connextion");
7
6
  const common_1 = require("@nestjs/common");
7
+ const connextion_1 = require("@nestjs-kitchen/connextion");
8
8
  const pg_1 = require("pg");
9
9
  const uid_1 = require("uid");
10
10
  const constants_1 = require("./constants");
@@ -13,7 +13,8 @@ const utils_1 = require("./utils");
13
13
  class PostgresInstance extends connextion_1.ConnextionInstance {
14
14
  constructor(name, options) {
15
15
  super(name, options);
16
- this.pool = undefined;
16
+ this.pools = [];
17
+ this.optionsArray = [];
17
18
  // Every instance should have its own als to avoid accessing wrong context.
18
19
  this[_a] = new node_async_hooks_1.AsyncLocalStorage();
19
20
  this.listener1 = (_cli) => {
@@ -23,10 +24,9 @@ class PostgresInstance extends connextion_1.ConnextionInstance {
23
24
  this.logger.error(err);
24
25
  };
25
26
  this.logger = new common_1.Logger(`Postgres][${name}`);
26
- this.debug = Boolean(process.env[constants_1.CONNEXTION_POSTGRES_DEBUG]) || options?.debug;
27
27
  }
28
28
  async end(pool) {
29
- if (!pool) {
29
+ if (!pool || pool.ending) {
30
30
  return;
31
31
  }
32
32
  try {
@@ -38,60 +38,90 @@ class PostgresInstance extends connextion_1.ConnextionInstance {
38
38
  this.logger.error(error);
39
39
  }
40
40
  }
41
- dispose() {
42
- if (!this.pool) {
41
+ async dispose() {
42
+ if (!this.pools) {
43
43
  return;
44
44
  }
45
- const pool = this.pool;
46
- this.pool = undefined;
47
- return this.end(pool);
45
+ const pools = this.pools;
46
+ this.pools = [];
47
+ for (const pool of pools) {
48
+ await this.end(pool);
49
+ }
50
+ return;
48
51
  }
49
- create(options) {
50
- this.dispose();
52
+ createPool(options) {
51
53
  try {
52
54
  const pool = new pg_1.Pool(options);
53
55
  //https://github.com/brianc/node-postgres/issues/2439#issuecomment-757691278
54
56
  pool.on('connect', this.listener1);
55
57
  pool.on('error', this.listener2);
56
- this.pool = pool;
58
+ return pool;
57
59
  }
58
60
  catch (error) {
59
61
  this.logger.error(error.message);
60
62
  }
61
63
  }
62
- async [(_a = constants_1.ALS, constants_1.GET_CLIENT)]() {
63
- if (!this.pool) {
64
- throw new errors_1.PostgresError('pool not found');
64
+ async create(options) {
65
+ await this.dispose();
66
+ this.optionsArray = (0, utils_1.normalizeOptions)(options);
67
+ if (!this.optionsArray.length) {
68
+ throw new Error('cannot find available options');
65
69
  }
66
- if (!this.debug) {
67
- const [client, err] = await (0, utils_1.plainPromise)(this.pool.connect());
68
- if (err) {
69
- throw new errors_1.PostgresError(err, err);
70
- }
71
- if (!client) {
72
- throw new errors_1.PostgresError('client not found');
73
- }
70
+ this.pools = this.optionsArray.map((options) => this.createPool(options)).filter(Boolean);
71
+ }
72
+ async getAndTestClient(promise) {
73
+ const client = await promise;
74
+ if (!client) {
74
75
  return client;
75
76
  }
76
- // Debug mode
77
- const logger = (0, utils_1.createDebugLogger)(this.logger.debug.bind(this.logger), this.debug);
78
- const debug = (0, utils_1.debugFactroy)(this.name, (0, uid_1.uid)(21), logger);
79
- const [client, err] = await (0, utils_1.plainPromise)(debug.pool.connect(this.pool.connect.bind(this.pool))());
80
- if (err) {
81
- throw new errors_1.PostgresError(err, err);
77
+ try {
78
+ // test if current client is still alive.
79
+ await client.query('SELECT 1;');
80
+ return client;
82
81
  }
83
- if (!client) {
84
- throw new errors_1.PostgresError('client not found');
82
+ catch (error) {
83
+ client.release(error);
84
+ throw error;
85
85
  }
86
- return new Proxy(client, {
87
- get(target, prop, receiver) {
88
- const value = Reflect.get(target, prop, receiver);
89
- if (debug.client[prop]) {
90
- return debug.client[prop](value.bind(target));
86
+ }
87
+ async [(_a = constants_1.ALS, constants_1.GET_CLIENT)]() {
88
+ let error = 'failed to get client';
89
+ for (let i = 0; i < this.optionsArray.length; i++) {
90
+ const pool = this.pools[i];
91
+ const debugMode = this.optionsArray[i].debug || !!process.env[constants_1.CONNEXTION_POSTGRES_DEBUG];
92
+ if (!debugMode) {
93
+ const [client, err] = await (0, utils_1.plainPromise)(this.getAndTestClient(pool.connect()));
94
+ if (client) {
95
+ return client;
96
+ }
97
+ error = err || 'client not found';
98
+ if (!(0, utils_1.isFailoverRequired)(error)) {
99
+ break;
91
100
  }
92
- return value;
93
101
  }
94
- });
102
+ else {
103
+ // Debug mode
104
+ const logger = (0, utils_1.createDebugLogger)(this.logger.debug.bind(this.logger), debugMode);
105
+ const debug = (0, utils_1.debugFactroy)(this.name, (0, uid_1.uid)(21), `${this.optionsArray[i].host}:${this.optionsArray[i].port}`, logger);
106
+ const [client, err] = await (0, utils_1.plainPromise)(this.getAndTestClient(debug.pool.connect(pool.connect.bind(pool))()));
107
+ if (client) {
108
+ return new Proxy(client, {
109
+ get(target, prop, receiver) {
110
+ const value = Reflect.get(target, prop, receiver);
111
+ if (debug.client[prop]) {
112
+ return debug.client[prop](value.bind(target));
113
+ }
114
+ return value;
115
+ }
116
+ });
117
+ }
118
+ error = err || 'client not found';
119
+ if (!(0, utils_1.isFailoverRequired)(error)) {
120
+ break;
121
+ }
122
+ }
123
+ }
124
+ throw new errors_1.PostgresError(error, typeof error === 'string' ? undefined : error);
95
125
  }
96
126
  async query(...rest) {
97
127
  if (!rest.length) {
@@ -116,7 +146,7 @@ class PostgresInstance extends connextion_1.ConnextionInstance {
116
146
  throw err;
117
147
  }
118
148
  finally {
119
- client.release(err ?? true);
149
+ client.release(err);
120
150
  }
121
151
  }
122
152
  async transactionQueryWithConfig(...rest) {
@@ -156,13 +186,13 @@ class PostgresInstance extends connextion_1.ConnextionInstance {
156
186
  };
157
187
  const onEnd = () => {
158
188
  res.off('error', onError);
159
- client.release(true);
189
+ client.release();
160
190
  };
161
191
  res.once('end', onEnd);
162
192
  res.once('error', onError);
163
193
  }
164
194
  else {
165
- client.release(err ?? true);
195
+ client.release(err);
166
196
  }
167
197
  }
168
198
  }
@@ -62,7 +62,7 @@ const createTransaction = (Postgres) => {
62
62
  }
63
63
  }
64
64
  };
65
- const releaseClients = async (err) => runWithClients((client) => client.release(err ?? true));
65
+ const releaseClients = async (err) => runWithClients((client) => client.release(err));
66
66
  // Ensure all clients are available.
67
67
  const [_, initErr] = await (0, utils_1.plainPromise)(initClients());
68
68
  if (initErr) {
package/dist/types.d.ts CHANGED
@@ -4,6 +4,19 @@ export interface ALSType {
4
4
  client: Promise<PoolClient>;
5
5
  queries: Promise<ALSQueryType>[];
6
6
  }
7
- export type PostgresInstanceOptions = PoolConfig & {
7
+ export type Options = PoolConfig & {
8
8
  debug?: boolean | ((data: Record<any, any>) => void);
9
+ } & {
10
+ /**
11
+ * Enable multi host connections for `High Availability` (HA).
12
+ *
13
+ * This will find and switch to connect the availabe host.
14
+ *
15
+ * If provided, will ignore `Options.host` and `Options.port`
16
+ */
17
+ hosts?: {
18
+ host: PoolConfig['host'];
19
+ port: PoolConfig['port'];
20
+ }[];
9
21
  };
22
+ export type PostgresInstanceOptions = Options;
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type PoolClient, type Submittable } from 'pg';
2
+ import type { Options } from './types';
2
3
  export declare const isSubmittable: (val: any) => val is Submittable;
3
4
  export declare const isObject: (val: any) => val is object;
4
5
  export declare const normalizeStrings: (strs?: string[]) => string[];
@@ -9,7 +10,7 @@ export declare const getCurrentDateStr: () => string;
9
10
  export declare const formatArray: (arr?: any) => string;
10
11
  export declare const extraceQueryTextAndValues: (...rest: any[]) => [text: string, values: any[]];
11
12
  export declare const createDebugLogger: (defaultLogger: (...rest: any) => void, customFormater?: Boolean | ((data: Record<any, any>) => void)) => (data: Record<any, any>) => void;
12
- export declare const debugFactroy: (name: string, queryId: string, logger: (data: Record<any, any>) => void) => {
13
+ export declare const debugFactroy: (name: string, queryId: string, host: string, logger: (data: Record<any, any>) => void) => {
13
14
  pool: {
14
15
  connect: <T extends () => Promise<PoolClient>>(callback: T) => () => Promise<PoolClient>;
15
16
  };
@@ -26,3 +27,7 @@ export declare const withResolvers: <T>() => {
26
27
  resolve: (value: T | PromiseLike<T>) => void;
27
28
  reject: (reason?: any) => void;
28
29
  };
30
+ export declare const normalizeOptions: (options: Options) => Omit<Options, "hosts">[];
31
+ export declare const isFailoverRequired: (err?: {
32
+ code?: string;
33
+ }) => boolean;
package/dist/utils.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.withResolvers = exports.setTransactionMetdata = exports.getTransactionMetdata = exports.copyMethodMetadata = exports.debugFactroy = exports.createDebugLogger = exports.extraceQueryTextAndValues = exports.formatArray = exports.getCurrentDateStr = exports.printTable = exports.truncateString = exports.plainPromise = exports.normalizeStrings = exports.isObject = exports.isSubmittable = void 0;
6
+ exports.isFailoverRequired = exports.normalizeOptions = exports.withResolvers = exports.setTransactionMetdata = exports.getTransactionMetdata = exports.copyMethodMetadata = exports.debugFactroy = exports.createDebugLogger = exports.extraceQueryTextAndValues = exports.formatArray = exports.getCurrentDateStr = exports.printTable = exports.truncateString = exports.plainPromise = exports.normalizeStrings = exports.isObject = exports.isSubmittable = void 0;
7
7
  const dayjs_1 = __importDefault(require("dayjs"));
8
8
  const constants_1 = require("./constants");
9
9
  const isSubmittable = (val) => {
@@ -87,7 +87,7 @@ const createDebugLogger = (defaultLogger, customFormater) => {
87
87
  };
88
88
  };
89
89
  exports.createDebugLogger = createDebugLogger;
90
- const debugFactroy = (name, queryId, logger) => {
90
+ const debugFactroy = (name, queryId, host, logger) => {
91
91
  return {
92
92
  pool: {
93
93
  connect: (callback) => {
@@ -104,12 +104,13 @@ const debugFactroy = (name, queryId, logger) => {
104
104
  finally {
105
105
  logger({
106
106
  Instance: name,
107
+ Host: host,
107
108
  Client: queryId,
108
109
  Type: 'Request new client',
109
110
  'Started On': startOn,
110
111
  'Ended On': (0, exports.getCurrentDateStr)(),
111
112
  Status: err ? 'Failed' : 'Successful',
112
- Error: err
113
+ Error: err?.code ? `[${err.code}]${err}` : err
113
114
  });
114
115
  }
115
116
  };
@@ -126,6 +127,7 @@ const debugFactroy = (name, queryId, logger) => {
126
127
  rest[0].on('end', () => {
127
128
  logger({
128
129
  Instance: name,
130
+ Host: host,
129
131
  Client: queryId,
130
132
  Type: 'Submittable',
131
133
  Text: text,
@@ -138,6 +140,7 @@ const debugFactroy = (name, queryId, logger) => {
138
140
  rest[0].on('error', (err) => {
139
141
  logger({
140
142
  Instance: name,
143
+ Host: host,
141
144
  Client: queryId,
142
145
  Type: 'Submittable',
143
146
  Text: text,
@@ -145,7 +148,7 @@ const debugFactroy = (name, queryId, logger) => {
145
148
  'Started On': startOn,
146
149
  'Ended On': (0, exports.getCurrentDateStr)(),
147
150
  Status: 'Failed',
148
- Error: err
151
+ Error: err?.code ? `[${err.code}]${err}` : err
149
152
  });
150
153
  });
151
154
  }
@@ -160,6 +163,7 @@ const debugFactroy = (name, queryId, logger) => {
160
163
  if (!submittable) {
161
164
  logger({
162
165
  Instance: name,
166
+ Host: host,
163
167
  Client: queryId,
164
168
  Type: 'Query',
165
169
  Text: text,
@@ -167,7 +171,7 @@ const debugFactroy = (name, queryId, logger) => {
167
171
  'Started On': startOn,
168
172
  'Ended On': (0, exports.getCurrentDateStr)(),
169
173
  Status: err ? 'Failed' : 'Successful',
170
- Error: err
174
+ Error: err?.code ? `[${err.code}]${err}` : err
171
175
  });
172
176
  }
173
177
  }
@@ -187,12 +191,13 @@ const debugFactroy = (name, queryId, logger) => {
187
191
  finally {
188
192
  logger({
189
193
  Instance: name,
194
+ Host: host,
190
195
  Client: queryId,
191
196
  Type: 'Release client',
192
197
  'Started On': startOn,
193
198
  'Ended On': (0, exports.getCurrentDateStr)(),
194
199
  Status: err ? 'Failed' : 'Successful',
195
- Error: err
200
+ Error: err?.code ? `[${err.code}]${err}` : err
196
201
  });
197
202
  }
198
203
  };
@@ -228,3 +233,42 @@ const withResolvers = () => {
228
233
  return { promise, resolve: resolve, reject: reject };
229
234
  };
230
235
  exports.withResolvers = withResolvers;
236
+ const normalizeOptions = (options) => {
237
+ const { host, hosts, port, ...rest } = options;
238
+ if (hosts) {
239
+ return hosts.map((ele) => ({
240
+ ...rest,
241
+ ...ele
242
+ }));
243
+ }
244
+ return [
245
+ {
246
+ ...rest,
247
+ host,
248
+ port
249
+ }
250
+ ];
251
+ };
252
+ exports.normalizeOptions = normalizeOptions;
253
+ const failoverErrorCodes = new Set([
254
+ 'ECONNREFUSED',
255
+ 'ETIMEDOUT',
256
+ 'EHOSTUNREACH',
257
+ 'ENOTFOUND',
258
+ 'EAI_AGAIN',
259
+ 'ECONNRESET',
260
+ 'EPIPE',
261
+ '57P01',
262
+ '57P02',
263
+ '57P03',
264
+ '55P03',
265
+ '55000',
266
+ '54000',
267
+ '53300',
268
+ '08006',
269
+ 'XX000'
270
+ ]);
271
+ const isFailoverRequired = (err) => {
272
+ return !!(err?.code && failoverErrorCodes.has(`${err.code}`));
273
+ };
274
+ exports.isFailoverRequired = isFailoverRequired;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@nestjs-kitchen/connextion-postgres",
3
3
  "private": false,
4
4
  "description": "A flexible module to provide node-postgres interface in NextJS.",
5
- "version": "2.0.6",
5
+ "version": "2.1.1",
6
6
  "homepage": "https://github.com/yikenman/nestjs-kitchen",
7
7
  "repository": "https://github.com/yikenman/nestjs-kitchen",
8
8
  "author": "yikenman",
@@ -23,15 +23,15 @@
23
23
  },
24
24
  "devDependencies": {
25
25
  "@nestjs/testing": "^11.0.0",
26
- "@types/jest": "^29.5.14",
26
+ "@types/jest": "^30.0.0",
27
27
  "@types/node": "^22.13.9",
28
- "jest": "^29.7.0",
28
+ "jest": "^30.0.5",
29
29
  "rimraf": "^6.0.1",
30
30
  "ts-jest": "^29.3.0",
31
31
  "ts-node": "^10.9.2",
32
32
  "tsconfig-paths": "^4.2.0",
33
33
  "typescript": "^5.8.2",
34
- "@nestjs-kitchen/connextion": "2.0.5"
34
+ "@nestjs-kitchen/connextion": "2.0.6"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=20.13.0"
@@ -51,7 +51,7 @@
51
51
  "@types/pg": "^8.11.10",
52
52
  "pg": "^8.13.1",
53
53
  "reflect-metadata": "^0.2.2",
54
- "@nestjs-kitchen/connextion": "2.0.5"
54
+ "@nestjs-kitchen/connextion": "2.0.6"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "rimraf dist && tsc -p tsconfig.build.json",