pogi 2.10.2 → 3.0.0-beta2

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.
Files changed (83) hide show
  1. package/.vscode/launch.json +47 -15
  2. package/CHANGELOG.md +11 -0
  3. package/docs/API/PgDb.md +25 -0
  4. package/docs/notification.md +19 -0
  5. package/jest.config.js +23 -0
  6. package/lib/bin/generateInterface.js +3 -3
  7. package/lib/bin/generateInterface.js.map +1 -1
  8. package/lib/connectionOptions.d.ts +10 -0
  9. package/lib/index.d.ts +1 -1
  10. package/lib/pgConverters.d.ts +9 -8
  11. package/lib/pgConverters.js +46 -32
  12. package/lib/pgConverters.js.map +1 -1
  13. package/lib/pgConverters.test.d.ts +1 -0
  14. package/lib/pgConverters.test.js +13 -0
  15. package/lib/pgConverters.test.js.map +1 -0
  16. package/lib/pgDb.d.ts +27 -27
  17. package/lib/pgDb.js +293 -100
  18. package/lib/pgDb.js.map +1 -1
  19. package/lib/pgDb.test.d.ts +1 -0
  20. package/lib/pgDb.test.js +1126 -0
  21. package/lib/pgDb.test.js.map +1 -0
  22. package/lib/pgDbInterface.d.ts +53 -0
  23. package/lib/pgDbInterface.js +11 -0
  24. package/lib/pgDbInterface.js.map +1 -0
  25. package/lib/pgDbOperators.d.ts +3 -3
  26. package/lib/pgDbOperators.js +4 -7
  27. package/lib/pgDbOperators.js.map +1 -1
  28. package/lib/pgDbOperators.test.d.ts +1 -0
  29. package/lib/pgDbOperators.test.js +313 -0
  30. package/lib/pgDbOperators.test.js.map +1 -0
  31. package/lib/pgSchema.d.ts +10 -9
  32. package/lib/pgSchema.js.map +1 -1
  33. package/lib/pgSchemaInterface.d.ts +12 -0
  34. package/lib/pgSchemaInterface.js +3 -0
  35. package/lib/pgSchemaInterface.js.map +1 -0
  36. package/lib/pgTable.d.ts +15 -40
  37. package/lib/pgTable.js +54 -54
  38. package/lib/pgTable.js.map +1 -1
  39. package/lib/pgTableInterface.d.ts +102 -0
  40. package/lib/pgTableInterface.js +4 -0
  41. package/lib/pgTableInterface.js.map +1 -0
  42. package/lib/pgUtils.d.ts +16 -6
  43. package/lib/pgUtils.js +162 -31
  44. package/lib/pgUtils.js.map +1 -1
  45. package/lib/queryAble.d.ts +20 -53
  46. package/lib/queryAble.js +149 -80
  47. package/lib/queryAble.js.map +1 -1
  48. package/lib/queryAbleInterface.d.ts +55 -0
  49. package/lib/queryAbleInterface.js +7 -0
  50. package/lib/queryAbleInterface.js.map +1 -0
  51. package/lib/queryWhere.d.ts +2 -2
  52. package/lib/queryWhere.js +19 -23
  53. package/lib/queryWhere.js.map +1 -1
  54. package/mkdocs.yml +1 -0
  55. package/package.json +21 -11
  56. package/src/bin/generateInterface.ts +2 -2
  57. package/src/connectionOptions.ts +48 -13
  58. package/src/index.d.ts +7 -0
  59. package/src/index.ts +1 -1
  60. package/src/pgConverters.test.ts +10 -0
  61. package/src/pgConverters.ts +34 -22
  62. package/src/pgDb.test.ts +1324 -0
  63. package/src/pgDb.ts +318 -122
  64. package/src/pgDbInterface.ts +57 -0
  65. package/src/pgDbOperators.test.ts +478 -0
  66. package/src/pgDbOperators.ts +45 -22
  67. package/src/pgSchema.ts +10 -9
  68. package/src/pgSchemaInterface.ts +12 -0
  69. package/src/pgTable.ts +66 -98
  70. package/src/pgTableInterface.ts +131 -0
  71. package/src/pgUtils.ts +166 -42
  72. package/src/queryAble.ts +167 -125
  73. package/src/queryAbleInterface.ts +104 -0
  74. package/src/queryWhere.ts +42 -43
  75. package/{spec/resources → src/test}/init.sql +23 -0
  76. package/src/test/pgServiceRestartTest.ts +1500 -0
  77. package/{spec/resources → src/test}/throw_exception.sql +0 -0
  78. package/{spec/resources → src/test}/tricky.sql +0 -0
  79. package/{src/tsconfig.json → tsconfig.json} +12 -11
  80. package/spec/run.js +0 -5
  81. package/spec/support/jasmine.json +0 -9
  82. package/src/test/pgDbOperatorSpec.ts +0 -492
  83. package/src/test/pgDbSpec.ts +0 -994
package/src/pgDb.ts CHANGED
@@ -1,14 +1,17 @@
1
- import { QueryAble, ResultFieldType } from "./queryAble";
1
+ import { QueryAble } from "./queryAble";
2
+ import { IPgDb, ResultFieldType, PostProcessResultFunc, Notification, TranzactionIsolationLevel } from "./pgDbInterface";
3
+ import { IPgTable } from "./pgTableInterface";
2
4
  import { PgTable } from "./pgTable";
3
5
  import { PgSchema } from "./pgSchema";
6
+ import { IPgSchema } from "./pgSchemaInterface";
4
7
  import * as PgConverters from "./pgConverters";
5
8
  import { pgUtils } from "./pgUtils";
6
9
  import * as _ from 'lodash';
7
10
  import * as pg from 'pg';
8
11
  import * as readline from 'readline';
9
12
  import * as fs from 'fs';
10
- import {PgDbLogger} from './pgDbLogger';
11
- import {ConnectionOptions} from './connectionOptions';
13
+ import { PgDbLogger } from './pgDbLogger';
14
+ import { ConnectionOptions } from './connectionOptions';
12
15
  import * as EventEmitter from 'events';
13
16
 
14
17
  const CONNECTION_URL_REGEXP = /^postgres:\/\/(?:([^:]+)(?::([^@]*))?@)?([^\/:]+)?(?::([^\/]+))?\/(.*)$/;
@@ -57,7 +60,7 @@ const GET_CURRENT_SCHEMAS = "SELECT current_schemas(false)";
57
60
  /** We get back fields as well that we don't have access to, thus we need to filter those schemas that we have permission for
58
61
  * ... TODO check it for tables */
59
62
  const LIST_SPECIAL_TYPE_FIELDS =
60
- `SELECT c.nspname as schema_name, b.relname as table_name, a.attname as column_name, a.atttypid as typid
63
+ `SELECT c.nspname as schema_name, b.relname as table_name, a.attname as column_name, a.atttypid as typid, t.typcategory
61
64
  FROM pg_attribute a
62
65
  JOIN pg_class b ON (a.attrelid = b.oid)
63
66
  JOIN pg_type t ON (a.atttypid = t.oid)
@@ -65,54 +68,59 @@ const LIST_SPECIAL_TYPE_FIELDS =
65
68
  WHERE (a.atttypid in (114, 3802, 1082, 1083, 1114, 1184, 1266, 3614) or t.typcategory='A')
66
69
  AND reltype>0 `;
67
70
  //AND c.nspname not in ('pg_catalog', 'pg_constraint', 'information_schema')
71
+ const TYPE2OID = `SELECT t.oid, typname FROM pg_catalog.pg_type t WHERE typname in (
72
+ '_bool',
73
+ 'int8','_int2','_int4','_int8','_float4','float8','_float8',
74
+ '_text','_varchar',
75
+ 'json','jsonb', '_json','_jsonb',
76
+ 'date','time','timestamp','timestamptz','timetz','_date','_time','_timestamp','_timestamptz','_timetz',
77
+ 'tsvector')`
68
78
 
69
79
  export enum FieldType { JSON, ARRAY, TIME, TSVECTOR }
70
80
 
71
- export enum TranzactionIsolationLevel {
72
- serializable = 'SERIALIZABLE',
73
- repeatableRead = 'REPEATABLE READ',
74
- readCommitted = 'READ COMMITTED',
75
- readUncommitted = 'READ UNCOMMITTED'
76
- }
77
-
78
- export type PostProcessResultFunc = (res: any[], fields: ResultFieldType[], logger: PgDbLogger) => void;
79
81
 
80
- /** LISTEN callback parameter */
81
- export interface Notification {
82
- processId: number,
83
- channel: string,
84
- payload?: string
85
- }
86
82
 
87
- export class PgDb extends QueryAble {
83
+ export class PgDb extends QueryAble implements IPgDb {
88
84
  protected static instances: { [index: string]: Promise<PgDb> };
89
85
  /*protected*/
90
- pool;
91
- connection;
86
+ pool: pg.Pool;
87
+ connection: pg.PoolClient | null = null;
92
88
 
93
- protected connectionForListen;
94
89
  /*protected*/
95
90
  config: ConnectionOptions;
96
91
  /*protected*/
97
- defaultSchemas; // for this.tables and this.fn
92
+ defaultSchemas: string[]; // for this.tables and this.fn
98
93
 
99
- db;
94
+ db: IPgDb;
100
95
  schemas: { [name: string]: PgSchema };
101
96
  tables: { [name: string]: PgTable<any> } = {};
102
- fn: { [name: string]: (...any) => any } = {};
97
+ fn: { [name: string]: (...args: any[]) => any } = {};
103
98
  [name: string]: any | PgSchema;
104
99
  /* protected */
105
- pgdbTypeParsers = {};
100
+ pgdbTypeParsers: Record<string, (s: any) => any> = {};
106
101
  /* protected */
107
- postProcessResult: PostProcessResultFunc;
108
-
109
- private constructor(pgdb: { defaultSchemas?, config?, schemas?, pool?, pgdbTypeParsers?, getLogger?: () => any, postProcessResult?: PostProcessResultFunc } = {}) {
102
+ knownOids: Record<number, boolean> = {};
103
+ /* protected */
104
+ postProcessResult: PostProcessResultFunc | undefined | null;
105
+
106
+ private constructor(pgdb: {
107
+ defaultSchemas?: string[],
108
+ config?: ConnectionOptions,
109
+ schemas?: Record<string, PgSchema>,
110
+ pool?: pg.Pool,
111
+ pgdbTypeParsers?: Record<string, (s: any) => any>,
112
+ knownOids?: Record<number, boolean>,
113
+ getLogger?: () => any,
114
+ postProcessResult?: PostProcessResultFunc | null
115
+ }) {
110
116
  super();
111
117
  this.schemas = {};
112
- this.config = pgdb.config;
113
- this.pool = pgdb.pool;
118
+ this.config = pgdb.config || {};
119
+ // pool is initialized in init, because it is asynchronous, but it is needed to call after constructor
120
+ this.pool = pgdb.pool!;
114
121
  this.postProcessResult = pgdb.postProcessResult;
115
122
  this.pgdbTypeParsers = pgdb.pgdbTypeParsers || {};
123
+ this.knownOids = pgdb.knownOids || {};
116
124
  this.db = this;
117
125
  if (pgdb.getLogger) {
118
126
  this.setLogger(pgdb.getLogger());
@@ -130,11 +138,11 @@ export class PgDb extends QueryAble {
130
138
  }
131
139
  }
132
140
 
133
- this.defaultSchemas = pgdb.defaultSchemas;
141
+ this.defaultSchemas = pgdb.defaultSchemas || [];
134
142
  this.setDefaultTablesAndFunctions();
135
143
  }
136
144
 
137
- setPostProcessResult(f: (res: any[], fields: ResultFieldType[], logger: PgDbLogger) => void) {
145
+ setPostProcessResult(f: null | ((res: any[], fields: ResultFieldType[], logger: PgDbLogger) => void)) {
138
146
  this.postProcessResult = f;
139
147
  }
140
148
 
@@ -154,7 +162,7 @@ export class PgDb extends QueryAble {
154
162
  if (!PgDb.instances) {
155
163
  PgDb.instances = {};
156
164
  }
157
- if (PgDb.instances[connectionString]) {
165
+ if (PgDb.instances[connectionString] != null) {
158
166
  return PgDb.instances[connectionString];
159
167
  } else {
160
168
  let pgdb = new PgDb({ config: config });
@@ -170,7 +178,7 @@ export class PgDb extends QueryAble {
170
178
  delete PgDb.instances[cs];
171
179
  }
172
180
  }
173
- await this.pool.end();
181
+ await new Promise<void>((resolve) => this.pool.end(resolve));
174
182
  }
175
183
 
176
184
  static async connect(config: ConnectionOptions): Promise<PgDb> {
@@ -213,7 +221,7 @@ export class PgDb extends QueryAble {
213
221
  let schemasAndTables = await this.query(LIST_SCHEMAS_TABLES);
214
222
  let functions = await this.query(GET_SCHEMAS_PROCEDURES);
215
223
 
216
- this.defaultSchemas = PgConverters.arraySplit(await this.queryOneField(GET_CURRENT_SCHEMAS));
224
+ this.defaultSchemas = await this.queryOneField(GET_CURRENT_SCHEMAS);
217
225
 
218
226
  let oldSchemaNames = Object.keys(this.schemas);
219
227
  for (let sc of oldSchemaNames) {
@@ -260,121 +268,208 @@ export class PgDb extends QueryAble {
260
268
 
261
269
  private async initFieldTypes() {
262
270
  //--- init field types -------------------------------------------
263
- let schemaNames = "'" + Object.keys(this.schemas).join("', '") + "'";
264
- if (schemaNames=="''") {
271
+ let schemaNames = "'" + Object.keys(this.schemas).join("', '") + "'";
272
+ if (schemaNames == "''") {
265
273
  this.getLogger(true).error("No readable schema found!");
266
274
  return;
267
275
  }
268
- let specialTypeFields: { schema_name: string, table_name: string, column_name: string, typid: number }[]
276
+ let type2oid = {};
277
+ let res = await this.query(TYPE2OID);
278
+ for (let tt of res || []) {
279
+ type2oid[tt.typname] = +tt.oid;
280
+ }
281
+
282
+ let specialTypeFields: { schema_name: string, table_name: string, column_name: string, typid: number, typcategory: string }[]
269
283
  = await this.query(LIST_SPECIAL_TYPE_FIELDS + ' AND c.nspname in (' + schemaNames + ')');
270
284
 
271
285
  for (let r of specialTypeFields) {
272
286
  if (this.schemas[r.schema_name][r.table_name]) {
273
287
  this.schemas[r.schema_name][r.table_name].fieldTypes[r.column_name] =
274
- ([3802, 114].indexOf(r.typid) > -1) ? FieldType.JSON :
275
- ([3614].indexOf(r.typid) > -1) ? FieldType.TSVECTOR :
276
- ([1082, 1083, 1114, 1184, 1266].indexOf(r.typid) > -1) ? FieldType.TIME :
288
+ ([type2oid['json'], type2oid['jsonb']].indexOf(r.typid) > -1) ? FieldType.JSON :
289
+ ([type2oid['tsvector']].indexOf(r.typid) > -1) ? FieldType.TSVECTOR :
290
+ ([type2oid['date'], type2oid['time'], type2oid['timestamp'], type2oid['timestamptz'], type2oid['timetz']].indexOf(r.typid) > -1) ? FieldType.TIME :
277
291
  FieldType.ARRAY;
278
292
  }
279
293
  }
280
294
 
295
+ // https://web.archive.org/web/20160613215445/https://doxygen.postgresql.org/include_2catalog_2pg__type_8h_source.html
296
+ // https://github.com/lib/pq/blob/master/oid/types.go
297
+
298
+ let builtInArrayTypeParsers: { oidList: number[], parser: (s: string) => any }[] = [
299
+ {
300
+ oidList: [
301
+ type2oid['_bool'] // bool[]
302
+ ],
303
+ parser: PgConverters.parseBooleanArray
304
+ },
305
+ {
306
+ oidList: [
307
+ type2oid['_int2'], // smallInt[] int2[]
308
+ type2oid['_int4'], // integer[] int4[]
309
+ type2oid['_float4'], // real[] float4[]
310
+ ],
311
+ parser: PgConverters.parseNumberArray
312
+ },
313
+ {
314
+ oidList: [
315
+ type2oid['_text'], // text[]
316
+ type2oid['_varchar'] // varchar[]
317
+ ],
318
+ parser: PgConverters.parseArray
319
+ },
320
+ {
321
+ oidList: [
322
+ type2oid['_json'], // json[]
323
+ type2oid['_jsonb'] // jsonb[]
324
+ ],
325
+ parser: PgConverters.parseJsonArray
326
+ },
327
+ {
328
+ oidList: [
329
+ type2oid['_date'], // date[]
330
+ type2oid['_time'],
331
+ type2oid['_timetz'],
332
+ type2oid['_timestamp'], //timestamp[]
333
+ type2oid['_timestamptz'],
334
+ ],
335
+ parser: PgConverters.parseDateArray
336
+ }
337
+ ];
338
+
339
+ builtInArrayTypeParsers.forEach(parserObj => {
340
+ parserObj.oidList.forEach(oid => {
341
+ pg.types.setTypeParser(oid, parserObj.parser);
342
+ delete this.pgdbTypeParsers[oid];
343
+ this.knownOids[oid] = true;
344
+ });
345
+ });
346
+
281
347
  for (let r of specialTypeFields) {
282
- // https://doxygen.postgresql.org/include_2catalog_2pg__type_8h_source.html
348
+ if (this.knownOids[r.typid] && !this.pgdbTypeParsers[r.typid]) {
349
+ continue;
350
+ }
283
351
  switch (r.typid) {
284
- case 114: // json
285
- case 3802: // jsonb
286
- case 1082: // date
287
- case 1083: // time
288
- case 1114: // timestamp
289
- case 1184: // timestamptz
290
- case 1266: // timetz
291
- case 3614: // tsvector
292
- break;
293
- case 1005: // smallInt[] int2[]
294
- case 1007: // integer[] int4[]
295
- case 1021: // real[] float4[]
296
- pg.types.setTypeParser(r.typid, PgConverters.arraySplitToNum);
297
- break;
298
- case 1009: // text[]
299
- case 1015: // varchar[]
300
- pg.types.setTypeParser(r.typid, PgConverters.arraySplit);
301
- break;
302
- case 3807:
303
- pg.types.setTypeParser(r.typid, PgConverters.arraySplitToJson);
304
- break;
305
- case 1016: // bigInt[] int8[]
306
- case 1022: // double[] float8[]
307
- //pg.types.setTypeParser(r.typid, arraySplitToNumWithValidation);
308
- break;
309
- case 1115: // timestamp[]
310
- case 1182: // date[]
311
- case 1183: // time[]
312
- case 1185: // timestamptz[]
313
- case 1270: // timetz[]
314
- pg.types.setTypeParser(r.typid, PgConverters.arraySplitToDate);
352
+ case type2oid['json']:
353
+ case type2oid['jsonb']:
354
+ case type2oid['date']: // date
355
+ case type2oid['time']: // time
356
+ case type2oid['timetz']: // timetz
357
+ case type2oid['timestamp']: // timestamp
358
+ case type2oid['timestamptz']: // timestamptz
359
+ case type2oid['tsvector']: // tsvector
360
+ case type2oid['_int8']: // bigInt[] int8[]
361
+ case type2oid['_float8']: // double[] float8[]
315
362
  break;
316
363
  default:
317
364
  //best guess otherwise user need to specify
318
- pg.types.setTypeParser(r.typid, PgConverters.arraySplit);
365
+ pg.types.setTypeParser(r.typid, PgConverters.parseArray);
366
+ delete this.pgdbTypeParsers[r.typid];
319
367
  }
320
368
  }
321
-
322
- //has to set outside of pgjs as it doesnt support exceptions (stop everything immediately)
323
- await this.setPgDbTypeParser('int8', PgConverters.numWithValidation); //int8 - 20
324
- await this.setPgDbTypeParser('float8', PgConverters.numWithValidation); //float8 - 701
325
- await this.setPgDbTypeParser('_int8', PgConverters.stringArrayToNumWithValidation);
326
- await this.setPgDbTypeParser('_float8', PgConverters.stringArrayToNumWithValidation);
369
+ this.pgdbTypeParsers[type2oid['int8']] = PgConverters.parseNumberWithValidation;
370
+ this.pgdbTypeParsers[type2oid['float8']] = PgConverters.parseNumberWithValidation;
371
+ this.pgdbTypeParsers[type2oid['_int8']] = PgConverters.parseNumberArrayWithValidation;
372
+ this.pgdbTypeParsers[type2oid['_float8']] = PgConverters.parseNumberArrayWithValidation;
373
+ this.knownOids[type2oid['int8']] = true;
374
+ this.knownOids[type2oid['float8']] = true;
375
+ this.knownOids[type2oid['_int8']] = true;
376
+ this.knownOids[type2oid['_float8']] = true;
377
+
378
+
379
+ let allUsedTypeFields = await this.queryOneColumn(`
380
+ SELECT distinct a.atttypid as typid
381
+ FROM pg_attribute a
382
+ JOIN pg_class b ON (a.attrelid = b.oid)
383
+ JOIN pg_type t ON (a.atttypid = t.oid)
384
+ JOIN pg_namespace c ON (b.relnamespace=c.oid)
385
+ WHERE
386
+ reltype>0 AND
387
+ c.nspname in (${schemaNames})`
388
+ );
389
+ allUsedTypeFields.forEach(oid => this.knownOids[oid] = true);
327
390
  }
328
391
 
329
392
  /**
330
393
  * if schemaName is null, it will be applied for all schemas
331
394
  */
332
- async setTypeParser(typeName: string, parser: (string) => any, schemaName?: string): Promise<void> {
395
+ async setTypeParser(typeName: string, parser: (s: string) => any, schemaName?: string): Promise<void> {
333
396
  try {
334
397
  if (schemaName) {
335
398
  let oid = await this.queryOneField(GET_OID_FOR_COLUMN_TYPE_FOR_SCHEMA, { typeName, schemaName });
336
399
  pg.types.setTypeParser(oid, parser);
337
400
  delete this.pgdbTypeParsers[oid];
401
+ this.knownOids[oid] = true;
338
402
  } else {
339
403
  let list = await this.queryOneColumn(GET_OID_FOR_COLUMN_TYPE, { typeName });
340
404
  list.forEach(oid => {
341
405
  pg.types.setTypeParser(oid, parser);
342
406
  delete this.pgdbTypeParsers[oid];
407
+ this.knownOids[oid] = true;
343
408
  });
344
409
  }
345
410
  } catch (e) {
346
- throw Error('Not existing type: ' + typeName);
411
+ throw new Error('Not existing type: ' + typeName);
347
412
  }
348
413
  }
349
414
 
350
- async setPgDbTypeParser(typeName: string, parser: (string) => any, schemaName?: string): Promise<void> {
415
+ async setPgDbTypeParser(typeName: string, parser: (s: string) => any, schemaName?: string): Promise<void> {
351
416
  try {
352
417
  if (schemaName) {
353
418
  let oid = await this.queryOneField(GET_OID_FOR_COLUMN_TYPE_FOR_SCHEMA, { typeName, schemaName });
354
419
  this.pgdbTypeParsers[oid] = parser;
420
+ this.knownOids[oid] = true;
355
421
  } else {
356
422
  let list = await this.queryOneColumn(GET_OID_FOR_COLUMN_TYPE, { typeName });
357
- list.forEach(oid => this.pgdbTypeParsers[oid] = parser);
423
+ list.forEach(oid => {
424
+ this.pgdbTypeParsers[oid] = parser;
425
+ this.knownOids[oid] = true;
426
+ });
358
427
  }
359
428
  } catch (e) {
360
- throw Error('Not existing type: ' + typeName);
429
+ throw new Error('Not existing type: ' + typeName);
430
+ }
431
+ }
432
+
433
+ async resetMissingParsers(connection: pg.PoolClient, oidList: number[]): Promise<void> {
434
+ let unknownOids = oidList.filter(oid => !this.knownOids[oid]);
435
+ if (unknownOids.length) {
436
+ let fieldsData = await connection.query(
437
+ `select oid, typcategory from pg_type where oid = ANY($1)`,
438
+ [unknownOids]
439
+ );
440
+
441
+ fieldsData.rows.forEach(fieldData => {
442
+ if (fieldData.typcategory == 'A') {
443
+ this.pgdbTypeParsers[fieldData.oid] = PgConverters.parseArray;
444
+ }
445
+ this.knownOids[fieldData.oid] = true;
446
+ });
361
447
  }
362
448
  }
363
449
 
364
450
  async dedicatedConnectionBegin(): Promise<PgDb> {
451
+ if (this.needToFixConnectionForListen()) {
452
+ await this.runRestartConnectionForListen();
453
+ }
365
454
  let pgDb = new PgDb(this);
366
455
  pgDb.connection = await this.pool.connect();
456
+ pgDb.connection.on('error', QueryAble.connectionErrorListener); //When there is an error, then the actual called query will throw exception
367
457
  return pgDb;
368
458
  }
369
459
 
370
460
  async dedicatedConnectionEnd(): Promise<PgDb> {
371
461
  if (this.connection) {
372
- await this.connection.release();
462
+ this.connection.off('error', QueryAble.connectionErrorListener);
463
+ try {
464
+ await this.connection.release();
465
+ } catch (err) {
466
+ this.getLogger().error('Error while dedicated connection end.', err);
467
+ }
373
468
  this.connection = null;
374
469
  }
375
470
  return this;
376
471
  }
377
-
472
+
378
473
  /**
379
474
  * transaction save point
380
475
  * https://www.postgresql.org/docs/current/sql-savepoint.html
@@ -384,7 +479,7 @@ export class PgDb extends QueryAble {
384
479
  name = (name || '').replace(/"/g, '');
385
480
  await this.query(`SAVEPOINT "${name}"`);
386
481
  } else {
387
- throw Error('No active transaction');
482
+ throw new Error('No active transaction');
388
483
  }
389
484
  return this;
390
485
  }
@@ -398,12 +493,12 @@ export class PgDb extends QueryAble {
398
493
  name = (name || '').replace(/"/g, '');
399
494
  await this.query(`RELEASE SAVEPOINT "${name}"`);
400
495
  } else {
401
- throw Error('No active transaction');
496
+ throw new Error('No active transaction');
402
497
  }
403
498
  return this;
404
499
  }
405
500
 
406
- async transactionBegin(options?: { isolationLevel?: TranzactionIsolationLevel, deferrable?:boolean, readOnly?: boolean}): Promise<PgDb> {
501
+ async transactionBegin(options?: { isolationLevel?: TranzactionIsolationLevel, deferrable?: boolean, readOnly?: boolean }): Promise<PgDb> {
407
502
  let pgDb = this.connection ? this : await this.dedicatedConnectionBegin();
408
503
  let q = 'BEGIN'
409
504
  if (options?.isolationLevel) {
@@ -440,13 +535,13 @@ export class PgDb extends QueryAble {
440
535
  return this.connection != null;
441
536
  }
442
537
 
443
- async execute(fileName: string, statementTransformerFunction?: (string) => string): Promise<void> {
538
+ async execute(fileName: string, statementTransformerFunction?: (s: string) => string): Promise<void> {
444
539
  let isTransactionInPlace = this.isTransactionActive();
445
540
  // statements must be run in a dedicated connection
446
541
  let pgdb = isTransactionInPlace ? this : await this.dedicatedConnectionBegin();
447
542
 
448
543
  /** run statements one after the other */
449
- let runStatementList = (statementList) => {
544
+ let runStatementList = (statementList: string[]) => {
450
545
  //this.getLogger(true).log('consumer start', commands.length);
451
546
  return new Promise((resolve, reject) => {
452
547
  let currentStatement = 0;
@@ -474,10 +569,10 @@ export class PgDb extends QueryAble {
474
569
 
475
570
  let lineCounter = 0;
476
571
  let promise = new Promise<void>((resolve, reject) => {
477
- let statementList = [];
478
- let tmp = '', t: RegExpExecArray;
479
- let consumer;
480
- let inQuotedString:string;
572
+ let statementList: string[] = [];
573
+ let tmp = '', t: RegExpExecArray | null;
574
+ let consumer: Promise<any> | null;
575
+ let inQuotedString: string | null;
481
576
  let rl = readline.createInterface({
482
577
  input: fs.createReadStream(fileName),
483
578
  terminal: false
@@ -498,7 +593,7 @@ export class PgDb extends QueryAble {
498
593
  let s = line.slice(SQL_TOKENIZER_REGEXP.lastIndex - 1);
499
594
  let token = s.match(SQL_$_ESCAPE_REGEXP);
500
595
  if (!token) {
501
- throw Error('Invalid sql in line: ' + line);
596
+ throw new Error('Invalid sql in line: ' + line);
502
597
  }
503
598
  inQuotedString = token[0];
504
599
  SQL_TOKENIZER_REGEXP.lastIndex += inQuotedString.length - 1;
@@ -563,7 +658,7 @@ export class PgDb extends QueryAble {
563
658
  }
564
659
  });
565
660
  });
566
- let error;
661
+ let error: Error | null;
567
662
  return promise
568
663
  .catch((e) => {
569
664
  error = e;
@@ -586,21 +681,37 @@ export class PgDb extends QueryAble {
586
681
  }
587
682
 
588
683
  private listeners = new EventEmitter();
684
+ private connectionForListen: pg.PoolClient | null = null;
685
+ private _needToRestartConnectionForListen = false;
686
+ private restartConnectionForListen: Promise<Error | null> | null = null;
589
687
 
590
688
  /**
591
689
  * LISTEN to a channel for a NOTIFY (https://www.postgresql.org/docs/current/sql-listen.html)
592
690
  * One connection will be dedicated for listening if there are any listeners.
593
691
  * When there is no other callback for a channel, LISTEN command is executed
594
692
  */
595
- async listen(channel: string, callback: (notification:Notification) => void) {
596
- if (!this.connectionForListen) {
597
- this.connectionForListen = await this.pool.connect();
598
- this.connectionForListen.on('notification', (notification: Notification) => this.listeners.emit(notification.channel, notification));
693
+ async listen(channel: string, callback: (notification: Notification) => void) {
694
+ let restartConnectionError: Error | null = null;
695
+ if (this.needToFixConnectionForListen()) {
696
+ restartConnectionError = await this.runRestartConnectionForListen();
599
697
  }
600
- if (!this.listeners.listenerCount(channel)) {
601
- await this.connectionForListen.query('LISTEN ' + channel);
698
+ if (this.listeners.listenerCount(channel)) {
699
+ this.listeners.on(channel, callback);
700
+ } else {
701
+ if (restartConnectionError) {
702
+ throw restartConnectionError;
703
+ }
704
+ try {
705
+ if (!this.connectionForListen) {
706
+ await this.initConnectionForListen();
707
+ }
708
+ await this.connectionForListen!.query(`LISTEN "${channel}"`);
709
+ } catch (err) {
710
+ this._needToRestartConnectionForListen = true;
711
+ throw err;
712
+ }
713
+ this.listeners.on(channel, callback);
602
714
  }
603
- this.listeners.on(channel, callback);
604
715
  }
605
716
 
606
717
  /**
@@ -608,37 +719,122 @@ export class PgDb extends QueryAble {
608
719
  * When all callback is removed from a channel UNLISTEN command is executed
609
720
  * When all callback is removed from all channel, dedicated connection is released
610
721
  */
611
- async unlisten(channel: string, callback?: (Notification) => void) {
612
- if (!this.connectionForListen) {
613
- this.listeners.removeAllListeners();
614
- return;
722
+ async unlisten(channel: string, callback?: (notification: Notification) => void) {
723
+ let restartConnectionError: Error | null = null;
724
+ if (this.needToFixConnectionForListen()) {
725
+ restartConnectionError = await this.runRestartConnectionForListen();
615
726
  }
616
- if (callback) {
727
+ if (callback && this.listeners.listenerCount(channel) > 1) {
617
728
  this.listeners.removeListener(channel, callback);
618
729
  } else {
730
+ if (restartConnectionError) {
731
+ throw restartConnectionError;
732
+ }
733
+ try {
734
+ await this.internalQuery({ connection: this.connectionForListen, sql: `UNLISTEN "${channel}"` });
735
+ if (this.connectionForListen && this.listeners.eventNames().length == 1) {
736
+ this.connectionForListen.removeAllListeners('notification');
737
+ this.connectionForListen.release();
738
+ this.connectionForListen = null;
739
+ }
740
+ } catch (err) {
741
+ this._needToRestartConnectionForListen = true;
742
+ throw err;
743
+ }
619
744
  this.listeners.removeAllListeners(channel);
620
745
  }
621
-
622
- if (!this.listeners.listenerCount(channel)) {
623
- await this.internalQuery({ connection: this.connectionForListen, sql: 'UNLISTEN ' + channel });
624
- }
625
- let allListeners = this.listeners.eventNames().reduce((sum, ename) => sum + this.listeners.listenerCount(ename), 0);
626
- if (!allListeners && this.connectionForListen) {
627
- this.connectionForListen.removeAllListeners('notification');
628
- this.connectionForListen.release();
629
- this.connectionForListen = null;
630
- }
631
746
  }
632
747
 
633
748
  /**
634
749
  * Notify a channel (https://www.postgresql.org/docs/current/sql-notify.html)
635
750
  */
636
751
  async notify(channel: string, payload?: string) {
752
+ if (this.needToFixConnectionForListen()) {
753
+ let restartConnectionError = await this.runRestartConnectionForListen();
754
+ if (restartConnectionError) {
755
+ throw restartConnectionError;
756
+ }
757
+ }
758
+ let hasConnectionForListen = !!this.connectionForListen;
637
759
  let connection = this.connectionForListen || this.connection;
638
760
  //let sql = 'NOTIFY ' + channel + ', :payload';
639
761
  let sql = 'SELECT pg_notify(:channel, :payload)';
640
762
  let params = { channel, payload };
641
- return this.internalQuery({ connection, sql, params });
763
+ try {
764
+ return this.internalQuery({ connection, sql, params });
765
+ } catch (err) {
766
+ if (hasConnectionForListen) {
767
+ this._needToRestartConnectionForListen = true;
768
+ }
769
+ throw err;
770
+ }
771
+ }
772
+
773
+ async runRestartConnectionForListen(): Promise<Error | null> {
774
+ if (!this._needToRestartConnectionForListen) {
775
+ return null;
776
+ }
777
+ let errorResult: Error | null = null;
778
+ if (!this.restartConnectionForListen) {
779
+ this.restartConnectionForListen = (async () => {
780
+ let eventNames = this.listeners.eventNames();
781
+ if (this.connectionForListen) {
782
+ try {
783
+ this.connectionForListen.on('notification', this.notificationListener);
784
+ this.connectionForListen.on('error', this.errorListener);
785
+ } catch (e) { }
786
+ try {
787
+ await this.connectionForListen.release();
788
+ } catch (e) { }
789
+ this.connectionForListen = null;
790
+ }
791
+ let error: Error | null = null;
792
+ if (eventNames.length) {
793
+ try {
794
+ await this.initConnectionForListen();
795
+ for (let channel of eventNames) {
796
+ await this.connectionForListen!.query(`LISTEN "${channel as string}"`);
797
+ }
798
+ } catch (err) {
799
+ error = <Error>err;
800
+ }
801
+ }
802
+ return error;
803
+ })();
804
+ errorResult = await this.restartConnectionForListen;
805
+ this.restartConnectionForListen = null;
806
+ } else {
807
+ errorResult = await this.restartConnectionForListen;
808
+ }
809
+ if (!errorResult) {
810
+ this._needToRestartConnectionForListen = false;
811
+ }
812
+ return errorResult;
813
+ }
814
+
815
+ needToFixConnectionForListen(): boolean {
816
+ return this._needToRestartConnectionForListen;
817
+ }
818
+
819
+ private async tryToFixConnectionForListenActively() {
820
+ await new Promise(r => setTimeout(r, 1000));
821
+ let error = await this.runRestartConnectionForListen();
822
+ if (error) {
823
+ await this.tryToFixConnectionForListenActively();
824
+ }
825
+ }
826
+
827
+ notificationListener = (notification: Notification) => this.listeners.emit(notification.channel, notification)
828
+
829
+ errorListener = (e: Error) => {
830
+ this._needToRestartConnectionForListen = true;
831
+ this.tryToFixConnectionForListenActively();
832
+ };
833
+
834
+ private async initConnectionForListen() {
835
+ this.connectionForListen = await this.pool.connect();
836
+ this.connectionForListen.on('notification', this.notificationListener);
837
+ this.connectionForListen.on('error', this.errorListener);
642
838
  }
643
839
  }
644
840