Skip to content

Commit 54264d0

Browse files
Merge pull request #236 from sqlitecloud/164-support-to-access-token
support to access token
2 parents b449bd0 + 2aa4f7c commit 54264d0

14 files changed

+226
-78
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11

2+
# support to 64bit integers: number, bigint or mixed
3+
SAFE_INTEGER_MODE=number
4+
25
# chinook database used in non-destructive tests
36
CHINOOK_DATABASE_URL="sqlitecloud://user:password@xxx.sqlite.cloud:8860/chinook.sqlite"
47

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sqlitecloud/drivers",
3-
"version": "1.0.438",
3+
"version": "1.0.507",
44
"description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients",
55
"main": "./lib/index.js",
66
"types": "./lib/index.d.ts",

src/drivers/connection-tls.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,11 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
141141
this.socket = undefined
142142
}, timeoutMs)
143143

144-
this.socket?.write(formattedCommands, 'utf-8', () => {
144+
this.socket?.write(formattedCommands, () => {
145145
clearTimeout(timeout) // Clear the timeout on successful write
146146
})
147147
} else {
148-
this.socket?.write(formattedCommands, 'utf-8')
148+
this.socket?.write(formattedCommands)
149149
}
150150

151151
return this

src/drivers/connection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* connection.ts - base abstract class for sqlitecloud server connections
33
*/
44

5-
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand } from './types'
5+
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand, SQLiteCloudDataTypes } from './types'
66
import { validateConfiguration } from './utilities'
77
import { OperationsQueue } from './queue'
88
import { anonimizeCommand, getUpdateResults } from './utilities'

src/drivers/database.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
SQLiteCloudArrayType,
2323
SQLiteCloudCommand,
2424
SQLiteCloudConfig,
25+
SQLiteCloudDataTypes,
2526
SQLiteCloudError
2627
} from './types'
2728
import { isBrowser, popCallback } from './utilities'

src/drivers/protocol.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// protocol.ts - low level protocol handling for SQLiteCloud transport
33
//
44

5-
import { SQLiteCloudCommand, SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types'
65
import { SQLiteCloudRowset } from './rowset'
6+
import { SAFE_INTEGER_MODE, SQLiteCloudCommand, SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types'
77

88
// explicitly importing buffer library to allow cross-platform support by replacing it
99
import { Buffer } from 'buffer'
@@ -302,7 +302,17 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl
302302
// console.debug(`popData - dataType: ${dataType}, spaceIndex: ${spaceIndex}, commandLength: ${commandLength}, commandEnd: ${commandEnd}`)
303303
switch (dataType) {
304304
case CMD_INT:
305-
return popResults(parseInt(buffer.subarray(1, spaceIndex).toString()))
305+
// SQLite uses 64-bit INTEGER, but JS uses 53-bit Number
306+
const value = BigInt(buffer.subarray(1, spaceIndex).toString())
307+
if (SAFE_INTEGER_MODE === 'bigint') {
308+
return popResults(value)
309+
}
310+
if (SAFE_INTEGER_MODE === 'mixed') {
311+
if (value <= BigInt(Number.MIN_SAFE_INTEGER) || BigInt(Number.MAX_SAFE_INTEGER) <= value) {
312+
return popResults(value)
313+
}
314+
}
315+
return popResults(Number(value))
306316
case CMD_FLOAT:
307317
return popResults(parseFloat(buffer.subarray(1, spaceIndex).toString()))
308318
case CMD_NULL:
@@ -334,7 +344,7 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl
334344
}
335345

336346
/** Format a command to be sent via SCSP protocol */
337-
export function formatCommand(command: SQLiteCloudCommand): string {
347+
export function formatCommand(command: SQLiteCloudCommand): Buffer {
338348
// core returns null if there's a space after the semi column
339349
// we want to maintain a compatibility with the standard sqlite3 driver
340350
command.query = command.query.trim()
@@ -346,22 +356,23 @@ export function formatCommand(command: SQLiteCloudCommand): string {
346356
return serializeData(command.query, false)
347357
}
348358

349-
function serializeCommand(data: SQLiteCloudDataTypes[], zeroString: boolean = false): string {
359+
function serializeCommand(data: SQLiteCloudDataTypes[], zeroString: boolean = false): Buffer {
350360
const n = data.length
351-
let serializedData = `${n} `
361+
let serializedData = Buffer.from(`${n} `)
352362

353363
for (let i = 0; i < n; i++) {
354364
// the first string is the sql and it must be zero-terminated
355365
const zs = i == 0 || zeroString
356-
serializedData += serializeData(data[i], zs)
366+
serializedData = Buffer.concat([serializedData, serializeData(data[i], zs)])
357367
}
358368

359-
const bytesTotal = Buffer.byteLength(serializedData, 'utf-8')
360-
const header = `${CMD_ARRAY}${bytesTotal} `
361-
return header + serializedData
369+
const bytesTotal = serializedData.byteLength
370+
const header = Buffer.from(`${CMD_ARRAY}${bytesTotal} `)
371+
372+
return Buffer.concat([header, serializedData])
362373
}
363374

364-
function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): string {
375+
function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): Buffer {
365376
if (typeof data === 'string') {
366377
let cmd = CMD_STRING
367378
if (zeroString) {
@@ -370,24 +381,28 @@ function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false):
370381
}
371382

372383
const header = `${cmd}${Buffer.byteLength(data, 'utf-8')} `
373-
return header + data
384+
return Buffer.from(header + data)
374385
}
375386

376387
if (typeof data === 'number') {
377388
if (Number.isInteger(data)) {
378-
return `${CMD_INT}${data} `
389+
return Buffer.from(`${CMD_INT}${data} `)
379390
} else {
380-
return `${CMD_FLOAT}${data} `
391+
return Buffer.from(`${CMD_FLOAT}${data} `)
381392
}
382393
}
383394

395+
if (typeof data === 'bigint') {
396+
return Buffer.from(`${CMD_INT}${data} `)
397+
}
398+
384399
if (Buffer.isBuffer(data)) {
385-
const header = `${CMD_BLOB}${data.length} `
386-
return header + data.toString('utf-8')
400+
const header = `${CMD_BLOB}${data.byteLength} `
401+
return Buffer.concat([Buffer.from(header), data])
387402
}
388403

389404
if (data === null || data === undefined) {
390-
return `${CMD_NULL} `
405+
return Buffer.from(`${CMD_NULL} `)
391406
}
392407

393408
if (Array.isArray(data)) {

src/drivers/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@ export const DEFAULT_TIMEOUT = 300 * 1000
1010
/** Default tls connection port */
1111
export const DEFAULT_PORT = 8860
1212

13+
/**
14+
* Support to SQLite 64bit integer
15+
*
16+
* number - (default) always return Number type (max: 2^53 - 1)
17+
* Precision is lost when selecting greater numbers from SQLite
18+
* bigint - always return BigInt type (max: 2^63 - 1) for all numbers from SQLite
19+
* (inlcuding `lastID` from WRITE statements)
20+
* mixed - use BigInt and Number types depending on the value size
21+
*/
22+
export let SAFE_INTEGER_MODE = 'number'
23+
if (typeof process !== 'undefined') {
24+
SAFE_INTEGER_MODE = process.env['SAFE_INTEGER_MODE']?.toLowerCase() || 'number'
25+
}
26+
if (SAFE_INTEGER_MODE == 'bigint') {
27+
console.debug('BigInt mode: Using Number for all INTEGER values from SQLite, including meta information from WRITE statements.')
28+
}
29+
if (SAFE_INTEGER_MODE == 'mixed') {
30+
console.debug('Mixed mode: Using BigInt for INTEGER values from SQLite (including meta information from WRITE statements) bigger then 2^53, Number otherwise.')
31+
}
32+
1333
/**
1434
* Configuration for SQLite cloud connection
1535
* @note Options are all lowecase so they 1:1 compatible with C SDK
@@ -26,6 +46,8 @@ export interface SQLiteCloudConfig {
2646
password_hashed?: boolean
2747
/** API key can be provided instead of username and password */
2848
apikey?: string
49+
/** Access Token provided in place of API Key or username/password */
50+
token?: string
2951

3052
/** Host name is required unless connectionstring is provided, eg: xxx.sqlitecloud.io */
3153
host?: string

src/drivers/utilities.ts

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22
// utilities.ts - utility methods to manipulate SQL statements
33
//
44

5-
import { SQLiteCloudConfig, SQLiteCloudError, SQLiteCloudDataTypes, DEFAULT_PORT, DEFAULT_TIMEOUT } from './types'
6-
import { SQLiteCloudArrayType } from './types'
5+
import { DEFAULT_PORT, DEFAULT_TIMEOUT, SQLiteCloudArrayType, SQLiteCloudConfig, SQLiteCloudDataTypes, SQLiteCloudError } from './types'
76

87
// explicitly importing these libraries to allow cross-platform support by replacing them
98
import { URL } from 'whatwg-url'
10-
import { Buffer } from 'buffer'
119

1210
//
1311
// determining running environment, thanks to browser-or-node
@@ -42,45 +40,47 @@ export function anonimizeError(error: Error): Error {
4240
export function getInitializationCommands(config: SQLiteCloudConfig): string {
4341
// we check the credentials using non linearizable so we're quicker
4442
// then we bring back linearizability unless specified otherwise
45-
let commands = 'SET CLIENT KEY NONLINEARIZABLE TO 1; '
43+
let commands = 'SET CLIENT KEY NONLINEARIZABLE TO 1;'
4644

4745
// first user authentication, then all other commands
4846
if (config.apikey) {
49-
commands += `AUTH APIKEY ${config.apikey}; `
47+
commands += `AUTH APIKEY ${config.apikey};`
48+
} else if (config.token) {
49+
commands += `AUTH TOKEN ${config.token};`
5050
} else {
51-
commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''}; `
51+
commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''};`
5252
}
5353

5454
if (config.compression) {
55-
commands += 'SET CLIENT KEY COMPRESSION TO 1; '
55+
commands += 'SET CLIENT KEY COMPRESSION TO 1;'
5656
}
5757
if (config.zerotext) {
58-
commands += 'SET CLIENT KEY ZEROTEXT TO 1; '
58+
commands += 'SET CLIENT KEY ZEROTEXT TO 1;'
5959
}
6060
if (config.noblob) {
61-
commands += 'SET CLIENT KEY NOBLOB TO 1; '
61+
commands += 'SET CLIENT KEY NOBLOB TO 1;'
6262
}
6363
if (config.maxdata) {
64-
commands += `SET CLIENT KEY MAXDATA TO ${config.maxdata}; `
64+
commands += `SET CLIENT KEY MAXDATA TO ${config.maxdata};`
6565
}
6666
if (config.maxrows) {
67-
commands += `SET CLIENT KEY MAXROWS TO ${config.maxrows}; `
67+
commands += `SET CLIENT KEY MAXROWS TO ${config.maxrows};`
6868
}
6969
if (config.maxrowset) {
70-
commands += `SET CLIENT KEY MAXROWSET TO ${config.maxrowset}; `
70+
commands += `SET CLIENT KEY MAXROWSET TO ${config.maxrowset};`
7171
}
7272

7373
// we ALWAYS set non linearizable to 1 when we start so we can be quicker on login
7474
// but then we need to put it back to its default value if "linearizable" unless set
7575
if (!config.non_linearizable) {
76-
commands += 'SET CLIENT KEY NONLINEARIZABLE TO 0; '
76+
commands += 'SET CLIENT KEY NONLINEARIZABLE TO 0;'
7777
}
7878

7979
if (config.database) {
8080
if (config.create && !config.memory) {
81-
commands += `CREATE DATABASE ${config.database} IF NOT EXISTS; `
81+
commands += `CREATE DATABASE ${config.database} IF NOT EXISTS;`
8282
}
83-
commands += `USE DATABASE ${config.database}; `
83+
commands += `USE DATABASE ${config.database};`
8484
}
8585

8686
return commands
@@ -109,13 +109,12 @@ export function getUpdateResults(results?: any): Record<string, any> | undefined
109109
switch (results[0]) {
110110
case SQLiteCloudArrayType.ARRAY_TYPE_SQLITE_EXEC:
111111
return {
112-
type: results[0],
113-
index: results[1],
112+
type: Number(results[0]),
113+
index: Number(results[1]),
114114
lastID: results[2], // ROWID (sqlite3_last_insert_rowid)
115115
changes: results[3], // CHANGES(sqlite3_changes)
116116
totalChanges: results[4], // TOTAL_CHANGES (sqlite3_total_changes)
117-
finalized: results[5], // FINALIZED
118-
//
117+
finalized: Number(results[5]), // FINALIZED
119118
rowId: results[2] // same as lastId
120119
}
121120
}
@@ -177,16 +176,19 @@ export function validateConfiguration(config: SQLiteCloudConfig): SQLiteCloudCon
177176
config.non_linearizable = parseBoolean(config.non_linearizable)
178177
config.insecure = parseBoolean(config.insecure)
179178

180-
const hasCredentials = (config.username && config.password) || config.apikey
179+
const hasCredentials = (config.username && config.password) || config.apikey || config.token
181180
if (!config.host || !hasCredentials) {
182181
console.error('SQLiteCloudConnection.validateConfiguration - missing arguments', config)
183-
throw new SQLiteCloudError('The user, password and host arguments or the ?apikey= must be specified.', { errorCode: 'ERR_MISSING_ARGS' })
182+
throw new SQLiteCloudError('The user, password and host arguments, the ?apikey= or the ?token= must be specified.', { errorCode: 'ERR_MISSING_ARGS' })
184183
}
185184

186185
if (!config.connectionstring) {
187186
// build connection string from configuration, values are already validated
187+
config.connectionstring = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}`
188188
if (config.apikey) {
189-
config.connectionstring = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}?apikey=${config.apikey}`
189+
config.connectionstring += `?apikey=${config.apikey}`
190+
} else if (config.token) {
191+
config.connectionstring += `?token=${config.token}`
190192
} else {
191193
config.connectionstring = `sqlitecloud://${encodeURIComponent(config.username || '')}:${encodeURIComponent(config.password || '')}@${config.host}:${
192194
config.port
@@ -215,13 +217,13 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf
215217
// all lowecase options
216218
const options: { [key: string]: string } = {}
217219
url.searchParams.forEach((value, key) => {
218-
options[key.toLowerCase().replace(/-/g, '_')] = value
220+
options[key.toLowerCase().replace(/-/g, '_')] = value.trim()
219221
})
220222

221223
const config: SQLiteCloudConfig = {
222224
...options,
223-
username: decodeURIComponent(url.username),
224-
password: decodeURIComponent(url.password),
225+
username: url.username ? decodeURIComponent(url.username) : undefined,
226+
password: url.password ? decodeURIComponent(url.password) : undefined,
225227
password_hashed: options.password_hashed ? parseBoolean(options.password_hashed) : undefined,
226228
host: url.hostname,
227229
// type cast values
@@ -241,13 +243,10 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf
241243
verbose: options.verbose ? parseBoolean(options.verbose) : undefined
242244
}
243245

244-
// either you use an apikey or username and password
245-
if (config.apikey) {
246-
if (config.username || config.password) {
247-
console.warn('SQLiteCloudConnection.parseconnectionstring - apikey and username/password are both specified, using apikey')
248-
}
249-
delete config.username
250-
delete config.password
246+
// either you use an apikey, token or username and password
247+
if (Number(!!config.apikey) + Number(!!config.token) + Number(!!(config.username || config.password)) > 1) {
248+
console.error('SQLiteCloudConnection.parseconnectionstring - choose between apikey, token or username/password')
249+
throw new SQLiteCloudError('Choose between apikey, token or username/password')
251250
}
252251

253252
const database = url.pathname.replace('/', '') // pathname is database name, remove the leading slash
@@ -257,7 +256,7 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf
257256

258257
return config
259258
} catch (error) {
260-
throw new SQLiteCloudError(`Invalid connection string: ${connectionstring}`)
259+
throw new SQLiteCloudError(`Invalid connection string: ${connectionstring} - error: ${error}`)
261260
}
262261
}
263262

0 commit comments

Comments
 (0)