monetdb 2.1.0 → 2.2.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 +1 -1
- package/dist/connection.d.ts +1 -0
- package/dist/connection.js +3 -0
- package/dist/connection.js.map +1 -1
- package/dist/mapi.d.ts +7 -13
- package/dist/mapi.js +171 -141
- package/dist/mapi.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/connection.ts +114 -110
- package/src/mapi.ts +938 -913
- package/dist/QueryStream.d.ts +0 -7
- package/dist/QueryStream.js +0 -13
- package/dist/QueryStream.js.map +0 -1
- package/dist/connections.d.ts +0 -1
- package/dist/connections.js +0 -49
- package/dist/connections.js.map +0 -1
package/src/mapi.ts
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
import {
|
1
|
+
import {Socket, SocketConnectOpts} from "node:net";
|
2
|
+
import {once, EventEmitter} from "events";
|
3
|
+
import {Buffer, constants} from "buffer";
|
4
|
+
import {createHash} from "node:crypto";
|
5
5
|
import defaults from "./defaults";
|
6
|
-
import {
|
7
|
-
import {
|
6
|
+
import {URL} from "node:url";
|
7
|
+
import {FileUploader, FileDownloader} from "./file-transfer";
|
8
8
|
|
9
9
|
const MAPI_BLOCK_SIZE = 1024 * 8 - 2;
|
10
10
|
const MAPI_HEADER_SIZE = 2;
|
@@ -31,976 +31,1001 @@ const MAX_REDIRECTS = 10;
|
|
31
31
|
const MAX_BUFF_SIZE = constants.MAX_LENGTH;
|
32
32
|
|
33
33
|
enum MAPI_STATE {
|
34
|
-
|
35
|
-
|
36
|
-
|
34
|
+
INIT = 1,
|
35
|
+
CONNECTED,
|
36
|
+
READY,
|
37
37
|
}
|
38
38
|
|
39
39
|
enum MAPI_LANGUAGE {
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
SQL = "sql",
|
41
|
+
MAPI = "mapi",
|
42
|
+
CONTROL = "control", // ? Not implemented
|
43
43
|
}
|
44
44
|
|
45
45
|
interface MapiConfig {
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
46
|
+
database: string;
|
47
|
+
username?: string;
|
48
|
+
password?: string;
|
49
|
+
language?: MAPI_LANGUAGE;
|
50
|
+
host?: string;
|
51
|
+
port?: number;
|
52
|
+
unixSocket?: string;
|
53
|
+
timeout?: number;
|
54
|
+
autoCommit?: boolean;
|
55
|
+
replySize?: number;
|
56
56
|
}
|
57
57
|
|
58
58
|
class HandShakeOption {
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
59
|
+
level: number;
|
60
|
+
name: string;
|
61
|
+
value: any;
|
62
|
+
fallback?: (v: any) => void;
|
63
|
+
sent: boolean;
|
64
|
+
constructor(
|
65
|
+
level: number,
|
66
|
+
name: string,
|
67
|
+
value: any,
|
68
|
+
fallback: (v: any) => void,
|
69
|
+
sent = false
|
70
|
+
) {
|
71
|
+
this.level = level;
|
72
|
+
this.name = name;
|
73
|
+
this.value = value;
|
74
|
+
this.fallback = fallback;
|
75
|
+
this.sent = sent;
|
76
|
+
}
|
77
77
|
}
|
78
78
|
|
79
79
|
function isMapiUri(uri: string): boolean {
|
80
|
-
|
81
|
-
|
80
|
+
const regx = new RegExp("^mapi:monetdb://*", "i");
|
81
|
+
return regx.test(uri);
|
82
82
|
}
|
83
83
|
|
84
84
|
function parseMapiUri(uri: string): MapiConfig {
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
85
|
+
if (isMapiUri(uri)) {
|
86
|
+
const url = new URL(uri.substring(5));
|
87
|
+
if (url.hostname) {
|
88
|
+
const host = url.hostname;
|
89
|
+
const port = parseInt(url.port);
|
90
|
+
const username = url.username;
|
91
|
+
const password = url.password;
|
92
|
+
const database = url.pathname.split("/")[1];
|
93
|
+
return {
|
94
|
+
host,
|
95
|
+
port,
|
96
|
+
username,
|
97
|
+
password,
|
98
|
+
database,
|
99
|
+
};
|
100
|
+
}
|
100
101
|
}
|
101
|
-
|
102
|
-
throw new Error(`Invalid MAPI URI ${uri}!`);
|
102
|
+
throw new Error(`Invalid MAPI URI ${uri}!`);
|
103
103
|
}
|
104
104
|
|
105
105
|
// validates and sets defaults on missing properties
|
106
106
|
function createMapiConfig(params: MapiConfig): MapiConfig {
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
107
|
+
const database =
|
108
|
+
params && params.database ? params.database : defaults.database;
|
109
|
+
if (typeof database != "string") {
|
110
|
+
throw new Error("database name must be string");
|
111
|
+
}
|
112
|
+
|
113
|
+
const username =
|
114
|
+
params && params.username ? params.username : defaults.username;
|
115
|
+
const password =
|
116
|
+
params && params.password ? params.password : defaults.password;
|
117
|
+
|
118
|
+
let host = params && params.host;
|
119
|
+
const unixSocket = params && params.unixSocket;
|
120
|
+
if (!unixSocket && !host) host = defaults.host;
|
121
|
+
if (typeof host != "string") {
|
122
|
+
throw new TypeError(`${host} is not valid hostname`);
|
123
|
+
}
|
124
|
+
const port =
|
125
|
+
params && params.port ? Number(params.port) : Number(defaults.port);
|
126
|
+
if (isNaN(port)) {
|
127
|
+
throw new TypeError(`${port} is not valid port`);
|
128
|
+
}
|
129
|
+
|
130
|
+
const timeout = params && params.timeout ? Number(params.timeout) : undefined;
|
131
|
+
if (timeout && isNaN(timeout)) {
|
132
|
+
throw new TypeError("timeout must be number");
|
133
|
+
}
|
134
|
+
const language =
|
135
|
+
params && params.language ? params.language : MAPI_LANGUAGE.SQL;
|
136
|
+
const autoCommit = params.autoCommit || defaults.autoCommit;
|
137
|
+
const replySize = params.replySize || defaults.replySize;
|
138
|
+
|
139
|
+
return {
|
140
|
+
database,
|
141
|
+
username,
|
142
|
+
password,
|
143
|
+
language,
|
144
|
+
host,
|
145
|
+
port,
|
146
|
+
timeout,
|
147
|
+
unixSocket,
|
148
|
+
autoCommit,
|
149
|
+
replySize,
|
150
|
+
};
|
151
151
|
}
|
152
152
|
|
153
153
|
class Column {
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
154
|
+
table: string;
|
155
|
+
name: string;
|
156
|
+
type: string;
|
157
|
+
length?: number;
|
158
|
+
index?: number;
|
159
|
+
constructor(
|
160
|
+
table: string,
|
161
|
+
name: string,
|
162
|
+
type: string,
|
163
|
+
index?: number,
|
164
|
+
length?: number
|
165
|
+
) {
|
166
|
+
this.table = table;
|
167
|
+
this.name = name;
|
168
|
+
this.type = type;
|
169
|
+
this.index = index;
|
170
|
+
this.length = length;
|
171
|
+
}
|
172
172
|
}
|
173
173
|
|
174
174
|
type QueryResult = {
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
175
|
+
id?: number;
|
176
|
+
type?: string;
|
177
|
+
queryId?: number;
|
178
|
+
rowCnt?: number;
|
179
|
+
affectedRows?: number;
|
180
|
+
columnCnt?: number;
|
181
|
+
queryTime?: number; // microseconds
|
182
|
+
sqlOptimizerTime?: number; // microseconds
|
183
|
+
malOptimizerTime?: number; // microseconds
|
184
|
+
columns?: Column[];
|
185
|
+
headers?: ResponseHeaders;
|
186
|
+
data?: any[];
|
187
187
|
};
|
188
188
|
|
189
189
|
class QueryStream extends EventEmitter {
|
190
|
-
|
191
|
-
|
192
|
-
|
190
|
+
constructor() {
|
191
|
+
super();
|
192
|
+
}
|
193
193
|
|
194
|
-
|
195
|
-
|
196
|
-
|
194
|
+
end(res?: QueryResult) {
|
195
|
+
this.emit("end", res);
|
196
|
+
}
|
197
197
|
}
|
198
198
|
|
199
199
|
function parseHeaderLine(hdrLine: string): Object {
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
200
|
+
if (hdrLine.startsWith(MSG_HEADER)) {
|
201
|
+
const [head, tail] = hdrLine.substring(1).trim().split("#");
|
202
|
+
let res = {};
|
203
|
+
const vals = head.trim().split(",\t");
|
204
|
+
switch (tail.trim()) {
|
205
|
+
case "table_name":
|
206
|
+
res = {tableNames: vals};
|
207
|
+
break;
|
208
|
+
case "name":
|
209
|
+
res = {columnNames: vals};
|
210
|
+
break;
|
211
|
+
case "type":
|
212
|
+
res = {columnTypes: vals};
|
213
|
+
break;
|
214
|
+
default:
|
215
|
+
res = {};
|
216
|
+
}
|
217
|
+
return res;
|
218
|
+
}
|
219
|
+
throw TypeError("Invalid header format!");
|
220
220
|
}
|
221
221
|
|
222
222
|
function parseTupleLine(line: string, types: string[]): any[] {
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
223
|
+
if (line.startsWith(MSG_TUPLE) && line.endsWith("]")) {
|
224
|
+
var resultline = [];
|
225
|
+
var cCol = 0;
|
226
|
+
var curtok = "";
|
227
|
+
var state = "INCRAP";
|
228
|
+
let endQuotes = 0;
|
229
|
+
/* mostly adapted from clients/R/MonetDB.R/src/mapisplit.c */
|
230
|
+
for (var curPos = 2; curPos < line.length - 1; curPos++) {
|
231
|
+
var chr = line.charAt(curPos);
|
232
|
+
switch (state) {
|
233
|
+
case "INCRAP":
|
234
|
+
if (chr != "\t" && chr != "," && chr != " ") {
|
235
|
+
if (chr == '"') {
|
236
|
+
state = "INQUOTES";
|
237
|
+
} else {
|
238
|
+
state = "INTOKEN";
|
239
|
+
curtok += chr;
|
240
|
+
}
|
241
|
+
}
|
242
|
+
break;
|
243
|
+
case "INTOKEN":
|
244
|
+
if (chr == "," || curPos == line.length - 2) {
|
245
|
+
if (curtok == "NULL" && endQuotes === 0) {
|
246
|
+
resultline.push(null);
|
247
|
+
} else {
|
248
|
+
switch (types[cCol]) {
|
249
|
+
case "boolean":
|
250
|
+
resultline.push(curtok == "true");
|
251
|
+
break;
|
252
|
+
case "tinyint":
|
253
|
+
case "smallint":
|
254
|
+
case "int":
|
255
|
+
case "wrd":
|
256
|
+
case "bigint":
|
257
|
+
resultline.push(parseInt(curtok));
|
258
|
+
break;
|
259
|
+
case "real":
|
260
|
+
case "double":
|
261
|
+
case "decimal":
|
262
|
+
resultline.push(parseFloat(curtok));
|
263
|
+
break;
|
264
|
+
case "json":
|
265
|
+
try {
|
266
|
+
resultline.push(JSON.parse(curtok));
|
267
|
+
} catch (e) {
|
268
|
+
resultline.push(curtok);
|
269
|
+
}
|
270
|
+
break;
|
271
|
+
default:
|
272
|
+
// we need to unescape double quotes
|
273
|
+
//valPtr = valPtr.replace(/[^\\]\\"/g, '"');
|
274
|
+
resultline.push(curtok);
|
275
|
+
break;
|
276
|
+
}
|
277
|
+
}
|
278
|
+
cCol++;
|
279
|
+
state = "INCRAP";
|
280
|
+
curtok = "";
|
281
|
+
endQuotes = 0;
|
282
|
+
} else {
|
283
|
+
curtok += chr;
|
284
|
+
}
|
285
|
+
break;
|
286
|
+
case "ESCAPED":
|
287
|
+
state = "INQUOTES";
|
288
|
+
switch (chr) {
|
289
|
+
case "t":
|
290
|
+
curtok += "\t";
|
291
|
+
break;
|
292
|
+
case "n":
|
293
|
+
curtok += "\n";
|
294
|
+
break;
|
295
|
+
case "r":
|
296
|
+
curtok += "\r";
|
297
|
+
break;
|
298
|
+
default:
|
299
|
+
curtok += chr;
|
300
|
+
}
|
301
|
+
break;
|
302
|
+
case "INQUOTES":
|
303
|
+
if (chr == '"') {
|
304
|
+
state = "INTOKEN";
|
305
|
+
endQuotes++;
|
306
|
+
break;
|
307
|
+
}
|
308
|
+
if (chr == "\\") {
|
309
|
+
state = "ESCAPED";
|
310
|
+
break;
|
311
|
+
}
|
312
|
+
curtok += chr;
|
313
|
+
break;
|
240
314
|
}
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
if (curtok == "NULL" && endQuotes === 0) {
|
246
|
-
resultline.push(null);
|
247
|
-
} else {
|
248
|
-
switch (types[cCol]) {
|
249
|
-
case "boolean":
|
250
|
-
resultline.push(curtok == "true");
|
251
|
-
break;
|
252
|
-
case "tinyint":
|
253
|
-
case "smallint":
|
254
|
-
case "int":
|
255
|
-
case "wrd":
|
256
|
-
case "bigint":
|
257
|
-
resultline.push(parseInt(curtok));
|
258
|
-
break;
|
259
|
-
case "real":
|
260
|
-
case "double":
|
261
|
-
case "decimal":
|
262
|
-
resultline.push(parseFloat(curtok));
|
263
|
-
break;
|
264
|
-
case "json":
|
265
|
-
try {
|
266
|
-
resultline.push(JSON.parse(curtok));
|
267
|
-
} catch (e) {
|
268
|
-
resultline.push(curtok);
|
269
|
-
}
|
270
|
-
break;
|
271
|
-
default:
|
272
|
-
// we need to unescape double quotes
|
273
|
-
//valPtr = valPtr.replace(/[^\\]\\"/g, '"');
|
274
|
-
resultline.push(curtok);
|
275
|
-
break;
|
276
|
-
}
|
277
|
-
}
|
278
|
-
cCol++;
|
279
|
-
state = "INCRAP";
|
280
|
-
curtok = "";
|
281
|
-
endQuotes = 0;
|
282
|
-
} else {
|
283
|
-
curtok += chr;
|
284
|
-
}
|
285
|
-
break;
|
286
|
-
case "ESCAPED":
|
287
|
-
state = "INQUOTES";
|
288
|
-
switch (chr) {
|
289
|
-
case "t":
|
290
|
-
curtok += "\t";
|
291
|
-
break;
|
292
|
-
case "n":
|
293
|
-
curtok += "\n";
|
294
|
-
break;
|
295
|
-
case "r":
|
296
|
-
curtok += "\r";
|
297
|
-
break;
|
298
|
-
default:
|
299
|
-
curtok += chr;
|
300
|
-
}
|
301
|
-
break;
|
302
|
-
case "INQUOTES":
|
303
|
-
if (chr == '"') {
|
304
|
-
state = "INTOKEN";
|
305
|
-
endQuotes++;
|
306
|
-
break;
|
307
|
-
}
|
308
|
-
if (chr == "\\") {
|
309
|
-
state = "ESCAPED";
|
310
|
-
break;
|
311
|
-
}
|
312
|
-
curtok += chr;
|
313
|
-
break;
|
314
|
-
}
|
315
|
-
}
|
316
|
-
return resultline;
|
317
|
-
}
|
318
|
-
throw TypeError("Invalid tuple format!");
|
315
|
+
}
|
316
|
+
return resultline;
|
317
|
+
}
|
318
|
+
throw TypeError("Invalid tuple format!");
|
319
319
|
}
|
320
320
|
|
321
321
|
interface ResponseCallbacks {
|
322
|
-
|
323
|
-
|
322
|
+
resolve: (v: QueryResult | QueryStream | Promise<any>) => void;
|
323
|
+
reject: (err: Error) => void;
|
324
324
|
}
|
325
325
|
|
326
326
|
interface ResponseHeaders {
|
327
|
-
|
328
|
-
|
329
|
-
|
327
|
+
tableNames?: string[];
|
328
|
+
columnNames?: string[];
|
329
|
+
columnTypes?: string[];
|
330
330
|
}
|
331
331
|
|
332
332
|
interface ResponseOpt {
|
333
|
-
|
334
|
-
|
335
|
-
|
333
|
+
stream?: boolean;
|
334
|
+
callbacks?: ResponseCallbacks;
|
335
|
+
fileHandler?: any;
|
336
336
|
}
|
337
337
|
|
338
338
|
class Response {
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
opt.
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
const l = this.segments.length;
|
373
|
-
let segment = (l > 0 && this.segments[l - 1]) || undefined;
|
374
|
-
let bytesCopied = 0;
|
375
|
-
let bytesProcessed = 0;
|
376
|
-
if (!this.complete()) {
|
377
|
-
// check if out of space
|
378
|
-
if (this.buff.length - this.offset < data.length) {
|
379
|
-
const bytes = this.expand(MAPI_BLOCK_SIZE);
|
380
|
-
console.log(`expanding by ${bytes} bytes!`);
|
381
|
-
}
|
382
|
-
|
383
|
-
if (segment === undefined || (segment && segment.isFull())) {
|
384
|
-
const hdr = data.readUInt16LE(0);
|
385
|
-
const last = (hdr & 1) === 1;
|
386
|
-
const bytes = hdr >> 1;
|
387
|
-
srcStartIndx = MAPI_HEADER_SIZE;
|
388
|
-
srcEndIndx = srcStartIndx + Math.min(bytes, data.length);
|
389
|
-
bytesCopied = data.copy(
|
390
|
-
this.buff,
|
391
|
-
this.offset,
|
392
|
-
srcStartIndx,
|
393
|
-
srcEndIndx
|
394
|
-
);
|
395
|
-
segment = new Segment(bytes, last, this.offset, bytesCopied);
|
396
|
-
this.segments.push(segment);
|
397
|
-
this.offset += bytesCopied;
|
398
|
-
bytesProcessed = MAPI_HEADER_SIZE + bytesCopied;
|
399
|
-
} else {
|
400
|
-
const byteCntToRead = segment.bytes - segment.bytesOffset;
|
401
|
-
srcEndIndx = srcStartIndx + byteCntToRead;
|
402
|
-
bytesCopied = data.copy(
|
403
|
-
this.buff,
|
404
|
-
this.offset,
|
405
|
-
srcStartIndx,
|
406
|
-
srcEndIndx
|
407
|
-
);
|
408
|
-
this.offset += bytesCopied;
|
409
|
-
segment.bytesOffset += bytesCopied;
|
410
|
-
// console.log(`segment is full ${segment.bytesOffset === segment.bytes}`);
|
411
|
-
bytesProcessed = bytesCopied;
|
412
|
-
}
|
413
|
-
if (this.isQueryResponse()) {
|
414
|
-
const tuples = [];
|
415
|
-
// const firstPackage = this.parseOffset === 0;
|
416
|
-
this.parseOffset += this.parseQueryResponse(
|
417
|
-
this.toString(this.parseOffset),
|
418
|
-
tuples
|
419
|
-
);
|
420
|
-
if (tuples.length > 0) {
|
421
|
-
if (this.queryStream) {
|
422
|
-
// emit header once
|
423
|
-
if (
|
424
|
-
this.headerEmitted === false &&
|
425
|
-
this.result &&
|
426
|
-
this.result.columns
|
427
|
-
) {
|
428
|
-
this.queryStream.emit("header", this.result.columns);
|
429
|
-
this.headerEmitted = true;
|
430
|
-
}
|
431
|
-
// emit tuples
|
432
|
-
this.queryStream.emit("data", tuples);
|
433
|
-
} else {
|
434
|
-
this.result.data = this.result.data || [];
|
435
|
-
for (let t of tuples) {
|
436
|
-
this.result.data.push(t);
|
437
|
-
}
|
438
|
-
}
|
339
|
+
chunks: Buffer[];
|
340
|
+
buff: Buffer;
|
341
|
+
offset: number;
|
342
|
+
parseOffset: number;
|
343
|
+
stream: boolean;
|
344
|
+
settled: boolean;
|
345
|
+
headerEmitted: boolean;
|
346
|
+
result?: QueryResult;
|
347
|
+
callbacks: ResponseCallbacks;
|
348
|
+
queryStream?: QueryStream;
|
349
|
+
headers?: ResponseHeaders;
|
350
|
+
fileHandler: any;
|
351
|
+
last: boolean;
|
352
|
+
leftOver: number;
|
353
|
+
private _str: string;
|
354
|
+
|
355
|
+
constructor(opt: ResponseOpt = {}) {
|
356
|
+
this.chunks = [];
|
357
|
+
//this.buff = Buffer.allocUnsafe(MAPI_BLOCK_SIZE).fill(0);
|
358
|
+
this.offset = 0;
|
359
|
+
this.parseOffset = 0;
|
360
|
+
this.settled = false;
|
361
|
+
this.last = false;
|
362
|
+
this.headerEmitted = false;
|
363
|
+
this.stream = opt.stream;
|
364
|
+
this.callbacks = opt.callbacks;
|
365
|
+
this.fileHandler = opt.fileHandler;
|
366
|
+
this.leftOver = 0;
|
367
|
+
this._str = "";
|
368
|
+
if (opt.stream) {
|
369
|
+
this.queryStream = new QueryStream();
|
370
|
+
if (opt.callbacks && opt.callbacks.resolve)
|
371
|
+
opt.callbacks.resolve(this.queryStream);
|
439
372
|
}
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
if (
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
373
|
+
}
|
374
|
+
|
375
|
+
append(data: Buffer): number {
|
376
|
+
//let srcStartIndx = 0;
|
377
|
+
//let srcEndIndx = srcStartIndx + data.length;
|
378
|
+
let bytesProcessed = 0;
|
379
|
+
|
380
|
+
if (this.leftOver == 0) {
|
381
|
+
// new mapi blk, read header
|
382
|
+
const hdr = data.readUInt16LE(0);
|
383
|
+
this.last = (hdr & 1) === 1;
|
384
|
+
this.leftOver = hdr >> 1;
|
385
|
+
data = data.subarray(MAPI_HEADER_SIZE);
|
386
|
+
bytesProcessed += MAPI_HEADER_SIZE;
|
387
|
+
//bytesCopied = data.copy(
|
388
|
+
// this.buff,
|
389
|
+
// this.offset,
|
390
|
+
// srcStartIndx,
|
391
|
+
// srcEndIndx
|
392
|
+
//);
|
393
|
+
//segment = new Segment(bytes, last, this.offset, bytesCopied);
|
394
|
+
//this.segments.push(segment);
|
395
|
+
//this.offset += bytesCopied;
|
396
|
+
//bytesProcessed = MAPI_HEADER_SIZE + bytesCopied;
|
397
|
+
}
|
398
|
+
//else {
|
399
|
+
// const byteCntToRead = segment.bytes - segment.bytesOffset;
|
400
|
+
// srcEndIndx = srcStartIndx + byteCntToRead;
|
401
|
+
// bytesCopied = data.copy(
|
402
|
+
// this.buff,
|
403
|
+
// this.offset,
|
404
|
+
// srcStartIndx,
|
405
|
+
// srcEndIndx
|
406
|
+
// );
|
407
|
+
// this.offset += bytesCopied;
|
408
|
+
// segment.bytesOffset += bytesCopied;
|
409
|
+
// // console.log(`segment is full ${segment.bytesOffset === segment.bytes}`);
|
410
|
+
// bytesProcessed = bytesCopied;
|
411
|
+
//}
|
412
|
+
|
413
|
+
const bytesToRead = Math.min(data.length, this.leftOver);
|
414
|
+
//console.log(`bytesToRead ${bytesToRead}`);
|
415
|
+
if (bytesToRead > 0) {
|
416
|
+
const chunk = data.subarray(0, bytesToRead);
|
417
|
+
this.chunks.push(chunk);
|
418
|
+
this.leftOver -= bytesToRead;
|
419
|
+
bytesProcessed += bytesToRead;
|
420
|
+
}
|
421
|
+
|
422
|
+
|
423
|
+
//if (this.isQueryResponse()) {
|
424
|
+
// const tuples = [];
|
425
|
+
// // const firstPackage = this.parseOffset === 0;
|
426
|
+
// this.parseOffset += this.parseQueryResponse(
|
427
|
+
// this.toString(this.parseOffset),
|
428
|
+
// tuples
|
429
|
+
// );
|
430
|
+
// console.log("tuples");
|
431
|
+
// console.log(tuples);
|
432
|
+
// if (tuples.length > 0) {
|
433
|
+
// if (this.queryStream) {
|
434
|
+
// // emit header once
|
435
|
+
// if (
|
436
|
+
// this.headerEmitted === false &&
|
437
|
+
// this.result &&
|
438
|
+
// this.result.columns
|
439
|
+
// ) {
|
440
|
+
// this.queryStream.emit("header", this.result.columns);
|
441
|
+
// this.headerEmitted = true;
|
442
|
+
// }
|
443
|
+
// // emit tuples
|
444
|
+
// this.queryStream.emit("data", tuples);
|
445
|
+
// } else {
|
446
|
+
// this.result.data = this.result.data || [];
|
447
|
+
// for (let t of tuples) {
|
448
|
+
// this.result.data.push(t);
|
449
|
+
// }
|
450
|
+
// }
|
451
|
+
// }
|
452
|
+
//}
|
453
|
+
//console.log(`bytesProcessed ${bytesToRead}`);
|
454
|
+
return bytesProcessed;
|
455
|
+
}
|
456
|
+
|
457
|
+
complete(): boolean {
|
458
|
+
return this.last && (this.leftOver == 0);
|
459
|
+
}
|
460
|
+
|
461
|
+
errorMessage(): string {
|
462
|
+
const msg = this.toString();
|
463
|
+
if (msg.startsWith(MSG_ERROR)) {
|
464
|
+
return msg;
|
465
|
+
}
|
466
|
+
return "";
|
467
|
+
}
|
468
|
+
|
469
|
+
isFileTransfer(): boolean {
|
470
|
+
return this.toString().startsWith(MSG_FILETRANS);
|
471
|
+
}
|
472
|
+
|
473
|
+
isPrompt(): boolean {
|
474
|
+
return this.complete() && this.toString() == MSG_PROMPT;
|
475
|
+
}
|
476
|
+
|
477
|
+
isRedirect(): boolean {
|
478
|
+
return this.toString().startsWith(MSG_REDIRECT);
|
479
|
+
}
|
480
|
+
|
481
|
+
isQueryResponse(): boolean {
|
482
|
+
if (this.result && this.result.type) {
|
483
|
+
return this.result.type.startsWith(MSG_Q);
|
484
|
+
}
|
485
|
+
return this.toString().startsWith(MSG_Q);
|
486
|
+
}
|
487
|
+
|
488
|
+
isMsgMore(): boolean {
|
489
|
+
// server wants more ?
|
490
|
+
return this.toString().startsWith(MSG_MORE);
|
491
|
+
}
|
492
|
+
|
493
|
+
toString(start?: number) {
|
494
|
+
if (this.complete() && !this.queryStream && this._str) {
|
495
|
+
if (start)
|
496
|
+
return this._str.substring(start);
|
497
|
+
return this._str;
|
543
498
|
}
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
sqlOptimizerTime,
|
570
|
-
] = rest;
|
571
|
-
this.result.id = parseInt(id);
|
572
|
-
this.result.rowCnt = parseInt(rowCnt);
|
573
|
-
this.result.columnCnt = parseInt(columnCnt);
|
574
|
-
this.result.queryId = parseInt(queryId);
|
575
|
-
this.result.queryTime = parseInt(queryTime);
|
576
|
-
this.result.malOptimizerTime = parseInt(malOptimizerTime);
|
577
|
-
this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
|
578
|
-
} else if (this.result.type === MSG_QUPDATE) {
|
579
|
-
const [
|
580
|
-
affectedRowCnt,
|
581
|
-
autoIncrementId,
|
582
|
-
queryId,
|
583
|
-
queryTime,
|
584
|
-
malOptimizerTime,
|
585
|
-
sqlOptimizerTime,
|
586
|
-
] = rest;
|
587
|
-
this.result.affectedRows = parseInt(affectedRowCnt);
|
588
|
-
this.result.queryId = parseInt(queryId);
|
589
|
-
this.result.queryTime = parseInt(queryTime);
|
590
|
-
this.result.malOptimizerTime = parseInt(malOptimizerTime);
|
591
|
-
this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
|
592
|
-
} else if (this.result.type === MSG_QSCHEMA) {
|
593
|
-
const [queryTime, malOptimizerTime] = rest;
|
594
|
-
this.result.queryTime = parseInt(queryTime);
|
595
|
-
this.result.malOptimizerTime = parseInt(malOptimizerTime);
|
596
|
-
} else if (this.result.type === MSG_QTRANS) {
|
597
|
-
// skip
|
598
|
-
} else if (this.result.type === MSG_QPREPARE) {
|
599
|
-
const [id, rowCnt, columnCnt, rows] = rest;
|
600
|
-
this.result.id = parseInt(id);
|
601
|
-
this.result.rowCnt = parseInt(rowCnt);
|
602
|
-
this.result.columnCnt = parseInt(columnCnt);
|
603
|
-
}
|
604
|
-
break;
|
605
|
-
case MSG_HEADER:
|
606
|
-
const header = parseHeaderLine(line);
|
607
|
-
if (this.result.headers !== undefined) {
|
608
|
-
this.result.headers = { ...this.result.headers, ...header };
|
609
|
-
} else {
|
610
|
-
this.result.headers = header;
|
611
|
-
}
|
612
|
-
// if we have all headers we can compile column info
|
613
|
-
const haveAllHeaders =
|
614
|
-
Boolean(this.result.headers.tableNames) &&
|
615
|
-
Boolean(this.result.headers.columnNames) &&
|
616
|
-
Boolean(this.result.headers.columnTypes);
|
617
|
-
if (this.result.columns === undefined && haveAllHeaders) {
|
618
|
-
const colums: Column[] = [];
|
619
|
-
for (let i = 0; i < this.result.columnCnt; i++) {
|
620
|
-
const table = this.result.headers.tableNames[i];
|
621
|
-
const name = this.result.headers.columnNames[i];
|
622
|
-
const type = this.result.headers.columnTypes[i];
|
623
|
-
colums.push({
|
624
|
-
table,
|
625
|
-
name,
|
626
|
-
type,
|
627
|
-
index: i,
|
628
|
-
});
|
499
|
+
const buff = Buffer.concat(this.chunks);
|
500
|
+
this._str = buff.toString("utf8");
|
501
|
+
if (start) return this._str.substring(start);
|
502
|
+
return this._str;
|
503
|
+
}
|
504
|
+
|
505
|
+
settle(res?: Promise<any>): void {
|
506
|
+
if (this.settled === false && this.complete()) {
|
507
|
+
const errMsg = this.errorMessage();
|
508
|
+
const err = errMsg ? new Error(errMsg) : null;
|
509
|
+
if (this.queryStream) {
|
510
|
+
if (err) this.queryStream.emit("error", err);
|
511
|
+
this.queryStream.end();
|
512
|
+
} else {
|
513
|
+
if (this.callbacks) {
|
514
|
+
if (err) {
|
515
|
+
this.callbacks.reject(err);
|
516
|
+
} else {
|
517
|
+
this.callbacks.resolve(res || this.result);
|
518
|
+
}
|
519
|
+
} else if (this.fileHandler && this.isQueryResponse()) {
|
520
|
+
this.fileHandler.resolve(this.result);
|
521
|
+
} else if (this.fileHandler && (err || this.fileHandler.err)) {
|
522
|
+
this.fileHandler.reject(err || this.fileHandler.err);
|
523
|
+
}
|
629
524
|
}
|
630
|
-
this.
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
res.push(tuple);
|
636
|
-
break;
|
637
|
-
default:
|
638
|
-
throw TypeError(`Invalid query response line!\n${line}`);
|
639
|
-
}
|
640
|
-
// line is processed advance offset
|
641
|
-
offset = eol + 1;
|
642
|
-
// get next line
|
643
|
-
eol = data.indexOf("\n", offset);
|
644
|
-
line = eol > 0 ? data.substring(offset, eol) : undefined;
|
645
|
-
}
|
646
|
-
return offset;
|
647
|
-
}
|
648
|
-
}
|
525
|
+
this.settled = true;
|
526
|
+
this._str = "";
|
527
|
+
this.chunks = [];
|
528
|
+
}
|
529
|
+
}
|
649
530
|
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
531
|
+
parseQueryResponse(data: string, res: any[]): number {
|
532
|
+
let offset = 0;
|
533
|
+
let eol = data.indexOf("\n");
|
534
|
+
let line = eol > 0 ? data.substring(0, eol) : undefined;
|
535
|
+
while (line) {
|
536
|
+
switch (line.charAt(0)) {
|
537
|
+
case MSG_Q:
|
538
|
+
// first line
|
539
|
+
this.result = this.result || {};
|
540
|
+
this.result.type = line.substring(0, 2);
|
541
|
+
const rest = line.substring(3).trim().split(" ");
|
542
|
+
if (this.result.type === MSG_QTABLE) {
|
543
|
+
const [
|
544
|
+
id,
|
545
|
+
rowCnt,
|
546
|
+
columnCnt,
|
547
|
+
rows,
|
548
|
+
queryId,
|
549
|
+
queryTime,
|
550
|
+
malOptimizerTime,
|
551
|
+
sqlOptimizerTime,
|
552
|
+
] = rest;
|
553
|
+
this.result.id = parseInt(id);
|
554
|
+
this.result.rowCnt = parseInt(rowCnt);
|
555
|
+
this.result.columnCnt = parseInt(columnCnt);
|
556
|
+
this.result.queryId = parseInt(queryId);
|
557
|
+
this.result.queryTime = parseInt(queryTime);
|
558
|
+
this.result.malOptimizerTime = parseInt(malOptimizerTime);
|
559
|
+
this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
|
560
|
+
} else if (this.result.type === MSG_QUPDATE) {
|
561
|
+
const [
|
562
|
+
affectedRowCnt,
|
563
|
+
autoIncrementId,
|
564
|
+
queryId,
|
565
|
+
queryTime,
|
566
|
+
malOptimizerTime,
|
567
|
+
sqlOptimizerTime,
|
568
|
+
] = rest;
|
569
|
+
this.result.affectedRows = parseInt(affectedRowCnt);
|
570
|
+
this.result.queryId = parseInt(queryId);
|
571
|
+
this.result.queryTime = parseInt(queryTime);
|
572
|
+
this.result.malOptimizerTime = parseInt(malOptimizerTime);
|
573
|
+
this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
|
574
|
+
} else if (this.result.type === MSG_QSCHEMA) {
|
575
|
+
const [queryTime, malOptimizerTime] = rest;
|
576
|
+
this.result.queryTime = parseInt(queryTime);
|
577
|
+
this.result.malOptimizerTime = parseInt(malOptimizerTime);
|
578
|
+
} else if (this.result.type === MSG_QTRANS) {
|
579
|
+
// skip
|
580
|
+
} else if (this.result.type === MSG_QPREPARE) {
|
581
|
+
const [id, rowCnt, columnCnt, rows] = rest;
|
582
|
+
this.result.id = parseInt(id);
|
583
|
+
this.result.rowCnt = parseInt(rowCnt);
|
584
|
+
this.result.columnCnt = parseInt(columnCnt);
|
585
|
+
}
|
586
|
+
break;
|
587
|
+
case MSG_HEADER:
|
588
|
+
const header = parseHeaderLine(line);
|
589
|
+
if (this.result.headers !== undefined) {
|
590
|
+
this.result.headers = {...this.result.headers, ...header};
|
591
|
+
} else {
|
592
|
+
this.result.headers = header;
|
593
|
+
}
|
594
|
+
// if we have all headers we can compile column info
|
595
|
+
const haveAllHeaders =
|
596
|
+
Boolean(this.result.headers.tableNames) &&
|
597
|
+
Boolean(this.result.headers.columnNames) &&
|
598
|
+
Boolean(this.result.headers.columnTypes);
|
599
|
+
if (this.result.columns === undefined && haveAllHeaders) {
|
600
|
+
const colums: Column[] = [];
|
601
|
+
for (let i = 0; i < this.result.columnCnt; i++) {
|
602
|
+
const table = this.result.headers.tableNames[i];
|
603
|
+
const name = this.result.headers.columnNames[i];
|
604
|
+
const type = this.result.headers.columnTypes[i];
|
605
|
+
colums.push({
|
606
|
+
table,
|
607
|
+
name,
|
608
|
+
type,
|
609
|
+
index: i,
|
610
|
+
});
|
611
|
+
}
|
612
|
+
this.result.columns = colums;
|
613
|
+
}
|
614
|
+
break;
|
615
|
+
case MSG_TUPLE:
|
616
|
+
const tuple = parseTupleLine(line, this.result.headers.columnTypes);
|
617
|
+
res.push(tuple);
|
618
|
+
break;
|
619
|
+
default:
|
620
|
+
throw TypeError(`Invalid query response line!\n${line}`);
|
621
|
+
}
|
622
|
+
// line is processed advance offset
|
623
|
+
offset = eol + 1;
|
624
|
+
// get next line
|
625
|
+
eol = data.indexOf("\n", offset);
|
626
|
+
line = eol > 0 ? data.substring(offset, eol) : undefined;
|
627
|
+
}
|
628
|
+
return offset;
|
629
|
+
}
|
630
|
+
|
631
|
+
streamResult() {
|
632
|
+
if (this.queryStream && this.isQueryResponse()) {
|
633
|
+
const tuples = [];
|
634
|
+
this.parseOffset += this.parseQueryResponse(
|
635
|
+
this.toString(this.parseOffset),
|
636
|
+
tuples
|
637
|
+
);
|
638
|
+
if (tuples.length > 0) {
|
639
|
+
// emit header once
|
640
|
+
if (
|
641
|
+
this.headerEmitted === false &&
|
642
|
+
this.result &&
|
643
|
+
this.result.columns
|
644
|
+
) {
|
645
|
+
this.queryStream.emit("header", this.result.columns);
|
646
|
+
this.headerEmitted = true;
|
647
|
+
}
|
648
|
+
// emit tuples
|
649
|
+
this.queryStream.emit("data", tuples);
|
650
|
+
}
|
651
|
+
}
|
652
|
+
}
|
670
653
|
}
|
671
654
|
|
672
655
|
class MapiConnection extends EventEmitter {
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
this.state = MAPI_STATE.INIT;
|
690
|
-
this.socket = this.createSocket(config.timeout);
|
691
|
-
// this.socket = new Socket();
|
692
|
-
// if (config.timeout) this.socket.setTimeout(config.timeout);
|
693
|
-
// this.socket.addListener("data", this.recv.bind(this));
|
694
|
-
// this.socket.addListener("error", this.handleSocketError.bind(this));
|
695
|
-
// this.socket.addListener("timeout", this.handleTimeout.bind(this));
|
696
|
-
// this.socket.addListener("close", () => {
|
697
|
-
// console.log("socket close event");
|
698
|
-
// this.emit("end");
|
699
|
-
// });
|
700
|
-
this.redirects = 0;
|
701
|
-
this.queue = [];
|
702
|
-
this.database = config.database;
|
703
|
-
this.language = config.language || MAPI_LANGUAGE.SQL;
|
704
|
-
this.unixSocket = config.unixSocket;
|
705
|
-
this.host = config.host;
|
706
|
-
this.port = config.port;
|
707
|
-
this.username = config.username;
|
708
|
-
this.password = config.password;
|
709
|
-
this.timeout = config.timeout;
|
710
|
-
}
|
711
|
-
|
712
|
-
private createSocket = (timeout?: number): Socket => {
|
713
|
-
const socket = new Socket();
|
714
|
-
if (timeout) socket.setTimeout(timeout);
|
715
|
-
socket.addListener("data", this.recv.bind(this));
|
716
|
-
socket.addListener("error", this.handleSocketError.bind(this));
|
717
|
-
socket.addListener("timeout", this.handleTimeout.bind(this));
|
718
|
-
socket.addListener("close", () => {
|
719
|
-
console.log("socket close event");
|
720
|
-
this.emit("end");
|
721
|
-
});
|
722
|
-
return socket;
|
723
|
-
};
|
724
|
-
|
725
|
-
connect(handShakeOptions: HandShakeOption[] = []): Promise<any[]> {
|
726
|
-
this.handShakeOptions = handShakeOptions;
|
727
|
-
// TODO unix socket
|
728
|
-
const opt: SocketConnectOpts = {
|
729
|
-
port: this.port,
|
730
|
-
host: this.host,
|
731
|
-
noDelay: true,
|
732
|
-
};
|
733
|
-
const socket =
|
734
|
-
this.socket && !this.socket.destroyed
|
735
|
-
? this.socket
|
736
|
-
: this.createSocket(this.timeout);
|
737
|
-
socket.connect(opt, () => {
|
738
|
-
this.state = MAPI_STATE.CONNECTED;
|
739
|
-
this.socket.setKeepAlive(true);
|
740
|
-
});
|
741
|
-
this.socket = socket;
|
742
|
-
|
743
|
-
return once(this, "ready");
|
744
|
-
}
|
745
|
-
|
746
|
-
ready(): boolean {
|
747
|
-
return this.state === MAPI_STATE.READY;
|
748
|
-
}
|
749
|
-
|
750
|
-
disconnect(): Promise<boolean> {
|
751
|
-
return new Promise((resolve, reject) => {
|
752
|
-
this.socket.end(() => {
|
753
|
-
this.redirects = 0;
|
656
|
+
state: MAPI_STATE;
|
657
|
+
socket: Socket;
|
658
|
+
database: string;
|
659
|
+
timeout: number;
|
660
|
+
username: string;
|
661
|
+
password: string;
|
662
|
+
host?: string;
|
663
|
+
unixSocket?: string;
|
664
|
+
port: number;
|
665
|
+
language: MAPI_LANGUAGE;
|
666
|
+
handShakeOptions?: HandShakeOption[];
|
667
|
+
redirects: number;
|
668
|
+
queue: Response[];
|
669
|
+
|
670
|
+
constructor(config: MapiConfig) {
|
671
|
+
super();
|
754
672
|
this.state = MAPI_STATE.INIT;
|
755
|
-
this.
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
673
|
+
this.redirects = 0;
|
674
|
+
this.queue = [];
|
675
|
+
this.database = config.database;
|
676
|
+
this.language = config.language || MAPI_LANGUAGE.SQL;
|
677
|
+
this.unixSocket = config.unixSocket;
|
678
|
+
this.host = config.host;
|
679
|
+
this.port = config.port;
|
680
|
+
this.username = config.username;
|
681
|
+
this.password = config.password;
|
682
|
+
this.timeout = config.timeout;
|
683
|
+
}
|
684
|
+
|
685
|
+
private createSocket = (timeout?: number): Socket => {
|
686
|
+
const socket = new Socket();
|
687
|
+
if (timeout) socket.setTimeout(timeout);
|
688
|
+
socket.addListener("data", this.recv.bind(this));
|
689
|
+
socket.addListener("error", this.handleSocketError.bind(this));
|
690
|
+
socket.addListener("timeout", this.handleTimeout.bind(this));
|
691
|
+
socket.once("end", () => {
|
692
|
+
console.log("Server has ended the connection");
|
693
|
+
});
|
694
|
+
return socket;
|
695
|
+
};
|
696
|
+
|
697
|
+
connect(handShakeOptions: HandShakeOption[] = []): Promise<any[]> {
|
698
|
+
this.handShakeOptions = handShakeOptions;
|
699
|
+
// TODO unix socket
|
700
|
+
const opt: SocketConnectOpts = {
|
701
|
+
port: this.port,
|
702
|
+
host: this.host,
|
703
|
+
noDelay: true,
|
704
|
+
};
|
705
|
+
const socket =
|
706
|
+
this.socket && !this.socket.destroyed
|
707
|
+
? this.socket
|
708
|
+
: this.createSocket(this.timeout);
|
709
|
+
socket.connect(opt, () => {
|
710
|
+
this.state = MAPI_STATE.CONNECTED;
|
711
|
+
this.socket.setKeepAlive(true);
|
712
|
+
});
|
713
|
+
this.socket = socket;
|
714
|
+
|
715
|
+
return once(this, "ready");
|
716
|
+
}
|
717
|
+
|
718
|
+
ready(): boolean {
|
719
|
+
return this.socket && this.socket.writable && this.state === MAPI_STATE.READY;
|
720
|
+
}
|
721
|
+
|
722
|
+
disconnect(): Promise<true> {
|
723
|
+
return new Promise((resolve, reject) => {
|
724
|
+
if (!this.socket || this.socket.destroyed) {
|
725
|
+
this.socket = null;
|
726
|
+
this.state = MAPI_STATE.INIT;
|
727
|
+
return resolve(true);
|
728
|
+
}
|
729
|
+
|
730
|
+
const onClose = () => {
|
731
|
+
cleanup();
|
732
|
+
this.socket = null;
|
733
|
+
this.state = MAPI_STATE.INIT;
|
734
|
+
resolve(true);
|
735
|
+
};
|
736
|
+
|
737
|
+
const onError = (err: Error) => {
|
738
|
+
console.error(err);
|
739
|
+
cleanup();
|
740
|
+
this.socket?.destroy(); // force cleanup
|
741
|
+
this.socket = null;
|
742
|
+
this.state = MAPI_STATE.INIT;
|
743
|
+
reject(err);
|
744
|
+
};
|
745
|
+
|
746
|
+
const cleanup = () => {
|
747
|
+
this.socket?.removeListener("close", onClose);
|
748
|
+
this.socket?.removeListener("error", onError);
|
749
|
+
};
|
750
|
+
|
751
|
+
this.socket.once("close", onClose);
|
752
|
+
this.socket.once("error", onError);
|
753
|
+
|
754
|
+
// Initiate graceful shutdown
|
755
|
+
this.socket.end();
|
756
|
+
});
|
757
|
+
}
|
758
|
+
|
759
|
+
destroy() {
|
760
|
+
if (this.socket && !this.socket.destroyed)
|
761
|
+
this.socket.destroy();
|
762
|
+
this.socket = null;
|
763
|
+
}
|
764
|
+
|
765
|
+
private login(challenge: string): void {
|
766
|
+
const challengeParts = challenge.split(":");
|
767
|
+
const [salt, identity, protocol, hashes, endian, algo, opt_level] =
|
768
|
+
challengeParts;
|
769
|
+
let password: string;
|
787
770
|
try {
|
788
|
-
|
771
|
+
password = createHash(algo).update(this.password).digest("hex");
|
789
772
|
} catch (err) {
|
790
|
-
|
791
|
-
"error",
|
792
|
-
|
793
|
-
|
794
|
-
|
773
|
+
console.error(err);
|
774
|
+
this.emit("error", new TypeError(`Algorithm ${algo} not supported`));
|
775
|
+
return;
|
776
|
+
}
|
777
|
+
let pwhash = null;
|
778
|
+
// try hash algorithms in the order provided by the server
|
779
|
+
for (const algo of hashes.split(",")) {
|
780
|
+
try {
|
781
|
+
const hash = createHash(algo);
|
782
|
+
pwhash = `{${algo}}` + hash.update(password + salt).digest("hex");
|
783
|
+
break;
|
784
|
+
} catch {}
|
795
785
|
}
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
786
|
+
if (pwhash) {
|
787
|
+
let counterResponse = `LIT:${this.username}:${pwhash}:${this.language}:${this.database}:`;
|
788
|
+
if (opt_level && opt_level.startsWith("sql=")) {
|
789
|
+
let level = 0;
|
790
|
+
counterResponse += "FILETRANS:";
|
791
|
+
try {
|
792
|
+
level = Number(opt_level.substring(4));
|
793
|
+
} catch (err) {
|
794
|
+
this.emit(
|
795
|
+
"error",
|
796
|
+
new TypeError("Invalid handshake options level in server challenge")
|
797
|
+
);
|
798
|
+
return;
|
799
|
+
}
|
800
|
+
// process handshake options
|
801
|
+
const options = [];
|
802
|
+
for (const opt of this.handShakeOptions) {
|
803
|
+
if (opt.level < level) {
|
804
|
+
options.push(`${opt.name}=${Number(opt.value)}`);
|
805
|
+
opt.sent = true;
|
806
|
+
}
|
807
|
+
}
|
808
|
+
if (options) counterResponse += options.join(",") + ":";
|
809
|
+
}
|
810
|
+
this.send(Buffer.from(counterResponse))
|
811
|
+
.then(() => this.queue.push(new Response()))
|
812
|
+
.catch((err) => this.emit("error", err));
|
813
|
+
} else {
|
814
|
+
this.emit(
|
815
|
+
"error",
|
816
|
+
new TypeError(`None of the hashes ${hashes} are supported`)
|
817
|
+
);
|
803
818
|
}
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
header.writeUint16LE(((2 * MAPI_BLOCK_SIZE) << 1) | 0, 0);
|
825
|
-
// invalid utf8 and too small
|
826
|
-
const badBody = Buffer.concat([
|
827
|
-
Buffer.from("ERROR"),
|
828
|
-
Buffer.from([0x80]),
|
829
|
-
]);
|
830
|
-
const outBuff = Buffer.concat([header, badBody]);
|
831
|
-
this.socket.write(outBuff, async (err?: Error) => {
|
832
|
-
if (err) reject(err);
|
833
|
-
resolve();
|
834
|
-
});
|
835
|
-
});
|
836
|
-
}
|
837
|
-
|
838
|
-
send(buff: Buffer): Promise<void> {
|
839
|
-
return new Promise((resolve, reject) => {
|
840
|
-
let last = 0;
|
841
|
-
let offset = 0;
|
842
|
-
while (last === 0) {
|
843
|
-
const seg = buff.subarray(offset, offset + MAPI_BLOCK_SIZE);
|
844
|
-
last = seg.length < MAPI_BLOCK_SIZE ? 1 : 0;
|
845
|
-
const header = Buffer.allocUnsafe(2).fill(0);
|
846
|
-
header.writeUint16LE((seg.length << 1) | last, 0);
|
847
|
-
const outBuff = Buffer.concat([header, seg]);
|
848
|
-
this.socket.write(outBuff, (err?: Error) => {
|
849
|
-
if (err) reject(err);
|
850
|
-
if (last) resolve();
|
819
|
+
}
|
820
|
+
|
821
|
+
/**
|
822
|
+
* Raise exception on server by sending bad packet
|
823
|
+
*/
|
824
|
+
requestAbort(): Promise<void> {
|
825
|
+
return new Promise((resolve, reject) => {
|
826
|
+
const header = Buffer.allocUnsafe(2).fill(0);
|
827
|
+
// larger than allowed and not final message
|
828
|
+
header.writeUint16LE(((2 * MAPI_BLOCK_SIZE) << 1) | 0, 0);
|
829
|
+
// invalid utf8 and too small
|
830
|
+
const badBody = Buffer.concat([
|
831
|
+
Buffer.from("ERROR"),
|
832
|
+
Buffer.from([0x80]),
|
833
|
+
]);
|
834
|
+
const outBuff = Buffer.concat([header, badBody]);
|
835
|
+
this.socket.write(outBuff, async (err?: Error) => {
|
836
|
+
if (err) reject(err);
|
837
|
+
resolve();
|
838
|
+
});
|
851
839
|
});
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
this.queue.
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
840
|
+
}
|
841
|
+
|
842
|
+
send(buff: Buffer): Promise<void> {
|
843
|
+
return new Promise((resolve, reject) => {
|
844
|
+
let last = 0;
|
845
|
+
let offset = 0;
|
846
|
+
while (last === 0) {
|
847
|
+
const seg = buff.subarray(offset, offset + MAPI_BLOCK_SIZE);
|
848
|
+
last = seg.length < MAPI_BLOCK_SIZE ? 1 : 0;
|
849
|
+
const header = Buffer.allocUnsafe(2).fill(0);
|
850
|
+
header.writeUint16LE((seg.length << 1) | last, 0);
|
851
|
+
const outBuff = Buffer.concat([header, seg]);
|
852
|
+
this.socket.write(outBuff, (err?: Error) => {
|
853
|
+
if (err) reject(err);
|
854
|
+
if (last) resolve();
|
855
|
+
});
|
856
|
+
offset += seg.length;
|
857
|
+
}
|
858
|
+
});
|
859
|
+
}
|
860
|
+
|
861
|
+
private handleTimeout() {
|
862
|
+
this.emit("error", new Error("Timeout"));
|
863
|
+
}
|
864
|
+
|
865
|
+
private handleSocketError(err: Error) {
|
866
|
+
console.error('Socket error: ', err);
|
867
|
+
this.destroy();
|
868
|
+
}
|
869
|
+
|
870
|
+
async request(
|
871
|
+
sql: string,
|
872
|
+
stream: boolean = false
|
873
|
+
): Promise<QueryResult | QueryStream> {
|
874
|
+
if (this.ready() === false) throw new Error("Not Connected");
|
875
|
+
await this.send(Buffer.from(sql));
|
876
|
+
return new Promise((resolve, reject) => {
|
877
|
+
const resp = new Response({
|
878
|
+
stream,
|
879
|
+
callbacks: {resolve, reject},
|
880
|
+
});
|
881
|
+
this.queue.push(resp);
|
882
|
+
});
|
883
|
+
}
|
884
|
+
|
885
|
+
async requestFileTransfer(buff: Buffer, fileHandler: any): Promise<void> {
|
886
|
+
await this.send(buff);
|
887
|
+
const resp = new Response({fileHandler});
|
888
|
+
this.queue.push(resp);
|
889
|
+
}
|
890
|
+
|
891
|
+
async requestFileTransferError(err: string, fileHandler: any): Promise<void> {
|
892
|
+
await this.send(Buffer.from(err));
|
893
|
+
const resp = new Response({fileHandler});
|
894
|
+
this.queue.push(resp);
|
895
|
+
}
|
896
|
+
|
897
|
+
private recv(data: Buffer): void {
|
898
|
+
let bytesLeftOver: number;
|
899
|
+
let resp: Response;
|
900
|
+
// process queue left to right,
|
901
|
+
// find 1st uncomplete response
|
902
|
+
// remove complete responses
|
903
|
+
while (this.queue.length) {
|
904
|
+
const next = this.queue[0];
|
905
|
+
if (next.complete() || next.settled) {
|
906
|
+
this.queue.shift();
|
907
|
+
} else {
|
908
|
+
resp = next;
|
909
|
+
break;
|
910
|
+
}
|
911
|
+
}
|
912
|
+
if (resp === undefined && this.queue.length === 0) {
|
913
|
+
// challenge message
|
914
|
+
// or direct call to send has being made
|
915
|
+
// request api appends Response to the queue
|
916
|
+
resp = new Response();
|
917
|
+
this.queue.push(resp);
|
918
|
+
}
|
919
|
+
let offset = 0;
|
920
|
+
let end = data.length;
|
921
|
+
do {
|
922
|
+
offset += resp.append(data.subarray(offset, end));
|
923
|
+
} while ((offset < end) && !resp.complete());
|
924
|
+
|
925
|
+
if (resp.queryStream)
|
926
|
+
resp.streamResult();
|
927
|
+
|
928
|
+
if (resp.complete())
|
929
|
+
this.handleResponse(resp);
|
930
|
+
|
931
|
+
bytesLeftOver = data.length - offset;
|
932
|
+
if (bytesLeftOver) {
|
933
|
+
//const msg = `some ${bytesLeftOver} bytes left over!`;
|
934
|
+
//console.warn(msg);
|
935
|
+
this.recv(data.subarray(offset));
|
936
|
+
}
|
937
|
+
}
|
938
|
+
|
939
|
+
private handleResponse(resp: Response): void {
|
940
|
+
const err = resp.errorMessage();
|
941
|
+
if (this.state == MAPI_STATE.CONNECTED) {
|
942
|
+
if (err) {
|
943
|
+
this.emit("error", new Error(err));
|
944
|
+
return;
|
945
|
+
}
|
946
|
+
if (resp.isRedirect()) {
|
947
|
+
this.redirects += 1;
|
948
|
+
if (this.redirects > MAX_REDIRECTS)
|
949
|
+
this.emit(
|
950
|
+
"error",
|
951
|
+
new Error(`Exceeded max number of redirects ${MAX_REDIRECTS}`)
|
952
|
+
);
|
953
|
+
return;
|
954
|
+
}
|
955
|
+
if (resp.isPrompt()) {
|
956
|
+
//console.log("login OK");
|
957
|
+
this.state = MAPI_STATE.READY;
|
958
|
+
this.emit("ready", this.state);
|
959
|
+
return;
|
960
|
+
}
|
961
|
+
return this.login(resp.toString());
|
962
|
+
}
|
963
|
+
|
964
|
+
if (resp.isFileTransfer()) {
|
965
|
+
//console.log("file transfer");
|
966
|
+
let fhandler: any;
|
967
|
+
const msg = resp.toString(MSG_FILETRANS.length).trim();
|
968
|
+
let mode: string, offset: string, file: string;
|
969
|
+
if (msg.startsWith("r ")) {
|
970
|
+
[mode, offset, file] = msg.split(" ");
|
971
|
+
fhandler =
|
972
|
+
resp.fileHandler || new FileUploader(this, file, parseInt(offset));
|
973
|
+
return resp.settle(fhandler.upload());
|
974
|
+
} else if (msg.startsWith("rb")) {
|
975
|
+
[mode, file] = msg.split(" ");
|
976
|
+
fhandler = resp.fileHandler || new FileUploader(this, file, 0);
|
977
|
+
return resp.settle(fhandler.upload());
|
978
|
+
} else if (msg.startsWith("w")) {
|
979
|
+
[mode, file] = msg.split(" ");
|
980
|
+
fhandler = resp.fileHandler || new FileDownloader(this, file);
|
981
|
+
return resp.settle(fhandler.download());
|
982
|
+
} else {
|
983
|
+
// no msg end of transfer
|
984
|
+
const fileHandler = resp.fileHandler;
|
985
|
+
// we do expect a final response from server
|
986
|
+
this.queue.push(new Response({fileHandler}));
|
987
|
+
return resp.settle(fileHandler.close());
|
988
|
+
}
|
989
|
+
}
|
990
|
+
|
991
|
+
if (resp.isMsgMore()) {
|
992
|
+
// console.log("server wants more");
|
993
|
+
if (resp.fileHandler instanceof FileUploader)
|
994
|
+
return resp.settle(resp.fileHandler.upload());
|
995
|
+
}
|
996
|
+
|
997
|
+
if (
|
998
|
+
resp.fileHandler instanceof FileDownloader &&
|
999
|
+
resp.fileHandler.ready()
|
1000
|
+
) {
|
1001
|
+
// end of download
|
1002
|
+
const fileHandler = resp.fileHandler;
|
1003
|
+
const buff = Buffer.concat(resp.chunks);
|
1004
|
+
fileHandler.writeChunk(buff);
|
1005
|
+
// we do expect a final response from server
|
1006
|
+
this.queue.push(new Response({fileHandler}));
|
1007
|
+
return resp.settle(fileHandler.close());
|
1008
|
+
}
|
1009
|
+
|
1010
|
+
if (resp.isQueryResponse()) {
|
1011
|
+
const data = [];
|
1012
|
+
resp.parseQueryResponse(
|
1013
|
+
resp.toString(),
|
1014
|
+
data
|
1015
|
+
);
|
1016
|
+
resp.result.data = data;
|
1017
|
+
}
|
1018
|
+
|
1019
|
+
resp.settle();
|
1020
|
+
}
|
996
1021
|
}
|
997
1022
|
|
998
1023
|
export {
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1024
|
+
MapiConfig,
|
1025
|
+
MapiConnection,
|
1026
|
+
parseMapiUri,
|
1027
|
+
createMapiConfig,
|
1028
|
+
HandShakeOption,
|
1029
|
+
QueryResult,
|
1030
|
+
QueryStream,
|
1006
1031
|
};
|