Skip to content

Commit

Permalink
Client refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobprall committed Dec 23, 2024
1 parent 2838977 commit 23635fb
Show file tree
Hide file tree
Showing 11 changed files with 394 additions and 100 deletions.
9 changes: 9 additions & 0 deletions README_Refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Refactor Summary
- Added SQLiteCloudClient class and createClient function
- Extracted PubSub from Database to SQLiteCloudClient
- Added fetch and fetchWithAuth
- Added Weblite endpoint methods for upload, download, delete, and listDatabases
- Refactored PubSub to be more intuitive and easier to use
- Added FileClient class and methods for file upload and download

TODO: Polish code, add error handling, Write tests
112 changes: 112 additions & 0 deletions demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@

/**
* Developer experience - current
*
*/

import { Database } from '@sqlitecloud/drivers'
import { PUBSUB_ENTITY_TYPE } from '@sqlitecloud/drivers/lib/drivers/pubsub' // forces user to import pubsub constants from hard to remember location

const db = new Database('connection-string')
const pubSub = await db.getPubSub() // couples database to pubsub

/* Database methods */
await db.sql`SELECT * FROM users`
db.exec('command')
db.run('command')
db.all('command')
db.each('command')
db.close()

/* PubSub usage */
/** Listen to a table */
pubSub.listen(PUBSUB_ENTITY_TYPE.TABLE, 'users', (error, results, data) => { // note extraneous "data"
console.log(error, results, data)
}, ['extra data'])

/** Listen to a channel */
pubSub.listen(PUBSUB_ENTITY_TYPE.CHANNEL, 'messages', (error, results, data) => {
console.log(error, results, data)
}, ['extra data'])

/** Create a channel */
pubSub.createChannel('messages')

/** Unlisten to a table */
pubSub.unlisten(PUBSUB_ENTITY_TYPE.TABLE, 'users')

/** Remove a channel (not currently exposed) */
// @ts-ignore
pubSub.removeChannel('messages')

/** Notify a channel */
pubSub.notifyChannel('messages', 'my message')


/**
* Developer experience - refactored
* In the refactor, Database still exists and works as before.
*/

import { createClient } from './src/refactor/SQLiteCloudClient'

const client = createClient('connection-string/chinook.db')

// Promise sql query
const { data, error } = await client.sql`SELECT * FROM albums`;

client.defaultDb = 'users'; // helper to set default database for SQL queries

const { data: sessions, error: sessionsError } = await client.sql`SELECT * FROM sessions`;
// or
const result = client.db.exec('SELECT * FROM sessions')

// Weblite
// upload database
const uploadDatabaseResponse = await client.weblite.upload('new_chinook.db', new File([''], 'new_chinook.db'), { replace: false });

// download database
const downloadDatabaseResponse = await client.weblite.download('new_chinook.db');

// delete database
const deleteDatabaseResponse = await client.weblite.delete('new_chinook.db');

// list databases
const listDatabasesResponse = await client.weblite.listDatabases();

// create database
const createDatabaseResponse = await client.weblite.create('new_chinook.db');

// SQLiteCloudFileClient
const createBucketResponse = await client.files.createBucket('myBucket');
const getBucketResponse = await client.files.getBucket('myBucket');
const deleteBucketResponse = await client.files.deleteBucket('myBucket');
const listBucketsResponse = await client.files.listBuckets();

// upload file
const uploadFileResponse = await client.files.upload('myBucket', 'myPath', new File([''], 'myFile.txt'), { contentType: 'text/plain' });

// download file
const downloadFileResponse = await client.files.download('myBucket', 'myPath');

// remove file
const removeFileResponse = await client.files.remove('myBucket', 'myPath');


// SQLiteCloudPubSubClient Refactor
await client.pubSub.create('messages')
await client.pubSub.notify('messages', 'my message')
await client.pubSub.subscribe('messages', (error, results) => {
console.log(error, results)
})
client.pubSub.unsubscribe('messages')
await client.pubSub.delete('messages')

await client.pubSub.listen({ tableName: 'users' }, (error, results) => {
console.log(error, results)
})

await client.pubSub.listen({ tableName: 'users', dbName: 'chinook.sqlite' }, (error, results) => { // note optional dbName
console.log(error, results)
})

4 changes: 2 additions & 2 deletions src/drivers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ export const DEFAULT_GLOBAL_OPTIONS = {
headers: DEFAULT_HEADERS
}

export const DEFAULT_WEBLITE_VERSION = 'v2'
export const WEBLITE_PORT = 8090
export const DEFAULT_API_VERSION = 'v2'
export const DEFAULT_API_PORT = 8090
1 change: 0 additions & 1 deletion src/drivers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ export type RowsCallback<T = Record<string, any>> = (error: Error | null, rows?:
export type RowCallback<T = Record<string, any>> = (error: Error | null, row?: T) => void
export type RowCountCallback = (error: Error | null, rowCount?: number) => void
export type PubSubCallback<T = any> = (error: Error | null, results?: T, data?: any) => void
export type PubSubRefactorCallback<T = any> = (error: Error | null, results?: T) => void

/**
* Certain responses include arrays with various types of metadata.
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,3 @@ export {
export { SQLiteCloudRowset, SQLiteCloudRow } from './drivers/rowset'
export { parseconnectionstring, validateConfiguration, getInitializationCommands, sanitizeSQLiteIdentifier } from './drivers/utilities'
export * as protocol from './drivers/protocol'
export { createClient } from './SQLiteCloudClient'
69 changes: 69 additions & 0 deletions src/refactor/SQLiteCloudClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Database } from '../drivers/database'
import { Fetch, fetchWithAuth } from './fetch'
import { SQLiteCloudPubSubClient } from './SQLiteCloudPubSubClient'
import { SQLiteCloudWebliteClient } from './SQLiteCloudWebliteClient'
import { SQLiteCloudFileClient } from './SQLiteCloudFileClient'
import { SQLiteCloudCommand } from '../drivers/types'
import { getDefaultDatabase } from './utils'

interface SQLiteCloudClientConfig {
connectionString: string
fetch?: Fetch
}



export class SQLiteCloudClient {
private connectionString: string
private fetch: Fetch

constructor(config: SQLiteCloudClientConfig | string) {
let connectionString: string
let customFetch: Fetch | undefined

if (typeof config === 'string') {
connectionString = config
} else {
connectionString = config.connectionString
customFetch = config.fetch
}

this.connectionString = connectionString
this.fetch = fetchWithAuth(this.connectionString, customFetch)
this.defaultDb = getDefaultDatabase(this.connectionString) ?? ''
}

async sql(sql: TemplateStringsArray | string | SQLiteCloudCommand, ...values: any[]) {
this.db.exec(`USE DATABASE ${this.defaultDb}`)
try {
const result = await this.db.sql(sql, ...values)
return { data: result, error: null }
} catch (error) {
return { error, data: null }
}
}

get pubSub() {
return new SQLiteCloudPubSubClient(this.db)
}

get db() {
return new Database(this.connectionString)
}

get weblite() {
return new SQLiteCloudWebliteClient(this.connectionString, this.fetch)
}

get files() {
return new SQLiteCloudFileClient(this.connectionString, this.fetch)
}

set defaultDb(dbName: string) {
this.defaultDb = dbName
}
}

export function createClient(config: SQLiteCloudClientConfig | string): SQLiteCloudClient {
return new SQLiteCloudClient(config)
}
105 changes: 105 additions & 0 deletions src/refactor/SQLiteCloudFileClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { SQLiteCloudError } from "../drivers/types"
import { getAPIUrl } from "./utils"
import { Fetch, fetchWithAuth } from "./fetch"

interface SQLiteCloudFile {
createBucket(bucket: string, path: string): Promise<Response>
getBucket(bucket: string): Promise<any>
deleteBucket(bucket: string): Promise<Response>
listBuckets(): Promise<any>
upload(bucket: string, pathname: string, file: File | Buffer | Blob | string, options: { contentType: string }): Promise<Response>
download(bucket: string, pathname: string): Promise<Blob>
remove(bucket: string, pathName: string): Promise<Response>
list(bucket: string): Promise<any>
}

const FILES_DATABASE = 'files.sqlite'

export class SQLiteCloudFileClient implements SQLiteCloudFile {
private filesUrl: string
private webliteSQLUrl: string
private fetch: Fetch

constructor(connectionString: string, sql?: Fetch) {
this.filesUrl = getAPIUrl(connectionString, 'files')
this.webliteSQLUrl = getAPIUrl(connectionString, 'weblite/sql')
this.fetch = fetchWithAuth(connectionString, fetch)
}

async createBucket(bucket: string) {
const url = `${this.webliteSQLUrl}?sql=USE DATABASE files; INSERT INTO files (Bucket) VALUES ('${bucket}');`
const response = await this.fetch(url, { method: 'POST' })
if (!response.ok) {
throw new SQLiteCloudError(`Failed to create bucket: ${response.statusText}`)
}
return response.json()
}

async getBucket(bucket: string) {
const url = `${this.filesUrl}/${bucket}`
const response = await this.fetch(url, { method: 'GET' })
if (!response.ok) {
throw new SQLiteCloudError(`Failed to get bucket: ${response.statusText}`)
}

return response.json()
}

async deleteBucket(bucket: string) {
const url = `${this.filesUrl}/${bucket}`
const response = await this.fetch(url, { method: 'DELETE' })
if (!response.ok) {
throw new SQLiteCloudError(`Failed to delete bucket: ${response.statusText}`)
}
return response.json()
}

async listBuckets() {
const encodedUrl = encodeURIComponent(`${this.webliteSQLUrl}?sql=USE DATABASE files.sqlite; SELECT * FROM files`)
const response = await this.fetch(encodedUrl)
if (!response.ok) {
throw new SQLiteCloudError(`Failed to list buckets: ${response.statusText}`)
}
return response.json()
}

async upload(bucket: string, pathname: string, file: File | Buffer | Blob | string, options: { contentType: string }) {
const url = `${this.filesUrl}/${bucket}/${pathname}`;
const headers = {
'Content-Type': options?.contentType || 'application/octet-stream'
}
const response = await this.fetch(url, { method: 'POST', body: file, headers })
if (!response.ok) {
throw new SQLiteCloudError(`Failed to upload file: ${response.statusText}`)
}
return response.json()
}

async download(bucket: string, pathname: string) {
const url = `${this.filesUrl}/${bucket}/${pathname}`;
const response = await this.fetch(url, { method: 'GET' })
if (!response.ok) {
throw new SQLiteCloudError(`Failed to download file: ${response.statusText}`)
}
return response.blob()
}

async remove(bucket: string, pathName: string) {
const url = `${this.filesUrl}/${bucket}/${pathName}`
const response = await this.fetch(url, { method: 'DELETE' })
if (!response.ok) {
throw new SQLiteCloudError(`Failed to remove file: ${response.statusText}`)
}
return response.json()
}

async list(bucket: string) {
const encodedUrl = encodeURIComponent(`${this.webliteSQLUrl}?sql=USE DATABASE files.sqlite; SELECT * FROM files WHERE bucket = '${bucket}'`)
const response = await this.fetch(encodedUrl)
if (!response.ok) {
throw new SQLiteCloudError(`Failed to list files: ${response.statusText}`)
}
return response.json()
}
}

Loading

0 comments on commit 23635fb

Please sign in to comment.