vv-db-connection 0.0.0-stage → 1.0.2
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 +193 -2
- package/distjs/src/db/adapters/fb.d.ts +14 -0
- package/distjs/src/db/adapters/fb.js +269 -0
- package/distjs/src/db/adapters/fb.js.map +1 -0
- package/distjs/src/db/adapters/mssql.d.ts +14 -0
- package/distjs/src/db/adapters/mssql.js +155 -0
- package/distjs/src/db/adapters/mssql.js.map +1 -0
- package/distjs/src/db/adapters/ora.d.ts +15 -0
- package/distjs/src/db/adapters/ora.js +137 -0
- package/distjs/src/db/adapters/ora.js.map +1 -0
- package/distjs/src/db/adapters/pg.d.ts +16 -0
- package/distjs/src/db/adapters/pg.js +153 -0
- package/distjs/src/db/adapters/pg.js.map +1 -0
- package/distjs/src/db/connection.d.ts +49 -0
- package/distjs/src/db/connection.js +5 -0
- package/distjs/src/db/connection.js.map +1 -0
- package/distjs/src/db/index.d.ts +16 -0
- package/distjs/src/db/index.js +131 -0
- package/distjs/src/db/index.js.map +1 -0
- package/distjs/src/db/kind.d.ts +2 -0
- package/distjs/src/db/kind.js +2 -0
- package/distjs/src/db/kind.js.map +1 -0
- package/distjs/src/db/placeholders.d.ts +2 -0
- package/distjs/src/db/placeholders.js +21 -0
- package/distjs/src/db/placeholders.js.map +1 -0
- package/distjs/src/db/types.d.ts +40 -0
- package/distjs/src/db/types.js +2 -0
- package/distjs/src/db/types.js.map +1 -0
- package/distjs/src/index.d.ts +4 -0
- package/distjs/src/index.js +2 -0
- package/distjs/src/index.js.map +1 -0
- package/distjs/src/tresult.d.ts +7 -0
- package/distjs/src/tresult.js +2 -0
- package/distjs/src/tresult.js.map +1 -0
- package/package.json +52 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vitalii Vasilev
|
|
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,194 @@
|
|
|
1
|
-
#
|
|
1
|
+
# vv-db-connection
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A single connection layer over **MSSQL**, **Oracle**, **PostgreSQL** and **Firebird**. One `createDbConnection` factory, one unified query interface — the rest of the application never touches driver-specific code.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i vv-db-connection
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Supported databases
|
|
10
|
+
|
|
11
|
+
| `kind` | Driver | Default port |
|
|
12
|
+
|-----------|-----------------|--------------|
|
|
13
|
+
| `mssql` | `mssql` | 1433 |
|
|
14
|
+
| `pg` | `pg` | 5432 |
|
|
15
|
+
| `ora` | `oracledb` | 1521 |
|
|
16
|
+
| `fb` | `node-firebird` | 3050 |
|
|
17
|
+
|
|
18
|
+
## Initialization
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createDbConnection } from 'vv-db-connection'
|
|
22
|
+
import type { TConnectionParam } from 'vv-db-connection'
|
|
23
|
+
|
|
24
|
+
const connection: TConnectionParam = {
|
|
25
|
+
kind: 'pg',
|
|
26
|
+
host: 'localhost',
|
|
27
|
+
port: 5432,
|
|
28
|
+
database: 'app',
|
|
29
|
+
user: 'admin',
|
|
30
|
+
password: 'secret',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const db = createDbConnection(connection)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`createDbConnection(connection)` picks the adapter from `connection.kind` and returns a `DbConnection`. The connection is passed as a plain object — its schema and types are owned by this package. Create as many connections as you need, in parallel, each with its own driver and pool:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const src = createDbConnection(mssqlConnection) // source
|
|
40
|
+
const dst = createDbConnection(pgConnection) // target
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The connection is opened lazily — `createDbConnection` only holds the config; the first `exec`/`ping`/`execStream` call opens it.
|
|
44
|
+
|
|
45
|
+
### Connection fields
|
|
46
|
+
|
|
47
|
+
| `kind` | Type | Fields |
|
|
48
|
+
|-----------|--------------|-------------------------------------------------------------------------------|
|
|
49
|
+
| `mssql` | `TConnMssql` | `server`, `port?`, `database?`, `auth`, `trustServerCertificate?`, `encrypt?` |
|
|
50
|
+
| `pg` | `TConnPg` | `host`, `port?`, `database`, `user`, `password`, `ssl?` |
|
|
51
|
+
| `ora` | `TConnOra` | `host`, `port?`, `service`, `user`, `password`, `privilege?` |
|
|
52
|
+
| `fb` | `TConnFb` | `host`, `port?`, `database`, `user`, `password`, `charset?` |
|
|
53
|
+
|
|
54
|
+
MSSQL `auth` is a union: `{ kind: 'mssql', user, password }` or `{ kind: 'ntlm', domain, user, password }`.
|
|
55
|
+
|
|
56
|
+
Oracle `privilege` is one of `SYSDBA | SYSOPER | SYSASM | SYSBACKUP | SYSDG | SYSKM | SYSRAC`; the connection string is assembled internally as `host:port/service`.
|
|
57
|
+
|
|
58
|
+
Firebird `charset` is `'UTF8'` (default) or `'WIN1251'`.
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
### `exec` — buffered query
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const result = await db.exec('SELECT * FROM orders WHERE status = :status', [
|
|
66
|
+
{ name: 'status', value: 'open' },
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
if (!result.ok) {
|
|
70
|
+
console.error(result.error)
|
|
71
|
+
} else {
|
|
72
|
+
for (const resultSet of result.result) {
|
|
73
|
+
console.log(resultSet.schema) // TDbCol[]
|
|
74
|
+
console.log(resultSet.rows) // object[]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Collects all rows into memory and returns `TResult<TDbRes[]>`. Each element of the array is one result set (relevant for multi-statement queries on MSSQL).
|
|
80
|
+
|
|
81
|
+
### `execStream` — streaming query
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
await db.execStream(query, params, (event) => {
|
|
85
|
+
if (event.kind === 'schema') { /* event.schema: TDbCol[] */ }
|
|
86
|
+
if (event.kind === 'row') { /* event.row: object */ }
|
|
87
|
+
if (event.kind === 'finish') { /* event.error?: string */ }
|
|
88
|
+
})
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Fires the callback incrementally as data arrives — for large result sets where buffering everything is undesirable. Events are always emitted in this order: `schema` → `row` × N → `finish`.
|
|
92
|
+
|
|
93
|
+
A query that goes silent is cut off on every database: no data within 12 hours after the query was sent, or no new data within 12 hours after the previous portion, ends it with `finish` carrying a `timeout: ...` error, and the call returns at once. The adapter is told to drop its connection (an `AbortSignal`) and is not waited for, so a driver stuck in its own teardown cannot hold the caller. `exec` goes through the same path and has the same limits.
|
|
94
|
+
|
|
95
|
+
### `execTxDynamic` — transaction
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const result = await db.execTxDynamic(async (exec) => {
|
|
99
|
+
await exec('INSERT INTO t (id) VALUES (:id)', [{ name: 'id', value: 1 }])
|
|
100
|
+
const res = await exec('SELECT MAX(id) AS id FROM t', [])
|
|
101
|
+
// res.rows: Record<string, unknown>[]
|
|
102
|
+
}, timeoutMs)
|
|
103
|
+
|
|
104
|
+
// result: TResult<void>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Opens a transaction, hands the callback an `exec` bound to it, and commits once the callback resolves. Anything thrown inside rolls back and comes out as `{ ok: false, error }`. Statements use the same `:name` parameters, and every call returns `{ rows }`, so a value generated inside the transaction can be read back before commit.
|
|
108
|
+
|
|
109
|
+
`timeoutMs` is optional (`0` or omitted — no limit). On expiry the connection is torn down and the call resolves as `{ ok: false, error: 'TIMEOUT' }`.
|
|
110
|
+
|
|
111
|
+
A dedicated connection is opened for the transaction and closed afterwards, on the commit and rollback paths alike. Firebird runs with `READ COMMITTED` isolation; the other adapters use the driver default.
|
|
112
|
+
|
|
113
|
+
### `ping`
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const result = await db.ping() // TResult<null>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A minimal read-only query that requires no privileges; returns `{ ok: true }` on success. Useful for waiting for the database to come up.
|
|
120
|
+
|
|
121
|
+
### `version`
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const result = await db.version() // TResult<string>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Queries the server version string.
|
|
128
|
+
|
|
129
|
+
### `kind` / `title` / `user`
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
db.kind() // 'pg' | 'mssql' | 'ora' | 'fb'
|
|
133
|
+
db.title() // e.g. "localhost:5432 [mydb]"
|
|
134
|
+
db.user() // e.g. "admin"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`kind` is the discriminator of the active connection (`TDbKind`); `title` and `user` are a human-readable description for log output.
|
|
138
|
+
|
|
139
|
+
## Query parameters
|
|
140
|
+
|
|
141
|
+
All methods accept named parameters in `:name` syntax, regardless of the underlying driver. Each adapter translates them to the driver's native format:
|
|
142
|
+
|
|
143
|
+
| Driver | Native format | Notes |
|
|
144
|
+
|------------|--------------------------------------|-------|
|
|
145
|
+
| MSSQL | `@name` (mssql `request.input`) | `:name` converted to `@name` |
|
|
146
|
+
| PostgreSQL | `$1, $2, …` | same `:name` used multiple times maps to the same `$N` |
|
|
147
|
+
| Oracle | `:name` (native bind) | passed as a named bind object |
|
|
148
|
+
| Firebird | `?` (positional) | replaced in left-to-right order of appearance |
|
|
149
|
+
|
|
150
|
+
In Firebird a `:name` inside a string literal, a quoted identifier or a comment is left as it is. In an `EXECUTE BLOCK` only the header — everything up to the `AS` that closes it — is treated as placeholders; past that begins PSQL, where `:name` refers to a local variable or a block parameter and is passed through untouched.
|
|
151
|
+
|
|
152
|
+
## Data types
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
type TDbCol = {
|
|
156
|
+
name: string
|
|
157
|
+
kind: string // driver-specific type name
|
|
158
|
+
len?: number
|
|
159
|
+
scale?: number
|
|
160
|
+
precision?: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
type TDbRes = {
|
|
164
|
+
schema: TDbCol[]
|
|
165
|
+
rows: object[]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
type TDbStreamEvent =
|
|
169
|
+
| { kind: 'schema'; schema: TDbCol[] }
|
|
170
|
+
| { kind: 'row'; row: object }
|
|
171
|
+
| { kind: 'finish'; error?: string }
|
|
172
|
+
|
|
173
|
+
type TDbTxResult = { rows: Record<string, unknown>[] }
|
|
174
|
+
type TDbTxExec = (query: string, params: TDbParam[]) => Promise<TDbTxResult>
|
|
175
|
+
type TDbTxCb = (exec: TDbTxExec) => Promise<void>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Unlike `exec`, a statement inside a transaction returns a single set of rows — no schema and no multi-result-set handling.
|
|
179
|
+
|
|
180
|
+
## Adapter notes
|
|
181
|
+
|
|
182
|
+
**MSSQL** — `request.stream = true`, rows delivered via events without driver-side buffering. A new `ConnectionPool` is created per query and closed in the `done` handler.
|
|
183
|
+
|
|
184
|
+
**PostgreSQL** — non-streaming `client.query()` (buffered inside `pg`). A fresh `Client` per query, disconnected in a `finally` block. A backend does not notice its client is gone until it has something to send, so an aborted query is cancelled from a connection of its own (`pg_cancel_backend`).
|
|
185
|
+
|
|
186
|
+
**Oracle** — `conn.queryStream()` with `OUT_FORMAT_OBJECT`, rows arrive as plain objects. A connection is obtained from the default pool and closed in a `finally` block.
|
|
187
|
+
|
|
188
|
+
**Firebird** — `node-firebird` has no streaming API; `db.sequentially()` calls the row callback once per row without loading the full result into memory. Column schema is inferred from the keys of the first row (types reported as `"unknown"`). Text blobs are fetched as text (`blobAsText`); binary blobs (`SUB_TYPE 0`) are not supported. With `charset: WIN1251` blob segments are re-encoded on the way in, because the driver decodes them with a hardcoded UTF8. The blobs of one fetch batch are read one after another, not all at once as the driver does, and rows are handed over batch by batch instead of being collected into an array first. A lost connection ends the query with `fb: connection lost`: on a closed socket the driver would quietly attach anew and leave the read waiting for an answer that never comes, so that reconnect is cancelled and the driver's `error` / `reconnect` events are taken as the same loss.
|
|
189
|
+
|
|
190
|
+
The Firebird adapter patches several `node-firebird` internals (its wire protocol layer) to work around bugs with Firebird 6 — see the note block in `src/db/adapters/fb.ts` for the full rationale.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IDbAdapter, TDbParam, TDbStreamEvent, TDbTxCb } from '../types.js';
|
|
2
|
+
import type { TConnFb } from '../connection.js';
|
|
3
|
+
import type { TResult } from '../../tresult.js';
|
|
4
|
+
export declare class FbAdapter implements IDbAdapter {
|
|
5
|
+
private readonly p;
|
|
6
|
+
constructor(p: TConnFb);
|
|
7
|
+
kind(): 'fb';
|
|
8
|
+
title(): string;
|
|
9
|
+
user(): string;
|
|
10
|
+
versionQuery(): string;
|
|
11
|
+
pingQuery(): string;
|
|
12
|
+
execStream(query: string, params: TDbParam[], callback: (event: TDbStreamEvent) => void, signal?: AbortSignal): Promise<void>;
|
|
13
|
+
execTxDynamic(cb: TDbTxCb, timeoutMs?: number): Promise<TResult<void>>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import * as Firebird from 'node-firebird';
|
|
2
|
+
import Connection from 'node-firebird/lib/wire/connection.js';
|
|
3
|
+
import { mapPlaceholders, SQL_ATOM } from '../placeholders.js';
|
|
4
|
+
const fbAttachProto = Connection.prototype;
|
|
5
|
+
if (!fbAttachProto.attach.__vvPatched) {
|
|
6
|
+
const origAttach = fbAttachProto.attach;
|
|
7
|
+
const patched = function (options, callback, db) {
|
|
8
|
+
const self = this;
|
|
9
|
+
return origAttach.call(this, options, function (err, ret) {
|
|
10
|
+
if (err) {
|
|
11
|
+
// Mark the close as wanted (skips the driver's self-reconnect) and
|
|
12
|
+
// rip the socket so the descriptor is released at once.
|
|
13
|
+
try {
|
|
14
|
+
self._isDetach = true;
|
|
15
|
+
clearTimeout(self._retry_connection_id);
|
|
16
|
+
self._socket?.destroy?.();
|
|
17
|
+
}
|
|
18
|
+
catch { /* cleanup must never mask the attach error */ }
|
|
19
|
+
}
|
|
20
|
+
if (callback)
|
|
21
|
+
callback(err, ret);
|
|
22
|
+
}, db);
|
|
23
|
+
};
|
|
24
|
+
patched.__vvPatched = true;
|
|
25
|
+
fbAttachProto.attach = patched;
|
|
26
|
+
}
|
|
27
|
+
// Index of the AS that closes the header of an EXECUTE BLOCK, or -1. Both lists the
|
|
28
|
+
// header may carry - the input parameters and RETURNS - are parenthesized, so the AS
|
|
29
|
+
// we want is the first one outside any parentheses, and outside literals and comments.
|
|
30
|
+
function execBlockBodyAt(query) {
|
|
31
|
+
if (!/^\s*EXECUTE\s+BLOCK\b/i.test(query))
|
|
32
|
+
return -1;
|
|
33
|
+
const scan = new RegExp(`${SQL_ATOM.source}|[()]|\\bAS\\b`, 'gi');
|
|
34
|
+
let depth = 0;
|
|
35
|
+
for (let m = scan.exec(query); m !== null; m = scan.exec(query)) {
|
|
36
|
+
const token = m[0];
|
|
37
|
+
if (token === '(') {
|
|
38
|
+
depth++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (token === ')') {
|
|
42
|
+
depth--;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (depth === 0 && /^as$/i.test(token))
|
|
46
|
+
return m.index;
|
|
47
|
+
}
|
|
48
|
+
return -1;
|
|
49
|
+
}
|
|
50
|
+
// Converts :name placeholders to ? (positional) and collects values in order.
|
|
51
|
+
// For EXECUTE BLOCK statements only the header is processed - past the AS begins PSQL,
|
|
52
|
+
// where :name is a reference to a local variable or to a parameter of the block itself
|
|
53
|
+
// and must be left alone. That AS is not always followed by BEGIN: declarations of local
|
|
54
|
+
// variables sit between the two, and a header that ends with "as declare variable x"
|
|
55
|
+
// used to be missed, which turned every variable reference in the body into a parameter
|
|
56
|
+
// nobody had passed and failed the query with "param not found".
|
|
57
|
+
function buildQuery(query, params) {
|
|
58
|
+
const values = [];
|
|
59
|
+
const bodyAt = execBlockBodyAt(query);
|
|
60
|
+
const sql = mapPlaceholders(query, (name) => {
|
|
61
|
+
const found = params.find((p) => p.name === name);
|
|
62
|
+
if (!found)
|
|
63
|
+
throw new Error(`fb: param not found: ${name}`);
|
|
64
|
+
values.push(found.value);
|
|
65
|
+
return '?';
|
|
66
|
+
}, bodyAt < 0 ? undefined : bodyAt);
|
|
67
|
+
return { sql, values };
|
|
68
|
+
}
|
|
69
|
+
// A lost connection must end the query with an error. node-firebird 2 rejects the
|
|
70
|
+
// pending operations on a closed socket (so a running query does end), but then still
|
|
71
|
+
// schedules a self-reconnect a second later - a fresh attach that nobody asked for and
|
|
72
|
+
// nobody tracks, on which the statement handles of the dropped connection are never
|
|
73
|
+
// valid. Here the close is seen right after the driver has scheduled that reconnect,
|
|
74
|
+
// the timer is cancelled, and the query ends with an error. The "error" and "reconnect"
|
|
75
|
+
// events of the database are taken as the same loss. The "error" listener stays for
|
|
76
|
+
// good: the driver emits it from the socket handler, and without a listener the loss is
|
|
77
|
+
// silently swallowed instead of surfacing.
|
|
78
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
79
|
+
function watchConnection(db, onLost) {
|
|
80
|
+
// The typings leave out that Database is an EventEmitter.
|
|
81
|
+
const events = db;
|
|
82
|
+
const conn = events.connection;
|
|
83
|
+
events.on('error', (err) => onLost(`fb: connection lost: ${String(err)}`));
|
|
84
|
+
events.on('reconnect', () => onLost('fb: connection lost, the driver reattached on its own'));
|
|
85
|
+
conn?._socket?.on('close', () => {
|
|
86
|
+
// Set by our own detach at the end of the query - a close that was asked for.
|
|
87
|
+
if (conn._isDetach)
|
|
88
|
+
return;
|
|
89
|
+
clearTimeout(conn._retry_connection_id);
|
|
90
|
+
onLost('fb: connection lost');
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Drops the connection at once, without a detach the server would have to answer.
|
|
94
|
+
function dropConnection(db) {
|
|
95
|
+
const conn = db.connection;
|
|
96
|
+
if (!conn)
|
|
97
|
+
return;
|
|
98
|
+
conn._isDetach = true;
|
|
99
|
+
clearTimeout(conn._retry_connection_id);
|
|
100
|
+
try {
|
|
101
|
+
conn._socket?.destroy();
|
|
102
|
+
}
|
|
103
|
+
catch { }
|
|
104
|
+
}
|
|
105
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
106
|
+
export class FbAdapter {
|
|
107
|
+
p;
|
|
108
|
+
constructor(p) {
|
|
109
|
+
this.p = p;
|
|
110
|
+
}
|
|
111
|
+
kind() { return 'fb'; }
|
|
112
|
+
title() { return `${this.p.host}:${this.p.port || 'DEFAULT'} [${this.p.database}]`; }
|
|
113
|
+
user() { return this.p.user; }
|
|
114
|
+
versionQuery() { return "SELECT rdb$get_context('SYSTEM', 'ENGINE_VERSION') AS ver FROM rdb$database"; }
|
|
115
|
+
pingQuery() { return 'SELECT 1 AS p FROM rdb$database'; }
|
|
116
|
+
async execStream(query, params, callback, signal) {
|
|
117
|
+
const { sql, values } = buildQuery(query, params);
|
|
118
|
+
const options = {
|
|
119
|
+
host: this.p.host,
|
|
120
|
+
port: this.p.port ?? 3050,
|
|
121
|
+
database: this.p.database,
|
|
122
|
+
user: this.p.user,
|
|
123
|
+
password: this.p.password,
|
|
124
|
+
encoding: (this.p.charset ?? 'UTF8'),
|
|
125
|
+
// Without it a text blob arrives as an async reader function instead of a
|
|
126
|
+
// value, and JSON.stringify silently drops such a field from the row.
|
|
127
|
+
blobAsText: true,
|
|
128
|
+
};
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
let done = false;
|
|
131
|
+
let db;
|
|
132
|
+
const finish = (error) => {
|
|
133
|
+
if (done)
|
|
134
|
+
return;
|
|
135
|
+
done = true;
|
|
136
|
+
signal?.removeEventListener('abort', onAbort);
|
|
137
|
+
callback({ kind: 'finish', error });
|
|
138
|
+
resolve();
|
|
139
|
+
};
|
|
140
|
+
const onAbort = () => {
|
|
141
|
+
if (db)
|
|
142
|
+
dropConnection(db);
|
|
143
|
+
finish(String(signal?.reason ?? 'aborted'));
|
|
144
|
+
};
|
|
145
|
+
if (signal?.aborted) {
|
|
146
|
+
finish(String(signal.reason ?? 'aborted'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
signal?.addEventListener('abort', onAbort);
|
|
150
|
+
Firebird.attach(options, (attachErr, attached) => {
|
|
151
|
+
if (attachErr) {
|
|
152
|
+
finish(String(attachErr));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Given up while the attach was on its way - nobody is waiting for this one.
|
|
156
|
+
if (done) {
|
|
157
|
+
dropConnection(attached);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
db = attached;
|
|
161
|
+
watchConnection(db, (error) => {
|
|
162
|
+
dropConnection(attached);
|
|
163
|
+
finish(error);
|
|
164
|
+
});
|
|
165
|
+
let schemaEmitted = false;
|
|
166
|
+
db.sequentially(sql, values, (row, _index) => {
|
|
167
|
+
if (done)
|
|
168
|
+
return;
|
|
169
|
+
if (!schemaEmitted) {
|
|
170
|
+
schemaEmitted = true;
|
|
171
|
+
const schema = Object.keys(row).map((name) => ({ name, kind: 'unknown' }));
|
|
172
|
+
callback({ kind: 'schema', schema });
|
|
173
|
+
}
|
|
174
|
+
callback({ kind: 'row', row });
|
|
175
|
+
}, (seqErr) => {
|
|
176
|
+
if (done)
|
|
177
|
+
return;
|
|
178
|
+
attached.detach();
|
|
179
|
+
finish(seqErr ? String(seqErr) : undefined);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async execTxDynamic(cb, timeoutMs) {
|
|
185
|
+
const options = {
|
|
186
|
+
host: this.p.host,
|
|
187
|
+
port: this.p.port ?? 3050,
|
|
188
|
+
database: this.p.database,
|
|
189
|
+
user: this.p.user,
|
|
190
|
+
password: this.p.password,
|
|
191
|
+
encoding: (this.p.charset ?? 'UTF8'),
|
|
192
|
+
// Without it a text blob arrives as an async reader function instead of a
|
|
193
|
+
// value, and JSON.stringify silently drops such a field from the row.
|
|
194
|
+
blobAsText: true,
|
|
195
|
+
};
|
|
196
|
+
const db = await new Promise((resolve, reject) => {
|
|
197
|
+
Firebird.attach(options, (err, d) => { if (err)
|
|
198
|
+
reject(err);
|
|
199
|
+
else
|
|
200
|
+
resolve(d); });
|
|
201
|
+
});
|
|
202
|
+
const txWork = async () => {
|
|
203
|
+
try {
|
|
204
|
+
const transaction = await new Promise((resolve, reject) => {
|
|
205
|
+
db.transaction(Firebird.ISOLATION_READ_COMMITTED, (err, tx) => { if (err)
|
|
206
|
+
reject(err);
|
|
207
|
+
else
|
|
208
|
+
resolve(tx); });
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
const exec = (query, params) => {
|
|
212
|
+
const { sql: sqlStr, values } = buildQuery(query, params);
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
transaction.query(sqlStr, values, (err, rows) => {
|
|
215
|
+
if (err)
|
|
216
|
+
reject(err);
|
|
217
|
+
else
|
|
218
|
+
resolve({ rows: rows ?? [] });
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
await cb(exec);
|
|
223
|
+
await new Promise((resolve, reject) => {
|
|
224
|
+
transaction.commit((err) => { if (err)
|
|
225
|
+
reject(err);
|
|
226
|
+
else
|
|
227
|
+
resolve(); });
|
|
228
|
+
});
|
|
229
|
+
db.detach();
|
|
230
|
+
return { ok: true, result: undefined };
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
try {
|
|
234
|
+
await new Promise((resolve) => { transaction.rollback(() => resolve()); });
|
|
235
|
+
}
|
|
236
|
+
catch { }
|
|
237
|
+
db.detach();
|
|
238
|
+
return { ok: false, error: String(err) };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
try {
|
|
243
|
+
db.detach();
|
|
244
|
+
}
|
|
245
|
+
catch { }
|
|
246
|
+
return { ok: false, error: String(err) };
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
if (!timeoutMs)
|
|
250
|
+
return txWork();
|
|
251
|
+
let timer;
|
|
252
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
253
|
+
timer = setTimeout(() => {
|
|
254
|
+
try {
|
|
255
|
+
db.detach();
|
|
256
|
+
}
|
|
257
|
+
catch { }
|
|
258
|
+
resolve({ ok: false, error: 'TIMEOUT' });
|
|
259
|
+
}, timeoutMs);
|
|
260
|
+
});
|
|
261
|
+
try {
|
|
262
|
+
return await Promise.race([txWork(), timeoutPromise]);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=fb.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fb.js","sourceRoot":"","sources":["../../../../src/db/adapters/fb.ts"],"names":[],"mappings":"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","sourcesContent":["import * as Firebird from 'node-firebird'\nimport Connection from 'node-firebird/lib/wire/connection'\nimport type { IDbAdapter, TDbCol, TDbParam, TDbStreamEvent, TDbTxCb, TDbTxResult } from '../types'\nimport type { TConnFb } from '../connection'\nimport type { TResult } from '../../tresult'\nimport { mapPlaceholders, SQL_ATOM } from '../placeholders'\n\n// ---- monkey-patch: node-firebird@2 leaks the TCP socket on a failed attach ----\n// Connection.attach() answers an attach error with doError(err, cb) and never\n// disconnects, and the Firebird server does not close the connection after a\n// failed op_attach either - so a failed attach (bad credentials, wrong path, DB\n// down) leaves a live socket behind. A long-running process then leaks file\n// descriptors up to EMFILE, and short-lived scripts/tests never exit. The driver\n// does not hand the connection out on the error path, so this is patched at the\n// prototype, with the same teardown dropConnection() uses. The driver version is\n// pinned in package.json; see .hermes.md and re-check this patch on upgrade.\ntype FbConnInternal = {\n\t_isDetach?: boolean\n\t_retry_connection_id?: ReturnType<typeof setTimeout>\n\t_socket?: { destroy?: () => void } | null\n}\ntype FbAttachFn = (\n\tthis: FbConnInternal,\n\toptions: unknown,\n\tcallback?: (err: unknown, ret: unknown) => void,\n\tdb?: unknown,\n) => unknown\n\nconst fbAttachProto = Connection.prototype as unknown as { attach: FbAttachFn & { __vvPatched?: true } }\nif (!fbAttachProto.attach.__vvPatched) {\n\tconst origAttach = fbAttachProto.attach\n\tconst patched = function (this: FbConnInternal, options: unknown, callback?: (err: unknown, ret: unknown) => void, db?: unknown): unknown {\n\t\tconst self = this\n\t\treturn origAttach.call(this, options, function (err: unknown, ret: unknown): void {\n\t\t\tif (err) {\n\t\t\t\t// Mark the close as wanted (skips the driver's self-reconnect) and\n\t\t\t\t// rip the socket so the descriptor is released at once.\n\t\t\t\ttry {\n\t\t\t\t\tself._isDetach = true\n\t\t\t\t\tclearTimeout(self._retry_connection_id)\n\t\t\t\t\tself._socket?.destroy?.()\n\t\t\t\t} catch { /* cleanup must never mask the attach error */ }\n\t\t\t}\n\t\t\tif (callback) callback(err, ret)\n\t\t}, db)\n\t} as FbAttachFn & { __vvPatched?: true }\n\tpatched.__vvPatched = true\n\tfbAttachProto.attach = patched\n}\n\n\n// Index of the AS that closes the header of an EXECUTE BLOCK, or -1. Both lists the\n// header may carry - the input parameters and RETURNS - are parenthesized, so the AS\n// we want is the first one outside any parentheses, and outside literals and comments.\nfunction execBlockBodyAt(query: string): number {\n\tif (!/^\\s*EXECUTE\\s+BLOCK\\b/i.test(query)) return -1\n\tconst scan = new RegExp(`${SQL_ATOM.source}|[()]|\\\\bAS\\\\b`, 'gi')\n\tlet depth = 0\n\tfor (let m = scan.exec(query); m !== null; m = scan.exec(query)) {\n\t\tconst token = m[0]\n\t\tif (token === '(') { depth++; continue }\n\t\tif (token === ')') { depth--; continue }\n\t\tif (depth === 0 && /^as$/i.test(token)) return m.index\n\t}\n\treturn -1\n}\n\n// Converts :name placeholders to ? (positional) and collects values in order.\n// For EXECUTE BLOCK statements only the header is processed - past the AS begins PSQL,\n// where :name is a reference to a local variable or to a parameter of the block itself\n// and must be left alone. That AS is not always followed by BEGIN: declarations of local\n// variables sit between the two, and a header that ends with \"as declare variable x\"\n// used to be missed, which turned every variable reference in the body into a parameter\n// nobody had passed and failed the query with \"param not found\".\nfunction buildQuery(query: string, params: TDbParam[]): { sql: string; values: unknown[] } {\n\tconst values: unknown[] = []\n\tconst bodyAt = execBlockBodyAt(query)\n\tconst sql = mapPlaceholders(query, (name) => {\n\t\tconst found = params.find((p) => p.name === name)\n\t\tif (!found) throw new Error(`fb: param not found: ${name}`)\n\t\tvalues.push(found.value)\n\t\treturn '?'\n\t}, bodyAt < 0 ? undefined : bodyAt)\n\treturn { sql, values }\n}\n\n// A lost connection must end the query with an error. node-firebird 2 rejects the\n// pending operations on a closed socket (so a running query does end), but then still\n// schedules a self-reconnect a second later - a fresh attach that nobody asked for and\n// nobody tracks, on which the statement handles of the dropped connection are never\n// valid. Here the close is seen right after the driver has scheduled that reconnect,\n// the timer is cancelled, and the query ends with an error. The \"error\" and \"reconnect\"\n// events of the database are taken as the same loss. The \"error\" listener stays for\n// good: the driver emits it from the socket handler, and without a listener the loss is\n// silently swallowed instead of surfacing.\n/* eslint-disable @typescript-eslint/no-explicit-any */\nfunction watchConnection(db: Firebird.Database, onLost: (error: string) => void): void {\n\t// The typings leave out that Database is an EventEmitter.\n\tconst events: any = db\n\tconst conn: any = events.connection\n\tevents.on('error', (err: unknown) => onLost(`fb: connection lost: ${String(err)}`))\n\tevents.on('reconnect', () => onLost('fb: connection lost, the driver reattached on its own'))\n\tconn?._socket?.on('close', () => {\n\t\t// Set by our own detach at the end of the query - a close that was asked for.\n\t\tif (conn._isDetach) return\n\t\tclearTimeout(conn._retry_connection_id)\n\t\tonLost('fb: connection lost')\n\t})\n}\n\n// Drops the connection at once, without a detach the server would have to answer.\nfunction dropConnection(db: Firebird.Database): void {\n\tconst conn: any = (db as any).connection\n\tif (!conn) return\n\tconn._isDetach = true\n\tclearTimeout(conn._retry_connection_id)\n\ttry { conn._socket?.destroy() } catch { }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\nexport class FbAdapter implements IDbAdapter {\n\tconstructor(private readonly p: TConnFb) {}\n\n\tkind(): 'fb' { return 'fb' }\n\ttitle(): string { return `${this.p.host}:${this.p.port || 'DEFAULT'} [${this.p.database}]` }\n\tuser(): string { return this.p.user }\n\tversionQuery(): string { return \"SELECT rdb$get_context('SYSTEM', 'ENGINE_VERSION') AS ver FROM rdb$database\" }\n\tpingQuery(): string { return 'SELECT 1 AS p FROM rdb$database' }\n\n\tasync execStream(query: string, params: TDbParam[], callback: (event: TDbStreamEvent) => void, signal?: AbortSignal): Promise<void> {\n\t\tconst { sql, values } = buildQuery(query, params)\n\t\tconst options: Firebird.Options = {\n\t\t\thost: this.p.host,\n\t\t\tport: this.p.port ?? 3050,\n\t\t\tdatabase: this.p.database,\n\t\t\tuser: this.p.user,\n\t\t\tpassword: this.p.password,\n\t\t\tencoding: (this.p.charset ?? 'UTF8') as Firebird.SupportedCharacterSet,\n\t\t\t// Without it a text blob arrives as an async reader function instead of a\n\t\t\t// value, and JSON.stringify silently drops such a field from the row.\n\t\t\tblobAsText: true,\n\t\t}\n\t\treturn new Promise<void>((resolve) => {\n\t\t\tlet done = false\n\t\t\tlet db: Firebird.Database | undefined\n\t\t\tconst finish = (error?: string): void => {\n\t\t\t\tif (done) return\n\t\t\t\tdone = true\n\t\t\t\tsignal?.removeEventListener('abort', onAbort)\n\t\t\t\tcallback({ kind: 'finish', error })\n\t\t\t\tresolve()\n\t\t\t}\n\t\t\tconst onAbort = (): void => {\n\t\t\t\tif (db) dropConnection(db)\n\t\t\t\tfinish(String(signal?.reason ?? 'aborted'))\n\t\t\t}\n\t\t\tif (signal?.aborted) { finish(String(signal.reason ?? 'aborted')); return }\n\t\t\tsignal?.addEventListener('abort', onAbort)\n\n\t\t\tFirebird.attach(options, (attachErr, attached) => {\n\t\t\t\tif (attachErr) { finish(String(attachErr)); return }\n\t\t\t\t// Given up while the attach was on its way - nobody is waiting for this one.\n\t\t\t\tif (done) { dropConnection(attached); return }\n\t\t\t\tdb = attached\n\t\t\t\twatchConnection(db, (error) => {\n\t\t\t\t\tdropConnection(attached)\n\t\t\t\t\tfinish(error)\n\t\t\t\t})\n\t\t\t\tlet schemaEmitted = false\n\t\t\t\tdb.sequentially(\n\t\t\t\t\tsql,\n\t\t\t\t\tvalues,\n\t\t\t\t\t(row: Record<string, unknown>, _index: number) => {\n\t\t\t\t\t\tif (done) return\n\t\t\t\t\t\tif (!schemaEmitted) {\n\t\t\t\t\t\t\tschemaEmitted = true\n\t\t\t\t\t\t\tconst schema: TDbCol[] = Object.keys(row).map((name) => ({ name, kind: 'unknown' }))\n\t\t\t\t\t\t\tcallback({ kind: 'schema', schema })\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcallback({ kind: 'row', row })\n\t\t\t\t\t},\n\t\t\t\t\t(seqErr?: Error) => {\n\t\t\t\t\t\tif (done) return\n\t\t\t\t\t\tattached.detach()\n\t\t\t\t\t\tfinish(seqErr ? String(seqErr) : undefined)\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t})\n\t\t})\n\t}\n\n\tasync execTxDynamic(cb: TDbTxCb, timeoutMs?: number): Promise<TResult<void>> {\n\t\tconst options: Firebird.Options = {\n\t\t\thost: this.p.host,\n\t\t\tport: this.p.port ?? 3050,\n\t\t\tdatabase: this.p.database,\n\t\t\tuser: this.p.user,\n\t\t\tpassword: this.p.password,\n\t\t\tencoding: (this.p.charset ?? 'UTF8') as Firebird.SupportedCharacterSet,\n\t\t\t// Without it a text blob arrives as an async reader function instead of a\n\t\t\t// value, and JSON.stringify silently drops such a field from the row.\n\t\t\tblobAsText: true,\n\t\t}\n\t\tconst db = await new Promise<Firebird.Database>((resolve, reject) => {\n\t\t\tFirebird.attach(options, (err, d) => { if (err) reject(err); else resolve(d) })\n\t\t})\n\t\tconst txWork = async (): Promise<TResult<void>> => {\n\t\t\ttry {\n\t\t\t\tconst transaction = await new Promise<Firebird.Transaction>((resolve, reject) => {\n\t\t\t\t\tdb.transaction(Firebird.ISOLATION_READ_COMMITTED, (err, tx) => { if (err) reject(err); else resolve(tx) })\n\t\t\t\t})\n\t\t\t\ttry {\n\t\t\t\t\tconst exec = (query: string, params: TDbParam[]): Promise<TDbTxResult> => {\n\t\t\t\t\t\tconst { sql: sqlStr, values } = buildQuery(query, params)\n\t\t\t\t\t\treturn new Promise<TDbTxResult>((resolve, reject) => {\n\t\t\t\t\t\t\ttransaction.query(sqlStr, values, (err: Error | null, rows: Record<string, unknown>[]) => {\n\t\t\t\t\t\t\t\tif (err) reject(err)\n\t\t\t\t\t\t\t\telse resolve({ rows: rows ?? [] })\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t\tawait cb(exec)\n\t\t\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\t\t\ttransaction.commit((err: Error | null) => { if (err) reject(err); else resolve() })\n\t\t\t\t\t})\n\t\t\t\t\tdb.detach()\n\t\t\t\t\treturn { ok: true, result: undefined }\n\t\t\t\t} catch (err) {\n\t\t\t\t\ttry { await new Promise<void>((resolve) => { transaction.rollback(() => resolve()) }) } catch {}\n\t\t\t\t\tdb.detach()\n\t\t\t\t\treturn { ok: false, error: String(err) }\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\ttry { db.detach() } catch {}\n\t\t\t\treturn { ok: false, error: String(err) }\n\t\t\t}\n\t\t}\n\t\tif (!timeoutMs) return txWork()\n\t\tlet timer: ReturnType<typeof setTimeout> | undefined\n\t\tconst timeoutPromise = new Promise<TResult<void>>((resolve) => {\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\ttry { db.detach() } catch {}\n\t\t\t\tresolve({ ok: false, error: 'TIMEOUT' })\n\t\t\t}, timeoutMs)\n\t\t})\n\t\ttry {\n\t\t\treturn await Promise.race([txWork(), timeoutPromise])\n\t\t} finally {\n\t\t\tclearTimeout(timer)\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IDbAdapter, TDbParam, TDbStreamEvent, TDbTxCb } from '../types.js';
|
|
2
|
+
import type { TConnMssql } from '../connection.js';
|
|
3
|
+
import type { TResult } from '../../tresult.js';
|
|
4
|
+
export declare class MssqlAdapter implements IDbAdapter {
|
|
5
|
+
private readonly p;
|
|
6
|
+
constructor(p: TConnMssql);
|
|
7
|
+
kind(): 'mssql';
|
|
8
|
+
title(): string;
|
|
9
|
+
user(): string;
|
|
10
|
+
versionQuery(): string;
|
|
11
|
+
pingQuery(): string;
|
|
12
|
+
execStream(query: string, params: TDbParam[], callback: (event: TDbStreamEvent) => void, signal?: AbortSignal): Promise<void>;
|
|
13
|
+
execTxDynamic(cb: TDbTxCb, timeoutMs?: number): Promise<TResult<void>>;
|
|
14
|
+
}
|