@webda/postgres 4.0.0-beta.1 → 4.0.0-beta.3
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 +125 -0
- package/lib/index.d.ts +4 -1
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/pgpool.d.ts +40 -0
- package/lib/pgpool.d.ts.map +1 -0
- package/lib/pgpool.js +101 -0
- package/lib/pgpool.js.map +1 -0
- package/lib/postgrespubsub.service.d.ts +131 -0
- package/lib/postgrespubsub.service.d.ts.map +1 -0
- package/lib/postgrespubsub.service.js +208 -0
- package/lib/postgrespubsub.service.js.map +1 -0
- package/lib/postgresqueue.service.d.ts +144 -0
- package/lib/postgresqueue.service.d.ts.map +1 -0
- package/lib/postgresqueue.service.js +210 -0
- package/lib/postgresqueue.service.js.map +1 -0
- package/lib/postgresstore.service.d.ts +132 -0
- package/lib/postgresstore.service.d.ts.map +1 -0
- package/lib/postgresstore.service.js +327 -0
- package/lib/postgresstore.service.js.map +1 -0
- package/lib/sqlstore.d.ts +122 -40
- package/lib/sqlstore.d.ts.map +1 -0
- package/lib/sqlstore.js +313 -103
- package/lib/sqlstore.js.map +1 -1
- package/package.json +33 -30
- package/test/config.json +2 -53
- package/webda.module.json +2386 -2414
- package/lib/postgresstore.d.ts +0 -114
- package/lib/postgresstore.js +0 -271
- package/lib/postgresstore.js.map +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @webda/postgres module
|
|
2
|
+
|
|
3
|
+
This module is part of Webda Application Framework that allows you to quickly develop applications with all modern prerequisites: Security, Extensibility, GraphQL, REST, CloudNative [https://webda.io](https://webda.io)
|
|
4
|
+
|
|
5
|
+
<img src="https://webda.io/images/webda.svg" width="128" />
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
[](https://gitter.im/loopingz/webda?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
|
10
|
+
[](https://codecov.io/gh/loopingz/webda.io)
|
|
11
|
+
[](https://sonarcloud.io/summary/new_code?id=loopingz_webda.io)
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
<!-- README_HEADER -->
|
|
15
|
+
|
|
16
|
+
# @webda/postgres
|
|
17
|
+
|
|
18
|
+
> PostgreSQL-backed Store for Webda — persists model instances as JSONB rows in Postgres with automatic table creation, view generation, and full WebdaQL query support.
|
|
19
|
+
|
|
20
|
+
## When to use it
|
|
21
|
+
|
|
22
|
+
- You want a relational database backing store for Webda models with ACID guarantees.
|
|
23
|
+
- You are already running PostgreSQL and want Webda models to live alongside existing tables.
|
|
24
|
+
- You need to query across models using familiar SQL tooling while keeping the Webda model API.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add @webda/postgres
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"services": {
|
|
37
|
+
"postStore": {
|
|
38
|
+
"type": "PostgresStore",
|
|
39
|
+
"model": "MyApp/Post",
|
|
40
|
+
"postgresqlServer": {
|
|
41
|
+
"host": "localhost",
|
|
42
|
+
"port": 5432,
|
|
43
|
+
"database": "myapp",
|
|
44
|
+
"user": "webda",
|
|
45
|
+
"password": "${POSTGRES_PASSWORD}"
|
|
46
|
+
},
|
|
47
|
+
"autoCreateTable": true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or rely on libpq environment variables (`PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, `PGPASSWORD`):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"services": {
|
|
58
|
+
"postStore": {
|
|
59
|
+
"type": "PostgresStore",
|
|
60
|
+
"model": "MyApp/Post",
|
|
61
|
+
"autoCreateTable": true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Parameter | Type | Default | Required | Description |
|
|
68
|
+
|---|---|---|---|---|
|
|
69
|
+
| `postgresqlServer` | `ClientConfig` \| `PoolConfig` | libpq env vars | No | PostgreSQL connection config (host, port, database, user, password) |
|
|
70
|
+
| `usePool` | boolean | `false` | No | Use a connection pool (`pg.Pool`) instead of a single client |
|
|
71
|
+
| `autoCreateTable` | boolean | `true` | No | Automatically create the JSONB table if it does not exist |
|
|
72
|
+
| `viewPrefix` | string | `""` | No | Prefix for auto-generated SQL views |
|
|
73
|
+
| `views` | string[] | `["regex:.*"]` | No | Regexp list of model names to generate views for |
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { CoreModel, Model, Expose } from "@webda/core";
|
|
79
|
+
|
|
80
|
+
@Model()
|
|
81
|
+
export class Post extends CoreModel {
|
|
82
|
+
@Expose() title: string;
|
|
83
|
+
@Expose() content: string;
|
|
84
|
+
@Expose() publishedAt: Date;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// With PostgresStore configured, the standard Webda model API is backed by Postgres:
|
|
88
|
+
const post = new Post();
|
|
89
|
+
post.title = "Hello Postgres";
|
|
90
|
+
await post.save(); // INSERT INTO posts ...
|
|
91
|
+
|
|
92
|
+
const found = await Post.ref(post.uuid).get(); // SELECT ... WHERE uuid = ?
|
|
93
|
+
const results = await Post.query("title CONTAINS 'Hello'"); // filtered query
|
|
94
|
+
|
|
95
|
+
// The table schema uses JSONB:
|
|
96
|
+
// CREATE TABLE IF NOT EXISTS posts (
|
|
97
|
+
// uuid TEXT PRIMARY KEY,
|
|
98
|
+
// data JSONB NOT NULL
|
|
99
|
+
// );
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Reference
|
|
103
|
+
|
|
104
|
+
- API reference: see the auto-generated typedoc at `docs/pages/Modules/postgres/`.
|
|
105
|
+
- Source: [`packages/postgres`](https://github.com/loopingz/webda.io/tree/main/packages/postgres)
|
|
106
|
+
- Related: [`@webda/mongodb`](../mongodb) for a document-store alternative; [`@webda/aws`](../aws) for `DynamoStore`; [`@webda/core`](../core) for the `Store` base class.
|
|
107
|
+
|
|
108
|
+
<!-- README_FOOTER -->
|
|
109
|
+
## Sponsors
|
|
110
|
+
|
|
111
|
+
<!--
|
|
112
|
+
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](mailto:sponsor@webda.io)
|
|
113
|
+
-->
|
|
114
|
+
|
|
115
|
+
Arize AI is a machine learning observability and model monitoring platform. It helps you visualize, monitor, and explain your machine learning models. [Learn more](https://arize.com)
|
|
116
|
+
|
|
117
|
+
[<img src="https://arize.com/hubfs/arize/brand/arize-logomark-1.png" width="200">](https://arize.com)
|
|
118
|
+
|
|
119
|
+
Loopingz is a software development company that provides consulting and development services. [Learn more](https://loopingz.com)
|
|
120
|
+
|
|
121
|
+
[<img src="https://loopingz.com/images/logo.png" width="200">](https://loopingz.com)
|
|
122
|
+
|
|
123
|
+
Tellae is an innovative consulting firm specialized in cities transportation issues. We provide our clients, both public and private, with solutions to support your strategic and operational decisions. [Learn more](https://tellae.fr)
|
|
124
|
+
|
|
125
|
+
[<img src="https://tellae.fr/" width="200">](https://tellae.fr)
|
package/lib/index.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,eAAe,CAAC"}
|
package/lib/index.js
CHANGED
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,eAAe,CAAC"}
|
package/lib/pgpool.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pg, { PoolConfig } from "pg";
|
|
2
|
+
/**
|
|
3
|
+
* Build the registry key for a config. Exposed for tests.
|
|
4
|
+
*
|
|
5
|
+
* @param config - the pg pool config (may be undefined — pg falls back to PG* env)
|
|
6
|
+
* @returns the registry key
|
|
7
|
+
*/
|
|
8
|
+
export declare function poolKey(config: PoolConfig | undefined): string;
|
|
9
|
+
/**
|
|
10
|
+
* Acquire a shared `pg.Pool` for the given config. Pools are cached by
|
|
11
|
+
* stable config-hash; callers that pass the same config (regardless of
|
|
12
|
+
* key order) get the same pool back. Each `acquirePool` increments a
|
|
13
|
+
* refcount; the pool is `end()`ed when the last consumer calls
|
|
14
|
+
* {@link releasePool}.
|
|
15
|
+
*
|
|
16
|
+
* Use this for stateless workloads (queries, queue receive, pub/sub
|
|
17
|
+
* publish). LISTEN must use a dedicated `pg.Client` because the
|
|
18
|
+
* subscription is bound to a single connection — pooled connections
|
|
19
|
+
* rotate and would silently drop the subscription.
|
|
20
|
+
*
|
|
21
|
+
* @param config - the pg pool config
|
|
22
|
+
* @returns the shared pool
|
|
23
|
+
*/
|
|
24
|
+
export declare function acquirePool(config: PoolConfig | undefined): pg.Pool;
|
|
25
|
+
/**
|
|
26
|
+
* Release a previously-acquired pool. When the refcount reaches zero,
|
|
27
|
+
* the pool is `end()`ed and removed from the registry. Releasing a
|
|
28
|
+
* config that was never acquired (or already drained) is a no-op so
|
|
29
|
+
* `stop()` paths can call this unconditionally.
|
|
30
|
+
*
|
|
31
|
+
* @param config - the same config that was passed to {@link acquirePool}
|
|
32
|
+
* @returns a promise that resolves once the pool has been ended (or immediately if still in use / unknown)
|
|
33
|
+
*/
|
|
34
|
+
export declare function releasePool(config: PoolConfig | undefined): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Test-only: drain and forget every shared pool. Used between specs to
|
|
37
|
+
* keep the registry from leaking handles across files.
|
|
38
|
+
*/
|
|
39
|
+
export declare function __resetPools(): Promise<void>;
|
|
40
|
+
//# sourceMappingURL=pgpool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pgpool.d.ts","sourceRoot":"","sources":["../src/pgpool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAyCpC;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,CAE9D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,EAAE,CAAC,IAAI,CAUnE;AAED;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED;;;GAGG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAUlD"}
|
package/lib/pgpool.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
/**
|
|
3
|
+
* Stable JSON serializer that sorts object keys so two configs with the
|
|
4
|
+
* same content but different key order hash to the same registry key.
|
|
5
|
+
*
|
|
6
|
+
* Functions and classes (e.g. `pg.Pool` custom types) are dropped — pools
|
|
7
|
+
* configured with non-serializable hooks must be created directly.
|
|
8
|
+
*
|
|
9
|
+
* @param value - any JSON-serializable value
|
|
10
|
+
* @returns a stable string representation
|
|
11
|
+
*/
|
|
12
|
+
function stableStringify(value) {
|
|
13
|
+
if (value === null || typeof value !== "object") {
|
|
14
|
+
return JSON.stringify(value);
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
18
|
+
}
|
|
19
|
+
const keys = Object.keys(value).sort();
|
|
20
|
+
return ("{" +
|
|
21
|
+
keys
|
|
22
|
+
.map(k => {
|
|
23
|
+
const v = value[k];
|
|
24
|
+
if (typeof v === "function")
|
|
25
|
+
return undefined;
|
|
26
|
+
return JSON.stringify(k) + ":" + stableStringify(v);
|
|
27
|
+
})
|
|
28
|
+
.filter(s => s !== undefined)
|
|
29
|
+
.join(",") +
|
|
30
|
+
"}");
|
|
31
|
+
}
|
|
32
|
+
const registry = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* Build the registry key for a config. Exposed for tests.
|
|
35
|
+
*
|
|
36
|
+
* @param config - the pg pool config (may be undefined — pg falls back to PG* env)
|
|
37
|
+
* @returns the registry key
|
|
38
|
+
*/
|
|
39
|
+
export function poolKey(config) {
|
|
40
|
+
return stableStringify(config ?? {});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Acquire a shared `pg.Pool` for the given config. Pools are cached by
|
|
44
|
+
* stable config-hash; callers that pass the same config (regardless of
|
|
45
|
+
* key order) get the same pool back. Each `acquirePool` increments a
|
|
46
|
+
* refcount; the pool is `end()`ed when the last consumer calls
|
|
47
|
+
* {@link releasePool}.
|
|
48
|
+
*
|
|
49
|
+
* Use this for stateless workloads (queries, queue receive, pub/sub
|
|
50
|
+
* publish). LISTEN must use a dedicated `pg.Client` because the
|
|
51
|
+
* subscription is bound to a single connection — pooled connections
|
|
52
|
+
* rotate and would silently drop the subscription.
|
|
53
|
+
*
|
|
54
|
+
* @param config - the pg pool config
|
|
55
|
+
* @returns the shared pool
|
|
56
|
+
*/
|
|
57
|
+
export function acquirePool(config) {
|
|
58
|
+
const key = poolKey(config);
|
|
59
|
+
const entry = registry.get(key);
|
|
60
|
+
if (entry) {
|
|
61
|
+
entry.refCount++;
|
|
62
|
+
return entry.pool;
|
|
63
|
+
}
|
|
64
|
+
const pool = new pg.Pool(config);
|
|
65
|
+
registry.set(key, { pool, refCount: 1 });
|
|
66
|
+
return pool;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Release a previously-acquired pool. When the refcount reaches zero,
|
|
70
|
+
* the pool is `end()`ed and removed from the registry. Releasing a
|
|
71
|
+
* config that was never acquired (or already drained) is a no-op so
|
|
72
|
+
* `stop()` paths can call this unconditionally.
|
|
73
|
+
*
|
|
74
|
+
* @param config - the same config that was passed to {@link acquirePool}
|
|
75
|
+
* @returns a promise that resolves once the pool has been ended (or immediately if still in use / unknown)
|
|
76
|
+
*/
|
|
77
|
+
export async function releasePool(config) {
|
|
78
|
+
const key = poolKey(config);
|
|
79
|
+
const entry = registry.get(key);
|
|
80
|
+
if (!entry)
|
|
81
|
+
return;
|
|
82
|
+
entry.refCount--;
|
|
83
|
+
if (entry.refCount > 0)
|
|
84
|
+
return;
|
|
85
|
+
registry.delete(key);
|
|
86
|
+
await entry.pool.end().catch(() => {
|
|
87
|
+
/* already ended */
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Test-only: drain and forget every shared pool. Used between specs to
|
|
92
|
+
* keep the registry from leaking handles across files.
|
|
93
|
+
*/
|
|
94
|
+
export async function __resetPools() {
|
|
95
|
+
const entries = [...registry.values()];
|
|
96
|
+
registry.clear();
|
|
97
|
+
await Promise.all(entries.map(e => e.pool.end().catch(() => {
|
|
98
|
+
/* already ended */
|
|
99
|
+
})));
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=pgpool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pgpool.js","sourceRoot":"","sources":["../src/pgpool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { PubSubService, ServiceParameters } from "@webda/core";
|
|
2
|
+
import { CancelablePromise } from "@webda/utils";
|
|
3
|
+
import pg, { ClientConfig } from "pg";
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for {@link PostgresPubSubService}.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PostgresPubSubParameters extends ServiceParameters {
|
|
8
|
+
/**
|
|
9
|
+
* Channel name passed to LISTEN / NOTIFY. Must be a valid Postgres
|
|
10
|
+
* identifier (lowercased, no quoting). Defaults to the service name.
|
|
11
|
+
* @pattern /^[a-z_][a-z0-9_]*$/
|
|
12
|
+
* @example "myapp_events"
|
|
13
|
+
* @example "user_notifications"
|
|
14
|
+
* @example "cache_invalidation"
|
|
15
|
+
* @example "orders"
|
|
16
|
+
*/
|
|
17
|
+
channel?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Connection settings forwarded to `pg.Client`. By default `pg` reads
|
|
20
|
+
* standard PG* environment variables.
|
|
21
|
+
*/
|
|
22
|
+
postgresqlServer?: ClientConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Reconnect delay in milliseconds when the LISTEN connection drops. A
|
|
25
|
+
* randomized jitter is added to keep crash-loop reconnects from
|
|
26
|
+
* stampeding.
|
|
27
|
+
*
|
|
28
|
+
* @default 500
|
|
29
|
+
*/
|
|
30
|
+
reconnectDelay?: number;
|
|
31
|
+
/**
|
|
32
|
+
* @override
|
|
33
|
+
* @param params - the input parameters
|
|
34
|
+
* @returns this
|
|
35
|
+
*/
|
|
36
|
+
load(params?: any): this;
|
|
37
|
+
}
|
|
38
|
+
interface Subscriber<T> {
|
|
39
|
+
callback: (event: T) => Promise<void>;
|
|
40
|
+
proto?: {
|
|
41
|
+
new (): T;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pub/sub backed by Postgres' native LISTEN / NOTIFY. A long-lived
|
|
46
|
+
* `pg.Client` (NOT a pool — pools rotate connections, but each LISTEN is
|
|
47
|
+
* scoped to the connection that issued it) holds the subscription;
|
|
48
|
+
* publishes go through `pg_notify(channel, payload)`. The 8 kB NOTIFY
|
|
49
|
+
* payload cap is enforced in {@link sendMessage} — for larger payloads,
|
|
50
|
+
* stash them in a row and notify the row id.
|
|
51
|
+
*
|
|
52
|
+
* Disconnects trigger a randomized-backoff reconnect so the listener
|
|
53
|
+
* survives transient network or restart blips.
|
|
54
|
+
*
|
|
55
|
+
* @WebdaModda PostgresPubSub
|
|
56
|
+
*/
|
|
57
|
+
export default class PostgresPubSubService<T = any, K extends PostgresPubSubParameters = PostgresPubSubParameters> extends PubSubService<T, K> {
|
|
58
|
+
/**
|
|
59
|
+
* Long-lived listener client. One per service instance because LISTEN is
|
|
60
|
+
* scoped to the connection that issued it — a pool would silently rotate
|
|
61
|
+
* and drop the subscription.
|
|
62
|
+
*/
|
|
63
|
+
protected client?: pg.Client;
|
|
64
|
+
/**
|
|
65
|
+
* Shared pool used for the publish side (`pg_notify`). Acquired from
|
|
66
|
+
* the per-config pool registry so multiple services that target the
|
|
67
|
+
* same database share connections instead of each spinning up their
|
|
68
|
+
* own pool. Subscribe-only services skip this and never publish.
|
|
69
|
+
*/
|
|
70
|
+
protected publishPool?: pg.Pool;
|
|
71
|
+
/**
|
|
72
|
+
* Local callback registrations. Notifications dispatch to all of them.
|
|
73
|
+
*/
|
|
74
|
+
protected callbacks: Set<Subscriber<T>>;
|
|
75
|
+
/**
|
|
76
|
+
* Set during {@link stop} so reconnect handlers don't try to come back
|
|
77
|
+
* after teardown.
|
|
78
|
+
*/
|
|
79
|
+
protected stopping: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Channel name used for LISTEN/NOTIFY. Resolved at init time so we can
|
|
82
|
+
* default to the service's name when not configured.
|
|
83
|
+
* @returns the channel name
|
|
84
|
+
*/
|
|
85
|
+
protected channel(): string;
|
|
86
|
+
/**
|
|
87
|
+
* @override
|
|
88
|
+
* @returns this service
|
|
89
|
+
*/
|
|
90
|
+
init(): Promise<this>;
|
|
91
|
+
/**
|
|
92
|
+
* Open a fresh client, run LISTEN, and wire the notification handler.
|
|
93
|
+
*/
|
|
94
|
+
protected connect(): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Schedule a reconnect after a short randomized backoff.
|
|
97
|
+
*/
|
|
98
|
+
protected handleDisconnect(): void;
|
|
99
|
+
/**
|
|
100
|
+
* Decode a notification payload and run every registered callback
|
|
101
|
+
* against it.
|
|
102
|
+
* @param payload - the raw NOTIFY payload string
|
|
103
|
+
*/
|
|
104
|
+
protected dispatch(payload: string): void;
|
|
105
|
+
/**
|
|
106
|
+
* @override
|
|
107
|
+
* @param event - the event to publish
|
|
108
|
+
*/
|
|
109
|
+
sendMessage(event: T): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* @override
|
|
112
|
+
* @returns 0 — pub/sub is transient, no queueing
|
|
113
|
+
*/
|
|
114
|
+
size(): Promise<number>;
|
|
115
|
+
/**
|
|
116
|
+
* @override
|
|
117
|
+
* @param callback - invoked with each event received
|
|
118
|
+
* @param eventPrototype - optional class to rehydrate JSON into
|
|
119
|
+
* @param onBind - invoked once the subscription is registered
|
|
120
|
+
* @returns a cancelable subscription handle
|
|
121
|
+
*/
|
|
122
|
+
consume(callback: (event: T) => Promise<void>, eventPrototype?: {
|
|
123
|
+
new (): T;
|
|
124
|
+
}, onBind?: () => void): CancelablePromise;
|
|
125
|
+
/**
|
|
126
|
+
* @override
|
|
127
|
+
*/
|
|
128
|
+
stop(): Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
export { PostgresPubSubService };
|
|
131
|
+
//# sourceMappingURL=postgrespubsub.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgrespubsub.service.d.ts","sourceRoot":"","sources":["../src/postgrespubsub.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAa,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAE,EAAE,EAAE,YAAY,EAAc,MAAM,IAAI,CAAC;AAUlD;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,iBAAiB;IAC7D;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;OAIG;IACH,IAAI,CAAC,MAAM,GAAE,GAAQ,GAAG,IAAI,CAI3B;CACF;AAED,UAAU,UAAU,CAAC,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,CAAA;KAAE,CAAC;CACvB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,OAAO,OAAO,qBAAqB,CACxC,CAAC,GAAG,GAAG,EACP,CAAC,SAAS,wBAAwB,GAAG,wBAAwB,CAC7D,SAAQ,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC;IAC3B;;;;OAIG;IACH,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,SAAS,CAAC,WAAW,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC;IAChC;;OAEG;IACH,SAAS,CAAC,SAAS,EAAE,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAa;IACpD;;;OAGG;IACH,SAAS,CAAC,QAAQ,UAAS;IAE3B;;;;OAIG;IACH,SAAS,CAAC,OAAO,IAAI,MAAM,CAE1B;IAED;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAI1B;IAED;;OAEG;IACH,UAAgB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAmBvC;IAED;;OAEG;IACH,SAAS,CAAC,gBAAgB,IAAI,IAAI,CAQjC;IAED;;;;OAIG;IACH,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAuBxC;IAED;;;OAGG;IACG,WAAW,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBzC;IAED;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAE5B;IAED;;;;;;OAMG;IACH,OAAO,CACL,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,EACrC,cAAc,CAAC,EAAE;QAAE,QAAQ,CAAC,CAAA;KAAE,EAC9B,MAAM,CAAC,EAAE,MAAM,IAAI,GAClB,iBAAiB,CAUnB;IAED;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAoB1B;CACF;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { PubSubService, ServiceParameters } from "@webda/core";
|
|
2
|
+
import { CancelablePromise, JSONUtils } from "@webda/utils";
|
|
3
|
+
import { useLog } from "@webda/workout";
|
|
4
|
+
import pg from "pg";
|
|
5
|
+
import { acquirePool, releasePool } from "./pgpool.js";
|
|
6
|
+
/**
|
|
7
|
+
* NOTIFY's payload limit is 8000 bytes (the underlying NAMEDATALEN /
|
|
8
|
+
* NOTIFY_PAYLOAD_LIMIT). We keep a small safety margin to leave room for
|
|
9
|
+
* the protocol overhead.
|
|
10
|
+
*/
|
|
11
|
+
const NOTIFY_PAYLOAD_MAX = 7900;
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for {@link PostgresPubSubService}.
|
|
14
|
+
*/
|
|
15
|
+
export class PostgresPubSubParameters extends ServiceParameters {
|
|
16
|
+
/**
|
|
17
|
+
* @override
|
|
18
|
+
* @param params - the input parameters
|
|
19
|
+
* @returns this
|
|
20
|
+
*/
|
|
21
|
+
load(params = {}) {
|
|
22
|
+
super.load(params);
|
|
23
|
+
this.reconnectDelay ?? (this.reconnectDelay = 500);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Pub/sub backed by Postgres' native LISTEN / NOTIFY. A long-lived
|
|
29
|
+
* `pg.Client` (NOT a pool — pools rotate connections, but each LISTEN is
|
|
30
|
+
* scoped to the connection that issued it) holds the subscription;
|
|
31
|
+
* publishes go through `pg_notify(channel, payload)`. The 8 kB NOTIFY
|
|
32
|
+
* payload cap is enforced in {@link sendMessage} — for larger payloads,
|
|
33
|
+
* stash them in a row and notify the row id.
|
|
34
|
+
*
|
|
35
|
+
* Disconnects trigger a randomized-backoff reconnect so the listener
|
|
36
|
+
* survives transient network or restart blips.
|
|
37
|
+
*
|
|
38
|
+
* @WebdaModda PostgresPubSub
|
|
39
|
+
*/
|
|
40
|
+
export default class PostgresPubSubService extends PubSubService {
|
|
41
|
+
constructor() {
|
|
42
|
+
super(...arguments);
|
|
43
|
+
/**
|
|
44
|
+
* Local callback registrations. Notifications dispatch to all of them.
|
|
45
|
+
*/
|
|
46
|
+
this.callbacks = new Set();
|
|
47
|
+
/**
|
|
48
|
+
* Set during {@link stop} so reconnect handlers don't try to come back
|
|
49
|
+
* after teardown.
|
|
50
|
+
*/
|
|
51
|
+
this.stopping = false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Channel name used for LISTEN/NOTIFY. Resolved at init time so we can
|
|
55
|
+
* default to the service's name when not configured.
|
|
56
|
+
* @returns the channel name
|
|
57
|
+
*/
|
|
58
|
+
channel() {
|
|
59
|
+
return this.parameters.channel ?? this.getName().toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @override
|
|
63
|
+
* @returns this service
|
|
64
|
+
*/
|
|
65
|
+
async init() {
|
|
66
|
+
await super.init();
|
|
67
|
+
await this.connect();
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Open a fresh client, run LISTEN, and wire the notification handler.
|
|
72
|
+
*/
|
|
73
|
+
async connect() {
|
|
74
|
+
// Validate the channel name before opening any connection: LISTEN
|
|
75
|
+
// can't be parameterized so the channel name gets inlined into SQL,
|
|
76
|
+
// which makes it a query-injection vector. Failing fast here keeps
|
|
77
|
+
// the bad-input case independent of database reachability.
|
|
78
|
+
const ch = this.channel();
|
|
79
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(ch)) {
|
|
80
|
+
throw new Error(`Invalid channel name "${ch}" — must match /^[a-z_][a-z0-9_]*$/`);
|
|
81
|
+
}
|
|
82
|
+
const client = new pg.Client(this.parameters.postgresqlServer);
|
|
83
|
+
client.on("notification", (msg) => {
|
|
84
|
+
if (msg.channel !== this.channel())
|
|
85
|
+
return;
|
|
86
|
+
this.dispatch(msg.payload ?? "");
|
|
87
|
+
});
|
|
88
|
+
client.on("error", err => useLog("WARN", `PostgresPubSub client error: ${err.message}`));
|
|
89
|
+
client.on("end", () => this.handleDisconnect());
|
|
90
|
+
await client.connect();
|
|
91
|
+
await client.query(`LISTEN ${ch}`);
|
|
92
|
+
this.client = client;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Schedule a reconnect after a short randomized backoff.
|
|
96
|
+
*/
|
|
97
|
+
handleDisconnect() {
|
|
98
|
+
if (this.stopping)
|
|
99
|
+
return;
|
|
100
|
+
this.client = undefined;
|
|
101
|
+
const delay = this.parameters.reconnectDelay + Math.floor(Math.random() * 250);
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
if (this.stopping)
|
|
104
|
+
return;
|
|
105
|
+
this.connect().catch(err => useLog("WARN", `PostgresPubSub reconnect failed: ${err.message}`));
|
|
106
|
+
}, delay);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Decode a notification payload and run every registered callback
|
|
110
|
+
* against it.
|
|
111
|
+
* @param payload - the raw NOTIFY payload string
|
|
112
|
+
*/
|
|
113
|
+
dispatch(payload) {
|
|
114
|
+
let raw;
|
|
115
|
+
try {
|
|
116
|
+
raw = payload === "" ? undefined : JSONUtils.parse(payload);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
this.metrics?.errors?.inc();
|
|
120
|
+
useLog("WARN", `PostgresPubSub invalid payload: ${err.message}`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.metrics?.messages_received?.inc();
|
|
124
|
+
for (const sub of this.callbacks) {
|
|
125
|
+
const event = sub.proto && raw ? Object.assign(new sub.proto(), raw) : raw;
|
|
126
|
+
const start = Date.now();
|
|
127
|
+
sub
|
|
128
|
+
.callback(event)
|
|
129
|
+
.catch(err => {
|
|
130
|
+
this.metrics?.errors?.inc();
|
|
131
|
+
useLog("ERROR", `PostgresPubSub callback failed: ${err.message}`);
|
|
132
|
+
})
|
|
133
|
+
.finally(() => {
|
|
134
|
+
this.metrics?.processing_duration?.observe((Date.now() - start) / 1000);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* @override
|
|
140
|
+
* @param event - the event to publish
|
|
141
|
+
*/
|
|
142
|
+
async sendMessage(event) {
|
|
143
|
+
if (!this.client)
|
|
144
|
+
throw new Error("PostgresPubSub not connected");
|
|
145
|
+
const payload = JSONUtils.stringify(event);
|
|
146
|
+
if (Buffer.byteLength(payload, "utf-8") > NOTIFY_PAYLOAD_MAX) {
|
|
147
|
+
throw new Error(`PostgresPubSub payload exceeds Postgres' 8 kB NOTIFY limit. Stash large payloads in a row and notify the row id instead.`);
|
|
148
|
+
}
|
|
149
|
+
this.metrics?.messages_sent?.inc();
|
|
150
|
+
// Lazily acquire a shared publish pool: subscribe-only services never
|
|
151
|
+
// call sendMessage and so never pay the connection cost. Acquired
|
|
152
|
+
// pools are reference-counted in the registry, so multiple postgres
|
|
153
|
+
// services pointing at the same database share connections.
|
|
154
|
+
if (!this.publishPool) {
|
|
155
|
+
this.publishPool = acquirePool(this.parameters.postgresqlServer);
|
|
156
|
+
}
|
|
157
|
+
await this.publishPool.query("SELECT pg_notify($1, $2)", [this.channel(), payload]);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* @override
|
|
161
|
+
* @returns 0 — pub/sub is transient, no queueing
|
|
162
|
+
*/
|
|
163
|
+
async size() {
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* @override
|
|
168
|
+
* @param callback - invoked with each event received
|
|
169
|
+
* @param eventPrototype - optional class to rehydrate JSON into
|
|
170
|
+
* @param onBind - invoked once the subscription is registered
|
|
171
|
+
* @returns a cancelable subscription handle
|
|
172
|
+
*/
|
|
173
|
+
consume(callback, eventPrototype, onBind) {
|
|
174
|
+
const entry = { callback, proto: eventPrototype };
|
|
175
|
+
this.callbacks.add(entry);
|
|
176
|
+
onBind?.();
|
|
177
|
+
return new CancelablePromise(resolve => resolve(), async () => {
|
|
178
|
+
this.callbacks.delete(entry);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* @override
|
|
183
|
+
*/
|
|
184
|
+
async stop() {
|
|
185
|
+
this.stopping = true;
|
|
186
|
+
this.callbacks.clear();
|
|
187
|
+
if (this.client) {
|
|
188
|
+
const client = this.client;
|
|
189
|
+
this.client = undefined;
|
|
190
|
+
try {
|
|
191
|
+
await client.query(`UNLISTEN ${this.channel()}`);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
/* connection may already be dead */
|
|
195
|
+
}
|
|
196
|
+
await client.end().catch(() => {
|
|
197
|
+
/* already ended */
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (this.publishPool) {
|
|
201
|
+
this.publishPool = undefined;
|
|
202
|
+
await releasePool(this.parameters.postgresqlServer);
|
|
203
|
+
}
|
|
204
|
+
await super.stop();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export { PostgresPubSubService };
|
|
208
|
+
//# sourceMappingURL=postgrespubsub.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgrespubsub.service.js","sourceRoot":"","sources":["../src/postgrespubsub.service.ts"],"names":[],"mappings":"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"}
|