UNPKG

@sqlitecloud/drivers

Version:

SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients

329 lines (328 loc) 17 kB
"use strict"; // // utilities.ts - utility methods to manipulate SQL statements // Object.defineProperty(exports, "__esModule", { value: true }); exports.isNode = exports.isBrowser = void 0; exports.anonimizeCommand = anonimizeCommand; exports.anonimizeError = anonimizeError; exports.getInitializationCommands = getInitializationCommands; exports.sanitizeSQLiteIdentifier = sanitizeSQLiteIdentifier; exports.getUpdateResults = getUpdateResults; exports.popCallback = popCallback; exports.validateConfiguration = validateConfiguration; exports.parseconnectionstring = parseconnectionstring; exports.parseBoolean = parseBoolean; exports.parseBooleanToZeroOne = parseBooleanToZeroOne; exports.parseSafeIntegerMode = parseSafeIntegerMode; exports.parseWebsocketBlobTransferFormat = parseWebsocketBlobTransferFormat; exports.parseWebsocketMaxAttachments = parseWebsocketMaxAttachments; exports.encodeBigIntMarkers = encodeBigIntMarkers; exports.decodeBigIntMarkers = decodeBigIntMarkers; exports.decodeWebsocketRowsetData = decodeWebsocketRowsetData; const types_1 = require("./types"); const safe_imports_1 = require("./safe-imports"); const buffer_1 = require("buffer"); // explicitly importing these libraries to allow cross-platform support by replacing them // In React Native: Metro resolves 'whatwg-url' to 'react-native-url-polyfill' via package.json react-native field // In Web/Node: Uses standard whatwg-url package const URL = (0, safe_imports_1.getSafeURL)(); // // determining running environment, thanks to browser-or-node // https://www.npmjs.com/package/browser-or-node // exports.isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined'; exports.isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null; // // utility methods // /** Messages going to the server are sometimes logged when error conditions occour and need to be stripped of user credentials */ function anonimizeCommand(message) { // hide password in AUTH command if needed message = message.replace(/USER \S+/, 'USER ******'); message = message.replace(/PASSWORD \S+?(?=;)/, 'PASSWORD ******'); message = message.replace(/HASH \S+?(?=;)/, 'HASH ******'); return message; } /** Strip message code in error of user credentials */ function anonimizeError(error) { if (error === null || error === void 0 ? void 0 : error.message) { error.message = anonimizeCommand(error.message); } return error; } /** Initialization commands sent to database when connection is established */ function getInitializationCommands(config) { // we check the credentials using non linearizable so we're quicker // then we bring back linearizability unless specified otherwise let commands = 'SET CLIENT KEY NONLINEARIZABLE TO 1;'; // first user authentication, then all other commands if (config.apikey) { commands += `AUTH APIKEY ${config.apikey};`; } else if (config.token) { commands += `AUTH TOKEN ${config.token};`; } else { commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''};`; } if (config.compression) { commands += 'SET CLIENT KEY COMPRESSION TO 1;'; } if (config.zerotext) { commands += 'SET CLIENT KEY ZEROTEXT TO 1;'; } if (config.noblob) { commands += 'SET CLIENT KEY NOBLOB TO 1;'; } if (config.maxdata) { commands += `SET CLIENT KEY MAXDATA TO ${config.maxdata};`; } if (config.maxrows) { commands += `SET CLIENT KEY MAXROWS TO ${config.maxrows};`; } if (config.maxrowset) { commands += `SET CLIENT KEY MAXROWSET TO ${config.maxrowset};`; } // we ALWAYS set non linearizable to 1 when we start so we can be quicker on login // but then we need to put it back to its default value if "linearizable" unless set if (!config.non_linearizable) { commands += 'SET CLIENT KEY NONLINEARIZABLE TO 0;'; } if (config.database) { if (config.create && !config.memory) { commands += `CREATE DATABASE ${config.database} IF NOT EXISTS;`; } commands += `USE DATABASE ${config.database};`; } return commands; } /** Sanitizes an SQLite identifier (e.g., table name, column name). */ function sanitizeSQLiteIdentifier(identifier) { const trimmed = identifier.trim(); // it's not empty if (trimmed.length === 0) { throw new Error('Identifier cannot be empty.'); } // escape double quotes const escaped = trimmed.replace(/"/g, '""'); // Wrap in double quotes for safety return `"${escaped}"`; } /** Converts results of an update or insert call into a more meaning full result set */ function getUpdateResults(results) { if (results) { if (Array.isArray(results) && results.length > 0) { switch (results[0]) { case types_1.SQLiteCloudArrayType.ARRAY_TYPE_SQLITE_EXEC: return { type: Number(results[0]), index: Number(results[1]), lastID: results[2], // ROWID (sqlite3_last_insert_rowid) changes: results[3], // CHANGES(sqlite3_changes) totalChanges: results[4], // TOTAL_CHANGES (sqlite3_total_changes) finalized: Number(results[5]), // FINALIZED rowId: results[2] // same as lastId }; } } } return undefined; } /** * Many of the methods in our API may contain a callback as their last argument. * This method will take the arguments array passed to the method and return an object * containing the arguments array with the callbacks removed (if any), and the callback itself. * If there are multiple callbacks, the first one is returned as 'callback' and the last one * as 'completeCallback'. * * @returns args is a simple list of SQLiteCloudDataTypes, we flat them into a single array */ function popCallback(args) { const remaining = args; // at least 1 callback? if (args && args.length > 0 && typeof args[args.length - 1] === 'function') { // at least 2 callbacks? if (args.length > 1 && typeof args[args.length - 2] === 'function') { return { args: remaining.slice(0, -2).flat(), callback: args[args.length - 2], complete: args[args.length - 1] }; } return { args: remaining.slice(0, -1).flat(), callback: args[args.length - 1] }; } return { args: remaining.flat() }; } // // configuration validation // /** Validate configuration, apply defaults, throw if something is missing or misconfigured */ function validateConfiguration(config) { console.assert(config, 'SQLiteCloudConnection.validateConfiguration - missing config'); if (config.connectionstring) { const configOverrides = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined)); const connectionStringConfig = parseconnectionstring(config.connectionstring); config = Object.assign(Object.assign(Object.assign({}, connectionStringConfig), configOverrides), { connectionstring: config.connectionstring // keep original connection string }); } // apply defaults where needed config.port || (config.port = types_1.DEFAULT_PORT); config.timeout = config.timeout && config.timeout > 0 ? config.timeout : types_1.DEFAULT_TIMEOUT; config.clientid || (config.clientid = 'SQLiteCloud'); config.verbose = parseBoolean(config.verbose); config.noblob = parseBoolean(config.noblob); config.compression = config.compression != undefined && config.compression != null ? parseBoolean(config.compression) : true; // default: true config.safe_integer_mode = parseSafeIntegerMode(config.safe_integer_mode || types_1.SAFE_INTEGER_MODE); config.websocketBlobFormat = parseWebsocketBlobTransferFormat(config.websocketBlobFormat); config.websocketMaxAttachments = parseWebsocketMaxAttachments(config.websocketMaxAttachments); config.create = parseBoolean(config.create); config.non_linearizable = parseBoolean(config.non_linearizable); config.insecure = parseBoolean(config.insecure); const hasCredentials = (config.username && config.password) || config.apikey || config.token; if (!config.host || !hasCredentials) { console.error('SQLiteCloudConnection.validateConfiguration - missing arguments', config); throw new types_1.SQLiteCloudError('The user, password and host arguments, the ?apikey= or the ?token= must be specified.', { errorCode: 'ERR_MISSING_ARGS' }); } if (!config.connectionstring) { // build connection string from configuration, values are already validated config.connectionstring = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}`; if (config.apikey) { config.connectionstring += `?apikey=${config.apikey}`; } else if (config.token) { config.connectionstring += `?token=${config.token}`; } else { config.connectionstring = `sqlitecloud://${encodeURIComponent(config.username || '')}:${encodeURIComponent(config.password || '')}@${config.host}:${config.port}/${config.database}`; } } return config; } /** * Parse connectionstring like sqlitecloud://username:password@host:port/database?option1=xxx&option2=xxx * or sqlitecloud://host.sqlite.cloud:8860/chinook.sqlite?apikey=mIiLARzKm9XBVllbAzkB1wqrgijJ3Gx0X5z1Agm3xBo * into its basic components. */ function parseconnectionstring(connectionstring) { try { // The URL constructor throws a TypeError if the URL is not valid. // in spite of having the same structure as a regular url // protocol://username:password@host:port/database?option1=xxx&option2=xxx) // the sqlitecloud: protocol is not recognized by the URL constructor in browsers // so we need to replace it with https: to make it work const knownProtocolUrl = connectionstring.replace('sqlitecloud:', 'https:'); const url = new URL(knownProtocolUrl); // all lowecase options const options = {}; url.searchParams.forEach((value, key) => { options[key.toLowerCase().replace(/-/g, '_')] = value.trim(); }); const config = Object.assign(Object.assign({}, options), { username: url.username ? decodeURIComponent(url.username) : undefined, password: url.password ? decodeURIComponent(url.password) : undefined, password_hashed: options.password_hashed ? parseBoolean(options.password_hashed) : undefined, host: url.hostname, // type cast values port: url.port ? parseInt(url.port) : undefined, insecure: options.insecure ? parseBoolean(options.insecure) : undefined, timeout: options.timeout ? parseInt(options.timeout) : undefined, zerotext: options.zerotext ? parseBoolean(options.zerotext) : undefined, create: options.create ? parseBoolean(options.create) : undefined, memory: options.memory ? parseBoolean(options.memory) : undefined, compression: options.compression ? parseBoolean(options.compression) : undefined, non_linearizable: options.non_linearizable ? parseBoolean(options.non_linearizable) : undefined, noblob: options.noblob ? parseBoolean(options.noblob) : undefined, maxdata: options.maxdata ? parseInt(options.maxdata) : undefined, maxrows: options.maxrows ? parseInt(options.maxrows) : undefined, maxrowset: options.maxrowset ? parseInt(options.maxrowset) : undefined, safe_integer_mode: options.safe_integer_mode ? parseSafeIntegerMode(options.safe_integer_mode) : undefined, usewebsocket: options.usewebsocket ? parseBoolean(options.usewebsocket) : undefined, websocketBlobFormat: options.websocket_blob_format ? parseWebsocketBlobTransferFormat(options.websocket_blob_format, undefined) : undefined, websocketMaxAttachments: options.websocket_max_attachments ? parseWebsocketMaxAttachments(options.websocket_max_attachments) : undefined, verbose: options.verbose ? parseBoolean(options.verbose) : undefined }); // either you use an apikey, token or username and password if (Number(!!config.apikey) + Number(!!config.token) + Number(!!(config.username || config.password)) > 1) { console.error('SQLiteCloudConnection.parseconnectionstring - choose between apikey, token or username/password'); throw new types_1.SQLiteCloudError('Choose between apikey, token or username/password'); } const database = url.pathname.replace('/', ''); // pathname is database name, remove the leading slash if (database) { config.database = database; } return config; } catch (error) { throw new types_1.SQLiteCloudError(`Invalid connection string: ${connectionstring} - error: ${error}`); } } /** Returns true if value is 1 or true */ function parseBoolean(value) { if (typeof value === 'string') { return value.toLowerCase() === 'true' || value === '1'; } return value ? true : false; } /** Returns true if value is 1 or true */ function parseBooleanToZeroOne(value) { if (typeof value === 'string') { return value.toLowerCase() === 'true' || value === '1' ? 1 : 0; } return value ? 1 : 0; } /** Parse 64-bit integer handling mode */ function parseSafeIntegerMode(value) { const mode = value === null || value === void 0 ? void 0 : value.toLowerCase(); if (mode === 'number' || mode === 'bigint' || mode === 'mixed') { return mode; } return 'number'; } /** Parse websocket BLOB transport format, falling back to the driver default for new websocket clients. */ function parseWebsocketBlobTransferFormat(value, fallback = types_1.DEFAULT_WEBSOCKET_BLOB_TRANSFER_FORMAT) { const format = value === null || value === void 0 ? void 0 : value.toLowerCase(); if (format === 'base64-blobs-v1' || format === 'socketio-blobs-v1') { return format; } return fallback; } /** Parse the maximum number of socket.io binary attachments allowed for a websocket response. */ function parseWebsocketMaxAttachments(value) { const parsed = typeof value === 'string' ? Number.parseInt(value, 10) : value; if (Number.isSafeInteger(parsed) && parsed > 0) { return parsed; } return types_1.DEFAULT_WEBSOCKET_MAX_ATTACHMENTS; } const BIGINT_MARKER_RE = /^-?\d+n$/; const BLOB_COLUMN_TYPE_RE = /\bblob\b/i; /** Convert values that JSON cannot represent losslessly into sqlitecloud-js bigint markers. */ function encodeBigIntMarkers(value) { if (typeof value === 'bigint') { return `${value.toString()}n`; } if (Array.isArray(value)) { return value.map(item => encodeBigIntMarkers(item)); } if (value && typeof value === 'object' && !buffer_1.Buffer.isBuffer(value)) { const result = {}; Object.entries(value).forEach(([key, item]) => { result[key] = encodeBigIntMarkers(item); }); return result; } return value; } /** Convert sqlitecloud-js bigint markers back into BigInt values for lossless integer modes. */ function decodeBigIntMarkers(value, safeIntegerMode) { if (safeIntegerMode !== 'bigint' && safeIntegerMode !== 'mixed') { return value; } if (typeof value === 'string' && BIGINT_MARKER_RE.test(value)) { return BigInt(value.slice(0, -1)); } if (Array.isArray(value)) { return value.map(item => decodeBigIntMarkers(item, safeIntegerMode)); } if (value && typeof value === 'object' && !buffer_1.Buffer.isBuffer(value)) { const result = {}; Object.entries(value).forEach(([key, item]) => { result[key] = decodeBigIntMarkers(item, safeIntegerMode); }); return result; } return value; } /** Decode websocket rowset cells using metadata-aware rules for bigint markers and negotiated BLOB transport. */ function decodeWebsocketRowsetData(data, metadata, safeIntegerMode, blobTransferFormat) { if (!Array.isArray(data)) { return decodeBigIntMarkers(data, safeIntegerMode); } const blobColumnIndexes = new Set(metadata.columns.flatMap((column, index) => (column.type && BLOB_COLUMN_TYPE_RE.test(column.type) ? [index] : []))); const decodeCell = (value, columnIndex) => { if (blobTransferFormat === 'base64-blobs-v1' && blobColumnIndexes.has(columnIndex) && typeof value === 'string') { return buffer_1.Buffer.from(value, 'base64'); } return decodeBigIntMarkers(value, safeIntegerMode); }; if (data.every(row => !Array.isArray(row))) { return data.map((value, index) => decodeCell(value, index % metadata.numberOfColumns)); } return data.map(row => (Array.isArray(row) ? row.map((value, columnIndex) => decodeCell(value, columnIndex)) : decodeBigIntMarkers(row, safeIntegerMode))); }