monetdb 2.0.1 → 2.1.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.
Files changed (61) hide show
  1. package/README.md +2 -1
  2. package/dist/PrepareStatement.d.ts +12 -0
  3. package/dist/PrepareStatement.js +29 -0
  4. package/dist/PrepareStatement.js.map +1 -0
  5. package/dist/QueryStream.d.ts +7 -0
  6. package/dist/QueryStream.js +13 -0
  7. package/dist/QueryStream.js.map +1 -0
  8. package/dist/connection.d.ts +25 -0
  9. package/dist/connection.js +104 -0
  10. package/dist/connection.js.map +1 -0
  11. package/dist/connections.d.ts +1 -0
  12. package/dist/connections.js +49 -0
  13. package/dist/connections.js.map +1 -0
  14. package/dist/defaults.d.ts +10 -0
  15. package/dist/defaults.js +13 -0
  16. package/dist/defaults.js.map +1 -0
  17. package/dist/file-transfer.d.ts +34 -0
  18. package/dist/file-transfer.js +194 -0
  19. package/dist/file-transfer.js.map +1 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +9 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/mapi.d.ts +149 -0
  24. package/dist/mapi.js +844 -0
  25. package/dist/mapi.js.map +1 -0
  26. package/dist/monetize.d.ts +2 -0
  27. package/dist/monetize.js +57 -0
  28. package/dist/monetize.js.map +1 -0
  29. package/dist/tsconfig.tsbuildinfo +1 -0
  30. package/package.json +7 -2
  31. package/src/mapi.ts +104 -114
  32. package/.github/workflows/Linux.yml +0 -45
  33. package/.github/workflows/docs.yml +0 -79
  34. package/.github/workflows/macos.yml +0 -43
  35. package/.github/workflows/monetdb-versions.yml +0 -43
  36. package/docs/components/alert.tsx +0 -10
  37. package/docs/components/info.tsx +0 -6
  38. package/docs/next.config.js +0 -24
  39. package/docs/package-lock.json +0 -5069
  40. package/docs/package.json +0 -22
  41. package/docs/pages/_app.js +0 -9
  42. package/docs/pages/_meta.json +0 -18
  43. package/docs/pages/apis/_meta.json +0 -4
  44. package/docs/pages/apis/connection.mdx +0 -60
  45. package/docs/pages/apis/result.mdx +0 -39
  46. package/docs/pages/filetransfer.mdx +0 -43
  47. package/docs/pages/index.mdx +0 -27
  48. package/docs/pages/prepstmt.mdx +0 -13
  49. package/docs/pages/result.mdx +0 -41
  50. package/docs/theme.config.js +0 -35
  51. package/docs/v0/README.v0.md +0 -138
  52. package/docs/v1/MapiConnection.md +0 -53
  53. package/docs/v1/README.md +0 -532
  54. package/docs/v1/v1-notes.md +0 -173
  55. package/test/connection.ts +0 -43
  56. package/test/exec-queries.ts +0 -112
  57. package/test/filetransfer.ts +0 -94
  58. package/test/prepare-statement.ts +0 -27
  59. package/test/query-stream.ts +0 -41
  60. package/test/tmp/.gitignore +0 -4
  61. package/tsconfig.json +0 -24
package/dist/mapi.js ADDED
@@ -0,0 +1,844 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.QueryStream = exports.HandShakeOption = exports.createMapiConfig = exports.parseMapiUri = exports.MapiConnection = void 0;
16
+ const node_net_1 = require("node:net");
17
+ const events_1 = require("events");
18
+ const buffer_1 = require("buffer");
19
+ const node_crypto_1 = require("node:crypto");
20
+ const defaults_1 = __importDefault(require("./defaults"));
21
+ const node_url_1 = require("node:url");
22
+ const file_transfer_1 = require("./file-transfer");
23
+ const MAPI_BLOCK_SIZE = 1024 * 8 - 2;
24
+ const MAPI_HEADER_SIZE = 2;
25
+ const MSG_PROMPT = "";
26
+ const MSG_MORE = "\x01\x02\n";
27
+ const MSG_FILETRANS = "\x01\x03\n";
28
+ const MSG_INFO = "#";
29
+ const MSG_ERROR = "!";
30
+ const MSG_Q = "&";
31
+ const MSG_QTABLE = "&1";
32
+ const MSG_QUPDATE = "&2";
33
+ const MSG_QSCHEMA = "&3";
34
+ const MSG_QTRANS = "&4";
35
+ const MSG_QPREPARE = "&5";
36
+ const MSG_QBLOCK = "&6";
37
+ const MSG_HEADER = "%";
38
+ const MSG_TUPLE = "[";
39
+ const MSG_TUPLE_NOSLICE = "=";
40
+ const MSG_REDIRECT = "^";
41
+ const MSG_OK = "=OK";
42
+ const MAX_REDIRECTS = 10;
43
+ const MAX_BUFF_SIZE = buffer_1.constants.MAX_LENGTH;
44
+ var MAPI_STATE;
45
+ (function (MAPI_STATE) {
46
+ MAPI_STATE[MAPI_STATE["INIT"] = 1] = "INIT";
47
+ MAPI_STATE[MAPI_STATE["CONNECTED"] = 2] = "CONNECTED";
48
+ MAPI_STATE[MAPI_STATE["READY"] = 3] = "READY";
49
+ })(MAPI_STATE || (MAPI_STATE = {}));
50
+ var MAPI_LANGUAGE;
51
+ (function (MAPI_LANGUAGE) {
52
+ MAPI_LANGUAGE["SQL"] = "sql";
53
+ MAPI_LANGUAGE["MAPI"] = "mapi";
54
+ MAPI_LANGUAGE["CONTROL"] = "control";
55
+ })(MAPI_LANGUAGE || (MAPI_LANGUAGE = {}));
56
+ class HandShakeOption {
57
+ constructor(level, name, value, fallback, sent = false) {
58
+ this.level = level;
59
+ this.name = name;
60
+ this.value = value;
61
+ this.fallback = fallback;
62
+ this.sent = sent;
63
+ }
64
+ }
65
+ exports.HandShakeOption = HandShakeOption;
66
+ function isMapiUri(uri) {
67
+ const regx = new RegExp("^mapi:monetdb://*", "i");
68
+ return regx.test(uri);
69
+ }
70
+ function parseMapiUri(uri) {
71
+ if (isMapiUri(uri)) {
72
+ const url = new node_url_1.URL(uri.substring(5));
73
+ if (url.hostname) {
74
+ const host = url.hostname;
75
+ const port = parseInt(url.port);
76
+ const username = url.username;
77
+ const password = url.password;
78
+ const database = url.pathname.split("/")[1];
79
+ return {
80
+ host,
81
+ port,
82
+ username,
83
+ password,
84
+ database,
85
+ };
86
+ }
87
+ }
88
+ throw new Error(`Invalid MAPI URI ${uri}!`);
89
+ }
90
+ exports.parseMapiUri = parseMapiUri;
91
+ // validates and sets defaults on missing properties
92
+ function createMapiConfig(params) {
93
+ const database = params && params.database ? params.database : defaults_1.default.database;
94
+ if (typeof database != "string") {
95
+ throw new Error("database name must be string");
96
+ }
97
+ const username = params && params.username ? params.username : defaults_1.default.username;
98
+ const password = params && params.password ? params.password : defaults_1.default.password;
99
+ let host = params && params.host;
100
+ const unixSocket = params && params.unixSocket;
101
+ if (!unixSocket && !host)
102
+ host = defaults_1.default.host;
103
+ if (typeof host != "string") {
104
+ throw new TypeError(`${host} is not valid hostname`);
105
+ }
106
+ const port = params && params.port ? Number(params.port) : Number(defaults_1.default.port);
107
+ if (isNaN(port)) {
108
+ throw new TypeError(`${port} is not valid port`);
109
+ }
110
+ const timeout = params && params.timeout ? Number(params.timeout) : undefined;
111
+ if (timeout && isNaN(timeout)) {
112
+ throw new TypeError("timeout must be number");
113
+ }
114
+ const language = params && params.language ? params.language : MAPI_LANGUAGE.SQL;
115
+ const autoCommit = params.autoCommit || defaults_1.default.autoCommit;
116
+ const replySize = params.replySize || defaults_1.default.replySize;
117
+ return {
118
+ database,
119
+ username,
120
+ password,
121
+ language,
122
+ host,
123
+ port,
124
+ timeout,
125
+ unixSocket,
126
+ autoCommit,
127
+ replySize,
128
+ };
129
+ }
130
+ exports.createMapiConfig = createMapiConfig;
131
+ class Column {
132
+ constructor(table, name, type, index, length) {
133
+ this.table = table;
134
+ this.name = name;
135
+ this.type = type;
136
+ this.index = index;
137
+ this.length = length;
138
+ }
139
+ }
140
+ class QueryStream extends events_1.EventEmitter {
141
+ constructor() {
142
+ super();
143
+ }
144
+ end(res) {
145
+ this.emit("end", res);
146
+ }
147
+ }
148
+ exports.QueryStream = QueryStream;
149
+ function parseHeaderLine(hdrLine) {
150
+ if (hdrLine.startsWith(MSG_HEADER)) {
151
+ const [head, tail] = hdrLine.substring(1).trim().split("#");
152
+ let res = {};
153
+ const vals = head.trim().split(",\t");
154
+ switch (tail.trim()) {
155
+ case "table_name":
156
+ res = { tableNames: vals };
157
+ break;
158
+ case "name":
159
+ res = { columnNames: vals };
160
+ break;
161
+ case "type":
162
+ res = { columnTypes: vals };
163
+ break;
164
+ default:
165
+ res = {};
166
+ }
167
+ return res;
168
+ }
169
+ throw TypeError("Invalid header format!");
170
+ }
171
+ function parseTupleLine(line, types) {
172
+ if (line.startsWith(MSG_TUPLE) && line.endsWith("]")) {
173
+ var resultline = [];
174
+ var cCol = 0;
175
+ var curtok = "";
176
+ var state = "INCRAP";
177
+ let endQuotes = 0;
178
+ /* mostly adapted from clients/R/MonetDB.R/src/mapisplit.c */
179
+ for (var curPos = 2; curPos < line.length - 1; curPos++) {
180
+ var chr = line.charAt(curPos);
181
+ switch (state) {
182
+ case "INCRAP":
183
+ if (chr != "\t" && chr != "," && chr != " ") {
184
+ if (chr == '"') {
185
+ state = "INQUOTES";
186
+ }
187
+ else {
188
+ state = "INTOKEN";
189
+ curtok += chr;
190
+ }
191
+ }
192
+ break;
193
+ case "INTOKEN":
194
+ if (chr == "," || curPos == line.length - 2) {
195
+ if (curtok == "NULL" && endQuotes === 0) {
196
+ resultline.push(null);
197
+ }
198
+ else {
199
+ switch (types[cCol]) {
200
+ case "boolean":
201
+ resultline.push(curtok == "true");
202
+ break;
203
+ case "tinyint":
204
+ case "smallint":
205
+ case "int":
206
+ case "wrd":
207
+ case "bigint":
208
+ resultline.push(parseInt(curtok));
209
+ break;
210
+ case "real":
211
+ case "double":
212
+ case "decimal":
213
+ resultline.push(parseFloat(curtok));
214
+ break;
215
+ case "json":
216
+ try {
217
+ resultline.push(JSON.parse(curtok));
218
+ }
219
+ catch (e) {
220
+ resultline.push(curtok);
221
+ }
222
+ break;
223
+ default:
224
+ // we need to unescape double quotes
225
+ //valPtr = valPtr.replace(/[^\\]\\"/g, '"');
226
+ resultline.push(curtok);
227
+ break;
228
+ }
229
+ }
230
+ cCol++;
231
+ state = "INCRAP";
232
+ curtok = "";
233
+ endQuotes = 0;
234
+ }
235
+ else {
236
+ curtok += chr;
237
+ }
238
+ break;
239
+ case "ESCAPED":
240
+ state = "INQUOTES";
241
+ switch (chr) {
242
+ case "t":
243
+ curtok += "\t";
244
+ break;
245
+ case "n":
246
+ curtok += "\n";
247
+ break;
248
+ case "r":
249
+ curtok += "\r";
250
+ break;
251
+ default:
252
+ curtok += chr;
253
+ }
254
+ break;
255
+ case "INQUOTES":
256
+ if (chr == '"') {
257
+ state = "INTOKEN";
258
+ endQuotes++;
259
+ break;
260
+ }
261
+ if (chr == "\\") {
262
+ state = "ESCAPED";
263
+ break;
264
+ }
265
+ curtok += chr;
266
+ break;
267
+ }
268
+ }
269
+ return resultline;
270
+ }
271
+ throw TypeError("Invalid tuple format!");
272
+ }
273
+ class Response {
274
+ constructor(opt = {}) {
275
+ this.buff = buffer_1.Buffer.allocUnsafe(MAPI_BLOCK_SIZE).fill(0);
276
+ this.offset = 0;
277
+ this.parseOffset = 0;
278
+ this.segments = [];
279
+ this.settled = false;
280
+ this.headerEmitted = false;
281
+ this.stream = opt.stream;
282
+ this.callbacks = opt.callbacks;
283
+ this.fileHandler = opt.fileHandler;
284
+ if (opt.stream) {
285
+ this.queryStream = new QueryStream();
286
+ if (opt.callbacks && opt.callbacks.resolve)
287
+ opt.callbacks.resolve(this.queryStream);
288
+ }
289
+ }
290
+ append(data) {
291
+ let srcStartIndx = 0;
292
+ let srcEndIndx = srcStartIndx + data.length;
293
+ const l = this.segments.length;
294
+ let segment = (l > 0 && this.segments[l - 1]) || undefined;
295
+ let bytesCopied = 0;
296
+ let bytesProcessed = 0;
297
+ if (!this.complete()) {
298
+ // check if out of space
299
+ if (this.buff.length - this.offset < data.length) {
300
+ const bytes = this.expand(MAPI_BLOCK_SIZE);
301
+ console.log(`expanding by ${bytes} bytes!`);
302
+ }
303
+ if (segment === undefined || (segment && segment.isFull())) {
304
+ const hdr = data.readUInt16LE(0);
305
+ const last = (hdr & 1) === 1;
306
+ const bytes = hdr >> 1;
307
+ srcStartIndx = MAPI_HEADER_SIZE;
308
+ srcEndIndx = srcStartIndx + Math.min(bytes, data.length);
309
+ bytesCopied = data.copy(this.buff, this.offset, srcStartIndx, srcEndIndx);
310
+ segment = new Segment(bytes, last, this.offset, bytesCopied);
311
+ this.segments.push(segment);
312
+ this.offset += bytesCopied;
313
+ bytesProcessed = MAPI_HEADER_SIZE + bytesCopied;
314
+ }
315
+ else {
316
+ const byteCntToRead = segment.bytes - segment.bytesOffset;
317
+ srcEndIndx = srcStartIndx + byteCntToRead;
318
+ bytesCopied = data.copy(this.buff, this.offset, srcStartIndx, srcEndIndx);
319
+ this.offset += bytesCopied;
320
+ segment.bytesOffset += bytesCopied;
321
+ // console.log(`segment is full ${segment.bytesOffset === segment.bytes}`);
322
+ bytesProcessed = bytesCopied;
323
+ }
324
+ if (this.isQueryResponse()) {
325
+ const tuples = [];
326
+ // const firstPackage = this.parseOffset === 0;
327
+ this.parseOffset += this.parseQueryResponse(this.toString(this.parseOffset), tuples);
328
+ if (tuples.length > 0) {
329
+ if (this.queryStream) {
330
+ // emit header once
331
+ if (this.headerEmitted === false &&
332
+ this.result &&
333
+ this.result.columns) {
334
+ this.queryStream.emit("header", this.result.columns);
335
+ this.headerEmitted = true;
336
+ }
337
+ // emit tuples
338
+ this.queryStream.emit("data", tuples);
339
+ }
340
+ else {
341
+ this.result.data = this.result.data || [];
342
+ for (let t of tuples) {
343
+ this.result.data.push(t);
344
+ }
345
+ }
346
+ }
347
+ }
348
+ }
349
+ return bytesProcessed;
350
+ }
351
+ complete() {
352
+ const l = this.segments.length;
353
+ if (l > 0) {
354
+ const segment = this.segments[l - 1];
355
+ return segment.last && segment.isFull();
356
+ }
357
+ return false;
358
+ }
359
+ seekOffset() {
360
+ const len = this.segments.length;
361
+ if (len) {
362
+ const last = this.segments[len - 1];
363
+ if (last.isFull())
364
+ return last.offset + last.bytes;
365
+ return last.offset;
366
+ }
367
+ return 0;
368
+ }
369
+ expand(byteCount) {
370
+ if (this.buff.length + byteCount > MAX_BUFF_SIZE &&
371
+ this.fileHandler instanceof file_transfer_1.FileDownloader) {
372
+ const offset = this.seekOffset();
373
+ if (offset) {
374
+ this.fileHandler.writeChunk(this.buff.subarray(0, offset));
375
+ this.buff = this.buff.subarray(offset);
376
+ this.offset -= offset;
377
+ }
378
+ }
379
+ const buff = buffer_1.Buffer.allocUnsafe(this.buff.length + byteCount).fill(0);
380
+ const bytesCopied = this.buff.copy(buff);
381
+ this.buff = buff;
382
+ // should be byteCount
383
+ return this.buff.length - bytesCopied;
384
+ }
385
+ firstCharacter() {
386
+ return this.buff.toString("utf8", 0, 1);
387
+ }
388
+ errorMessage() {
389
+ if (this.firstCharacter() === MSG_ERROR) {
390
+ return this.buff.toString("utf8", 1);
391
+ }
392
+ return "";
393
+ }
394
+ isFileTransfer() {
395
+ return this.toString().startsWith(MSG_FILETRANS);
396
+ }
397
+ isPrompt() {
398
+ // perhaps use toString
399
+ return this.complete() && this.firstCharacter() === "\x00";
400
+ }
401
+ isRedirect() {
402
+ return this.firstCharacter() === MSG_REDIRECT;
403
+ }
404
+ isQueryResponse() {
405
+ if (this.result && this.result.type) {
406
+ return this.result.type.startsWith(MSG_Q);
407
+ }
408
+ return this.firstCharacter() === MSG_Q;
409
+ }
410
+ isMsgMore() {
411
+ // server wants more ?
412
+ return this.toString().startsWith(MSG_MORE);
413
+ }
414
+ toString(start) {
415
+ const res = this.buff.toString("utf8", 0, this.offset);
416
+ if (start)
417
+ return res.substring(start);
418
+ return res;
419
+ }
420
+ settle(res) {
421
+ if (this.settled === false && this.complete()) {
422
+ const errMsg = this.errorMessage();
423
+ const err = errMsg ? new Error(errMsg) : null;
424
+ if (this.queryStream) {
425
+ if (err)
426
+ this.queryStream.emit("error", err);
427
+ this.queryStream.end();
428
+ }
429
+ else {
430
+ if (this.callbacks) {
431
+ if (err) {
432
+ this.callbacks.reject(err);
433
+ }
434
+ else {
435
+ this.callbacks.resolve(res || this.result);
436
+ }
437
+ }
438
+ else if (this.fileHandler && this.isQueryResponse()) {
439
+ this.fileHandler.resolve(this.result);
440
+ }
441
+ else if (this.fileHandler && (err || this.fileHandler.err)) {
442
+ this.fileHandler.reject(err || this.fileHandler.err);
443
+ }
444
+ }
445
+ this.settled = true;
446
+ }
447
+ }
448
+ parseQueryResponse(data, res) {
449
+ let offset = 0;
450
+ let eol = data.indexOf("\n");
451
+ let line = eol > 0 ? data.substring(0, eol) : undefined;
452
+ while (line) {
453
+ switch (line.charAt(0)) {
454
+ case MSG_Q:
455
+ // first line
456
+ this.result = this.result || {};
457
+ this.result.type = line.substring(0, 2);
458
+ const rest = line.substring(3).trim().split(" ");
459
+ if (this.result.type === MSG_QTABLE) {
460
+ const [id, rowCnt, columnCnt, rows, queryId, queryTime, malOptimizerTime, sqlOptimizerTime,] = rest;
461
+ this.result.id = parseInt(id);
462
+ this.result.rowCnt = parseInt(rowCnt);
463
+ this.result.columnCnt = parseInt(columnCnt);
464
+ this.result.queryId = parseInt(queryId);
465
+ this.result.queryTime = parseInt(queryTime);
466
+ this.result.malOptimizerTime = parseInt(malOptimizerTime);
467
+ this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
468
+ }
469
+ else if (this.result.type === MSG_QUPDATE) {
470
+ const [affectedRowCnt, autoIncrementId, queryId, queryTime, malOptimizerTime, sqlOptimizerTime,] = rest;
471
+ this.result.affectedRows = parseInt(affectedRowCnt);
472
+ this.result.queryId = parseInt(queryId);
473
+ this.result.queryTime = parseInt(queryTime);
474
+ this.result.malOptimizerTime = parseInt(malOptimizerTime);
475
+ this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
476
+ }
477
+ else if (this.result.type === MSG_QSCHEMA) {
478
+ const [queryTime, malOptimizerTime] = rest;
479
+ this.result.queryTime = parseInt(queryTime);
480
+ this.result.malOptimizerTime = parseInt(malOptimizerTime);
481
+ }
482
+ else if (this.result.type === MSG_QTRANS) {
483
+ // skip
484
+ }
485
+ else if (this.result.type === MSG_QPREPARE) {
486
+ const [id, rowCnt, columnCnt, rows] = rest;
487
+ this.result.id = parseInt(id);
488
+ this.result.rowCnt = parseInt(rowCnt);
489
+ this.result.columnCnt = parseInt(columnCnt);
490
+ }
491
+ break;
492
+ case MSG_HEADER:
493
+ const header = parseHeaderLine(line);
494
+ if (this.result.headers !== undefined) {
495
+ this.result.headers = Object.assign(Object.assign({}, this.result.headers), header);
496
+ }
497
+ else {
498
+ this.result.headers = header;
499
+ }
500
+ // if we have all headers we can compile column info
501
+ const haveAllHeaders = Boolean(this.result.headers.tableNames) &&
502
+ Boolean(this.result.headers.columnNames) &&
503
+ Boolean(this.result.headers.columnTypes);
504
+ if (this.result.columns === undefined && haveAllHeaders) {
505
+ const colums = [];
506
+ for (let i = 0; i < this.result.columnCnt; i++) {
507
+ const table = this.result.headers.tableNames[i];
508
+ const name = this.result.headers.columnNames[i];
509
+ const type = this.result.headers.columnTypes[i];
510
+ colums.push({
511
+ table,
512
+ name,
513
+ type,
514
+ index: i,
515
+ });
516
+ }
517
+ this.result.columns = colums;
518
+ }
519
+ break;
520
+ case MSG_TUPLE:
521
+ const tuple = parseTupleLine(line, this.result.headers.columnTypes);
522
+ res.push(tuple);
523
+ break;
524
+ default:
525
+ throw TypeError(`Invalid query response line!\n${line}`);
526
+ }
527
+ // line is processed advance offset
528
+ offset = eol + 1;
529
+ // get next line
530
+ eol = data.indexOf("\n", offset);
531
+ line = eol > 0 ? data.substring(offset, eol) : undefined;
532
+ }
533
+ return offset;
534
+ }
535
+ }
536
+ class Segment {
537
+ constructor(bytes, last, offset, bytesOffset) {
538
+ this.bytes = bytes;
539
+ this.last = last;
540
+ this.offset = offset;
541
+ this.bytesOffset = bytesOffset;
542
+ }
543
+ isFull() {
544
+ return this.bytes === this.bytesOffset;
545
+ }
546
+ }
547
+ class MapiConnection extends events_1.EventEmitter {
548
+ constructor(config) {
549
+ super();
550
+ this.createSocket = (timeout) => {
551
+ const socket = new node_net_1.Socket();
552
+ if (timeout)
553
+ socket.setTimeout(timeout);
554
+ socket.addListener("data", this.recv.bind(this));
555
+ socket.addListener("error", this.handleSocketError.bind(this));
556
+ socket.addListener("timeout", this.handleTimeout.bind(this));
557
+ socket.addListener("close", () => {
558
+ console.log("socket close event");
559
+ this.emit("end");
560
+ });
561
+ return socket;
562
+ };
563
+ this.state = MAPI_STATE.INIT;
564
+ this.socket = this.createSocket(config.timeout);
565
+ // this.socket = new Socket();
566
+ // if (config.timeout) this.socket.setTimeout(config.timeout);
567
+ // this.socket.addListener("data", this.recv.bind(this));
568
+ // this.socket.addListener("error", this.handleSocketError.bind(this));
569
+ // this.socket.addListener("timeout", this.handleTimeout.bind(this));
570
+ // this.socket.addListener("close", () => {
571
+ // console.log("socket close event");
572
+ // this.emit("end");
573
+ // });
574
+ this.redirects = 0;
575
+ this.queue = [];
576
+ this.database = config.database;
577
+ this.language = config.language || MAPI_LANGUAGE.SQL;
578
+ this.unixSocket = config.unixSocket;
579
+ this.host = config.host;
580
+ this.port = config.port;
581
+ this.username = config.username;
582
+ this.password = config.password;
583
+ this.timeout = config.timeout;
584
+ }
585
+ connect(handShakeOptions = []) {
586
+ this.handShakeOptions = handShakeOptions;
587
+ // TODO unix socket
588
+ const opt = {
589
+ port: this.port,
590
+ host: this.host,
591
+ noDelay: true,
592
+ };
593
+ const socket = this.socket && !this.socket.destroyed
594
+ ? this.socket
595
+ : this.createSocket(this.timeout);
596
+ socket.connect(opt, () => {
597
+ this.state = MAPI_STATE.CONNECTED;
598
+ this.socket.setKeepAlive(true);
599
+ });
600
+ this.socket = socket;
601
+ return (0, events_1.once)(this, "ready");
602
+ }
603
+ ready() {
604
+ return this.state === MAPI_STATE.READY;
605
+ }
606
+ disconnect() {
607
+ return new Promise((resolve, reject) => {
608
+ this.socket.end(() => {
609
+ this.redirects = 0;
610
+ this.state = MAPI_STATE.INIT;
611
+ this.socket.destroy();
612
+ resolve(this.state === MAPI_STATE.INIT);
613
+ });
614
+ });
615
+ }
616
+ login(challenge) {
617
+ const challengeParts = challenge.split(":");
618
+ const [salt, identity, protocol, hashes, endian, algo, opt_level] = challengeParts;
619
+ let password;
620
+ try {
621
+ password = (0, node_crypto_1.createHash)(algo).update(this.password).digest("hex");
622
+ }
623
+ catch (err) {
624
+ console.error(err);
625
+ this.emit("error", new TypeError(`Algorithm ${algo} not supported`));
626
+ return;
627
+ }
628
+ let pwhash = null;
629
+ // try hash algorithms in the order provided by the server
630
+ for (const algo of hashes.split(",")) {
631
+ try {
632
+ const hash = (0, node_crypto_1.createHash)(algo);
633
+ pwhash = `{${algo}}` + hash.update(password + salt).digest("hex");
634
+ break;
635
+ }
636
+ catch (_a) { }
637
+ }
638
+ if (pwhash) {
639
+ let counterResponse = `LIT:${this.username}:${pwhash}:${this.language}:${this.database}:`;
640
+ if (opt_level && opt_level.startsWith("sql=")) {
641
+ let level = 0;
642
+ counterResponse += "FILETRANS:";
643
+ try {
644
+ level = Number(opt_level.substring(4));
645
+ }
646
+ catch (err) {
647
+ this.emit("error", new TypeError("Invalid handshake options level in server challenge"));
648
+ return;
649
+ }
650
+ // process handshake options
651
+ const options = [];
652
+ for (const opt of this.handShakeOptions) {
653
+ if (opt.level < level) {
654
+ options.push(`${opt.name}=${Number(opt.value)}`);
655
+ opt.sent = true;
656
+ }
657
+ }
658
+ if (options)
659
+ counterResponse += options.join(",") + ":";
660
+ }
661
+ this.send(buffer_1.Buffer.from(counterResponse))
662
+ .then(() => this.queue.push(new Response()))
663
+ .catch((err) => this.emit("error", err));
664
+ }
665
+ else {
666
+ this.emit("error", new TypeError(`None of the hashes ${hashes} are supported`));
667
+ }
668
+ }
669
+ /**
670
+ * Raise exception on server by sending bad packet
671
+ */
672
+ requestAbort() {
673
+ return new Promise((resolve, reject) => {
674
+ const header = buffer_1.Buffer.allocUnsafe(2).fill(0);
675
+ // larger than allowed and not final message
676
+ header.writeUint16LE(((2 * MAPI_BLOCK_SIZE) << 1) | 0, 0);
677
+ // invalid utf8 and too small
678
+ const badBody = buffer_1.Buffer.concat([
679
+ buffer_1.Buffer.from("ERROR"),
680
+ buffer_1.Buffer.from([0x80]),
681
+ ]);
682
+ const outBuff = buffer_1.Buffer.concat([header, badBody]);
683
+ this.socket.write(outBuff, (err) => __awaiter(this, void 0, void 0, function* () {
684
+ if (err)
685
+ reject(err);
686
+ resolve();
687
+ }));
688
+ });
689
+ }
690
+ send(buff) {
691
+ return new Promise((resolve, reject) => {
692
+ let last = 0;
693
+ let offset = 0;
694
+ while (last === 0) {
695
+ const seg = buff.subarray(offset, offset + MAPI_BLOCK_SIZE);
696
+ last = seg.length < MAPI_BLOCK_SIZE ? 1 : 0;
697
+ const header = buffer_1.Buffer.allocUnsafe(2).fill(0);
698
+ header.writeUint16LE((seg.length << 1) | last, 0);
699
+ const outBuff = buffer_1.Buffer.concat([header, seg]);
700
+ this.socket.write(outBuff, (err) => {
701
+ if (err)
702
+ reject(err);
703
+ if (last)
704
+ resolve();
705
+ });
706
+ offset += seg.length;
707
+ }
708
+ });
709
+ }
710
+ handleTimeout() {
711
+ this.emit("error", new Error("Timeout"));
712
+ }
713
+ handleSocketError(err) {
714
+ console.error(err);
715
+ }
716
+ request(sql, stream = false) {
717
+ return __awaiter(this, void 0, void 0, function* () {
718
+ if (this.ready() === false)
719
+ throw new Error("Not Connected");
720
+ yield this.send(buffer_1.Buffer.from(sql));
721
+ return new Promise((resolve, reject) => {
722
+ const resp = new Response({
723
+ stream,
724
+ callbacks: { resolve, reject },
725
+ });
726
+ this.queue.push(resp);
727
+ });
728
+ });
729
+ }
730
+ requestFileTransfer(buff, fileHandler) {
731
+ return __awaiter(this, void 0, void 0, function* () {
732
+ yield this.send(buff);
733
+ const resp = new Response({ fileHandler });
734
+ this.queue.push(resp);
735
+ });
736
+ }
737
+ requestFileTransferError(err, fileHandler) {
738
+ return __awaiter(this, void 0, void 0, function* () {
739
+ yield this.send(buffer_1.Buffer.from(err));
740
+ const resp = new Response({ fileHandler });
741
+ this.queue.push(resp);
742
+ });
743
+ }
744
+ recv(data) {
745
+ let bytesLeftOver;
746
+ let resp;
747
+ // process queue left to right, find 1st uncomplete response
748
+ // remove responses that are completed
749
+ while (this.queue.length) {
750
+ const next = this.queue[0];
751
+ if (next.complete() || next.settled) {
752
+ this.queue.shift();
753
+ }
754
+ else {
755
+ resp = next;
756
+ break;
757
+ }
758
+ }
759
+ if (resp === undefined && this.queue.length === 0) {
760
+ // challenge message
761
+ // or direct call to send has being made
762
+ // e.g. request api appends Response to the queue
763
+ resp = new Response();
764
+ this.queue.push(resp);
765
+ }
766
+ const offset = resp.append(data);
767
+ if (resp.complete())
768
+ this.handleResponse(resp);
769
+ bytesLeftOver = data.length - offset;
770
+ if (bytesLeftOver) {
771
+ const msg = `some ${bytesLeftOver} bytes left over!`;
772
+ console.warn(msg);
773
+ this.recv(data.subarray(offset));
774
+ }
775
+ }
776
+ handleResponse(resp) {
777
+ const err = resp.errorMessage();
778
+ if (this.state == MAPI_STATE.CONNECTED) {
779
+ if (err) {
780
+ this.emit("error", new Error(err));
781
+ return;
782
+ }
783
+ if (resp.isRedirect()) {
784
+ this.redirects += 1;
785
+ if (this.redirects > MAX_REDIRECTS)
786
+ this.emit("error", new Error(`Exceeded max number of redirects ${MAX_REDIRECTS}`));
787
+ return;
788
+ }
789
+ if (resp.isPrompt()) {
790
+ console.log("login OK");
791
+ this.state = MAPI_STATE.READY;
792
+ this.emit("ready", this.state);
793
+ return;
794
+ }
795
+ return this.login(resp.toString());
796
+ }
797
+ if (resp.isFileTransfer()) {
798
+ console.log("file transfer");
799
+ let fhandler;
800
+ const msg = resp.toString(MSG_FILETRANS.length).trim();
801
+ let mode, offset, file;
802
+ if (msg.startsWith("r ")) {
803
+ [mode, offset, file] = msg.split(" ");
804
+ fhandler =
805
+ resp.fileHandler || new file_transfer_1.FileUploader(this, file, parseInt(offset));
806
+ return resp.settle(fhandler.upload());
807
+ }
808
+ else if (msg.startsWith("rb")) {
809
+ [mode, file] = msg.split(" ");
810
+ fhandler = resp.fileHandler || new file_transfer_1.FileUploader(this, file, 0);
811
+ return resp.settle(fhandler.upload());
812
+ }
813
+ else if (msg.startsWith("w")) {
814
+ [mode, file] = msg.split(" ");
815
+ fhandler = resp.fileHandler || new file_transfer_1.FileDownloader(this, file);
816
+ return resp.settle(fhandler.download());
817
+ }
818
+ else {
819
+ // no msg end of transfer
820
+ const fileHandler = resp.fileHandler;
821
+ // we do expect a final response from server
822
+ this.queue.push(new Response({ fileHandler }));
823
+ return resp.settle(fileHandler.close());
824
+ }
825
+ }
826
+ if (resp.isMsgMore()) {
827
+ // console.log("server wants more");
828
+ if (resp.fileHandler instanceof file_transfer_1.FileUploader)
829
+ return resp.settle(resp.fileHandler.upload());
830
+ }
831
+ if (resp.fileHandler instanceof file_transfer_1.FileDownloader &&
832
+ resp.fileHandler.ready()) {
833
+ // end of download
834
+ const fileHandler = resp.fileHandler;
835
+ fileHandler.writeChunk(resp.buff);
836
+ // we do expect a final response from server
837
+ this.queue.push(new Response({ fileHandler }));
838
+ return resp.settle(fileHandler.close());
839
+ }
840
+ resp.settle();
841
+ }
842
+ }
843
+ exports.MapiConnection = MapiConnection;
844
+ //# sourceMappingURL=mapi.js.map