UNPKG

@sqlitecloud/drivers

Version:

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

232 lines (231 loc) 11.6 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; const types_1 = require("./types"); const types_2 = require("./types"); // explicitly importing these libraries to allow cross-platform support by replacing them const whatwg_url_1 = require("whatwg-url"); // // 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 { 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_2.SQLiteCloudArrayType.ARRAY_TYPE_SQLITE_EXEC: return { type: results[0], index: 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: 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) { config = Object.assign(Object.assign(Object.assign({}, config), parseconnectionstring(config.connectionstring)), { 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.create = parseBoolean(config.create); config.non_linearizable = parseBoolean(config.non_linearizable); config.insecure = parseBoolean(config.insecure); const hasCredentials = (config.username && config.password) || config.apikey; if (!config.host || !hasCredentials) { console.error('SQLiteCloudConnection.validateConfiguration - missing arguments', config); throw new types_1.SQLiteCloudError('The user, password and host arguments or the ?apikey= must be specified.', { errorCode: 'ERR_MISSING_ARGS' }); } if (!config.connectionstring) { // build connection string from configuration, values are already validated if (config.apikey) { config.connectionstring = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}?apikey=${config.apikey}`; } 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 whatwg_url_1.URL(knownProtocolUrl); // all lowecase options const options = {}; url.searchParams.forEach((value, key) => { options[key.toLowerCase().replace(/-/g, '_')] = value; }); const config = Object.assign(Object.assign({}, options), { username: decodeURIComponent(url.username), password: decodeURIComponent(url.password), 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, usewebsocket: options.usewebsocket ? parseBoolean(options.usewebsocket) : undefined, verbose: options.verbose ? parseBoolean(options.verbose) : undefined }); // either you use an apikey or username and password if (config.apikey) { if (config.username || config.password) { console.warn('SQLiteCloudConnection.parseconnectionstring - apikey and username/password are both specified, using apikey'); } delete config.username; delete config.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}`); } } /** 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; }