node-firebird 2.8.0 → 2.9.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.
- package/README.md +103 -2
- package/lib/index.d.ts +12 -9
- package/lib/index.js +11 -3
- package/lib/named-params.d.ts +42 -0
- package/lib/named-params.js +133 -0
- package/lib/srp.js +17 -4
- package/lib/types.d.ts +42 -20
- package/lib/uri.d.ts +57 -0
- package/lib/uri.js +193 -0
- package/lib/wire/connection.js +19 -2
- package/lib/wire/statement.d.ts +9 -1
- package/lib/wire/statement.js +25 -1
- package/lib/wire/transaction.d.ts +3 -1
- package/lib/wire/transaction.js +28 -5
- package/package.json +1 -1
- package/src/index.ts +18 -11
- package/src/named-params.ts +145 -0
- package/src/srp.ts +18 -5
- package/src/types.ts +43 -20
- package/src/uri.ts +204 -0
- package/src/wire/connection.ts +17 -2
- package/src/wire/statement.ts +31 -2
- package/src/wire/transaction.ts +32 -5
package/src/srp.ts
CHANGED
|
@@ -133,14 +133,18 @@ export function clientProof(user: string, password: string, salt: Buffer, A: big
|
|
|
133
133
|
|
|
134
134
|
n1 = modPow(n1, n2, PRIME.N);
|
|
135
135
|
n2 = toBigInt(getHash('sha1', user));
|
|
136
|
-
|
|
136
|
+
// K is hashed as the raw fixed-length digest, exactly like the server's
|
|
137
|
+
// digest.process(sessionKey) over a 20-byte UCharBuffer. Converting it
|
|
138
|
+
// through bigint dropped a leading zero byte (~0.4% of connections) and
|
|
139
|
+
// broke the proof (issue #421).
|
|
140
|
+
var M = toBigInt(getHash(hashAlgo, toBuffer(n1), toBuffer(n2), salt, toBuffer(A), toBuffer(B), K));
|
|
137
141
|
|
|
138
142
|
dump('n1-2', n1);
|
|
139
143
|
dump('n2-2', n2);
|
|
140
144
|
dump('proof:M', M);
|
|
141
145
|
|
|
142
146
|
return {
|
|
143
|
-
clientSessionKey: K,
|
|
147
|
+
clientSessionKey: toBigInt(K),
|
|
144
148
|
authData: M,
|
|
145
149
|
};
|
|
146
150
|
}
|
|
@@ -186,12 +190,19 @@ function pad(n: bigint): Buffer {
|
|
|
186
190
|
/**
|
|
187
191
|
* Scramble keys.
|
|
188
192
|
*
|
|
193
|
+
* The server hashes the minimal (stripped) magnitude bytes of A and B
|
|
194
|
+
* (RemotePassword::computeScramble → processStrippedInt in Firebird's
|
|
195
|
+
* srp.cpp, identical in 3.0 through master) — NOT the 128-byte padded
|
|
196
|
+
* form, which the engine only uses for k = H(N, pad(g)). Padding here
|
|
197
|
+
* made u diverge whenever A or B had a leading zero byte (~0.8% of
|
|
198
|
+
* connections), failing the proof (issue #421).
|
|
199
|
+
*
|
|
189
200
|
* @param A BigInt Client public key.
|
|
190
201
|
* @param B BigInt Server public key.
|
|
191
202
|
* @returns {BigInt}
|
|
192
203
|
*/
|
|
193
204
|
function getScramble(A: bigint, B: bigint, hashAlgo: string = 'sha1'): bigint {
|
|
194
|
-
return BigInt('0x' + getHash(hashAlgo,
|
|
205
|
+
return BigInt('0x' + getHash(hashAlgo, toBuffer(A), toBuffer(B)));
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
/**
|
|
@@ -208,8 +219,10 @@ function getScramble(A: bigint, B: bigint, hashAlgo: string = 'sha1'): bigint {
|
|
|
208
219
|
* @param A BigInt Client public key.
|
|
209
220
|
* @param B BigInt Server public key.
|
|
210
221
|
* @param a BigInt Client private key.
|
|
222
|
+
* @returns Buffer The raw session-key digest (fixed length, may start
|
|
223
|
+
* with a zero byte — significant for the proof).
|
|
211
224
|
*/
|
|
212
|
-
function clientSession(user: string, password: string, salt: Buffer, A: bigint, B: bigint, a: bigint, hashAlgo: string = 'sha1'):
|
|
225
|
+
function clientSession(user: string, password: string, salt: Buffer, A: bigint, B: bigint, a: bigint, hashAlgo: string = 'sha1'): Buffer {
|
|
213
226
|
var u = getScramble(A, B, 'sha1');
|
|
214
227
|
var x = getUserHash(user, salt, password, 'sha1');
|
|
215
228
|
var gx = modPow(PRIME.g, x, PRIME.N);
|
|
@@ -227,7 +240,7 @@ function clientSession(user: string, password: string, salt: Buffer, A: bigint,
|
|
|
227
240
|
var ux = (u * x) % PRIME.N;
|
|
228
241
|
var aux = (a + ux) % PRIME.N;
|
|
229
242
|
var sessionSecret = modPow(diff, aux, PRIME.N);
|
|
230
|
-
var K =
|
|
243
|
+
var K = Buffer.from(getHash('sha1', toBuffer(sessionSecret)), 'hex');
|
|
231
244
|
|
|
232
245
|
dump('B', B);
|
|
233
246
|
dump('u', u);
|
package/src/types.ts
CHANGED
|
@@ -100,9 +100,22 @@ export type BatchOptions = {
|
|
|
100
100
|
chunkSize?: number;
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Positional query parameters (array), or — when named placeholders are
|
|
105
|
+
* enabled via the `namedPlaceholders` connection/query option — values by
|
|
106
|
+
* placeholder name.
|
|
107
|
+
*/
|
|
108
|
+
export type QueryParams = any[] | Record<string, any>;
|
|
109
|
+
|
|
103
110
|
export type QueryOptions = {
|
|
104
111
|
timeout?: number;
|
|
105
112
|
scrollable?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Per-query override of the `namedPlaceholders` connection option
|
|
115
|
+
* (e.g. disable it for one EXECUTE BLOCK statement whose body uses
|
|
116
|
+
* `:variable` PSQL references).
|
|
117
|
+
*/
|
|
118
|
+
namedPlaceholders?: boolean;
|
|
106
119
|
/**
|
|
107
120
|
* Abort the query when the signal fires (Firebird 2.5+ / protocol 12+).
|
|
108
121
|
* If the signal is already aborted the query is not sent at all and the
|
|
@@ -118,11 +131,11 @@ export interface Database {
|
|
|
118
131
|
detach(callback?: SimpleCallback): Database;
|
|
119
132
|
transaction(options: TransactionOptions|Isolation|TransactionCallback, callback?: TransactionCallback): Database;
|
|
120
133
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): Database;
|
|
121
|
-
query(query: string, params:
|
|
122
|
-
execute(query: string, params:
|
|
134
|
+
query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): Database;
|
|
135
|
+
execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): Database;
|
|
123
136
|
/** Bulk-execute in its own transaction, all-or-nothing (Firebird 4.0+). */
|
|
124
|
-
executeBatch(query: string, rows:
|
|
125
|
-
sequentially(query: string, params:
|
|
137
|
+
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): Database;
|
|
138
|
+
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
126
139
|
drop(callback: SimpleCallback): void;
|
|
127
140
|
escape(value: any): string;
|
|
128
141
|
attachEvent(callback: any): this;
|
|
@@ -133,10 +146,10 @@ export interface Database {
|
|
|
133
146
|
|
|
134
147
|
// Promise / async-await API (see README § Promises / async–await).
|
|
135
148
|
// Result metadata is only available through the callback API.
|
|
136
|
-
queryAsync<T = any>(query: string, params?:
|
|
137
|
-
executeAsync<T = any>(query: string, params?:
|
|
138
|
-
executeBatchAsync(query: string, rows:
|
|
139
|
-
sequentiallyAsync(query: string, params:
|
|
149
|
+
queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
150
|
+
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
151
|
+
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
152
|
+
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
140
153
|
sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
141
154
|
transactionAsync(options?: TransactionOptions | Isolation): Promise<Transaction>;
|
|
142
155
|
startTransactionAsync(options?: TransactionOptions | Isolation): Promise<Transaction>;
|
|
@@ -158,21 +171,21 @@ export interface Database {
|
|
|
158
171
|
|
|
159
172
|
export interface Transaction {
|
|
160
173
|
newStatement(query: string, callback: (err: Error | null, statement: Statement) => void): void;
|
|
161
|
-
query(query: string, params:
|
|
162
|
-
execute(query: string, params:
|
|
174
|
+
query(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
|
|
175
|
+
execute(query: string, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
|
|
163
176
|
/** Bulk-execute within this transaction; per-record failures do not roll back (Firebird 4.0+). */
|
|
164
|
-
executeBatch(query: string, rows:
|
|
165
|
-
sequentially(query: string, params:
|
|
177
|
+
executeBatch(query: string, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
|
|
178
|
+
sequentially(query: string, params: QueryParams, rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
|
|
166
179
|
commit(callback?: SimpleCallback): void;
|
|
167
180
|
commitRetaining(callback?: SimpleCallback): void;
|
|
168
181
|
rollback(callback?: SimpleCallback): void;
|
|
169
182
|
rollbackRetaining(callback?: SimpleCallback): void;
|
|
170
183
|
|
|
171
184
|
// Promise / async-await API
|
|
172
|
-
queryAsync<T = any>(query: string, params?:
|
|
173
|
-
executeAsync<T = any>(query: string, params?:
|
|
174
|
-
executeBatchAsync(query: string, rows:
|
|
175
|
-
sequentiallyAsync(query: string, params:
|
|
185
|
+
queryAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
186
|
+
executeAsync<T = any>(query: string, params?: QueryParams, options?: QueryOptions): Promise<T[]>;
|
|
187
|
+
executeBatchAsync(query: string, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
188
|
+
sequentiallyAsync(query: string, params: QueryParams | undefined, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
176
189
|
sequentiallyAsync(query: string, rowCallback: SequentialCallback, options?: QueryOptions | boolean): Promise<void>;
|
|
177
190
|
newStatementAsync(query: string): Promise<Statement>;
|
|
178
191
|
commitAsync(): Promise<void>;
|
|
@@ -185,17 +198,17 @@ export interface Statement {
|
|
|
185
198
|
close(callback?: SimpleCallback): void;
|
|
186
199
|
drop(callback?: SimpleCallback): void;
|
|
187
200
|
release(callback?: SimpleCallback): void;
|
|
188
|
-
execute(transaction: Transaction, params:
|
|
201
|
+
execute(transaction: Transaction, params: QueryParams, callback: QueryCallback, options?: QueryOptions): void;
|
|
189
202
|
fetch(transaction: Transaction, count: number, callback: QueryCallback): void;
|
|
190
203
|
fetchScroll(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset: number, count: number, callback: QueryCallback): void;
|
|
191
204
|
fetchAll(transaction: Transaction, callback: QueryCallback): void;
|
|
192
205
|
|
|
193
206
|
/** Execute this prepared statement once per row (Firebird 4.0+ batch API). */
|
|
194
|
-
executeBatch(transaction: Transaction, rows:
|
|
207
|
+
executeBatch(transaction: Transaction, rows: QueryParams[], callback?: (err: any, result: BatchResult) => void, options?: BatchOptions): void;
|
|
195
208
|
|
|
196
209
|
// Promise / async-await API
|
|
197
|
-
executeAsync(transaction: Transaction, params?:
|
|
198
|
-
executeBatchAsync(transaction: Transaction, rows:
|
|
210
|
+
executeAsync(transaction: Transaction, params?: QueryParams, options?: QueryOptions): Promise<any>;
|
|
211
|
+
executeBatchAsync(transaction: Transaction, rows: QueryParams[], options?: BatchOptions): Promise<BatchResult>;
|
|
199
212
|
fetchAsync(transaction: Transaction, count: number | 'all'): Promise<any>;
|
|
200
213
|
fetchScrollAsync(transaction: Transaction, direction: 'NEXT' | 'PRIOR' | 'FIRST' | 'LAST' | 'ABSOLUTE' | 'RELATIVE' | number, offset?: number, count?: number): Promise<any>;
|
|
201
214
|
fetchAllAsync(transaction: Transaction): Promise<any>;
|
|
@@ -266,6 +279,16 @@ export interface Options {
|
|
|
266
279
|
blobReadChunkSize?: number;
|
|
267
280
|
wireCrypt?: number; // WIRE_CRYPT_DISABLE or WIRE_CRYPT_ENABLE
|
|
268
281
|
wireCompression?: boolean;
|
|
282
|
+
/**
|
|
283
|
+
* Enable named placeholders: SQL may use `:name` markers and params may
|
|
284
|
+
* be a values-by-name object (`db.query('... WHERE id = :id', { id: 1 })`).
|
|
285
|
+
* Placeholders are rewritten client-side to positional `?` before
|
|
286
|
+
* preparing; positional arrays keep working unchanged. Off by default
|
|
287
|
+
* because `EXECUTE BLOCK` bodies use `:variable` for PSQL references —
|
|
288
|
+
* with this option on, run such statements with positional params or a
|
|
289
|
+
* per-query `namedPlaceholders: false` override.
|
|
290
|
+
*/
|
|
291
|
+
namedPlaceholders?: boolean;
|
|
269
292
|
pluginName?: string;
|
|
270
293
|
parallelWorkers?: number;
|
|
271
294
|
maxInlineBlobSize?: number;
|
package/src/uri.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* Connection URI strings
|
|
4
|
+
*
|
|
5
|
+
***************************************/
|
|
6
|
+
|
|
7
|
+
import type { Options } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Option keys coerced to boolean when they arrive as URI query parameters.
|
|
11
|
+
* "1"/"true"/"yes"/"on" (case-insensitive) → true, everything else → false.
|
|
12
|
+
*/
|
|
13
|
+
const BOOLEAN_KEYS = new Set([
|
|
14
|
+
'lowercase_keys', 'blobAsText', 'wireCompression', 'manager',
|
|
15
|
+
'namedPlaceholders',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/** Option keys coerced to number when they arrive as URI query parameters. */
|
|
19
|
+
const NUMBER_KEYS = new Set([
|
|
20
|
+
'port', 'pageSize', 'timeout', 'retryConnectionInterval',
|
|
21
|
+
'blobChunkSize', 'blobReadChunkSize', 'wireCrypt', 'parallelWorkers',
|
|
22
|
+
'maxInlineBlobSize', 'maxNegotiatedProtocols', 'connectTimeout',
|
|
23
|
+
'min', 'idleTimeoutMillis',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function coerce(key: string, value: string): any {
|
|
27
|
+
if (BOOLEAN_KEYS.has(key)) {
|
|
28
|
+
return /^(1|true|yes|on)$/i.test(value);
|
|
29
|
+
}
|
|
30
|
+
if (NUMBER_KEYS.has(key)) {
|
|
31
|
+
var n = Number(value);
|
|
32
|
+
if (Number.isNaN(n)) {
|
|
33
|
+
throw new Error('Invalid numeric value for connection URI option "' + key + '": ' + value);
|
|
34
|
+
}
|
|
35
|
+
return n;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a firebird:// connection URI into an options object.
|
|
42
|
+
*
|
|
43
|
+
* firebird://user:password@host:port/database?option=value&...
|
|
44
|
+
*
|
|
45
|
+
* The database part:
|
|
46
|
+
* firebird://host/employee → alias "employee"
|
|
47
|
+
* firebird://host//var/db/prod.fdb → absolute path "/var/db/prod.fdb"
|
|
48
|
+
* firebird://host/var/db/prod.fdb → "/var/db/prod.fdb" (a database
|
|
49
|
+
* part with slashes is a path —
|
|
50
|
+
* aliases cannot contain "/")
|
|
51
|
+
* firebird://host/C:/db/prod.fdb → Windows path "C:/db/prod.fdb"
|
|
52
|
+
*
|
|
53
|
+
* Credentials and the database path are URL-decoded, so reserved characters
|
|
54
|
+
* can be percent-encoded (e.g. p%40ss for "p@ss"). Query parameters map
|
|
55
|
+
* 1:1 to option keys and are coerced to the option's type (booleans accept
|
|
56
|
+
* 1/true/yes/on). `user` and `password` may be given as query parameters
|
|
57
|
+
* instead of in the authority.
|
|
58
|
+
*/
|
|
59
|
+
export function parseConnectionUri(uri: string): Options {
|
|
60
|
+
var url: URL;
|
|
61
|
+
try {
|
|
62
|
+
url = new URL(uri);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
throw new Error('Invalid connection URI: ' + uri);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (url.protocol !== 'firebird:') {
|
|
68
|
+
throw new Error('Unsupported connection URI scheme "' + url.protocol.replace(/:$/, '') +
|
|
69
|
+
'" (expected firebird://...)');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var options: any = {};
|
|
73
|
+
|
|
74
|
+
if (url.hostname) {
|
|
75
|
+
// URL keeps IPv6 hostnames bracketed ([::1]); net.connect wants them bare
|
|
76
|
+
options.host = url.hostname.replace(/^\[(.*)\]$/, '$1');
|
|
77
|
+
}
|
|
78
|
+
if (url.port) {
|
|
79
|
+
options.port = Number(url.port);
|
|
80
|
+
}
|
|
81
|
+
if (url.username) {
|
|
82
|
+
options.user = decodeURIComponent(url.username);
|
|
83
|
+
}
|
|
84
|
+
if (url.password) {
|
|
85
|
+
options.password = decodeURIComponent(url.password);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var database = decodeURIComponent(url.pathname || '');
|
|
89
|
+
if (database.startsWith('/')) {
|
|
90
|
+
database = database.slice(1);
|
|
91
|
+
}
|
|
92
|
+
// A database part with path separators is a filesystem path, not an
|
|
93
|
+
// alias (aliases cannot contain "/") — restore the leading slash unless
|
|
94
|
+
// it is a Windows drive path or already absolute (double-slash form).
|
|
95
|
+
if (database.includes('/') && !database.startsWith('/') && !/^[A-Za-z]:\//.test(database)) {
|
|
96
|
+
database = '/' + database;
|
|
97
|
+
}
|
|
98
|
+
if (database) {
|
|
99
|
+
options.database = database;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
url.searchParams.forEach(function(value, key) {
|
|
103
|
+
options[key] = coerce(key, value);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return options as Options;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse a traditional ("old style") Firebird connection string:
|
|
111
|
+
*
|
|
112
|
+
* [host[/port]:]{path | alias}
|
|
113
|
+
*
|
|
114
|
+
* employee → alias "employee" (default host)
|
|
115
|
+
* /var/fb/prod.fdb → local path (default host)
|
|
116
|
+
* C:\fbdata\prod.fdb → Windows path — a single character
|
|
117
|
+
* before ":" is a drive letter, not
|
|
118
|
+
* a host (same rule as Firebird)
|
|
119
|
+
* db.example.com:employee → host + alias
|
|
120
|
+
* db.example.com/3051:/var/fb/prod.fdb → host + port + path
|
|
121
|
+
* myserver:C:\fbdata\prod.fdb → host + Windows path
|
|
122
|
+
* [::1]/3050:employee → IPv6 host + port + alias
|
|
123
|
+
*
|
|
124
|
+
* Unlike firebird:// URIs, traditional strings carry no credentials or
|
|
125
|
+
* options — the driver defaults apply (SYSDBA/masterkey, port 3050).
|
|
126
|
+
* The port must be numeric; /etc/services names are not resolved.
|
|
127
|
+
*/
|
|
128
|
+
export function parseOldStyleConnectionString(str: string): Options {
|
|
129
|
+
var options: any = {};
|
|
130
|
+
var host: string | null = null;
|
|
131
|
+
var port: string | null = null;
|
|
132
|
+
var database = str;
|
|
133
|
+
|
|
134
|
+
var ipv6 = /^\[([^\]]+)\](?:\/([^:]*))?:(.*)$/.exec(str);
|
|
135
|
+
if (ipv6) {
|
|
136
|
+
host = ipv6[1];
|
|
137
|
+
port = ipv6[2] !== undefined ? ipv6[2] : null;
|
|
138
|
+
database = ipv6[3];
|
|
139
|
+
} else {
|
|
140
|
+
var colon = str.indexOf(':');
|
|
141
|
+
if (colon === 0) {
|
|
142
|
+
throw new Error('Invalid connection string (empty host): ' + str);
|
|
143
|
+
}
|
|
144
|
+
// colon === 1 → single character before ":" is a drive letter;
|
|
145
|
+
// colon === -1 → no host part. Both leave the whole string as database.
|
|
146
|
+
if (colon > 1) {
|
|
147
|
+
var hostPart = str.slice(0, colon);
|
|
148
|
+
database = str.slice(colon + 1);
|
|
149
|
+
var slash = hostPart.indexOf('/');
|
|
150
|
+
if (slash !== -1) {
|
|
151
|
+
host = hostPart.slice(0, slash);
|
|
152
|
+
port = hostPart.slice(slash + 1);
|
|
153
|
+
if (!host) {
|
|
154
|
+
throw new Error('Invalid connection string (empty host): ' + str);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
host = hostPart;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!database) {
|
|
163
|
+
throw new Error('Invalid connection string (empty database): ' + str);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (host) {
|
|
167
|
+
options.host = host;
|
|
168
|
+
}
|
|
169
|
+
if (port !== null) {
|
|
170
|
+
var n = Number(port);
|
|
171
|
+
if (!/^\d+$/.test(port) || n < 1 || n > 65535) {
|
|
172
|
+
throw new Error('Invalid port in connection string "' + str +
|
|
173
|
+
'" (service names are not supported — use a numeric port)');
|
|
174
|
+
}
|
|
175
|
+
options.port = n;
|
|
176
|
+
}
|
|
177
|
+
options.database = database;
|
|
178
|
+
|
|
179
|
+
return options as Options;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Parse any connection string the driver accepts: a firebird:// URI, or a
|
|
186
|
+
* traditional [host[/port]:]database string when there is no scheme.
|
|
187
|
+
*/
|
|
188
|
+
export function parseConnectionString(str: string): Options {
|
|
189
|
+
return URI_SCHEME.test(str)
|
|
190
|
+
? parseConnectionUri(str)
|
|
191
|
+
: parseOldStyleConnectionString(str);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Accept either an options object or a connection string (firebird:// URI
|
|
196
|
+
* or traditional host[/port]:database) everywhere options are taken.
|
|
197
|
+
* Strings are parsed; objects pass through unchanged.
|
|
198
|
+
*/
|
|
199
|
+
export function normalizeOptions<T>(options: T | string): T {
|
|
200
|
+
if (typeof options === 'string') {
|
|
201
|
+
return parseConnectionString(options) as T;
|
|
202
|
+
}
|
|
203
|
+
return options;
|
|
204
|
+
}
|
package/src/wire/connection.ts
CHANGED
|
@@ -90,6 +90,12 @@ class Connection {
|
|
|
90
90
|
this._isDetach = false;
|
|
91
91
|
this._isUsed = false;
|
|
92
92
|
this._pooled = options.isPool||false;
|
|
93
|
+
// Credentials may be absent (e.g. a traditional host:database
|
|
94
|
+
// connection string) — apply the driver defaults once here, so every
|
|
95
|
+
// auth path (op_connect CNCT block, SRP proof, legacy cont_auth) sees
|
|
96
|
+
// the same values.
|
|
97
|
+
if (options && !options.user) options.user = Const.DEFAULT_USER;
|
|
98
|
+
if (options && !options.password) options.password = Const.DEFAULT_PASSWORD;
|
|
93
99
|
if (options && options.blobChunkSize > 65535) options.blobChunkSize = 65535;
|
|
94
100
|
if (options && options.blobReadChunkSize > 65535) options.blobReadChunkSize = 65535;
|
|
95
101
|
this.options = options;
|
|
@@ -426,8 +432,17 @@ class Connection {
|
|
|
426
432
|
const canDefer = defer && this.accept.protocolVersion >= Const.PROTOCOL_VERSION11;
|
|
427
433
|
|
|
428
434
|
self._socket.write(self._msg.getData(), canDefer);
|
|
429
|
-
if (canDefer
|
|
430
|
-
|
|
435
|
+
if (canDefer) {
|
|
436
|
+
// A deferred packet sits in the socket buffer until the next
|
|
437
|
+
// non-deferred write flushes it, but the server still answers it
|
|
438
|
+
// with its own op_response (delivered along with that next
|
|
439
|
+
// exchange). Queue a placeholder to consume that response —
|
|
440
|
+
// otherwise the queue pairs it with the NEXT request and every
|
|
441
|
+
// later response is off by one. The op itself is fire-and-forget,
|
|
442
|
+
// so complete the caller right away.
|
|
443
|
+
self._queue.push(undefined);
|
|
444
|
+
if (callback)
|
|
445
|
+
callback();
|
|
431
446
|
} else {
|
|
432
447
|
self._queue.push(callback);
|
|
433
448
|
}
|
package/src/wire/statement.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
***************************************/
|
|
6
6
|
|
|
7
|
-
import { fromCallback } from '../callback';
|
|
7
|
+
import { doError, fromCallback } from '../callback';
|
|
8
|
+
import { bindNamedParams, isNamedParamsObject } from '../named-params';
|
|
8
9
|
|
|
9
10
|
class Statement {
|
|
10
11
|
connection: any;
|
|
@@ -15,6 +16,12 @@ class Statement {
|
|
|
15
16
|
options: any;
|
|
16
17
|
handle: number;
|
|
17
18
|
plan: string;
|
|
19
|
+
/**
|
|
20
|
+
* Placeholder names in positional order when this statement was
|
|
21
|
+
* prepared from SQL with named placeholders (namedPlaceholders on),
|
|
22
|
+
* null/undefined otherwise. Set by Transaction.newStatement.
|
|
23
|
+
*/
|
|
24
|
+
namedParams?: string[] | null;
|
|
18
25
|
[key: string]: any;
|
|
19
26
|
|
|
20
27
|
constructor(connection: any) {
|
|
@@ -44,6 +51,15 @@ class Statement {
|
|
|
44
51
|
params = undefined;
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
if (this.namedParams && isNamedParamsObject(params)) {
|
|
55
|
+
try {
|
|
56
|
+
params = bindNamedParams(this.namedParams, params);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
doError(err, callback);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
this.options = options;
|
|
48
64
|
this.connection.executeStatement(transaction, this, params, callback, options);
|
|
49
65
|
}
|
|
@@ -71,9 +87,22 @@ class Statement {
|
|
|
71
87
|
|
|
72
88
|
/**
|
|
73
89
|
* Execute this statement once per row via the Firebird 4 batch API
|
|
74
|
-
* (protocol 16+). `rows` is an array of parameter arrays
|
|
90
|
+
* (protocol 16+). `rows` is an array of parameter arrays — or, when the
|
|
91
|
+
* statement was prepared with named placeholders, of values-by-name
|
|
92
|
+
* objects (the two forms can be mixed).
|
|
75
93
|
*/
|
|
76
94
|
executeBatch(transaction: any, rows: any[][], callback?: any, options?: any): void {
|
|
95
|
+
var names = this.namedParams;
|
|
96
|
+
if (names && Array.isArray(rows)) {
|
|
97
|
+
try {
|
|
98
|
+
rows = rows.map(function(row: any) {
|
|
99
|
+
return isNamedParamsObject(row) ? bindNamedParams(names as string[], row) : row;
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
doError(err, callback);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
77
106
|
this.connection.executeBatch(transaction, this, rows, callback, options);
|
|
78
107
|
}
|
|
79
108
|
|
package/src/wire/transaction.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { doCallback, doError, fromCallback } from '../callback';
|
|
2
|
+
import { parseNamedPlaceholders } from '../named-params';
|
|
2
3
|
import { noop } from '../utils';
|
|
3
4
|
import Const from './const';
|
|
4
5
|
|
|
@@ -51,15 +52,41 @@ class Transaction {
|
|
|
51
52
|
this.db = connection.db;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
/** Per-call options.namedPlaceholders overrides the connection option. */
|
|
56
|
+
private namedPlaceholdersEnabled(options?: any): boolean {
|
|
57
|
+
if (options && options.namedPlaceholders !== undefined)
|
|
58
|
+
return !!options.namedPlaceholders;
|
|
59
|
+
return !!(this.connection.options && this.connection.options.namedPlaceholders);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
newStatement(query: string, callback: (err: any, statement?: any) => void, options?: any): void {
|
|
55
63
|
var cnx = this.connection;
|
|
56
64
|
var self = this;
|
|
65
|
+
|
|
66
|
+
// With namedPlaceholders on, prepare the positional rewrite and
|
|
67
|
+
// remember the name order on the statement so statement.execute can
|
|
68
|
+
// accept a values-by-name object. The rewritten SQL is the cache key.
|
|
69
|
+
var names: string[] | null = null;
|
|
70
|
+
if (this.namedPlaceholdersEnabled(options)) {
|
|
71
|
+
var parsed = parseNamedPlaceholders(query);
|
|
72
|
+
if (parsed.names) {
|
|
73
|
+
query = parsed.sql;
|
|
74
|
+
names = parsed.names;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var deliver = function(err: any, statement?: any) {
|
|
79
|
+
if (statement)
|
|
80
|
+
statement.namedParams = names;
|
|
81
|
+
callback(err, statement);
|
|
82
|
+
};
|
|
83
|
+
|
|
57
84
|
var query_cache = cnx.getCachedQuery(query);
|
|
58
85
|
|
|
59
86
|
if (query_cache) {
|
|
60
|
-
|
|
87
|
+
deliver(null, query_cache);
|
|
61
88
|
} else {
|
|
62
|
-
cnx.prepare(self, query, false,
|
|
89
|
+
cnx.prepare(self, query, false, deliver);
|
|
63
90
|
}
|
|
64
91
|
}
|
|
65
92
|
|
|
@@ -148,7 +175,7 @@ class Transaction {
|
|
|
148
175
|
}
|
|
149
176
|
|
|
150
177
|
}, options);
|
|
151
|
-
});
|
|
178
|
+
}, options);
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
sequentially(query: string, params?: any, on?: any, callback?: any, options: any = {}): this {
|
|
@@ -255,7 +282,7 @@ class Transaction {
|
|
|
255
282
|
if (callback)
|
|
256
283
|
callback(err, result);
|
|
257
284
|
}, options);
|
|
258
|
-
});
|
|
285
|
+
}, options);
|
|
259
286
|
}
|
|
260
287
|
|
|
261
288
|
executeBatchAsync(query: string, rows: any[][], options?: any): Promise<any> {
|