diff --git a/.env.example b/.env.example index c610659..0f7aa32 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ +# support to 64bit integers: number, bigint or mixed +SAFE_INTEGER_MODE=number + # chinook database used in non-destructive tests CHINOOK_DATABASE_URL="sqlitecloud://user:password@xxx.sqlite.cloud:8860/chinook.sqlite" diff --git a/package-lock.json b/package-lock.json index b1e7e3d..5271e4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sqlitecloud/drivers", - "version": "1.0.438", + "version": "1.0.505", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@sqlitecloud/drivers", - "version": "1.0.438", + "version": "1.0.505", "license": "MIT", "dependencies": { "buffer": "^6.0.3", diff --git a/package.json b/package.json index df49d59..061fc63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sqlitecloud/drivers", - "version": "1.0.438", + "version": "1.0.507", "description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/src/drivers/connection-tls.ts b/src/drivers/connection-tls.ts index ed872e7..e0ecc40 100644 --- a/src/drivers/connection-tls.ts +++ b/src/drivers/connection-tls.ts @@ -141,11 +141,11 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection { this.socket = undefined }, timeoutMs) - this.socket?.write(formattedCommands, 'utf-8', () => { + this.socket?.write(formattedCommands, () => { clearTimeout(timeout) // Clear the timeout on successful write }) } else { - this.socket?.write(formattedCommands, 'utf-8') + this.socket?.write(formattedCommands) } return this diff --git a/src/drivers/connection.ts b/src/drivers/connection.ts index 1b23699..17d6368 100644 --- a/src/drivers/connection.ts +++ b/src/drivers/connection.ts @@ -2,7 +2,7 @@ * connection.ts - base abstract class for sqlitecloud server connections */ -import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand } from './types' +import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand, SQLiteCloudDataTypes } from './types' import { validateConfiguration } from './utilities' import { OperationsQueue } from './queue' import { anonimizeCommand, getUpdateResults } from './utilities' diff --git a/src/drivers/database.ts b/src/drivers/database.ts index 80aa1f5..09cde31 100644 --- a/src/drivers/database.ts +++ b/src/drivers/database.ts @@ -22,6 +22,7 @@ import { SQLiteCloudArrayType, SQLiteCloudCommand, SQLiteCloudConfig, + SQLiteCloudDataTypes, SQLiteCloudError } from './types' import { isBrowser, popCallback } from './utilities' diff --git a/src/drivers/protocol.ts b/src/drivers/protocol.ts index 5d139b7..a021cb3 100644 --- a/src/drivers/protocol.ts +++ b/src/drivers/protocol.ts @@ -2,8 +2,8 @@ // protocol.ts - low level protocol handling for SQLiteCloud transport // -import { SQLiteCloudCommand, SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types' import { SQLiteCloudRowset } from './rowset' +import { SAFE_INTEGER_MODE, SQLiteCloudCommand, SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types' // explicitly importing buffer library to allow cross-platform support by replacing it import { Buffer } from 'buffer' @@ -302,7 +302,17 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl // console.debug(`popData - dataType: ${dataType}, spaceIndex: ${spaceIndex}, commandLength: ${commandLength}, commandEnd: ${commandEnd}`) switch (dataType) { case CMD_INT: - return popResults(parseInt(buffer.subarray(1, spaceIndex).toString())) + // SQLite uses 64-bit INTEGER, but JS uses 53-bit Number + const value = BigInt(buffer.subarray(1, spaceIndex).toString()) + if (SAFE_INTEGER_MODE === 'bigint') { + return popResults(value) + } + if (SAFE_INTEGER_MODE === 'mixed') { + if (value <= BigInt(Number.MIN_SAFE_INTEGER) || BigInt(Number.MAX_SAFE_INTEGER) <= value) { + return popResults(value) + } + } + return popResults(Number(value)) case CMD_FLOAT: return popResults(parseFloat(buffer.subarray(1, spaceIndex).toString())) case CMD_NULL: @@ -334,7 +344,7 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl } /** Format a command to be sent via SCSP protocol */ -export function formatCommand(command: SQLiteCloudCommand): string { +export function formatCommand(command: SQLiteCloudCommand): Buffer { // core returns null if there's a space after the semi column // we want to maintain a compatibility with the standard sqlite3 driver command.query = command.query.trim() @@ -346,22 +356,23 @@ export function formatCommand(command: SQLiteCloudCommand): string { return serializeData(command.query, false) } -function serializeCommand(data: SQLiteCloudDataTypes[], zeroString: boolean = false): string { +function serializeCommand(data: SQLiteCloudDataTypes[], zeroString: boolean = false): Buffer { const n = data.length - let serializedData = `${n} ` + let serializedData = Buffer.from(`${n} `) for (let i = 0; i < n; i++) { // the first string is the sql and it must be zero-terminated const zs = i == 0 || zeroString - serializedData += serializeData(data[i], zs) + serializedData = Buffer.concat([serializedData, serializeData(data[i], zs)]) } - const bytesTotal = Buffer.byteLength(serializedData, 'utf-8') - const header = `${CMD_ARRAY}${bytesTotal} ` - return header + serializedData + const bytesTotal = serializedData.byteLength + const header = Buffer.from(`${CMD_ARRAY}${bytesTotal} `) + + return Buffer.concat([header, serializedData]) } -function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): string { +function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): Buffer { if (typeof data === 'string') { let cmd = CMD_STRING if (zeroString) { @@ -370,24 +381,28 @@ function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): } const header = `${cmd}${Buffer.byteLength(data, 'utf-8')} ` - return header + data + return Buffer.from(header + data) } if (typeof data === 'number') { if (Number.isInteger(data)) { - return `${CMD_INT}${data} ` + return Buffer.from(`${CMD_INT}${data} `) } else { - return `${CMD_FLOAT}${data} ` + return Buffer.from(`${CMD_FLOAT}${data} `) } } + if (typeof data === 'bigint') { + return Buffer.from(`${CMD_INT}${data} `) + } + if (Buffer.isBuffer(data)) { - const header = `${CMD_BLOB}${data.length} ` - return header + data.toString('utf-8') + const header = `${CMD_BLOB}${data.byteLength} ` + return Buffer.concat([Buffer.from(header), data]) } if (data === null || data === undefined) { - return `${CMD_NULL} ` + return Buffer.from(`${CMD_NULL} `) } if (Array.isArray(data)) { diff --git a/src/drivers/types.ts b/src/drivers/types.ts index 6a1c7a3..8e9f9e1 100644 --- a/src/drivers/types.ts +++ b/src/drivers/types.ts @@ -10,6 +10,26 @@ export const DEFAULT_TIMEOUT = 300 * 1000 /** Default tls connection port */ export const DEFAULT_PORT = 8860 +/** + * Support to SQLite 64bit integer + * + * number - (default) always return Number type (max: 2^53 - 1) + * Precision is lost when selecting greater numbers from SQLite + * bigint - always return BigInt type (max: 2^63 - 1) for all numbers from SQLite + * (inlcuding `lastID` from WRITE statements) + * mixed - use BigInt and Number types depending on the value size + */ +export let SAFE_INTEGER_MODE = 'number' +if (typeof process !== 'undefined') { + SAFE_INTEGER_MODE = process.env['SAFE_INTEGER_MODE']?.toLowerCase() || 'number' +} +if (SAFE_INTEGER_MODE == 'bigint') { + console.debug('BigInt mode: Using Number for all INTEGER values from SQLite, including meta information from WRITE statements.') +} +if (SAFE_INTEGER_MODE == 'mixed') { + console.debug('Mixed mode: Using BigInt for INTEGER values from SQLite (including meta information from WRITE statements) bigger then 2^53, Number otherwise.') +} + /** * Configuration for SQLite cloud connection * @note Options are all lowecase so they 1:1 compatible with C SDK @@ -26,6 +46,8 @@ export interface SQLiteCloudConfig { password_hashed?: boolean /** API key can be provided instead of username and password */ apikey?: string + /** Access Token provided in place of API Key or username/password */ + token?: string /** Host name is required unless connectionstring is provided, eg: xxx.sqlitecloud.io */ host?: string diff --git a/src/drivers/utilities.ts b/src/drivers/utilities.ts index b7075ab..5573adf 100644 --- a/src/drivers/utilities.ts +++ b/src/drivers/utilities.ts @@ -2,12 +2,10 @@ // utilities.ts - utility methods to manipulate SQL statements // -import { SQLiteCloudConfig, SQLiteCloudError, SQLiteCloudDataTypes, DEFAULT_PORT, DEFAULT_TIMEOUT } from './types' -import { SQLiteCloudArrayType } from './types' +import { DEFAULT_PORT, DEFAULT_TIMEOUT, SQLiteCloudArrayType, SQLiteCloudConfig, SQLiteCloudDataTypes, SQLiteCloudError } from './types' // explicitly importing these libraries to allow cross-platform support by replacing them import { URL } from 'whatwg-url' -import { Buffer } from 'buffer' // // determining running environment, thanks to browser-or-node @@ -42,45 +40,47 @@ export function anonimizeError(error: Error): Error { export function getInitializationCommands(config: SQLiteCloudConfig): string { // 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; ' + let commands = 'SET CLIENT KEY NONLINEARIZABLE TO 1;' // first user authentication, then all other commands if (config.apikey) { - commands += `AUTH APIKEY ${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 || ''}; ` + commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''};` } if (config.compression) { - commands += 'SET CLIENT KEY COMPRESSION TO 1; ' + commands += 'SET CLIENT KEY COMPRESSION TO 1;' } if (config.zerotext) { - commands += 'SET CLIENT KEY ZEROTEXT TO 1; ' + commands += 'SET CLIENT KEY ZEROTEXT TO 1;' } if (config.noblob) { - commands += 'SET CLIENT KEY NOBLOB TO 1; ' + commands += 'SET CLIENT KEY NOBLOB TO 1;' } if (config.maxdata) { - commands += `SET CLIENT KEY MAXDATA TO ${config.maxdata}; ` + commands += `SET CLIENT KEY MAXDATA TO ${config.maxdata};` } if (config.maxrows) { - commands += `SET CLIENT KEY MAXROWS TO ${config.maxrows}; ` + commands += `SET CLIENT KEY MAXROWS TO ${config.maxrows};` } if (config.maxrowset) { - commands += `SET CLIENT KEY MAXROWSET TO ${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; ' + commands += 'SET CLIENT KEY NONLINEARIZABLE TO 0;' } if (config.database) { if (config.create && !config.memory) { - commands += `CREATE DATABASE ${config.database} IF NOT EXISTS; ` + commands += `CREATE DATABASE ${config.database} IF NOT EXISTS;` } - commands += `USE DATABASE ${config.database}; ` + commands += `USE DATABASE ${config.database};` } return commands @@ -109,13 +109,12 @@ export function getUpdateResults(results?: any): Record | undefined switch (results[0]) { case SQLiteCloudArrayType.ARRAY_TYPE_SQLITE_EXEC: return { - type: results[0], - index: results[1], + 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: results[5], // FINALIZED - // + finalized: Number(results[5]), // FINALIZED rowId: results[2] // same as lastId } } @@ -177,16 +176,19 @@ export function validateConfiguration(config: SQLiteCloudConfig): SQLiteCloudCon config.non_linearizable = parseBoolean(config.non_linearizable) config.insecure = parseBoolean(config.insecure) - const hasCredentials = (config.username && config.password) || config.apikey + const hasCredentials = (config.username && config.password) || config.apikey || config.token if (!config.host || !hasCredentials) { console.error('SQLiteCloudConnection.validateConfiguration - missing arguments', config) - throw new SQLiteCloudError('The user, password and host arguments or the ?apikey= must be specified.', { errorCode: 'ERR_MISSING_ARGS' }) + throw new 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 = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}?apikey=${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 @@ -215,13 +217,13 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf // all lowecase options const options: { [key: string]: string } = {} url.searchParams.forEach((value, key) => { - options[key.toLowerCase().replace(/-/g, '_')] = value + options[key.toLowerCase().replace(/-/g, '_')] = value.trim() }) const config: SQLiteCloudConfig = { ...options, - username: decodeURIComponent(url.username), - password: decodeURIComponent(url.password), + 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 @@ -241,13 +243,10 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf 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 + // 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 SQLiteCloudError('Choose between apikey, token or username/password') } const database = url.pathname.replace('/', '') // pathname is database name, remove the leading slash @@ -257,7 +256,7 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf return config } catch (error) { - throw new SQLiteCloudError(`Invalid connection string: ${connectionstring}`) + throw new SQLiteCloudError(`Invalid connection string: ${connectionstring} - error: ${error}`) } } diff --git a/test/compare.test.ts b/test/compare.test.ts index 9656440..1ea1512 100644 --- a/test/compare.test.ts +++ b/test/compare.test.ts @@ -2,12 +2,11 @@ * compare.test.ts - test driver api against sqlite3 equivalents */ -import { CHINOOK_DATABASE_FILE, CHINOOK_FIRST_TRACK, LONG_TIMEOUT, removeDatabase, TESTING_SQL } from './shared' -import { getChinookDatabase, getTestingDatabase } from './shared' +import { CHINOOK_DATABASE_FILE, CHINOOK_FIRST_TRACK, getChinookDatabase, getTestingDatabase, LONG_TIMEOUT, removeDatabase, TESTING_SQL } from './shared' // https://github.com/TryGhost/node-sqlite3/wiki/API -import sqlite3 from 'sqlite3' import { join } from 'path' +import sqlite3 from 'sqlite3' const INSERT_SQL = "INSERT INTO people (name, hobby, age) VALUES ('Fantozzi Ugo', 'Competitive unicorn farting', 42); " const TESTING_DATABASE_FILE = join(__dirname, 'assets/testing.db') @@ -252,6 +251,29 @@ describe('Database.get', () => { }) }) + it('should handle big integer (>= 2^53) as Number by default', done => { + const chinookCloud = getChinookDatabase() + const chinookFile = getChinookDatabaseFile() + + const sql = 'SELECT 5876026397369593117 as bignumber;' + chinookCloud.get(sql, (cloudError, cloudRow) => { + expect(cloudError).toBeFalsy() + expect(typeof (cloudRow as any)['bignumber']).toBe('number') + expect((cloudRow as any)['bignumber']).toBe(5876026397369593000) + + chinookFile.get(sql, (fileError, fileRow) => { + expect(fileError).toBeNull() + // node-sqlite3 does not support bigint: https://github.com/TryGhost/node-sqlite3/issues/922 + expect(typeof (fileRow as any)['bignumber']).toBe('number') + expect((fileRow as any)['bignumber']).toBe(5876026397369593000) + + chinookCloud.close() + chinookFile.close() + done() + }) + }) + }) + // end get }) diff --git a/test/connection-tls.test.ts b/test/connection-tls.test.ts index 8425004..c72cc34 100644 --- a/test/connection-tls.test.ts +++ b/test/connection-tls.test.ts @@ -53,19 +53,19 @@ describe('connect', () => { }) */ -it( - 'should connect with config object string', - done => { - const configObj = getChinookConfig() - const connection = new SQLiteCloudTlsConnection(configObj, error => { - expect(error).toBeNull() - expect(connection.connected).toBe(true) - connection.close() - done() - }) - }, - LONG_TIMEOUT -) + it( + 'should connect with config object string', + done => { + const configObj = getChinookConfig() + const connection = new SQLiteCloudTlsConnection(configObj, error => { + expect(error).toBeNull() + expect(connection.connected).toBe(true) + connection.close() + done() + }) + }, + LONG_TIMEOUT + ) it( 'should connect with config object string and test command', @@ -174,7 +174,7 @@ it( expect(error).toBeDefined() expect(error).toBeInstanceOf(SQLiteCloudError) const sqliteCloudError = error as SQLiteCloudError - expect(sqliteCloudError.message).toBe('The user, password and host arguments or the ?apikey= must be specified.') + expect(sqliteCloudError.message).toBe('The user, password and host arguments, the ?apikey= or the ?token= must be specified.') expect(sqliteCloudError.errorCode).toBe('ERR_MISSING_ARGS') expect(sqliteCloudError.externalErrorCode).toBeUndefined() expect(sqliteCloudError.offsetCode).toBeUndefined() @@ -324,6 +324,23 @@ describe('send test commands', () => { LONG_TIMEOUT ) + it('should use Number as default behavior for integer INT64', done => { + const chinook = getConnection() + chinook.sendCommands('TEST INT64', (error, results) => { + let err = null + try { + expect(error).toBeNull() + expect(typeof results).toBe('number') + expect(results).toBe(9223372036854776000) + } catch (error) { + err = error + } finally { + chinook.close() + err ? done(err) : done() + } + }) + }) + it('should test null', done => { const connection = getConnection() connection.sendCommands('TEST NULL', (error, results) => { diff --git a/test/database.test.ts b/test/database.test.ts index ace2400..b1e092d 100644 --- a/test/database.test.ts +++ b/test/database.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from '@jest/globals' import { RowCountCallback } from '../src/drivers/types' import { Database, SQLiteCloudError, SQLiteCloudRow, SQLiteCloudRowset, sanitizeSQLiteIdentifier } from '../src/index' import { LONG_TIMEOUT, getChinookDatabase, getTestingDatabase, getTestingDatabaseAsync, removeDatabase, removeDatabaseAsync } from './shared' +const crypto = require('crypto') // // utility methods to setup and destroy temporary test databases @@ -266,7 +267,7 @@ describe('Database.get', () => { }) }) - // call close() right after the execution + // call close() right after the execution // of the query not in its callback chinook.close(error => { expect(error).toBeNull() @@ -604,6 +605,45 @@ describe('Database.sql (async)', () => { await removeDatabaseAsync(database) } }) + + it('binding should work with blob', async () => { + let database + try { + database = await getTestingDatabaseAsync() + const hash = crypto.createHash('sha256').update('my blob data').digest() + + await database.sql('CREATE TABLE IF NOT EXISTS blobs (id INTEGER PRIMARY KEY, hash BLOB NOT NULL, myindex INTEGER NOT NULL);') + let results = await database.sql('INSERT INTO blobs (hash, myindex) VALUES (?, ?);', hash, 1) + expect(results.changes).toEqual(1) + + results = await database.sql('SELECT * FROM blobs WHERE id = ? and hash = ?;', results.lastID, hash) + + expect(results).toHaveLength(1) + expect(results[0].hash).toEqual(hash) + expect(results[0].myindex).toEqual(1) + } finally { + await removeDatabaseAsync(database) + } + }) + + it('should insert bigInt value as a full 64 bit integer', async () => { + let database + try { + database = await getTestingDatabaseAsync() + const bigIntValue = BigInt(2 ** 63) - BigInt(1) // 9223372036854775807 + + await database.sql('CREATE TABLE IF NOT EXISTS bigints (id INTEGER PRIMARY KEY, value BIGINT NOT NULL);') + const meta = await database.sql('INSERT INTO bigints (value) VALUES (?);', bigIntValue) + + // by default, BigInt values are returned as Number loosing precision + // retrieving it as text preserves the value + const results = await database.sql('SELECT CAST(value AS TEXT) as bigIntAsString FROM bigints WHERE id = ?;', meta.lastID) + expect(results).toHaveLength(1) + expect(results[0].bigIntAsString).toEqual(bigIntValue.toString()) + } finally { + await removeDatabaseAsync(database) + } + }) }) it('should be connected', async () => { diff --git a/test/protocol.test.ts b/test/protocol.test.ts index cf80f2b..0e9f3fd 100644 --- a/test/protocol.test.ts +++ b/test/protocol.test.ts @@ -36,13 +36,13 @@ const testCases = [ { query: 'SELECT ?, ?, ?, ?, ?', parameters: ['world', 123, 3.14, null, Buffer.from('hello')], - expected: '=57 6 !21 SELECT ?, ?, ?, ?, ?\x00!6 world\x00:123 ,3.14 _ $5 hello', + expected: '=57 6 !21 SELECT ?, ?, ?, ?, ?\x00!6 world\x00:123 ,3.14 _ $5 hello' }, { query: 'SELECT ?', parameters: ["'hello world'"], - expected: "=32 2 !9 SELECT ?\x00!14 'hello world'\x00", - }, + expected: "=32 2 !9 SELECT ?\x00!14 'hello world'\x00" + } ] describe('Format command', () => { @@ -50,7 +50,7 @@ describe('Format command', () => { it(`should serialize ${JSON.stringify([query, ...parameters])}`, () => { const command: SQLiteCloudCommand = { query, parameters } const serialized = formatCommand(command) - expect(serialized).toEqual(expected) + expect(serialized).toEqual(Buffer.from(expected)) }) }) }) diff --git a/test/utilities.test.ts b/test/utilities.test.ts index 423b3ca..f3e26b5 100644 --- a/test/utilities.test.ts +++ b/test/utilities.test.ts @@ -3,7 +3,7 @@ // import { SQLiteCloudError } from '../src/index' -import { parseconnectionstring, sanitizeSQLiteIdentifier } from '../src/drivers/utilities' +import { getInitializationCommands, parseconnectionstring, sanitizeSQLiteIdentifier } from '../src/drivers/utilities' import { getTestingDatabaseName } from './shared' import { expect, describe, it } from '@jest/globals' @@ -162,6 +162,21 @@ describe('parseconnectionstring', () => { expect(config.timeout).toBe(123) }) + + it('expect error when both user/pass and api key are set', () => { + const connectionstring = 'sqlitecloud://user:password@host:1234/database?apikey=yyy' + expect(() => parseconnectionstring(connectionstring)).toThrowError('Choose between apikey, token or username/password') + }) + + it('expect error when both user/pass and token are set', () => { + const connectionstring = 'sqlitecloud://user:password@host:1234/database?token=yyy' + expect(() => parseconnectionstring(connectionstring)).toThrowError('Choose between apikey, token or username/password') + }) + + it('expect error when both apikey and token are set', () => { + const connectionstring = 'sqlitecloud://host:1234/database?apikey=xxx&token=yyy' + expect(() => parseconnectionstring(connectionstring)).toThrowError('Choose between apikey, token or username/password') + }) }) describe('getTestingDatabaseName', () => { @@ -190,3 +205,17 @@ describe('sanitizeSQLiteIdentifier()', () => { expect(sanitized).toBe('"chinook.sql; DROP TABLE \"\"albums\"\""') }) }) + +describe('getInitializationCommands()', () => { + it('should return commands with auth token command', () => { + const config = { + token: 'mytoken', + database: 'mydb', + } + + const result = getInitializationCommands(config) + + expect(result).toContain('AUTH TOKEN mytoken;') + expect(result).not.toContain('AUTH APIKEY') + }) +}) \ No newline at end of file