dynalitics-postgres 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +53 -2
- package/dist/driver.d.ts +4 -0
- package/dist/driver.js +111 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +12 -0
- package/package.json +53 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dynalitics contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dynalitics-postgres
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The **PostgreSQL adapter** for [dynalitics](https://www.npmjs.com/package/dynalitics). `dynalitics` itself never loads a database driver; this package is the piece that connects it to PostgreSQL through [`pg`](https://node-postgres.com). It depends on `pg` (pinned to `8.23.1`) and nothing else, so installing it brings in exactly one driver.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install dynalitics dynalitics-postgres # Node 22+, ESM only
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
Pass the exported `postgres` adapter as `adapter` when you create an instance. Everything else (table, consent, IP mode, the read API) is configured on `dynalitics`; see its README.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createDynalitics } from 'dynalitics';
|
|
15
|
+
import { postgres } from 'dynalitics-postgres';
|
|
16
|
+
|
|
17
|
+
const dyn = await createDynalitics({
|
|
18
|
+
adapter: postgres,
|
|
19
|
+
connection: {
|
|
20
|
+
writer: { connectionString: process.env.ANALYTICS_WRITER_URL! }, // INSERT only
|
|
21
|
+
reader: { connectionString: process.env.ANALYTICS_READER_URL! }, // SELECT only
|
|
22
|
+
},
|
|
23
|
+
table: 'public.dynalitics_events',
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Create the table first with `npx dynalitics sql --dialect postgres --table public.dynalitics_events --grants` (prints SQL, never connects).
|
|
28
|
+
|
|
29
|
+
## What the adapter does
|
|
30
|
+
|
|
31
|
+
**Connection strings.** For each credential it creates a `pg.Pool` with:
|
|
32
|
+
|
|
33
|
+
| Setting | Value |
|
|
34
|
+
| ------------------------- | ----------------------------------------------- |
|
|
35
|
+
| `max` | `poolSize` (writer 4, reader 2 by default) |
|
|
36
|
+
| `connectionTimeoutMillis` | `connectTimeoutMs` (10 s by default) |
|
|
37
|
+
| `idleTimeoutMillis` | 30 s |
|
|
38
|
+
| `statement_timeout` | 30 s, a server-side ceiling for every statement |
|
|
39
|
+
|
|
40
|
+
TLS, host, port and database come from the connection string itself. Use `sslmode=require` for anything that is not on localhost. An idle connection that dies (server restart, network blip) never crashes your process: the pool's `error` event is handled.
|
|
41
|
+
|
|
42
|
+
**Your own pool.** Pass `{ pool: yourPgPool }` instead of a connection string. dynalitics never closes it on `close()`.
|
|
43
|
+
|
|
44
|
+
**Deadlines.** Every read runs on its own pooled client. When the read handler's deadline passes (`limits.timeoutMs`, 10 s by default), the adapter sends `SELECT pg_cancel_backend(pid)` (allowed for your own role, waits at most 2 s) and destroys that connection, so a slow query cannot keep running unseen. A pool without `connect()` only gets a client-side `query_timeout`; in that case set `statement_timeout` on the reader role yourself.
|
|
45
|
+
|
|
46
|
+
**Errors.** The adapter never passes a raw `pg` error on. Data and constraint errors (SQLSTATE classes 22 and 23) forward no message text at all, because PostgreSQL prints the offending value. Permission errors (`42501`) become a fixed sentence. Connection, authentication and timeout errors keep their message, redacted and with quoted values removed. Connection drops, `57P0x` shutdowns, serialization failures and deadlocks (`40001`, `40P01`) and "too many connections" are marked transient, so the writer retries them.
|
|
47
|
+
|
|
48
|
+
## Versions
|
|
49
|
+
|
|
50
|
+
`dynalitics-postgres` and `dynalitics` are released together with the same version number, and the adapter declares `dynalitics` as a peer dependency at exactly that version. If the two ever disagree on the adapter contract, `createDynalitics` refuses to start and names both versions.
|
|
51
|
+
|
|
52
|
+
Tested against PostgreSQL 17 (integration tests, local Docker).
|
|
53
|
+
|
|
54
|
+
MIT licensed.
|
package/dist/driver.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type Classification, type Driver, type DriverOptions } from 'dynalitics/adapter';
|
|
2
|
+
export declare function classifyPg(err: unknown): Classification;
|
|
3
|
+
export declare function createPostgresDriver(o: DriverOptions): Promise<Driver>;
|
|
4
|
+
//# sourceMappingURL=driver.d.ts.map
|
package/dist/driver.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import pgModule from 'pg';
|
|
2
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, IDLE_TIMEOUT_MS, errCode, errMessage, isTransientError, toDriverError, } from 'dynalitics/adapter';
|
|
3
|
+
const TRANSIENT_CODES = new Set([
|
|
4
|
+
'ECONNRESET',
|
|
5
|
+
'ECONNREFUSED',
|
|
6
|
+
'ETIMEDOUT',
|
|
7
|
+
'EPIPE',
|
|
8
|
+
'57P01',
|
|
9
|
+
'57P02',
|
|
10
|
+
'57P03',
|
|
11
|
+
'53300',
|
|
12
|
+
'53400',
|
|
13
|
+
'40001',
|
|
14
|
+
'40P01',
|
|
15
|
+
]);
|
|
16
|
+
const CANCEL_GUARD_MS = 2000;
|
|
17
|
+
export function classifyPg(err) {
|
|
18
|
+
const code = errCode(err);
|
|
19
|
+
const msg = errMessage(err);
|
|
20
|
+
const timeout = msg.includes('read timeout') || msg.includes('statement timeout') || code === '57014';
|
|
21
|
+
const transient = timeout ||
|
|
22
|
+
TRANSIENT_CODES.has(code) ||
|
|
23
|
+
code.startsWith('08') ||
|
|
24
|
+
msg.includes('connection terminated') ||
|
|
25
|
+
msg.includes('connection timeout');
|
|
26
|
+
return {
|
|
27
|
+
transient,
|
|
28
|
+
timeout,
|
|
29
|
+
// SQLSTATE class 22 (data exception) and 23 (integrity constraint violation)
|
|
30
|
+
data: /^(22|23)/.test(code),
|
|
31
|
+
permission: code === '42501',
|
|
32
|
+
// connection (08), network, server shutdown (57P0x) and invalid authorization (28xxx) classes
|
|
33
|
+
forward: transient || code.startsWith('28') || code === 'ENOTFOUND' || code === 'EAI_AGAIN',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Asks the server to stop the backend (own-role queries only), never waiting longer than CANCEL_GUARD_MS. */
|
|
37
|
+
async function cancelBackend(pool, pid) {
|
|
38
|
+
if (pid === undefined)
|
|
39
|
+
return;
|
|
40
|
+
const attempt = pool
|
|
41
|
+
.query({ text: 'SELECT pg_cancel_backend($1)', values: [pid], query_timeout: CANCEL_GUARD_MS })
|
|
42
|
+
.then(() => undefined, () => undefined);
|
|
43
|
+
await Promise.race([
|
|
44
|
+
attempt,
|
|
45
|
+
new Promise((resolve) => setTimeout(resolve, CANCEL_GUARD_MS).unref?.()),
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
// async on purpose: a throwing pool constructor must become a rejection, not a synchronous throw.
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
50
|
+
export async function createPostgresDriver(o) {
|
|
51
|
+
let pool;
|
|
52
|
+
let owned = false;
|
|
53
|
+
if ('pool' in o.credential) {
|
|
54
|
+
pool = o.credential.pool;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const pg = pgModule;
|
|
58
|
+
pool = new pg.Pool({
|
|
59
|
+
connectionString: o.credential.connectionString,
|
|
60
|
+
max: o.max,
|
|
61
|
+
connectionTimeoutMillis: o.connectTimeoutMs,
|
|
62
|
+
idleTimeoutMillis: IDLE_TIMEOUT_MS,
|
|
63
|
+
statement_timeout: o.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
64
|
+
});
|
|
65
|
+
// An idle client dying must not crash the host process with an unhandled 'error' event.
|
|
66
|
+
pool.on?.('error', () => undefined);
|
|
67
|
+
owned = true;
|
|
68
|
+
}
|
|
69
|
+
/** Host pools without connect(): client-side deadline only (the host must set statement_timeout). */
|
|
70
|
+
async function viaPool(sql, params, timeoutMs) {
|
|
71
|
+
return (await pool.query({ text: sql, values: [...params], query_timeout: timeoutMs })).rows;
|
|
72
|
+
}
|
|
73
|
+
async function viaClient(connect, sql, params, timeoutMs) {
|
|
74
|
+
const client = await connect();
|
|
75
|
+
let destroy = false;
|
|
76
|
+
try {
|
|
77
|
+
const running = client.query({ text: sql, values: [...params] });
|
|
78
|
+
running.catch(() => undefined); // an abandoned query may reject later; that must never be unhandled
|
|
79
|
+
const result = await new Promise((resolve, reject) => {
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
destroy = true;
|
|
82
|
+
// The slot is only freed after the cancel was attempted and the connection is being destroyed.
|
|
83
|
+
void cancelBackend(pool, client.processID).then(() => reject(new Error('Query read timeout')));
|
|
84
|
+
}, timeoutMs);
|
|
85
|
+
running.then((r) => (clearTimeout(timer), resolve(r)), (e) => (clearTimeout(timer), reject(e)));
|
|
86
|
+
});
|
|
87
|
+
return result.rows;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
client.release(destroy);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
async query(q, { timeoutMs }) {
|
|
95
|
+
try {
|
|
96
|
+
return pool.connect === undefined
|
|
97
|
+
? await viaPool(q.sql, q.params, timeoutMs)
|
|
98
|
+
: await viaClient(() => pool.connect?.(), q.sql, q.params, timeoutMs);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
throw toDriverError(err, o.redact, classifyPg);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
isTransient: isTransientError,
|
|
105
|
+
async close() {
|
|
106
|
+
if (owned)
|
|
107
|
+
await pool.end?.();
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=driver.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DriverAdapter } from 'dynalitics/adapter';
|
|
2
|
+
/**
|
|
3
|
+
* The PostgreSQL adapter for dynalitics: `createDynalitics({ adapter: postgres, ... })`.
|
|
4
|
+
* apiVersion is a literal on purpose: it states the contract THIS package was built for (spec 5.2).
|
|
5
|
+
*/
|
|
6
|
+
export declare const postgres: DriverAdapter;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createPostgresDriver } from './driver.js';
|
|
2
|
+
/**
|
|
3
|
+
* The PostgreSQL adapter for dynalitics: `createDynalitics({ adapter: postgres, ... })`.
|
|
4
|
+
* apiVersion is a literal on purpose: it states the contract THIS package was built for (spec 5.2).
|
|
5
|
+
*/
|
|
6
|
+
export const postgres = Object.freeze({
|
|
7
|
+
apiVersion: 1,
|
|
8
|
+
dialect: 'postgres',
|
|
9
|
+
packageName: 'dynalitics-postgres',
|
|
10
|
+
createDriver: createPostgresDriver,
|
|
11
|
+
});
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dynalitics-postgres",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "PostgreSQL adapter for dynalitics: brings in the pg driver and nothing else.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"analytics",
|
|
7
|
+
"dynalitics",
|
|
8
|
+
"postgres",
|
|
9
|
+
"pg",
|
|
10
|
+
"adapter"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "dynalitics contributors",
|
|
14
|
+
"homepage": "https://github.com/lucsoftsl/dynalitics/tree/main/packages/dynalitics-postgres#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/lucsoftsl/dynalitics/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/lucsoftsl/dynalitics.git",
|
|
21
|
+
"directory": "packages/dynalitics-postgres"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"!dist/**/*.map",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -b"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"pg": "8.23.1"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"dynalitics": "0.1.1"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"provenance": true
|
|
54
|
+
}
|
|
55
|
+
}
|