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/lib/uri.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/***************************************
|
|
3
|
+
*
|
|
4
|
+
* Connection URI strings
|
|
5
|
+
*
|
|
6
|
+
***************************************/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.parseConnectionUri = parseConnectionUri;
|
|
9
|
+
exports.parseOldStyleConnectionString = parseOldStyleConnectionString;
|
|
10
|
+
exports.parseConnectionString = parseConnectionString;
|
|
11
|
+
exports.normalizeOptions = normalizeOptions;
|
|
12
|
+
/**
|
|
13
|
+
* Option keys coerced to boolean when they arrive as URI query parameters.
|
|
14
|
+
* "1"/"true"/"yes"/"on" (case-insensitive) → true, everything else → false.
|
|
15
|
+
*/
|
|
16
|
+
const BOOLEAN_KEYS = new Set([
|
|
17
|
+
'lowercase_keys', 'blobAsText', 'wireCompression', 'manager',
|
|
18
|
+
'namedPlaceholders',
|
|
19
|
+
]);
|
|
20
|
+
/** Option keys coerced to number when they arrive as URI query parameters. */
|
|
21
|
+
const NUMBER_KEYS = new Set([
|
|
22
|
+
'port', 'pageSize', 'timeout', 'retryConnectionInterval',
|
|
23
|
+
'blobChunkSize', 'blobReadChunkSize', 'wireCrypt', 'parallelWorkers',
|
|
24
|
+
'maxInlineBlobSize', 'maxNegotiatedProtocols', 'connectTimeout',
|
|
25
|
+
'min', 'idleTimeoutMillis',
|
|
26
|
+
]);
|
|
27
|
+
function coerce(key, value) {
|
|
28
|
+
if (BOOLEAN_KEYS.has(key)) {
|
|
29
|
+
return /^(1|true|yes|on)$/i.test(value);
|
|
30
|
+
}
|
|
31
|
+
if (NUMBER_KEYS.has(key)) {
|
|
32
|
+
var n = Number(value);
|
|
33
|
+
if (Number.isNaN(n)) {
|
|
34
|
+
throw new Error('Invalid numeric value for connection URI option "' + key + '": ' + value);
|
|
35
|
+
}
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
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
|
+
function parseConnectionUri(uri) {
|
|
60
|
+
var url;
|
|
61
|
+
try {
|
|
62
|
+
url = new URL(uri);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
throw new Error('Invalid connection URI: ' + uri);
|
|
66
|
+
}
|
|
67
|
+
if (url.protocol !== 'firebird:') {
|
|
68
|
+
throw new Error('Unsupported connection URI scheme "' + url.protocol.replace(/:$/, '') +
|
|
69
|
+
'" (expected firebird://...)');
|
|
70
|
+
}
|
|
71
|
+
var options = {};
|
|
72
|
+
if (url.hostname) {
|
|
73
|
+
// URL keeps IPv6 hostnames bracketed ([::1]); net.connect wants them bare
|
|
74
|
+
options.host = url.hostname.replace(/^\[(.*)\]$/, '$1');
|
|
75
|
+
}
|
|
76
|
+
if (url.port) {
|
|
77
|
+
options.port = Number(url.port);
|
|
78
|
+
}
|
|
79
|
+
if (url.username) {
|
|
80
|
+
options.user = decodeURIComponent(url.username);
|
|
81
|
+
}
|
|
82
|
+
if (url.password) {
|
|
83
|
+
options.password = decodeURIComponent(url.password);
|
|
84
|
+
}
|
|
85
|
+
var database = decodeURIComponent(url.pathname || '');
|
|
86
|
+
if (database.startsWith('/')) {
|
|
87
|
+
database = database.slice(1);
|
|
88
|
+
}
|
|
89
|
+
// A database part with path separators is a filesystem path, not an
|
|
90
|
+
// alias (aliases cannot contain "/") — restore the leading slash unless
|
|
91
|
+
// it is a Windows drive path or already absolute (double-slash form).
|
|
92
|
+
if (database.includes('/') && !database.startsWith('/') && !/^[A-Za-z]:\//.test(database)) {
|
|
93
|
+
database = '/' + database;
|
|
94
|
+
}
|
|
95
|
+
if (database) {
|
|
96
|
+
options.database = database;
|
|
97
|
+
}
|
|
98
|
+
url.searchParams.forEach(function (value, key) {
|
|
99
|
+
options[key] = coerce(key, value);
|
|
100
|
+
});
|
|
101
|
+
return options;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse a traditional ("old style") Firebird connection string:
|
|
105
|
+
*
|
|
106
|
+
* [host[/port]:]{path | alias}
|
|
107
|
+
*
|
|
108
|
+
* employee → alias "employee" (default host)
|
|
109
|
+
* /var/fb/prod.fdb → local path (default host)
|
|
110
|
+
* C:\fbdata\prod.fdb → Windows path — a single character
|
|
111
|
+
* before ":" is a drive letter, not
|
|
112
|
+
* a host (same rule as Firebird)
|
|
113
|
+
* db.example.com:employee → host + alias
|
|
114
|
+
* db.example.com/3051:/var/fb/prod.fdb → host + port + path
|
|
115
|
+
* myserver:C:\fbdata\prod.fdb → host + Windows path
|
|
116
|
+
* [::1]/3050:employee → IPv6 host + port + alias
|
|
117
|
+
*
|
|
118
|
+
* Unlike firebird:// URIs, traditional strings carry no credentials or
|
|
119
|
+
* options — the driver defaults apply (SYSDBA/masterkey, port 3050).
|
|
120
|
+
* The port must be numeric; /etc/services names are not resolved.
|
|
121
|
+
*/
|
|
122
|
+
function parseOldStyleConnectionString(str) {
|
|
123
|
+
var options = {};
|
|
124
|
+
var host = null;
|
|
125
|
+
var port = null;
|
|
126
|
+
var database = str;
|
|
127
|
+
var ipv6 = /^\[([^\]]+)\](?:\/([^:]*))?:(.*)$/.exec(str);
|
|
128
|
+
if (ipv6) {
|
|
129
|
+
host = ipv6[1];
|
|
130
|
+
port = ipv6[2] !== undefined ? ipv6[2] : null;
|
|
131
|
+
database = ipv6[3];
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
var colon = str.indexOf(':');
|
|
135
|
+
if (colon === 0) {
|
|
136
|
+
throw new Error('Invalid connection string (empty host): ' + str);
|
|
137
|
+
}
|
|
138
|
+
// colon === 1 → single character before ":" is a drive letter;
|
|
139
|
+
// colon === -1 → no host part. Both leave the whole string as database.
|
|
140
|
+
if (colon > 1) {
|
|
141
|
+
var hostPart = str.slice(0, colon);
|
|
142
|
+
database = str.slice(colon + 1);
|
|
143
|
+
var slash = hostPart.indexOf('/');
|
|
144
|
+
if (slash !== -1) {
|
|
145
|
+
host = hostPart.slice(0, slash);
|
|
146
|
+
port = hostPart.slice(slash + 1);
|
|
147
|
+
if (!host) {
|
|
148
|
+
throw new Error('Invalid connection string (empty host): ' + str);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
host = hostPart;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!database) {
|
|
157
|
+
throw new Error('Invalid connection string (empty database): ' + str);
|
|
158
|
+
}
|
|
159
|
+
if (host) {
|
|
160
|
+
options.host = host;
|
|
161
|
+
}
|
|
162
|
+
if (port !== null) {
|
|
163
|
+
var n = Number(port);
|
|
164
|
+
if (!/^\d+$/.test(port) || n < 1 || n > 65535) {
|
|
165
|
+
throw new Error('Invalid port in connection string "' + str +
|
|
166
|
+
'" (service names are not supported — use a numeric port)');
|
|
167
|
+
}
|
|
168
|
+
options.port = n;
|
|
169
|
+
}
|
|
170
|
+
options.database = database;
|
|
171
|
+
return options;
|
|
172
|
+
}
|
|
173
|
+
const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
174
|
+
/**
|
|
175
|
+
* Parse any connection string the driver accepts: a firebird:// URI, or a
|
|
176
|
+
* traditional [host[/port]:]database string when there is no scheme.
|
|
177
|
+
*/
|
|
178
|
+
function parseConnectionString(str) {
|
|
179
|
+
return URI_SCHEME.test(str)
|
|
180
|
+
? parseConnectionUri(str)
|
|
181
|
+
: parseOldStyleConnectionString(str);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Accept either an options object or a connection string (firebird:// URI
|
|
185
|
+
* or traditional host[/port]:database) everywhere options are taken.
|
|
186
|
+
* Strings are parsed; objects pass through unchanged.
|
|
187
|
+
*/
|
|
188
|
+
function normalizeOptions(options) {
|
|
189
|
+
if (typeof options === 'string') {
|
|
190
|
+
return parseConnectionString(options);
|
|
191
|
+
}
|
|
192
|
+
return options;
|
|
193
|
+
}
|
package/lib/wire/connection.js
CHANGED
|
@@ -84,6 +84,14 @@ class Connection {
|
|
|
84
84
|
this._isDetach = false;
|
|
85
85
|
this._isUsed = false;
|
|
86
86
|
this._pooled = options.isPool || false;
|
|
87
|
+
// Credentials may be absent (e.g. a traditional host:database
|
|
88
|
+
// connection string) — apply the driver defaults once here, so every
|
|
89
|
+
// auth path (op_connect CNCT block, SRP proof, legacy cont_auth) sees
|
|
90
|
+
// the same values.
|
|
91
|
+
if (options && !options.user)
|
|
92
|
+
options.user = const_1.default.DEFAULT_USER;
|
|
93
|
+
if (options && !options.password)
|
|
94
|
+
options.password = const_1.default.DEFAULT_PASSWORD;
|
|
87
95
|
if (options && options.blobChunkSize > 65535)
|
|
88
96
|
options.blobChunkSize = 65535;
|
|
89
97
|
if (options && options.blobReadChunkSize > 65535)
|
|
@@ -339,8 +347,17 @@ class Connection {
|
|
|
339
347
|
}
|
|
340
348
|
const canDefer = defer && this.accept.protocolVersion >= const_1.default.PROTOCOL_VERSION11;
|
|
341
349
|
self._socket.write(self._msg.getData(), canDefer);
|
|
342
|
-
if (canDefer
|
|
343
|
-
|
|
350
|
+
if (canDefer) {
|
|
351
|
+
// A deferred packet sits in the socket buffer until the next
|
|
352
|
+
// non-deferred write flushes it, but the server still answers it
|
|
353
|
+
// with its own op_response (delivered along with that next
|
|
354
|
+
// exchange). Queue a placeholder to consume that response —
|
|
355
|
+
// otherwise the queue pairs it with the NEXT request and every
|
|
356
|
+
// later response is off by one. The op itself is fire-and-forget,
|
|
357
|
+
// so complete the caller right away.
|
|
358
|
+
self._queue.push(undefined);
|
|
359
|
+
if (callback)
|
|
360
|
+
callback();
|
|
344
361
|
}
|
|
345
362
|
else {
|
|
346
363
|
self._queue.push(callback);
|
package/lib/wire/statement.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ declare class Statement {
|
|
|
12
12
|
options: any;
|
|
13
13
|
handle: number;
|
|
14
14
|
plan: string;
|
|
15
|
+
/**
|
|
16
|
+
* Placeholder names in positional order when this statement was
|
|
17
|
+
* prepared from SQL with named placeholders (namedPlaceholders on),
|
|
18
|
+
* null/undefined otherwise. Set by Transaction.newStatement.
|
|
19
|
+
*/
|
|
20
|
+
namedParams?: string[] | null;
|
|
15
21
|
[key: string]: any;
|
|
16
22
|
constructor(connection: any);
|
|
17
23
|
close(callback?: (err?: any) => void): void;
|
|
@@ -23,7 +29,9 @@ declare class Statement {
|
|
|
23
29
|
fetchAll(transaction: any, callback: (err: any, result?: any) => void): void;
|
|
24
30
|
/**
|
|
25
31
|
* Execute this statement once per row via the Firebird 4 batch API
|
|
26
|
-
* (protocol 16+). `rows` is an array of parameter arrays
|
|
32
|
+
* (protocol 16+). `rows` is an array of parameter arrays — or, when the
|
|
33
|
+
* statement was prepared with named placeholders, of values-by-name
|
|
34
|
+
* objects (the two forms can be mixed).
|
|
27
35
|
*/
|
|
28
36
|
executeBatch(transaction: any, rows: any[][], callback?: any, options?: any): void;
|
|
29
37
|
executeAsync(transaction: any, params?: any, options?: any): Promise<any>;
|
package/lib/wire/statement.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
***************************************/
|
|
7
7
|
const callback_1 = require("../callback");
|
|
8
|
+
const named_params_1 = require("../named-params");
|
|
8
9
|
class Statement {
|
|
9
10
|
constructor(connection) {
|
|
10
11
|
this.connection = connection;
|
|
@@ -28,6 +29,15 @@ class Statement {
|
|
|
28
29
|
callback = params;
|
|
29
30
|
params = undefined;
|
|
30
31
|
}
|
|
32
|
+
if (this.namedParams && (0, named_params_1.isNamedParamsObject)(params)) {
|
|
33
|
+
try {
|
|
34
|
+
params = (0, named_params_1.bindNamedParams)(this.namedParams, params);
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
(0, callback_1.doError)(err, callback);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
31
41
|
this.options = options;
|
|
32
42
|
this.connection.executeStatement(transaction, this, params, callback, options);
|
|
33
43
|
}
|
|
@@ -51,9 +61,23 @@ class Statement {
|
|
|
51
61
|
}
|
|
52
62
|
/**
|
|
53
63
|
* Execute this statement once per row via the Firebird 4 batch API
|
|
54
|
-
* (protocol 16+). `rows` is an array of parameter arrays
|
|
64
|
+
* (protocol 16+). `rows` is an array of parameter arrays — or, when the
|
|
65
|
+
* statement was prepared with named placeholders, of values-by-name
|
|
66
|
+
* objects (the two forms can be mixed).
|
|
55
67
|
*/
|
|
56
68
|
executeBatch(transaction, rows, callback, options) {
|
|
69
|
+
var names = this.namedParams;
|
|
70
|
+
if (names && Array.isArray(rows)) {
|
|
71
|
+
try {
|
|
72
|
+
rows = rows.map(function (row) {
|
|
73
|
+
return (0, named_params_1.isNamedParamsObject)(row) ? (0, named_params_1.bindNamedParams)(names, row) : row;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
(0, callback_1.doError)(err, callback);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
57
81
|
this.connection.executeBatch(transaction, this, rows, callback, options);
|
|
58
82
|
}
|
|
59
83
|
/* Promise / async-await API — wrappers over the callback methods above. */
|
|
@@ -4,7 +4,9 @@ declare class Transaction {
|
|
|
4
4
|
handle: number;
|
|
5
5
|
[key: string]: any;
|
|
6
6
|
constructor(connection: any);
|
|
7
|
-
|
|
7
|
+
/** Per-call options.namedPlaceholders overrides the connection option. */
|
|
8
|
+
private namedPlaceholdersEnabled;
|
|
9
|
+
newStatement(query: string, callback: (err: any, statement?: any) => void, options?: any): void;
|
|
8
10
|
execute(query: string, params?: any, callback?: any, options?: any): void;
|
|
9
11
|
sequentially(query: string, params?: any, on?: any, callback?: any, options?: any): this;
|
|
10
12
|
query(query: string, params?: any, callback?: any, options?: any): void;
|
package/lib/wire/transaction.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
const callback_1 = require("../callback");
|
|
6
|
+
const named_params_1 = require("../named-params");
|
|
6
7
|
const utils_1 = require("../utils");
|
|
7
8
|
const const_1 = __importDefault(require("./const"));
|
|
8
9
|
/***************************************
|
|
@@ -45,15 +46,37 @@ class Transaction {
|
|
|
45
46
|
this.connection = connection;
|
|
46
47
|
this.db = connection.db;
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
+
/** Per-call options.namedPlaceholders overrides the connection option. */
|
|
50
|
+
namedPlaceholdersEnabled(options) {
|
|
51
|
+
if (options && options.namedPlaceholders !== undefined)
|
|
52
|
+
return !!options.namedPlaceholders;
|
|
53
|
+
return !!(this.connection.options && this.connection.options.namedPlaceholders);
|
|
54
|
+
}
|
|
55
|
+
newStatement(query, callback, options) {
|
|
49
56
|
var cnx = this.connection;
|
|
50
57
|
var self = this;
|
|
58
|
+
// With namedPlaceholders on, prepare the positional rewrite and
|
|
59
|
+
// remember the name order on the statement so statement.execute can
|
|
60
|
+
// accept a values-by-name object. The rewritten SQL is the cache key.
|
|
61
|
+
var names = null;
|
|
62
|
+
if (this.namedPlaceholdersEnabled(options)) {
|
|
63
|
+
var parsed = (0, named_params_1.parseNamedPlaceholders)(query);
|
|
64
|
+
if (parsed.names) {
|
|
65
|
+
query = parsed.sql;
|
|
66
|
+
names = parsed.names;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
var deliver = function (err, statement) {
|
|
70
|
+
if (statement)
|
|
71
|
+
statement.namedParams = names;
|
|
72
|
+
callback(err, statement);
|
|
73
|
+
};
|
|
51
74
|
var query_cache = cnx.getCachedQuery(query);
|
|
52
75
|
if (query_cache) {
|
|
53
|
-
|
|
76
|
+
deliver(null, query_cache);
|
|
54
77
|
}
|
|
55
78
|
else {
|
|
56
|
-
cnx.prepare(self, query, false,
|
|
79
|
+
cnx.prepare(self, query, false, deliver);
|
|
57
80
|
}
|
|
58
81
|
}
|
|
59
82
|
execute(query, params, callback, options) {
|
|
@@ -124,7 +147,7 @@ class Transaction {
|
|
|
124
147
|
break;
|
|
125
148
|
}
|
|
126
149
|
}, options);
|
|
127
|
-
});
|
|
150
|
+
}, options);
|
|
128
151
|
}
|
|
129
152
|
sequentially(query, params, on, callback, options = {}) {
|
|
130
153
|
if (params instanceof Function) {
|
|
@@ -221,7 +244,7 @@ class Transaction {
|
|
|
221
244
|
if (callback)
|
|
222
245
|
callback(err, result);
|
|
223
246
|
}, options);
|
|
224
|
-
});
|
|
247
|
+
}, options);
|
|
225
248
|
}
|
|
226
249
|
executeBatchAsync(query, rows, options) {
|
|
227
250
|
var self = this;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { doError, doCallback, fromCallback } from './callback';
|
|
|
3
3
|
import Connection from './wire/connection';
|
|
4
4
|
import Pool from './pool';
|
|
5
5
|
import { escape as escapeValue } from './utils';
|
|
6
|
+
import { parseConnectionUri, parseConnectionString, normalizeOptions } from './uri';
|
|
6
7
|
import type {
|
|
7
8
|
Options,
|
|
8
9
|
SvcMgrOptions,
|
|
@@ -54,9 +55,10 @@ export const escape = escapeValue;
|
|
|
54
55
|
*/
|
|
55
56
|
export let connection: Connection | undefined;
|
|
56
57
|
|
|
57
|
-
export function attach(options: Options, callback: DatabaseCallback): void;
|
|
58
|
+
export function attach(options: Options | string, callback: DatabaseCallback): void;
|
|
58
59
|
export function attach(options: SvcMgrOptions, callback: ServiceManagerCallback): void;
|
|
59
60
|
export function attach(options: any, callback: any): void {
|
|
61
|
+
options = normalizeOptions(options);
|
|
60
62
|
var host = options.host || Const.DEFAULT_HOST;
|
|
61
63
|
var port = options.port || Const.DEFAULT_PORT;
|
|
62
64
|
var manager = options.manager || false;
|
|
@@ -81,8 +83,8 @@ export function attach(options: any, callback: any): void {
|
|
|
81
83
|
}, options);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
export function drop(options: Options, callback: SimpleCallback): void {
|
|
85
|
-
attach(options, function(err: any, db: any) {
|
|
86
|
+
export function drop(options: Options | string, callback: SimpleCallback): void {
|
|
87
|
+
attach(normalizeOptions(options), function(err: any, db: any) {
|
|
86
88
|
if (err) {
|
|
87
89
|
callback({ error: err, message: "Drop error" });
|
|
88
90
|
return;
|
|
@@ -92,7 +94,8 @@ export function drop(options: Options, callback: SimpleCallback): void {
|
|
|
92
94
|
});
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
export function create(options: Options, callback: DatabaseCallback): void {
|
|
97
|
+
export function create(options: Options | string, callback: DatabaseCallback): void {
|
|
98
|
+
options = normalizeOptions(options);
|
|
96
99
|
var host = options.host || Const.DEFAULT_HOST;
|
|
97
100
|
var port = options.port || Const.DEFAULT_PORT;
|
|
98
101
|
var cnx = connection = new Connection(host, port, function(err: any) {
|
|
@@ -116,7 +119,8 @@ export function create(options: Options, callback: DatabaseCallback): void {
|
|
|
116
119
|
}, options);
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
export function attachOrCreate(options: Options, callback: DatabaseCallback): void {
|
|
122
|
+
export function attachOrCreate(options: Options | string, callback: DatabaseCallback): void {
|
|
123
|
+
options = normalizeOptions(options);
|
|
120
124
|
|
|
121
125
|
var host = options.host || Const.DEFAULT_HOST;
|
|
122
126
|
var port = options.port || Const.DEFAULT_PORT;
|
|
@@ -154,10 +158,13 @@ export function attachOrCreate(options: Options, callback: DatabaseCallback): vo
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
// Pooling
|
|
157
|
-
export function pool(max: number, options: Options): ConnectionPool {
|
|
158
|
-
return new Pool(attach, max, Object.assign({}, options, { isPool: true }));
|
|
161
|
+
export function pool(max: number, options: Options | string): ConnectionPool {
|
|
162
|
+
return new Pool(attach, max, Object.assign({}, normalizeOptions(options), { isPool: true }));
|
|
159
163
|
}
|
|
160
164
|
|
|
165
|
+
export { parseConnectionUri, parseConnectionString };
|
|
166
|
+
export { parseNamedPlaceholders } from './named-params';
|
|
167
|
+
|
|
161
168
|
/*
|
|
162
169
|
* Promise / async-await API.
|
|
163
170
|
* Wrappers over the callback functions above; the callback API stays
|
|
@@ -166,19 +173,19 @@ export function pool(max: number, options: Options): ConnectionPool {
|
|
|
166
173
|
*/
|
|
167
174
|
|
|
168
175
|
export function attachAsync(options: SvcMgrOptions): Promise<ServiceManager>;
|
|
169
|
-
export function attachAsync(options: Options): Promise<Database>;
|
|
176
|
+
export function attachAsync(options: Options | string): Promise<Database>;
|
|
170
177
|
export function attachAsync(options: any): Promise<any> {
|
|
171
178
|
return fromCallback(function(cb) { attach(options, cb); });
|
|
172
179
|
}
|
|
173
180
|
|
|
174
|
-
export function createAsync(options: Options): Promise<Database> {
|
|
181
|
+
export function createAsync(options: Options | string): Promise<Database> {
|
|
175
182
|
return fromCallback(function(cb) { create(options, cb); });
|
|
176
183
|
}
|
|
177
184
|
|
|
178
|
-
export function attachOrCreateAsync(options: Options): Promise<Database> {
|
|
185
|
+
export function attachOrCreateAsync(options: Options | string): Promise<Database> {
|
|
179
186
|
return fromCallback(function(cb) { attachOrCreate(options, cb); });
|
|
180
187
|
}
|
|
181
188
|
|
|
182
|
-
export function dropAsync(options: Options): Promise<void> {
|
|
189
|
+
export function dropAsync(options: Options | string): Promise<void> {
|
|
183
190
|
return fromCallback(function(cb) { drop(options, cb); });
|
|
184
191
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* Named placeholders (:name → ?)
|
|
4
|
+
*
|
|
5
|
+
***************************************/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Result of scanning a SQL string for named placeholders.
|
|
9
|
+
*/
|
|
10
|
+
export interface ParsedNamedPlaceholders {
|
|
11
|
+
/** SQL with every named placeholder replaced by a positional "?". */
|
|
12
|
+
sql: string;
|
|
13
|
+
/**
|
|
14
|
+
* Placeholder names in positional order (a repeated name appears once
|
|
15
|
+
* per occurrence), or null when the SQL contains none.
|
|
16
|
+
*/
|
|
17
|
+
names: string[] | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const IDENT_START = /[A-Za-z_]/;
|
|
21
|
+
const IDENT_PART = /[A-Za-z0-9_$]/;
|
|
22
|
+
|
|
23
|
+
// Parsing is pure string work, so identical SQL (the common case with
|
|
24
|
+
// query builders and hot paths) is scanned only once.
|
|
25
|
+
const CACHE_MAX = 100;
|
|
26
|
+
const cache = new Map<string, ParsedNamedPlaceholders>();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Scan `sql` for named placeholders (`:name`) and rewrite them to positional
|
|
30
|
+
* `?` markers, returning the rewritten SQL and the names in positional
|
|
31
|
+
* order. Placeholders inside string literals ('...'), quoted identifiers
|
|
32
|
+
* ("..."), line comments (--), block comments and Firebird alternative
|
|
33
|
+
* string literals (q'{...}') are left untouched.
|
|
34
|
+
*
|
|
35
|
+
* Note: the scanner has no SQL grammar — inside an EXECUTE BLOCK body every
|
|
36
|
+
* `:variable` reference looks like a placeholder too. Use positional params
|
|
37
|
+
* (or per-call `namedPlaceholders: false`) for EXECUTE BLOCK.
|
|
38
|
+
*/
|
|
39
|
+
export function parseNamedPlaceholders(sql: string): ParsedNamedPlaceholders {
|
|
40
|
+
var cached = cache.get(sql);
|
|
41
|
+
if (cached)
|
|
42
|
+
return cached;
|
|
43
|
+
|
|
44
|
+
var out = '';
|
|
45
|
+
var names: string[] = [];
|
|
46
|
+
var i = 0;
|
|
47
|
+
var n = sql.length;
|
|
48
|
+
|
|
49
|
+
while (i < n) {
|
|
50
|
+
var c = sql[i];
|
|
51
|
+
|
|
52
|
+
if (c === "'" || c === '"') {
|
|
53
|
+
// string literal or quoted identifier; doubled quotes escape
|
|
54
|
+
var quote = c;
|
|
55
|
+
var end = i + 1;
|
|
56
|
+
while (end < n) {
|
|
57
|
+
if (sql[end] === quote) {
|
|
58
|
+
if (sql[end + 1] === quote) {
|
|
59
|
+
end += 2;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
end++;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
end++;
|
|
66
|
+
}
|
|
67
|
+
out += sql.slice(i, end);
|
|
68
|
+
i = end;
|
|
69
|
+
} else if (c === '-' && sql[i + 1] === '-') {
|
|
70
|
+
var eol = sql.indexOf('\n', i);
|
|
71
|
+
if (eol === -1) eol = n;
|
|
72
|
+
out += sql.slice(i, eol);
|
|
73
|
+
i = eol;
|
|
74
|
+
} else if (c === '/' && sql[i + 1] === '*') {
|
|
75
|
+
var close = sql.indexOf('*/', i + 2);
|
|
76
|
+
close = close === -1 ? n : close + 2;
|
|
77
|
+
out += sql.slice(i, close);
|
|
78
|
+
i = close;
|
|
79
|
+
} else if ((c === 'q' || c === 'Q') && sql[i + 1] === "'" && i + 2 < n &&
|
|
80
|
+
(i === 0 || !IDENT_PART.test(sql[i - 1]))) {
|
|
81
|
+
// Firebird 3+ alternative string literal: q'{...}' / q'!...!'
|
|
82
|
+
var open = sql[i + 2];
|
|
83
|
+
var closer = open === '(' ? ')'
|
|
84
|
+
: open === '[' ? ']'
|
|
85
|
+
: open === '{' ? '}'
|
|
86
|
+
: open === '<' ? '>'
|
|
87
|
+
: open;
|
|
88
|
+
var stop = sql.indexOf(closer + "'", i + 3);
|
|
89
|
+
stop = stop === -1 ? n : stop + 2;
|
|
90
|
+
out += sql.slice(i, stop);
|
|
91
|
+
i = stop;
|
|
92
|
+
} else if (c === ':' && i + 1 < n && IDENT_START.test(sql[i + 1])) {
|
|
93
|
+
var end2 = i + 2;
|
|
94
|
+
while (end2 < n && IDENT_PART.test(sql[end2]))
|
|
95
|
+
end2++;
|
|
96
|
+
names.push(sql.slice(i + 1, end2));
|
|
97
|
+
out += '?';
|
|
98
|
+
i = end2;
|
|
99
|
+
} else {
|
|
100
|
+
out += c;
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var result: ParsedNamedPlaceholders = names.length
|
|
106
|
+
? { sql: out, names: names }
|
|
107
|
+
: { sql: sql, names: null };
|
|
108
|
+
|
|
109
|
+
if (cache.size >= CACHE_MAX) {
|
|
110
|
+
cache.delete(cache.keys().next().value as string);
|
|
111
|
+
}
|
|
112
|
+
cache.set(sql, result);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* True when `params` is a plain values-by-name object (and not one of the
|
|
118
|
+
* values the driver accepts as a single positional parameter, like Date or
|
|
119
|
+
* Buffer).
|
|
120
|
+
*/
|
|
121
|
+
export function isNamedParamsObject(params: any): params is Record<string, any> {
|
|
122
|
+
return params !== null &&
|
|
123
|
+
typeof params === 'object' &&
|
|
124
|
+
!Array.isArray(params) &&
|
|
125
|
+
!Buffer.isBuffer(params) &&
|
|
126
|
+
!(params instanceof Date);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Map a values-by-name object onto the positional order collected by
|
|
131
|
+
* parseNamedPlaceholders. A name may be bound multiple times; every name
|
|
132
|
+
* must be an own property of `params` (a present key holding null is a
|
|
133
|
+
* NULL parameter, a missing key is an error).
|
|
134
|
+
*/
|
|
135
|
+
export function bindNamedParams(names: string[], params: Record<string, any>): any[] {
|
|
136
|
+
var missing: string[] = [];
|
|
137
|
+
var values = names.map(function(name) {
|
|
138
|
+
if (!Object.prototype.hasOwnProperty.call(params, name))
|
|
139
|
+
missing.push(name);
|
|
140
|
+
return params[name];
|
|
141
|
+
});
|
|
142
|
+
if (missing.length)
|
|
143
|
+
throw new Error('Missing value for named placeholder(s): ' + missing.join(', '));
|
|
144
|
+
return values;
|
|
145
|
+
}
|