@pcg/postgres-pubsub 1.0.0-alpha.0

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.
@@ -0,0 +1,70 @@
1
+ import { OnModuleInit } from "@nestjs/common";
2
+ import { EventEmitter } from "events";
3
+ import { PubSubEngine } from "graphql-subscriptions";
4
+ import { Client, ClientConfig } from "pg";
5
+ import { BaseEntity, DataSource } from "typeorm";
6
+ import { Logger } from "@pcg/core";
7
+ import { MaybeNull } from "@pcg/predicates";
8
+ import { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions.js";
9
+
10
+ //#region src/postgres-pubsub.d.ts
11
+ type PubSubSubscriptionCallback = (...args: unknown[]) => void;
12
+ interface PubSubSubscription {
13
+ id: number;
14
+ channel: string;
15
+ callback: PubSubSubscriptionCallback;
16
+ }
17
+ /**
18
+ * Entity reference
19
+ * @example
20
+ * {
21
+ * __ref: 'Media#jsv:12jg839hkvgs'
22
+ * }
23
+ */
24
+ interface EntityRef {
25
+ __ref: string;
26
+ }
27
+ interface EntityWithId extends BaseEntity {
28
+ id: string;
29
+ }
30
+ interface ObjectWithId {
31
+ id: string;
32
+ }
33
+ declare class PostgresPubSub extends PubSubEngine implements OnModuleInit {
34
+ protected readonly dbConfig: PostgresConnectionOptions;
35
+ private readonly loggerFactory;
36
+ protected logger: Logger;
37
+ protected readonly dataSource: DataSource;
38
+ protected dbClient: Client;
39
+ protected ee: EventEmitter<[never]>;
40
+ protected subscriptions: PubSubSubscription[];
41
+ protected config: ClientConfig;
42
+ protected readonly retryLimit = 5;
43
+ protected isReinitializing: boolean;
44
+ onModuleInit(): Promise<void>;
45
+ keepDbConnectionAlive(): Promise<void>;
46
+ protected reinit(): Promise<void>;
47
+ protected addDbClientEventListeners(): void;
48
+ protected reconnect(): Promise<Client>;
49
+ publish(triggerName: string, payload: any, retryCount?: number): Promise<void>;
50
+ subscribe(triggerName: string, callback: PubSubSubscriptionCallback): Promise<number>;
51
+ unsubscribe(subId: number): Promise<void>;
52
+ private processNotification;
53
+ protected toRef(object: object): MaybeNull<EntityRef>;
54
+ protected minify(payload: any): any;
55
+ protected unref(entityRef: EntityRef): Promise<BaseEntity | ObjectWithId>;
56
+ protected unminify(payload: any): Promise<any>;
57
+ protected isEntityRef(value: unknown): value is EntityRef;
58
+ protected isEntityWithId(value: unknown): value is EntityWithId;
59
+ asyncIterator<T>(triggers: string | readonly string[]): AsyncIterator<T>;
60
+ }
61
+ //#endregion
62
+ //#region src/postgres-pubsub.module.d.ts
63
+ /**
64
+ * PostgresPubSubModule provides PostgreSQL NOTIFY/LISTEN based PubSub engine
65
+ * for GraphQL subscriptions in NestJS applications.
66
+ */
67
+ declare class PostgresPubSubModule {}
68
+ //#endregion
69
+ export { EntityRef, EntityWithId, ObjectWithId, PostgresPubSub, PostgresPubSubModule, PubSubSubscription, PubSubSubscriptionCallback };
70
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,265 @@
1
+ import { Global, Injectable, Module } from "@nestjs/common";
2
+ import { Interval } from "@nestjs/schedule";
3
+ import { InjectDataSource } from "@nestjs/typeorm";
4
+ import { EventEmitter } from "events";
5
+ import { PubSubEngine } from "graphql-subscriptions";
6
+ import { Client, escapeIdentifier, escapeLiteral } from "pg";
7
+ import { BaseEntity, DataSource } from "typeorm";
8
+ import { InjectDbConfig, InjectLoggerFactory, LoggerFactory, wait } from "@pcg/core";
9
+ import { isObject } from "@pcg/predicates";
10
+
11
+ //#region \0@oxc-project+runtime@0.95.0/helpers/decorateMetadata.js
12
+ function __decorateMetadata(k, v) {
13
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
14
+ }
15
+
16
+ //#endregion
17
+ //#region \0@oxc-project+runtime@0.95.0/helpers/decorate.js
18
+ function __decorate(decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ }
24
+
25
+ //#endregion
26
+ //#region src/postgres-pubsub.ts
27
+ var _ref, _ref2;
28
+ let PostgresPubSub = class PostgresPubSub$1 extends PubSubEngine {
29
+ dbClient;
30
+ ee = new EventEmitter();
31
+ subscriptions = [];
32
+ config;
33
+ retryLimit = 5;
34
+ isReinitializing = false;
35
+ async onModuleInit() {
36
+ this.logger = this.loggerFactory.create({ scope: this.constructor.name });
37
+ this.config = {
38
+ host: this.dbConfig.host,
39
+ port: this.dbConfig.port,
40
+ database: this.dbConfig.database,
41
+ user: this.dbConfig.username,
42
+ password: this.dbConfig.password,
43
+ ssl: this.dbConfig.ssl,
44
+ connectionTimeoutMillis: 3e4,
45
+ query_timeout: 3e4,
46
+ statement_timeout: 3e4,
47
+ keepAlive: true,
48
+ keepAliveInitialDelayMillis: 1e4
49
+ };
50
+ await this.reinit();
51
+ }
52
+ async keepDbConnectionAlive() {
53
+ if (this.isReinitializing) return;
54
+ try {
55
+ await this.dbClient.query("SELECT pg_backend_pid()");
56
+ } catch (error) {
57
+ if (error instanceof Error) this.logger.error("PubSub PostgreSQL connection check failed:", error);
58
+ await this.reinit();
59
+ }
60
+ }
61
+ async reinit() {
62
+ this.isReinitializing = true;
63
+ this.logger.info("🔌 Initializing PubSub PostgreSQL client...");
64
+ if (this.dbClient) {
65
+ this.dbClient.removeAllListeners();
66
+ this.dbClient.once("error", (error) => {
67
+ this.logger.error(`Previous DB client errored after reconnecting already:`, error);
68
+ });
69
+ this.dbClient.end();
70
+ }
71
+ this.dbClient = await this.reconnect();
72
+ this.logger.info("✅ PubSub PostgreSQL client connected");
73
+ this.addDbClientEventListeners();
74
+ this.isReinitializing = false;
75
+ }
76
+ addDbClientEventListeners() {
77
+ this.dbClient.on("notification", (message) => {
78
+ this.processNotification(message);
79
+ });
80
+ this.dbClient.on("end", () => {
81
+ this.logger.warn("⛓️‍💥 PubSub PostgreSQL client ended");
82
+ if (!this.isReinitializing) this.reinit();
83
+ });
84
+ this.dbClient.on("error", (error) => {
85
+ this.logger.error("🔴 PubSub PostgreSQL client error", error);
86
+ if (!this.isReinitializing) this.reinit();
87
+ });
88
+ }
89
+ async reconnect() {
90
+ this.logger.info("🔌 Connecting to PubSub PostgreSQL for notification streaming");
91
+ const startTime = Date.now();
92
+ const retryTimeout = 3e3;
93
+ for (let attempt = 1; attempt < this.retryLimit; attempt++) {
94
+ this.logger.info(`🔌 PostgreSQL connection attempt #${String(attempt)}...`);
95
+ try {
96
+ const dbClient = new Client(this.config);
97
+ const connecting = new Promise((resolve, reject) => {
98
+ dbClient.once("connect", resolve);
99
+ dbClient.once("end", () => {
100
+ reject(Error("Connection ended."));
101
+ });
102
+ dbClient.once("error", reject);
103
+ });
104
+ await Promise.all([dbClient.connect(), connecting]);
105
+ this.logger.info("✅ PostgreSQL connection succeeded");
106
+ return dbClient;
107
+ } catch (error) {
108
+ if (error instanceof Error) this.logger.error("🔌 PostgreSQL connection attempt failed:", error);
109
+ await wait(500);
110
+ if (Date.now() - startTime > retryTimeout) throw new Error(`🚫 Stopping PostgreSQL connection attempts after ${String(retryTimeout)}ms timeout has been reached.`);
111
+ }
112
+ }
113
+ throw new Error("🔴 Reconnecting notification client to PostgreSQL database failed.");
114
+ }
115
+ async publish(triggerName, payload, retryCount = 1) {
116
+ if (!payload) throw new Error("Payload is required argument");
117
+ const channel = escapeIdentifier(triggerName);
118
+ const message = escapeLiteral(JSON.stringify(this.minify(payload)));
119
+ try {
120
+ await this.dbClient.query(`NOTIFY ${channel}, ${message}`);
121
+ } catch (error) {
122
+ if (error instanceof Error) {
123
+ this.logger.error(`Can't publish PostgreSQL notification to channel "${channel}"`, error, { payload });
124
+ if (/connection/i.exec(error.message)) {
125
+ if (retryCount < 3) {
126
+ this.logger.error(`Try to reconnect to PubSub PostgreSQL Client...(Retry count: ${String(retryCount)})`);
127
+ await this.dbClient.end();
128
+ await this.dbClient.connect();
129
+ await this.publish(triggerName, payload, retryCount + 1);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ await Promise.resolve();
135
+ }
136
+ async subscribe(triggerName, callback) {
137
+ if (!this.subscriptions.some((s) => s.channel === triggerName)) {
138
+ const channel = escapeIdentifier(triggerName);
139
+ await this.dbClient.query(`LISTEN ${channel}`);
140
+ }
141
+ this.ee.on(triggerName, callback);
142
+ const subId = Math.floor(Math.random() * 1e5);
143
+ this.subscriptions.push({
144
+ id: subId,
145
+ channel: triggerName,
146
+ callback
147
+ });
148
+ return await Promise.resolve(subId);
149
+ }
150
+ async unsubscribe(subId) {
151
+ const index = this.subscriptions.findIndex((s) => s.id === subId);
152
+ if (index !== -1) {
153
+ const subscription = this.subscriptions[index];
154
+ this.subscriptions.splice(index, 1);
155
+ this.ee.removeListener(subscription.channel, subscription.callback);
156
+ if (!this.subscriptions.some((s) => s.channel === subscription.channel)) {
157
+ const channel = escapeIdentifier(subscription.channel);
158
+ await this.dbClient.query(`UNLISTEN ${channel}`);
159
+ }
160
+ }
161
+ }
162
+ async processNotification(message) {
163
+ try {
164
+ const minifiedPayload = JSON.parse(message.payload ?? "");
165
+ if (minifiedPayload.id) if (Object.keys(minifiedPayload).length === 1) this.ee.emit(message.channel, minifiedPayload);
166
+ else this.ee.emit(message.channel, await this.unminify(minifiedPayload));
167
+ else if (Array.isArray(minifiedPayload)) {
168
+ const payload = await this.unminify(minifiedPayload);
169
+ this.ee.emit(message.channel, payload);
170
+ } else {
171
+ const payload = {};
172
+ for (const [key, value] of Object.entries(minifiedPayload)) payload[key] = await this.unminify(value);
173
+ this.ee.emit(message.channel, payload);
174
+ }
175
+ } catch (error) {
176
+ if (error instanceof Error) this.logger.error(`Can't extract PostgreSQL notificaton from channel ${message.channel}`, error, { message });
177
+ }
178
+ }
179
+ toRef(object) {
180
+ if (this.isEntityWithId(object)) return { __ref: `${object.constructor.name}#${object.id}` };
181
+ return null;
182
+ }
183
+ minify(payload) {
184
+ if (Array.isArray(payload)) return payload.map((item) => this.minify(item));
185
+ if (!isObject(payload)) return payload;
186
+ if (this.isEntityWithId(payload)) return this.toRef(payload) ?? payload;
187
+ const minifiedObject = {};
188
+ for (const key of Object.keys(payload)) {
189
+ const value = payload[key];
190
+ if (value instanceof Date) minifiedObject[key] = value.toISOString();
191
+ else if (Array.isArray(value)) minifiedObject[key] = value.map((item) => this.minify(item));
192
+ else if (isObject(value)) minifiedObject[key] = this.minify(value);
193
+ else minifiedObject[key] = value;
194
+ }
195
+ return minifiedObject;
196
+ }
197
+ async unref(entityRef) {
198
+ const logger = this.logger.child({
199
+ action: this.unref.name,
200
+ entityRef
201
+ });
202
+ const [entityName, id] = entityRef.__ref.split("#");
203
+ if (!entityName || !id) {
204
+ logger.error(`Invalid ref ${entityRef.__ref}`);
205
+ return { id: entityRef.__ref };
206
+ }
207
+ const entityMetadata = this.dataSource.entityMetadatas.find((entityMetadata$1) => entityMetadata$1.name === entityName);
208
+ if (!entityMetadata?.target) {
209
+ logger.error(`Entity ${entityName} not found in registry`);
210
+ return { id };
211
+ }
212
+ const instance = await this.dataSource.getRepository(entityMetadata.target).findOneBy({ id });
213
+ if (!instance) {
214
+ logger.error(`Entity ${entityName} with id ${id} not found in database`);
215
+ return { id };
216
+ }
217
+ return instance;
218
+ }
219
+ async unminify(payload) {
220
+ if (Array.isArray(payload)) return await Promise.all(payload.map((item) => this.unminify(item)));
221
+ if (this.isEntityRef(payload)) return await this.unref(payload);
222
+ if (!isObject(payload)) return payload;
223
+ const unminifiedObject = {};
224
+ for (const key of Object.keys(payload)) {
225
+ const value = payload[key];
226
+ if (typeof value === "string" && !isNaN(Date.parse(value))) unminifiedObject[key] = new Date(value);
227
+ else if (this.isEntityRef(value)) unminifiedObject[key] = await this.unref(value);
228
+ else if (Array.isArray(value)) unminifiedObject[key] = await Promise.all(value.map((item) => this.unminify(item)));
229
+ else if (isObject(value)) unminifiedObject[key] = await this.unminify(value);
230
+ else unminifiedObject[key] = value;
231
+ }
232
+ return unminifiedObject;
233
+ }
234
+ isEntityRef(value) {
235
+ return typeof value === "object" && value !== null && "__ref" in value;
236
+ }
237
+ isEntityWithId(value) {
238
+ return value instanceof BaseEntity && "id" in value;
239
+ }
240
+ asyncIterator(triggers) {
241
+ return super.asyncIterableIterator(triggers);
242
+ }
243
+ };
244
+ __decorate([InjectDbConfig(), __decorateMetadata("design:type", Object)], PostgresPubSub.prototype, "dbConfig", void 0);
245
+ __decorate([InjectLoggerFactory(), __decorateMetadata("design:type", typeof (_ref = typeof LoggerFactory !== "undefined" && LoggerFactory) === "function" ? _ref : Object)], PostgresPubSub.prototype, "loggerFactory", void 0);
246
+ __decorate([InjectDataSource(), __decorateMetadata("design:type", typeof (_ref2 = typeof DataSource !== "undefined" && DataSource) === "function" ? _ref2 : Object)], PostgresPubSub.prototype, "dataSource", void 0);
247
+ __decorate([
248
+ Interval("KEEP_DB_CONNECTION_ALIVE", 5 * 1e3),
249
+ __decorateMetadata("design:type", Function),
250
+ __decorateMetadata("design:paramtypes", []),
251
+ __decorateMetadata("design:returntype", Promise)
252
+ ], PostgresPubSub.prototype, "keepDbConnectionAlive", null);
253
+ PostgresPubSub = __decorate([Injectable()], PostgresPubSub);
254
+
255
+ //#endregion
256
+ //#region src/postgres-pubsub.module.ts
257
+ let PostgresPubSubModule = class PostgresPubSubModule$1 {};
258
+ PostgresPubSubModule = __decorate([Global(), Module({
259
+ providers: [PostgresPubSub],
260
+ exports: [PostgresPubSub]
261
+ })], PostgresPubSubModule);
262
+
263
+ //#endregion
264
+ export { PostgresPubSub, PostgresPubSubModule };
265
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["PostgresPubSub","payload: any","minifiedObject: Record<string, unknown>","entityMetadata","unminifiedObject: Record<string, unknown>","PostgresPubSubModule"],"sources":["../src/postgres-pubsub.ts","../src/postgres-pubsub.module.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unsafe-return */\n/* eslint-disable @typescript-eslint/no-unsafe-member-access */\n/* eslint-disable @typescript-eslint/no-unsafe-argument */\n/* eslint-disable @typescript-eslint/no-unsafe-assignment */\n/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { Injectable, OnModuleInit } from '@nestjs/common';\nimport { Interval } from '@nestjs/schedule';\nimport { InjectDataSource } from '@nestjs/typeorm';\nimport { EventEmitter } from 'events';\nimport { PubSubEngine } from 'graphql-subscriptions';\nimport {\n Client, ClientConfig, Notification, escapeIdentifier, escapeLiteral,\n} from 'pg';\nimport {\n BaseEntity, DataSource, FindOptionsWhere,\n} from 'typeorm';\n\nimport {\n InjectDbConfig,\n InjectLoggerFactory,\n Logger,\n LoggerFactory,\n wait,\n} from '@pcg/core';\nimport { isObject, MaybeNull } from '@pcg/predicates';\n\nimport type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';\n\nexport type PubSubSubscriptionCallback = (...args: unknown[]) => void;\n\nexport interface PubSubSubscription {\n id: number;\n channel: string;\n callback: PubSubSubscriptionCallback;\n}\n\n/**\n * Entity reference\n * @example\n * {\n * __ref: 'Media#jsv:12jg839hkvgs'\n * }\n */\nexport interface EntityRef {\n __ref: string;\n}\n\nexport interface EntityWithId extends BaseEntity {\n id: string;\n}\n\nexport interface ObjectWithId {\n id: string;\n}\n\n@Injectable()\nexport class PostgresPubSub extends PubSubEngine implements OnModuleInit {\n @InjectDbConfig() declare protected readonly dbConfig: PostgresConnectionOptions;\n\n @InjectLoggerFactory() declare private readonly loggerFactory: LoggerFactory;\n declare protected logger: Logger;\n\n @InjectDataSource() declare protected readonly dataSource: DataSource;\n\n protected dbClient!: Client;\n protected ee = new EventEmitter();\n protected subscriptions: PubSubSubscription[] = [];\n\n protected config!: ClientConfig;\n protected readonly retryLimit = 5;\n protected isReinitializing = false;\n\n async onModuleInit() {\n this.logger = this.loggerFactory.create({\n scope: this.constructor.name,\n });\n\n this.config = {\n host: this.dbConfig.host,\n port: this.dbConfig.port,\n database: this.dbConfig.database,\n user: this.dbConfig.username,\n password: this.dbConfig.password,\n ssl: this.dbConfig.ssl as boolean | undefined,\n\n // Pool configuration\n connectionTimeoutMillis: 30000, // 30 seconds to establish connection\n\n // Query timeout settings\n query_timeout: 30000, // 30 seconds for queries to complete\n statement_timeout: 30000, // 30 seconds for statements\n\n // Automatic reconnection\n keepAlive: true,\n keepAliveInitialDelayMillis: 10000, // Start keep-alive after 10 seconds\n };\n\n await this.reinit();\n }\n\n // @see fix suggestion [here](https://community.fly.io/t/postgresql-connection-issues-have-returned/6424/6)\n @Interval('KEEP_DB_CONNECTION_ALIVE', 5 * 1000) // each 5s\n async keepDbConnectionAlive() {\n if (this.isReinitializing) {\n return;\n }\n\n try {\n await this.dbClient.query('SELECT pg_backend_pid()');\n } catch (error) {\n if (error instanceof Error) {\n this.logger.error('PubSub PostgreSQL connection check failed:', error);\n }\n\n await this.reinit();\n }\n }\n\n protected async reinit() {\n this.isReinitializing = true;\n\n this.logger.info('🔌 Initializing PubSub PostgreSQL client...');\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dbClient may be undefined on first call\n if (this.dbClient) {\n this.dbClient.removeAllListeners();\n this.dbClient.once('error', (error) => {\n this.logger.error(`Previous DB client errored after reconnecting already:`, error);\n });\n void this.dbClient.end();\n }\n\n this.dbClient = await this.reconnect();\n\n this.logger.info('✅ PubSub PostgreSQL client connected');\n\n this.addDbClientEventListeners();\n\n this.isReinitializing = false;\n }\n\n protected addDbClientEventListeners() {\n this.dbClient.on('notification', (message: Notification) => {\n void this.processNotification(message);\n });\n\n this.dbClient.on('end', () => {\n this.logger.warn('⛓️‍💥 PubSub PostgreSQL client ended');\n if (!this.isReinitializing) {\n void this.reinit();\n }\n });\n\n this.dbClient.on('error', (error: Error) => {\n this.logger.error('🔴 PubSub PostgreSQL client error', error);\n\n if (!this.isReinitializing) {\n void this.reinit();\n }\n });\n }\n\n protected async reconnect() {\n this.logger.info('🔌 Connecting to PubSub PostgreSQL for notification streaming');\n const startTime = Date.now();\n const retryTimeout = 3000;\n\n for (let attempt = 1; attempt < this.retryLimit; attempt++) {\n this.logger.info(`🔌 PostgreSQL connection attempt #${String(attempt)}...`);\n\n try {\n const dbClient = new Client(this.config);\n const connecting = new Promise((resolve, reject) => {\n dbClient.once('connect', resolve);\n dbClient.once('end', () => {\n reject(Error('Connection ended.'));\n });\n dbClient.once('error', reject);\n });\n await Promise.all([\n dbClient.connect(),\n connecting,\n ]);\n this.logger.info('✅ PostgreSQL connection succeeded');\n\n return dbClient;\n } catch (error) {\n if (error instanceof Error) {\n this.logger.error('🔌 PostgreSQL connection attempt failed:', error);\n }\n await wait(500);\n\n if ((Date.now() - startTime) > retryTimeout) {\n throw new Error(`🚫 Stopping PostgreSQL connection attempts after ${String(retryTimeout)}ms timeout has been reached.`);\n }\n }\n }\n\n throw new Error('🔴 Reconnecting notification client to PostgreSQL database failed.');\n }\n\n public async publish(triggerName: string, payload: any, retryCount = 1): Promise<void> {\n if (!payload) {\n throw new Error('Payload is required argument');\n }\n\n const channel = escapeIdentifier(triggerName);\n\n const message = escapeLiteral(JSON.stringify(this.minify(payload)));\n\n // this.logger.info('Publish PostgreSQL notification', {\n // channel,\n // message,\n // payload,\n // });\n\n try {\n await this.dbClient.query(`NOTIFY ${channel}, ${message}`);\n } catch (error) {\n if (error instanceof Error) {\n this.logger.error(`Can't publish PostgreSQL notification to channel \"${channel}\"`, error, {\n payload,\n });\n\n if (/connection/i.exec(error.message)) {\n if (retryCount < 3) {\n this.logger.error(`Try to reconnect to PubSub PostgreSQL Client...(Retry count: ${String(retryCount)})`);\n await this.dbClient.end();\n await this.dbClient.connect();\n await this.publish(triggerName, payload, retryCount + 1);\n }\n }\n }\n }\n\n await Promise.resolve();\n }\n\n public async subscribe(triggerName: string, callback: PubSubSubscriptionCallback): Promise<number> {\n if (!this.subscriptions.some((s) => s.channel === triggerName)) {\n const channel = escapeIdentifier(triggerName);\n await this.dbClient.query(`LISTEN ${channel}`);\n }\n\n this.ee.on(triggerName, callback);\n\n const subId = Math.floor(Math.random() * 100000);\n\n this.subscriptions.push({\n id: subId,\n channel: triggerName,\n callback,\n });\n\n return await Promise.resolve(subId);\n }\n\n public async unsubscribe(subId: number) {\n const index = this.subscriptions.findIndex((s) => s.id === subId);\n if (index !== -1) {\n const subscription = this.subscriptions[index];\n this.subscriptions.splice(index, 1);\n this.ee.removeListener(subscription.channel, subscription.callback);\n\n if (!this.subscriptions.some((s) => s.channel === subscription.channel)) {\n const channel = escapeIdentifier(subscription.channel);\n await this.dbClient.query(`UNLISTEN ${channel}`);\n }\n }\n }\n\n private async processNotification(message: Notification) {\n try {\n const minifiedPayload = JSON.parse(message.payload ?? '');\n\n // this.logger.info('Process PostgreSQL notification', {\n // channel: message.channel,\n // minifiedObject,\n // });\n\n if (minifiedPayload.id) {\n if (Object.keys(minifiedPayload).length === 1) {\n this.ee.emit(message.channel, minifiedPayload);\n } else {\n this.ee.emit(message.channel, await this.unminify(minifiedPayload));\n }\n } else if (Array.isArray(minifiedPayload)) {\n const payload = await this.unminify(minifiedPayload);\n\n this.ee.emit(message.channel, payload);\n } else {\n const payload: any = {\n };\n for (const [key, value] of Object.entries(minifiedPayload)) {\n payload[key] = await this.unminify(value);\n }\n\n this.ee.emit(message.channel, payload);\n }\n } catch (error) {\n if (error instanceof Error) {\n this.logger.error(`Can't extract PostgreSQL notificaton from channel ${message.channel}`, error, {\n message,\n });\n }\n }\n }\n\n protected toRef(object: object): MaybeNull<EntityRef> {\n // this.logger.info('Create reference', {\n // object,\n // isEntityWithId: this.isEntityWithId(object),\n // });\n\n if (this.isEntityWithId(object)) {\n return {\n __ref: `${object.constructor.name}#${object.id}`,\n };\n }\n\n return null;\n }\n\n protected minify(payload: any): any {\n if (Array.isArray(payload)) {\n return payload.map((item) => this.minify(item));\n }\n\n if (!isObject(payload)) {\n return payload;\n }\n\n // this.logger.info('Minifying object', {\n // object,\n // isEntityWithId: this.isEntityWithId(object),\n // });\n\n if (this.isEntityWithId(payload)) {\n const ref = this.toRef(payload);\n\n return (ref ?? payload);\n }\n\n const minifiedObject: Record<string, unknown> = {\n };\n\n for (const key of Object.keys(payload)) {\n const value = payload[key];\n\n if (value instanceof Date) {\n minifiedObject[key] = value.toISOString();\n } else if (Array.isArray(value)) {\n minifiedObject[key] = value.map((item) => this.minify(item));\n } else if (isObject(value)) {\n minifiedObject[key] = this.minify(value);\n } else {\n minifiedObject[key] = value;\n }\n }\n\n return minifiedObject;\n }\n\n protected async unref(entityRef: EntityRef): Promise<BaseEntity | ObjectWithId> {\n const logger = this.logger.child({\n action: this.unref.name,\n entityRef,\n });\n\n // logger.info('Unref entity');\n\n const [entityName, id] = entityRef.__ref.split('#');\n\n if (!entityName || !id) {\n logger.error(`Invalid ref ${entityRef.__ref}`);\n\n return {\n id: entityRef.__ref,\n };\n }\n\n const entityMetadata = this.dataSource.entityMetadatas.find(\n (entityMetadata) => entityMetadata.name === entityName,\n );\n\n if (!entityMetadata?.target) {\n logger.error(`Entity ${entityName} not found in registry`);\n\n return {\n id,\n };\n }\n\n const instance = await this.dataSource.getRepository(entityMetadata.target).findOneBy({\n id,\n } as FindOptionsWhere<EntityWithId>);\n\n if (!instance) {\n logger.error(`Entity ${entityName} with id ${id} not found in database`);\n\n return {\n id,\n };\n }\n\n // logger.info('Unref entity success', {\n // instance,\n // });\n\n return instance as BaseEntity;\n }\n\n protected async unminify(payload: any): Promise<any> {\n // this.logger.info('Unminifying object', {\n // object,\n // isEntityRef: this.isEntityRef(object),\n // });\n\n if (Array.isArray(payload)) {\n return await Promise.all(payload.map((item) => this.unminify(item)));\n }\n\n if (this.isEntityRef(payload)) {\n return await this.unref(payload);\n }\n\n if (!isObject(payload)) {\n return payload;\n }\n\n const unminifiedObject: Record<string, unknown> = {\n };\n\n for (const key of Object.keys(payload)) {\n const value = payload[key];\n\n if (typeof value === 'string' && !isNaN(Date.parse(value))) {\n unminifiedObject[key] = new Date(value);\n } else if (this.isEntityRef(value)) {\n unminifiedObject[key] = await this.unref(value);\n } else if (Array.isArray(value)) {\n unminifiedObject[key] = await Promise.all(value.map((item) => this.unminify(item)));\n } else if (isObject(value)) {\n unminifiedObject[key] = await this.unminify(value);\n } else {\n unminifiedObject[key] = value;\n }\n }\n\n return unminifiedObject;\n }\n\n protected isEntityRef(value: unknown): value is EntityRef {\n return typeof value === 'object' && value !== null && '__ref' in value;\n }\n\n protected isEntityWithId(value: unknown): value is EntityWithId {\n return value instanceof BaseEntity && 'id' in value;\n }\n\n public asyncIterator<T>(triggers: string | readonly string[]) {\n return super.asyncIterableIterator(triggers) as AsyncIterator<T>;\n }\n}\n","import { Global, Module } from '@nestjs/common';\n\nimport { PostgresPubSub } from './postgres-pubsub.js';\n\n/**\n * PostgresPubSubModule provides PostgreSQL NOTIFY/LISTEN based PubSub engine\n * for GraphQL subscriptions in NestJS applications.\n */\n@Global()\n@Module({\n providers: [\n PostgresPubSub,\n ],\n exports: [\n PostgresPubSub,\n ],\n})\nexport class PostgresPubSubModule {}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@pcg/postgres-pubsub",
3
+ "version": "1.0.0-alpha.0",
4
+ "description": "PostgreSQL NOTIFY/LISTEN PubSub engine for NestJS and GraphQL subscriptions",
5
+ "license": "MIT",
6
+ "author": {
7
+ "email": "code@deepvision.team",
8
+ "name": "DeepVision Code"
9
+ },
10
+ "contributors": [
11
+ "Vitaliy Angolenko <v.angolenko@deepvision.software>",
12
+ "Sergii Sadovyi <s.sadovyi@deepvision.software>"
13
+ ],
14
+ "type": "module",
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "dependencies": {
21
+ "graphql-subscriptions": "^3.0.0",
22
+ "pg": "^8.16.3",
23
+ "@pcg/core": "1.0.0-alpha.1",
24
+ "@pcg/predicates": "1.0.0-alpha.1"
25
+ },
26
+ "devDependencies": {
27
+ "@types/pg": "^8.16.0"
28
+ },
29
+ "peerDependencies": {
30
+ "@nestjs/common": "^11.0.0",
31
+ "@nestjs/config": "^4.0.0",
32
+ "@nestjs/schedule": "^5.0.0",
33
+ "@nestjs/typeorm": "^11.0.0",
34
+ "typeorm": "^0.3.0"
35
+ },
36
+ "scripts": {
37
+ "dev": "tsdown --watch",
38
+ "build": "tsdown",
39
+ "lint": "eslint \"src/**/*.ts\" --fix"
40
+ }
41
+ }