@sqlitecloud/drivers 0.0.34

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.
@@ -0,0 +1,458 @@
1
+ "use strict";
2
+ /**
3
+ * transport-tls.ts - handles low level communication with sqlitecloud server via tls socket and binary protocol
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.TlsSocketTransport = void 0;
10
+ const types_1 = require("./types");
11
+ const rowset_1 = require("./rowset");
12
+ const connection_1 = require("./connection");
13
+ const connection_2 = require("./connection");
14
+ const tls_1 = __importDefault(require("tls"));
15
+ const lz4 = require('lz4js');
16
+ /**
17
+ * The server communicates with clients via commands defined
18
+ * in the SQLiteCloud Server Protocol (SCSP), see more at:
19
+ * https://github.com/sqlitecloud/sdk/blob/master/PROTOCOL.md
20
+ */
21
+ const CMD_STRING = '+';
22
+ const CMD_ZEROSTRING = '!';
23
+ const CMD_ERROR = '-';
24
+ const CMD_INT = ':';
25
+ const CMD_FLOAT = ',';
26
+ const CMD_ROWSET = '*';
27
+ const CMD_ROWSET_CHUNK = '/';
28
+ const CMD_JSON = '#';
29
+ const CMD_NULL = '_';
30
+ const CMD_BLOB = '$';
31
+ const CMD_COMPRESSED = '%';
32
+ const CMD_COMMAND = '^';
33
+ const CMD_ARRAY = '=';
34
+ // const CMD_RAWJSON = '{'
35
+ // const CMD_PUBSUB = '|'
36
+ // const CMD_RECONNECT = '@'
37
+ /**
38
+ * Implementation of SQLiteCloudConnection that connects directly to the database via tls socket and raw, binary protocol.
39
+ * SQLiteCloud low-level connection, will do messaging, handle socket, authentication, etc.
40
+ * A connection socket is established when the connection is created and closed when the connection is closed.
41
+ * All operations are serialized by waiting for any pending operations to complete. Once a connection is closed,
42
+ * it cannot be reopened and you must create a new connection.
43
+ */
44
+ class TlsSocketTransport {
45
+ /** True if connection is open */
46
+ get connected() {
47
+ return !!this.socket;
48
+ }
49
+ /* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
50
+ connect(config, callback) {
51
+ // connection established while we were waiting in line?
52
+ console.assert(!this.connected, 'Connection already established');
53
+ // clear all listeners and call done in the operations queue
54
+ const finish = error => {
55
+ if (this.socket) {
56
+ this.socket.removeAllListeners('data');
57
+ this.socket.removeAllListeners('error');
58
+ this.socket.removeAllListeners('close');
59
+ if (error) {
60
+ this.close();
61
+ }
62
+ }
63
+ };
64
+ this.config = config;
65
+ // connect to tls socket, initialize connection, setup event handlers
66
+ this.socket = tls_1.default.connect(this.config.port, this.config.host, this.config.tlsOptions, () => {
67
+ var _a;
68
+ if (!((_a = this.socket) === null || _a === void 0 ? void 0 : _a.authorized)) {
69
+ const anonimizedError = (0, connection_2.anonimizeError)(this.socket.authorizationError);
70
+ console.error('Connection was not authorized', anonimizedError);
71
+ this.close();
72
+ finish(new types_1.SQLiteCloudError('Connection was not authorized', { cause: anonimizedError }));
73
+ }
74
+ else {
75
+ // the connection was closed before it was even opened,
76
+ // eg. client closed the connection before the server accepted it
77
+ if (this.socket === null) {
78
+ finish(new types_1.SQLiteCloudError('Connection was closed before it was done opening'));
79
+ return;
80
+ }
81
+ // send initialization commands
82
+ console.assert(this.socket, 'Connection already closed');
83
+ const commands = (0, connection_1.getInitializationCommands)(config);
84
+ this.processCommands(commands, error => {
85
+ if (error && this.socket) {
86
+ this.close();
87
+ }
88
+ if (callback) {
89
+ callback === null || callback === void 0 ? void 0 : callback.call(this, error);
90
+ callback = undefined;
91
+ }
92
+ finish(error);
93
+ });
94
+ }
95
+ });
96
+ this.socket.on('close', () => {
97
+ this.socket = null;
98
+ finish(new types_1.SQLiteCloudError('Connection was closed'));
99
+ });
100
+ this.socket.once('error', (error) => {
101
+ console.error('Connection error', error);
102
+ finish(new types_1.SQLiteCloudError('Connection error', { cause: error }));
103
+ });
104
+ return this;
105
+ }
106
+ /** Will send a command immediately (no queueing), return the rowset/result or throw an error */
107
+ processCommands(commands, callback) {
108
+ var _a, _b, _c;
109
+ // connection needs to be established?
110
+ if (!this.socket) {
111
+ callback === null || callback === void 0 ? void 0 : callback.call(this, new types_1.SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' }));
112
+ return this;
113
+ }
114
+ // compose commands following SCPC protocol
115
+ commands = formatCommand(commands);
116
+ let buffer = Buffer.alloc(0);
117
+ const rowsetChunks = [];
118
+ const startedOn = new Date();
119
+ // define what to do if an answer does not arrive within the set timeout
120
+ let socketTimeout;
121
+ // clear all listeners and call done in the operations queue
122
+ const finish = (error, result) => {
123
+ clearTimeout(socketTimeout);
124
+ if (this.socket) {
125
+ this.socket.removeAllListeners('data');
126
+ this.socket.removeAllListeners('error');
127
+ this.socket.removeAllListeners('close');
128
+ }
129
+ if (callback) {
130
+ callback === null || callback === void 0 ? void 0 : callback.call(this, error, result);
131
+ callback = undefined;
132
+ }
133
+ };
134
+ // define the Promise that waits for the server response
135
+ const readData = (data) => {
136
+ var _a, _b, _c;
137
+ try {
138
+ // on first ondata event, dataType is read from data, on subsequent ondata event, is read from buffer that is the concatanations of data received on each ondata event
139
+ let dataType = buffer.length === 0 ? data.subarray(0, 1).toString() : buffer.subarray(0, 1).toString('utf8');
140
+ buffer = Buffer.concat([buffer, data]);
141
+ const commandLength = hasCommandLength(dataType);
142
+ if (commandLength) {
143
+ const commandLength = parseCommandLength(buffer);
144
+ const hasReceivedEntireCommand = buffer.length - buffer.indexOf(' ') - 1 >= commandLength ? true : false;
145
+ if (hasReceivedEntireCommand) {
146
+ if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.verbose) {
147
+ let bufferString = buffer.toString('utf8');
148
+ if (bufferString.length > 1000) {
149
+ bufferString = bufferString.substring(0, 100) + '...' + bufferString.substring(bufferString.length - 40);
150
+ }
151
+ // const elapsedMs = new Date().getTime() - startedOn.getTime()
152
+ // console.debug(`Receive: ${bufferString} - ${elapsedMs}ms`)
153
+ }
154
+ // need to decompress this buffer before decoding?
155
+ if (dataType === CMD_COMPRESSED) {
156
+ ;
157
+ ({ buffer, dataType } = decompressBuffer(buffer));
158
+ }
159
+ if (dataType !== CMD_ROWSET_CHUNK) {
160
+ (_b = this.socket) === null || _b === void 0 ? void 0 : _b.off('data', readData);
161
+ const { data } = popData(buffer);
162
+ finish(null, data);
163
+ }
164
+ else {
165
+ // @ts-expect-error
166
+ // check if rowset received the ending chunk
167
+ if (data.subarray(data.indexOf(' ') + 1, data.length).toString() === '0 0 0 ') {
168
+ const parsedData = parseRowsetChunks(rowsetChunks);
169
+ finish === null || finish === void 0 ? void 0 : finish.call(this, null, parsedData);
170
+ }
171
+ else {
172
+ // no ending string? ask server for another chunk
173
+ rowsetChunks.push(buffer);
174
+ buffer = Buffer.alloc(0);
175
+ const okCommand = formatCommand('OK');
176
+ (_c = this.socket) === null || _c === void 0 ? void 0 : _c.write(okCommand);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ else {
182
+ // command with no explicit len so make sure that the final character is a space
183
+ const lastChar = buffer.subarray(buffer.length - 1, buffer.length).toString('utf8');
184
+ if (lastChar == ' ') {
185
+ const { data } = popData(buffer);
186
+ finish(null, data);
187
+ }
188
+ }
189
+ }
190
+ catch (error) {
191
+ console.assert(error instanceof Error);
192
+ if (error instanceof Error) {
193
+ finish(error);
194
+ }
195
+ }
196
+ };
197
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.once('close', () => {
198
+ finish(new types_1.SQLiteCloudError('Connection was closed', { cause: (0, connection_2.anonimizeCommand)(commands) }));
199
+ });
200
+ (_b = this.socket) === null || _b === void 0 ? void 0 : _b.write(commands, 'utf8', () => {
201
+ var _a, _b;
202
+ socketTimeout = setTimeout(() => {
203
+ const timeoutError = new types_1.SQLiteCloudError('Request timed out', { cause: (0, connection_2.anonimizeCommand)(commands) });
204
+ // console.debug(`Request timed out, config.timeout is ${this.config?.timeout as number}ms`, timeoutError)
205
+ finish(timeoutError);
206
+ }, (_a = this.config) === null || _a === void 0 ? void 0 : _a.timeout);
207
+ (_b = this.socket) === null || _b === void 0 ? void 0 : _b.on('data', readData);
208
+ });
209
+ (_c = this.socket) === null || _c === void 0 ? void 0 : _c.once('error', (error) => {
210
+ console.error('Socket error', error);
211
+ this.close();
212
+ finish(new types_1.SQLiteCloudError('Socket error', { cause: (0, connection_2.anonimizeError)(error) }));
213
+ });
214
+ return this;
215
+ }
216
+ /** Disconnect from server, release connection. */
217
+ close() {
218
+ console.assert(this.socket !== null, 'TlsSocketTransport.close - connection already closed');
219
+ if (this.socket) {
220
+ this.socket.destroy();
221
+ this.socket = null;
222
+ }
223
+ this.socket = undefined;
224
+ return this;
225
+ }
226
+ }
227
+ exports.TlsSocketTransport = TlsSocketTransport;
228
+ //
229
+ // utility functions
230
+ //
231
+ /** Analyze first character to check if corresponding data type has LEN */
232
+ function hasCommandLength(firstCharacter) {
233
+ return firstCharacter == CMD_INT || firstCharacter == CMD_FLOAT || firstCharacter == CMD_NULL ? false : true;
234
+ }
235
+ /** Analyze a command with explict LEN and extract it */
236
+ function parseCommandLength(data) {
237
+ return parseInt(data.subarray(1, data.indexOf(' ')).toString('utf8'));
238
+ }
239
+ /** Receive a compressed buffer, decompress with lz4, return buffer and datatype */
240
+ function decompressBuffer(buffer) {
241
+ const spaceIndex = buffer.indexOf(' ');
242
+ buffer = buffer.subarray(spaceIndex + 1);
243
+ // extract compressed size
244
+ const compressedSize = parseInt(buffer.subarray(0, buffer.indexOf(' ') + 1).toString('utf8'));
245
+ buffer = buffer.subarray(buffer.indexOf(' ') + 1);
246
+ // extract decompressed size
247
+ const decompressedSize = parseInt(buffer.subarray(0, buffer.indexOf(' ') + 1).toString('utf8'));
248
+ buffer = buffer.subarray(buffer.indexOf(' ') + 1);
249
+ // extract compressed dataType
250
+ const dataType = buffer.subarray(0, 1).toString('utf8');
251
+ const decompressedBuffer = Buffer.alloc(decompressedSize);
252
+ const compressedBuffer = buffer.subarray(buffer.length - compressedSize);
253
+ // lz4js library is javascript and doesn't have types so we silence the type check
254
+ // eslint-disable-next-line
255
+ const decompressionResult = lz4.decompressBlock(compressedBuffer, decompressedBuffer, 0, compressedSize, 0);
256
+ buffer = Buffer.concat([buffer.subarray(0, buffer.length - compressedSize), decompressedBuffer]);
257
+ if (decompressionResult <= 0 || decompressionResult !== decompressedSize) {
258
+ throw new Error(`lz4 decompression error at offset ${decompressionResult}`);
259
+ }
260
+ return { buffer, dataType: dataType };
261
+ }
262
+ /** Parse error message or extended error message */
263
+ function parseError(buffer, spaceIndex) {
264
+ const errorBuffer = buffer.subarray(spaceIndex + 1);
265
+ const errorString = errorBuffer.toString('utf8');
266
+ const parts = errorString.split(' ');
267
+ let errorCodeStr = parts.shift() || '0'; // Default errorCode is '0' if not present
268
+ let extErrCodeStr = '0'; // Default extended error code
269
+ let offsetCodeStr = '-1'; // Default offset code
270
+ // Split the errorCode by ':' to check for extended error codes
271
+ const errorCodeParts = errorCodeStr.split(':');
272
+ errorCodeStr = errorCodeParts[0];
273
+ if (errorCodeParts.length > 1) {
274
+ extErrCodeStr = errorCodeParts[1];
275
+ if (errorCodeParts.length > 2) {
276
+ offsetCodeStr = errorCodeParts[2];
277
+ }
278
+ }
279
+ // Rest of the error string is the error message
280
+ const errorMessage = parts.join(' ');
281
+ // Parse error codes to integers safely, defaulting to 0 if NaN
282
+ const errorCode = parseInt(errorCodeStr);
283
+ const extErrCode = parseInt(extErrCodeStr);
284
+ const offsetCode = parseInt(offsetCodeStr);
285
+ // create an Error object and add the custom properties
286
+ throw new types_1.SQLiteCloudError(errorMessage, {
287
+ errorCode: errorCode.toString(),
288
+ externalErrorCode: extErrCode.toString(),
289
+ offsetCode
290
+ });
291
+ }
292
+ /** Parse an array of items (each of which will be parsed by type separately) */
293
+ function parseArray(buffer, spaceIndex) {
294
+ const parsedData = [];
295
+ const array = buffer.subarray(spaceIndex + 1, buffer.length);
296
+ const numberOfItems = parseInt(array.subarray(0, spaceIndex - 2).toString('utf8'));
297
+ let arrayItems = array.subarray(array.indexOf(' ') + 1, array.length);
298
+ for (let i = 0; i < numberOfItems; i++) {
299
+ const { data, fwdBuffer: buffer } = popData(arrayItems);
300
+ parsedData.push(data);
301
+ arrayItems = buffer;
302
+ }
303
+ return parsedData;
304
+ }
305
+ /** Parse header in a rowset or chunk of a chunked rowset */
306
+ function parseRowsetHeader(buffer) {
307
+ const index = parseInt(buffer.subarray(0, buffer.indexOf(':') + 1).toString());
308
+ buffer = buffer.subarray(buffer.indexOf(':') + 1);
309
+ // extract rowset header
310
+ const { data, fwdBuffer } = popIntegers(buffer, 3);
311
+ return {
312
+ index,
313
+ metadata: {
314
+ version: data[0],
315
+ numberOfRows: data[1],
316
+ numberOfColumns: data[2],
317
+ columns: []
318
+ },
319
+ fwdBuffer
320
+ };
321
+ }
322
+ /** Extract column names and, optionally, more metadata out of a rowset's header */
323
+ function parseRowsetColumnsMetadata(buffer, metadata) {
324
+ function popForward() {
325
+ const { data, fwdBuffer: fwdBuffer } = popData(buffer); // buffer in parent scope
326
+ buffer = fwdBuffer;
327
+ return data;
328
+ }
329
+ for (let i = 0; i < metadata.numberOfColumns; i++) {
330
+ metadata.columns.push({ name: popForward() });
331
+ }
332
+ // extract additional metadata if rowset has version 2
333
+ if (metadata.version == 2) {
334
+ for (let i = 0; i < metadata.numberOfColumns; i++)
335
+ metadata.columns[i].type = popForward();
336
+ for (let i = 0; i < metadata.numberOfColumns; i++)
337
+ metadata.columns[i].database = popForward();
338
+ for (let i = 0; i < metadata.numberOfColumns; i++)
339
+ metadata.columns[i].table = popForward();
340
+ for (let i = 0; i < metadata.numberOfColumns; i++)
341
+ metadata.columns[i].column = popForward(); // original column name
342
+ }
343
+ return buffer;
344
+ }
345
+ /** Parse a regular rowset (no chunks) */
346
+ function parseRowset(buffer, spaceIndex) {
347
+ buffer = buffer.subarray(spaceIndex + 1, buffer.length);
348
+ const { metadata, fwdBuffer } = parseRowsetHeader(buffer);
349
+ buffer = parseRowsetColumnsMetadata(fwdBuffer, metadata);
350
+ // decode each rowset item
351
+ const data = [];
352
+ for (let j = 0; j < metadata.numberOfRows * metadata.numberOfColumns; j++) {
353
+ const { data: rowData, fwdBuffer } = popData(buffer);
354
+ data.push(rowData);
355
+ buffer = fwdBuffer;
356
+ }
357
+ console.assert(data && data.length === metadata.numberOfRows * metadata.numberOfColumns, 'SQLiteCloudConnection.parseRowset - invalid rowset data');
358
+ return new rowset_1.SQLiteCloudRowset(metadata, data);
359
+ }
360
+ /**
361
+ * Parse a chunk of a chunked rowset command, eg:
362
+ * *LEN 0:VERS NROWS NCOLS DATA
363
+ */
364
+ function parseRowsetChunks(buffers) {
365
+ let metadata = { version: 1, numberOfColumns: 0, numberOfRows: 0, columns: [] };
366
+ const data = [];
367
+ for (let i = 0; i < buffers.length; i++) {
368
+ let buffer = buffers[i];
369
+ // validate and skip data type
370
+ const dataType = buffer.subarray(0, 1).toString();
371
+ console.assert(dataType === CMD_ROWSET_CHUNK);
372
+ buffer = buffer.subarray(buffer.indexOf(' ') + 1);
373
+ // chunk header, eg: 0:VERS NROWS NCOLS
374
+ const { index: chunkIndex, metadata: chunkMetadata, fwdBuffer } = parseRowsetHeader(buffer);
375
+ buffer = fwdBuffer;
376
+ // first chunk? extract columns metadata
377
+ if (chunkIndex === 1) {
378
+ metadata = chunkMetadata;
379
+ buffer = parseRowsetColumnsMetadata(buffer, metadata);
380
+ }
381
+ else {
382
+ metadata.numberOfRows += chunkMetadata.numberOfRows;
383
+ }
384
+ // extract single rowset row
385
+ for (let k = 0; k < chunkMetadata.numberOfRows * metadata.numberOfColumns; k++) {
386
+ const { data: itemData, fwdBuffer } = popData(buffer);
387
+ data.push(itemData);
388
+ buffer = fwdBuffer;
389
+ }
390
+ }
391
+ console.assert(data && data.length === metadata.numberOfRows * metadata.numberOfColumns, 'SQLiteCloudConnection.parseRowsetChunks - invalid rowset data');
392
+ return new rowset_1.SQLiteCloudRowset(metadata, data);
393
+ }
394
+ /** Pop one or more space separated integers from beginning of buffer, move buffer forward */
395
+ function popIntegers(buffer, numberOfIntegers = 1) {
396
+ const data = [];
397
+ for (let i = 0; i < numberOfIntegers; i++) {
398
+ const spaceIndex = buffer.indexOf(' ');
399
+ data[i] = parseInt(buffer.subarray(0, spaceIndex).toString());
400
+ buffer = buffer.subarray(spaceIndex + 1);
401
+ }
402
+ return { data, fwdBuffer: buffer };
403
+ }
404
+ /** Parse command, extract its data, return the data and the buffer moved to the first byte after the command */
405
+ function popData(buffer) {
406
+ function popResults(data) {
407
+ const fwdBuffer = buffer.subarray(commandEnd);
408
+ return { data, fwdBuffer };
409
+ }
410
+ // first character is the data type
411
+ console.assert(buffer && buffer instanceof Buffer);
412
+ const dataType = buffer.subarray(0, 1).toString('utf8');
413
+ console.assert(dataType !== CMD_COMPRESSED, "Compressed data shouldn't be decompressed before parsing");
414
+ console.assert(dataType !== CMD_ROWSET_CHUNK, 'Chunked data should be parsed by parseRowsetChunks');
415
+ let spaceIndex = buffer.indexOf(' ');
416
+ if (spaceIndex === -1) {
417
+ spaceIndex = buffer.length - 1;
418
+ }
419
+ let commandEnd = -1;
420
+ if (dataType === CMD_INT || dataType === CMD_FLOAT || dataType === CMD_NULL) {
421
+ commandEnd = spaceIndex + 1;
422
+ }
423
+ else {
424
+ const commandLength = parseInt(buffer.subarray(1, spaceIndex).toString());
425
+ commandEnd = spaceIndex + 1 + commandLength;
426
+ }
427
+ switch (dataType) {
428
+ case CMD_INT:
429
+ return popResults(parseInt(buffer.subarray(1, spaceIndex).toString()));
430
+ case CMD_FLOAT:
431
+ return popResults(parseFloat(buffer.subarray(1, spaceIndex).toString()));
432
+ case CMD_NULL:
433
+ return popResults(null);
434
+ case CMD_STRING:
435
+ return popResults(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8'));
436
+ case CMD_ZEROSTRING:
437
+ return popResults(buffer.subarray(spaceIndex + 1, commandEnd - 1).toString('utf8'));
438
+ case CMD_COMMAND:
439
+ return popResults(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8'));
440
+ case CMD_JSON:
441
+ return popResults(JSON.parse(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8')));
442
+ case CMD_BLOB:
443
+ return popResults(buffer.subarray(spaceIndex + 1, commandEnd));
444
+ case CMD_ARRAY:
445
+ return popResults(parseArray(buffer, spaceIndex));
446
+ case CMD_ROWSET:
447
+ return popResults(parseRowset(buffer, spaceIndex));
448
+ case CMD_ERROR:
449
+ parseError(buffer, spaceIndex); // throws custom error
450
+ break;
451
+ }
452
+ throw new TypeError(`Data type: ${dataType} is not defined in SCSP`);
453
+ }
454
+ /** Format a command to be sent via SCSP protocol */
455
+ function formatCommand(command) {
456
+ const commandLength = Buffer.byteLength(command, 'utf-8');
457
+ return `+${commandLength} ${command}`;
458
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * transport-ws.ts - handles low level communication with sqlitecloud server via socket.io websocket
3
+ */
4
+ import { SQLiteCloudConfig, ErrorCallback, ResultsCallback } from './types';
5
+ import { ConnectionTransport } from './connection';
6
+ /**
7
+ * Implementation of TransportConnection that connects to the database indirectly
8
+ * via SQLite Cloud Gateway, a socket.io based deamon that responds to sql query
9
+ * requests by returning results and rowsets in json format. The gateway handles
10
+ * connect, disconnect, retries, order of operations, timeouts, etc.
11
+ */
12
+ export declare class WebSocketTransport implements ConnectionTransport {
13
+ /** Configuration passed to connect */
14
+ private config?;
15
+ /** Socket.io used to communicated with SQLiteCloud server */
16
+ private socket?;
17
+ /** True if connection is open */
18
+ get connected(): boolean;
19
+ connect(config: SQLiteCloudConfig, callback?: ErrorCallback): this;
20
+ /** Will send a command immediately (no queueing), return the rowset/result or throw an error */
21
+ processCommands(commands: string, callback?: ResultsCallback): this;
22
+ /** Disconnect socket.io from server */
23
+ close(): this;
24
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /**
3
+ * transport-ws.ts - handles low level communication with sqlitecloud server via socket.io websocket
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WebSocketTransport = void 0;
7
+ const types_1 = require("./types");
8
+ const rowset_1 = require("./rowset");
9
+ const socket_io_client_1 = require("socket.io-client");
10
+ /**
11
+ * Implementation of TransportConnection that connects to the database indirectly
12
+ * via SQLite Cloud Gateway, a socket.io based deamon that responds to sql query
13
+ * requests by returning results and rowsets in json format. The gateway handles
14
+ * connect, disconnect, retries, order of operations, timeouts, etc.
15
+ */
16
+ class WebSocketTransport {
17
+ /** True if connection is open */
18
+ get connected() {
19
+ return !!this.socket;
20
+ }
21
+ /* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
22
+ connect(config, callback) {
23
+ var _a;
24
+ try {
25
+ // connection established while we were waiting in line?
26
+ console.assert(!this.connected, 'Connection already established');
27
+ if (!this.socket) {
28
+ this.config = config;
29
+ const connectionString = this.config.connectionString;
30
+ const gatewayUrl = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.gatewayUrl) || `ws://${this.config.host}:4000`;
31
+ this.socket = (0, socket_io_client_1.io)(gatewayUrl, { auth: { token: connectionString } });
32
+ }
33
+ callback === null || callback === void 0 ? void 0 : callback.call(this, null);
34
+ }
35
+ catch (error) {
36
+ callback === null || callback === void 0 ? void 0 : callback.call(this, error);
37
+ }
38
+ return this;
39
+ }
40
+ /** Will send a command immediately (no queueing), return the rowset/result or throw an error */
41
+ processCommands(commands, callback) {
42
+ // connection needs to be established?
43
+ if (!this.socket) {
44
+ callback === null || callback === void 0 ? void 0 : callback.call(this, new types_1.SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' }));
45
+ return this;
46
+ }
47
+ this.socket.emit('v1/sql', { sql: commands, row: 'array' }, (response) => {
48
+ if (response === null || response === void 0 ? void 0 : response.error) {
49
+ const error = new types_1.SQLiteCloudError(response.error.detail, Object.assign({}, response.error));
50
+ callback === null || callback === void 0 ? void 0 : callback.call(this, error);
51
+ }
52
+ else {
53
+ const { data, metadata } = response;
54
+ if (data && metadata) {
55
+ if (metadata.numberOfRows !== undefined && metadata.numberOfColumns !== undefined && metadata.columns !== undefined) {
56
+ console.assert(Array.isArray(data), 'SQLiteCloudWebsocketConnection.processCommands - data is not an array');
57
+ // we can recreate a SQLiteCloudRowset from the response which we know to be an array of arrays
58
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
59
+ const rowset = new rowset_1.SQLiteCloudRowset(metadata, data.flat());
60
+ callback === null || callback === void 0 ? void 0 : callback.call(this, null, rowset);
61
+ return;
62
+ }
63
+ }
64
+ callback === null || callback === void 0 ? void 0 : callback.call(this, null, response === null || response === void 0 ? void 0 : response.data);
65
+ }
66
+ });
67
+ return this;
68
+ }
69
+ /** Disconnect socket.io from server */
70
+ close() {
71
+ var _a;
72
+ console.assert(this.socket !== null, 'WebsocketTransport.close - connection already closed');
73
+ if (this.socket) {
74
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.close();
75
+ this.socket = undefined;
76
+ }
77
+ this.socket = undefined;
78
+ return this;
79
+ }
80
+ }
81
+ exports.WebSocketTransport = WebSocketTransport;
package/lib/types.d.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * types.ts - shared types and interfaces
3
+ */
4
+ /// <reference types="node" />
5
+ /// <reference types="node" />
6
+ import tls from 'tls';
7
+ /** Configuration for SQLite cloud connection */
8
+ export interface SQLiteCloudConfig {
9
+ /** Connection string in the form of sqlitecloud://user:password@host:port/database?options */
10
+ connectionString?: string;
11
+ /** User name is required unless connectionString is provided */
12
+ username?: string;
13
+ /** Password is required unless connection string is provided */
14
+ password?: string;
15
+ /** True if password is hashed, default is false */
16
+ passwordHashed?: boolean;
17
+ /** Host name is required unless connectionString is provided */
18
+ host?: string;
19
+ /** Port number for tls socket */
20
+ port?: number;
21
+ /** Optional query timeout passed directly to TLS socket */
22
+ timeout?: number;
23
+ /** Name of database to open */
24
+ database?: string;
25
+ /** Create the database if it doesn't exist? */
26
+ createDatabase?: boolean;
27
+ /** Database will be created in memory */
28
+ dbMemory?: boolean;
29
+ /** Enable SQLite compatibility mode */
30
+ sqliteMode?: boolean;
31
+ compression?: boolean;
32
+ /** Request for immediate responses from the server node without waiting for linerizability guarantees */
33
+ nonlinearizable?: boolean;
34
+ /** Server should send BLOB columns */
35
+ noBlob?: boolean;
36
+ /** Do not send columns with more than max_data bytes */
37
+ maxData?: number;
38
+ /** Server should chunk responses with more than maxRows */
39
+ maxRows?: number;
40
+ /** Server should limit total number of rows in a set to maxRowset */
41
+ maxRowset?: number;
42
+ /** Custom options and configurations for tls socket, eg: additional certificates */
43
+ tlsOptions?: tls.ConnectionOptions;
44
+ /** True if we should force use of SQLite Cloud Gateway and websocket connections, default: true in browsers, false in node.js */
45
+ useWebsocket?: boolean;
46
+ /** Url where we can connect to a SQLite Cloud Gateway that has a socket.io deamon waiting to connect, eg. ws://host:4000 */
47
+ gatewayUrl?: string;
48
+ /** Optional identifier used for verbose logging */
49
+ clientId?: string;
50
+ /** True if connection should enable debug logs */
51
+ verbose?: boolean;
52
+ }
53
+ /** Metadata information for a set of rows resulting from a query */
54
+ export interface SQLCloudRowsetMetadata {
55
+ /** Rowset version 1 has column's name, version 2 has extended metadata */
56
+ version: number;
57
+ /** Number of rows */
58
+ numberOfRows: number;
59
+ /** Number of columns */
60
+ numberOfColumns: number;
61
+ /** Columns' metadata */
62
+ columns: {
63
+ /** Column name in query (may be altered from original name) */
64
+ name: string;
65
+ /** Declare column type */
66
+ type?: string;
67
+ /** Database name */
68
+ database?: string;
69
+ /** Database table */
70
+ table?: string;
71
+ /** Original name of the column */
72
+ column?: string;
73
+ }[];
74
+ }
75
+ /** Basic types that can be returned by SQLiteCloud APIs */
76
+ export type SQLiteCloudDataTypes = string | number | boolean | Record<string | number, unknown> | Buffer | null | undefined;
77
+ /** Custom error reported by SQLiteCloud drivers */
78
+ export declare class SQLiteCloudError extends Error {
79
+ constructor(message: string, args?: Partial<SQLiteCloudError>);
80
+ /** Upstream error that cause this error */
81
+ cause?: Error | string;
82
+ /** Error code returned by drivers or server */
83
+ errorCode?: string;
84
+ /** Additional error code */
85
+ externalErrorCode?: string;
86
+ /** Additional offset code in commands */
87
+ offsetCode?: number;
88
+ }
89
+ export type ErrorCallback = (error: Error | null) => void;
90
+ export type ResultsCallback<T = any> = (error: Error | null, results?: T) => void;
91
+ export type RowsCallback<T = Record<string, any>> = (error: Error | null, rows?: T[]) => void;
92
+ export type RowCallback<T = Record<string, any>> = (error: Error | null, row?: T) => void;
93
+ export type RowCountCallback = (error: Error | null, rowCount?: number) => void;
94
+ /**
95
+ * Certain responses include arrays with various types of metadata.
96
+ * The first entry is always an array type from this list. This enum
97
+ * is called SQCLOUD_ARRAY_TYPE in the C API.
98
+ */
99
+ export declare enum SQLiteCloudArrayType {
100
+ ARRAY_TYPE_SQLITE_EXEC = 10,
101
+ ARRAY_TYPE_DB_STATUS = 11,
102
+ ARRAY_TYPE_METADATA = 12,
103
+ ARRAY_TYPE_VM_STEP = 20,
104
+ ARRAY_TYPE_VM_COMPILE = 21,
105
+ ARRAY_TYPE_VM_STEP_ONE = 22,
106
+ ARRAY_TYPE_VM_SQL = 23,
107
+ ARRAY_TYPE_VM_STATUS = 24,
108
+ ARRAY_TYPE_VM_LIST = 25,
109
+ ARRAY_TYPE_BACKUP_INIT = 40,
110
+ ARRAY_TYPE_BACKUP_STEP = 41,
111
+ ARRAY_TYPE_BACKUP_END = 42,
112
+ ARRAY_TYPE_SQLITE_STATUS = 50
113
+ }