Skip to content

Commit dbbcde8

Browse files
Minor refactorings, types, etc
1 parent 1ebe100 commit dbbcde8

9 files changed

Lines changed: 136 additions & 85 deletions

File tree

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,19 @@ npm install sqlitecloud-js
2020
```ts
2121
import { Database } from 'sqlitecloud-js'
2222

23-
let name = 'Ava Jones'
23+
let database = new Database('sqlitecloud://user:password@xxx.sqlite.cloud:8860/chinook.db')
2424

25-
let results = await database.sql`SELECT * FROM people WHERE name = ${name}`
26-
// => returns [{ id: 5, name: 'Ava Jones', age: 22, hobby: 'Time traveling' }]
25+
let name = 'Breaking The Rules'
26+
27+
let results = await database.sql`SELECT * FROM tracks WHERE name = ${name}`
28+
// => returns [{ AlbumId: 1, Name: 'Breaking The Rules', Composer: 'Angus Young... }]
2729
```
2830

2931
Use `Database.sql` for executing both your prepared statements and plain SQL queries asynchronously. This method returns an array of rows for SELECT queries and supports the standard syntax for UPDATE, INSERT, and DELETE.
3032

3133
We aim for full compatibility with the established [sqlite3](https://www.npmjs.com/package/sqlite3) API, with the primary distinction being that our driver connects to SQLiteCloud databases. This allows you to migrate your local SQLite databases to the cloud while continuing to use your existing codebase.
3234

33-
The package is developed entirely in TypeScript and is fully compatible with JavaScript, without requiring any native libraries. This makes it a straightforward and effective tool for managing cloud-based databases in a familiar SQLite environment.
35+
The package is developed entirely in TypeScript and is fully compatible with JavaScript. It doesn't require any native libraries. This makes it a straightforward and effective tool for managing cloud-based databases in a familiar SQLite environment.
3436

3537
## API
3638

src/connection.ts

Lines changed: 14 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import lz4 from 'lz4'
88
import { SQLiteCloudConfig, SQLCloudRowsetMetadata, SQLiteCloudError, SQLiteCloudDataTypes, ErrorCallback, ResultsCallback } from './types'
99
import { SQLiteCloudRowset } from './rowset'
1010
import { parseConnectionString, parseBoolean } from './utilities'
11+
import { OperationsQueue } from './queue'
1112

1213
/**
1314
* The server communicates with clients via commands defined
@@ -157,15 +158,15 @@ export class SQLiteCloudConnection {
157158
}
158159

159160
/* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
160-
public async connect(callback?: ErrorCallback) {
161+
public connect(callback?: ErrorCallback): this {
161162
if (this.socket) {
162163
callback?.call(this, null)
163-
return
164+
return this
164165
}
165166

166167
this.operations.enqueue(done => {
167168
// connect to tls socket, initialize connection, setup event handlers
168-
let client: tls.TLSSocket = tls.connect(this.config.port as number, this.config.host, this.config.tlsOptions, () => {
169+
const client: tls.TLSSocket = tls.connect(this.config.port as number, this.config.host, this.config.tlsOptions, () => {
169170
if (!client.authorized) {
170171
this.log('Connection was not authorized', client.authorizationError)
171172
this.close()
@@ -199,13 +200,15 @@ export class SQLiteCloudConnection {
199200

200201
// send initialization commands (will be enqued in the operations queue)
201202
const commands = this.initializationCommands
202-
this.sendCommands(commands, (error, _results) => {
203+
this.sendCommands(commands, error => {
203204
callback?.call(this, error)
204205
})
206+
207+
return this
205208
}
206209

207210
/** Will send a command and return the resulting rowset or result or throw an error */
208-
public sendCommands<T = SQLiteCloudRowset>(commands: string, callback?: ResultsCallback) {
211+
public sendCommands(commands: string, callback?: ResultsCallback): this {
209212
this.operations.enqueue(done => {
210213
// connection needs to be established?
211214
if (!this.socket) {
@@ -308,14 +311,18 @@ export class SQLiteCloudConnection {
308311
finish?.call(this, new SQLiteCloudError('Socket error', { cause: error }))
309312
})
310313
})
314+
315+
return this
311316
}
312317

313318
/** Disconnect from server, release connection. */
314-
public close() {
319+
public close(): this {
315320
if (this.socket) {
316321
this.socket.destroy()
317322
}
323+
this.operations.clear()
318324
this.socket = undefined
325+
return this
319326
}
320327
}
321328

@@ -508,7 +515,7 @@ function parseRowsetChunks(buffers: Buffer[]) {
508515
}
509516

510517
/** Pop one or more space separated integers from beginning of buffer, move buffer forward */
511-
function popIntegers(buffer: Buffer, numberOfIntegers: number = 1): { data: number[]; fwdBuffer: Buffer } {
518+
function popIntegers(buffer: Buffer, numberOfIntegers = 1): { data: number[]; fwdBuffer: Buffer } {
512519
const data: number[] = []
513520
for (let i = 0; i < numberOfIntegers; i++) {
514521
const spaceIndex = buffer.indexOf(' ')
@@ -584,42 +591,3 @@ function formatCommand(command: string): string {
584591
const commandLength = Buffer.byteLength(command, 'utf-8')
585592
return `+${commandLength} ${command}`
586593
}
587-
588-
//
589-
// OperationsQueue is used to linearize operations on the connection
590-
//
591-
592-
type OperationCallback = (error?: Error) => void
593-
type Operation = (done: OperationCallback) => void
594-
595-
class OperationsQueue {
596-
private queue: Operation[] = []
597-
private isProcessing = false
598-
599-
/** Add operations to the queue, process immediately if possible, else wait for previous operations to complete */
600-
enqueue(operation: Operation) {
601-
this.queue.push(operation)
602-
if (!this.isProcessing) {
603-
this.processNext()
604-
}
605-
}
606-
607-
/** Process the next operation in the queue */
608-
private processNext() {
609-
if (this.queue.length === 0) {
610-
this.isProcessing = false
611-
return
612-
}
613-
614-
this.isProcessing = true
615-
const operation = this.queue.shift()
616-
operation?.(error => {
617-
if (error) {
618-
console.warn('OperationQueue.processNext - error in operation', error)
619-
}
620-
621-
// process the next operation in the queue
622-
this.processNext()
623-
})
624-
}
625-
}

src/database.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
// https://github.com/TryGhost/node-sqlite3
88
// https://github.com/TryGhost/node-sqlite3/blob/master/lib/sqlite3.d.ts
99

10+
/* eslint-disable @typescript-eslint/no-unused-vars */
11+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
12+
1013
import { SQLiteCloudConnection } from './connection'
1114
import { SQLiteCloudRowset } from './rowset'
1215
import { SQLiteCloudConfig, SQLiteCloudError, RowCountCallback, SQLiteCloudArrayType } from './types'
@@ -29,7 +32,8 @@ export class Database {
2932
const connection = new SQLiteCloudConnection(this.config)
3033
this.connections = [connection]
3134

32-
this.getConnection((error, _connection) => {
35+
// get a connection for the only purpose of opening the database
36+
this.getConnection(error => {
3337
if (error) {
3438
this.handleError(null, error, callback)
3539
} else {
@@ -133,7 +137,7 @@ export class Database {
133137
}
134138

135139
/** Set a configuration option for the database */
136-
public configure(_option: any, _value: any): this {
140+
public configure(_option: string, _value: any): this {
137141
// https://github.com/TryGhost/node-sqlite3/wiki/API#configureoption-value
138142
return this
139143
}
@@ -272,7 +276,7 @@ export class Database {
272276
} else {
273277
if (rowset && rowset instanceof SQLiteCloudRowset) {
274278
if (callback) {
275-
for (const row of rowset as SQLiteCloudRowset) {
279+
for (const row of rowset) {
276280
callback.call(this, null, row)
277281
}
278282
}
@@ -355,13 +359,14 @@ export class Database {
355359
* and an error occurred, an error event with the error object as the only parameter
356360
* will be emitted on the database object.
357361
*/
358-
public loadExtension(_path: string, callback?: ErrorCallback) {
362+
public loadExtension(_path: string, callback?: ErrorCallback): this {
359363
// TODO sqlitecloud-js / implement database loadExtension #17
360364
if (callback) {
361365
callback.call(this, new Error('Database.loadExtension - Not implemented'))
362366
} else {
363367
this.emitEvent('error', new Error('Database.loadExtension - Not implemented'))
364368
}
369+
return this
365370
}
366371

367372
/**

src/queue.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// queue.ts - OperationsQueue is used to linearize operations on the connection
3+
//
4+
5+
type OperationCallback = (error?: Error) => void
6+
type Operation = (done: OperationCallback) => void
7+
8+
export class OperationsQueue {
9+
private queue: Operation[] = []
10+
private isProcessing = false
11+
12+
/** Add operations to the queue, process immediately if possible, else wait for previous operations to complete */
13+
public enqueue(operation: Operation) {
14+
this.queue.push(operation)
15+
if (!this.isProcessing) {
16+
this.processNext()
17+
}
18+
}
19+
20+
/** Clear the queue */
21+
public clear() {
22+
this.queue = []
23+
this.isProcessing = false
24+
}
25+
26+
/** Process the next operation in the queue */
27+
private processNext() {
28+
if (this.queue.length === 0) {
29+
this.isProcessing = false
30+
return
31+
}
32+
33+
this.isProcessing = true
34+
const operation = this.queue.shift()
35+
operation?.(error => {
36+
if (error) {
37+
console.warn('OperationQueue.processNext - error in operation', error)
38+
}
39+
40+
// process the next operation in the queue
41+
this.processNext()
42+
})
43+
}
44+
}

src/rowset.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class SQLiteCloudRowset extends Array<SQLiteCloudRow> {
4141
this.#data = data
4242

4343
// adjust missing column names, duplicate column names, etc.
44-
let columnNames = this.columnsNames
44+
const columnNames = this.columnsNames
4545
for (let i = 0; i < metadata.numberOfColumns; i++) {
4646
if (!columnNames[i]) {
4747
columnNames[i] = `column_${i}`

src/statement.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { popCallback, prepareSql } from './utilities'
66
import { Database } from './database'
77
import { ErrorCallback, RowCallback, RowsCallback, RowCountCallback, ResultsCallback } from './types'
88

9+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
10+
911
/** A statement generated by Database.prepare used to prepare SQL with ? bindings */
1012
export class Statement<T> {
1113
constructor(database: Database, sql: string, ...params: any[]) {
@@ -64,7 +66,7 @@ export class Statement<T> {
6466
public run(callback?: ResultsCallback<T>): this
6567
public run(params: any, callback?: ResultsCallback<T>): this
6668
public run(...params: any[]): this {
67-
const { args, callback } = popCallback<RowCallback>(params || [])
69+
const { args, callback } = popCallback<ResultsCallback>(params || [])
6870
if (args?.length > 0) {
6971
// apply new bindings then execute
7072
this.bind(...args, (error: Error) => {

src/utilities.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
import { SQLiteCloudConfig, SQLiteCloudError, SQLiteCloudDataTypes } from './types'
66

7+
//
8+
// utility methods
9+
//
10+
711
/** Takes a generic value and escapes it so it can replace ? as a binding in a prepared SQL statement */
812
export function escapeSqlParameter(param: SQLiteCloudDataTypes): string {
913
if (param === null || param === undefined) {
@@ -89,17 +93,18 @@ export function prepareSql(sql: string, ...params: (SQLiteCloudDataTypes | SQLit
8993
* as 'completeCallback'.
9094
*/
9195
export function popCallback<T extends ErrorCallback = ErrorCallback>(
92-
args: SQLiteCloudDataTypes[]
96+
args: (SQLiteCloudDataTypes | T | ErrorCallback)[]
9397
): { args: SQLiteCloudDataTypes[]; callback?: T | undefined; complete?: ErrorCallback } {
98+
const remaining = args as SQLiteCloudDataTypes[]
9499
// at least 1 callback?
95100
if (args && args.length > 0 && typeof args[args.length - 1] === 'function') {
96101
// at least 2 callbacks?
97102
if (args.length > 1 && typeof args[args.length - 2] === 'function') {
98-
return { args: args.slice(0, -2), callback: args[args.length - 2] as T, complete: args[args.length - 1] as T }
103+
return { args: remaining.slice(0, -2), callback: args[args.length - 2] as T, complete: args[args.length - 1] as T }
99104
}
100-
return { args: args.slice(0, -1), callback: args[args.length - 1] as T }
105+
return { args: remaining.slice(0, -1), callback: args[args.length - 1] as T }
101106
}
102-
return { args }
107+
return { args: remaining }
103108
}
104109

105110
/** Parse connectionString like sqlitecloud://usernam:password@host:port/database?option1=xxx&option2=xxx into its components */

test/database.test.ts

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,18 @@ describe('database', () => {
3939
}
4040
})
4141

42-
afterEach(() => {
42+
afterEach(done => {
4343
if (database) {
44-
database.close()
44+
database.close(error => {
45+
// @ts-expect-error
46+
database = undefined
47+
done(error)
48+
})
49+
} else {
50+
// @ts-expect-error
51+
database = undefined
52+
done()
4553
}
46-
// @ts-expect-error
47-
database = undefined
4854
})
4955

5056
describe('run', () => {
@@ -243,21 +249,17 @@ describe('database', () => {
243249
const chinook = new Database(CHINOOK_DATABASE_URL)
244250
try {
245251
chinook.exec('SET BOGUS STATEMENT TO 1;', error => {
246-
try {
247-
expect(error).toBeInstanceOf(SQLiteCloudError)
248-
expect(error).toMatchObject({
249-
errorCode: '10002',
250-
externalErrorCode: '0',
251-
name: 'SQLiteCloudError',
252-
offsetCode: -1,
253-
message: 'Unable to find command SET BOGUS STATEMENT TO 1;'
254-
})
255-
256-
chinook.close()
257-
done()
258-
} catch (error) {
259-
done(error)
260-
}
252+
expect(error).toBeInstanceOf(SQLiteCloudError)
253+
expect(error).toMatchObject({
254+
errorCode: '10002',
255+
externalErrorCode: '0',
256+
name: 'SQLiteCloudError',
257+
offsetCode: -1,
258+
message: 'Unable to find command SET BOGUS STATEMENT TO 1;'
259+
})
260+
261+
chinook.close()
262+
done()
261263
})
262264
} catch (error) {
263265
done(error)
@@ -266,6 +268,29 @@ describe('database', () => {
266268
})
267269

268270
describe('sql (async)', () => {
271+
it('should select from chinook', async () => {
272+
const chinook = new Database(CHINOOK_DATABASE_URL)
273+
let name = 'Breaking The Rules'
274+
const results = await chinook.sql`SELECT * FROM tracks WHERE name = ${name}`
275+
expect(results).toBeDefined()
276+
277+
const row = results[0]
278+
expect(row).toBeDefined()
279+
expect(row).toMatchObject({
280+
AlbumId: 1,
281+
Bytes: 8596840,
282+
Composer: 'Angus Young, Malcolm Young, Brian Johnson',
283+
GenreId: 1,
284+
MediaTypeId: 1,
285+
Milliseconds: 263288,
286+
Name: 'Breaking The Rules',
287+
TrackId: 12,
288+
UnitPrice: 0.99
289+
})
290+
291+
chinook.close()
292+
})
293+
269294
it('should select and return multiple rows', async () => {
270295
const results = await database.sql('SELECT * FROM people ORDER BY id')
271296
expect(results).toBeDefined()

0 commit comments

Comments
 (0)