Skip to content

Commit 23635fb

Browse files
committed
Client refactor
1 parent 2838977 commit 23635fb

File tree

11 files changed

+394
-100
lines changed

11 files changed

+394
-100
lines changed

README_Refactor.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Refactor Summary
2+
- Added SQLiteCloudClient class and createClient function
3+
- Extracted PubSub from Database to SQLiteCloudClient
4+
- Added fetch and fetchWithAuth
5+
- Added Weblite endpoint methods for upload, download, delete, and listDatabases
6+
- Refactored PubSub to be more intuitive and easier to use
7+
- Added FileClient class and methods for file upload and download
8+
9+
TODO: Polish code, add error handling, Write tests

demo.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
2+
/**
3+
* Developer experience - current
4+
*
5+
*/
6+
7+
import { Database } from '@sqlitecloud/drivers'
8+
import { PUBSUB_ENTITY_TYPE } from '@sqlitecloud/drivers/lib/drivers/pubsub' // forces user to import pubsub constants from hard to remember location
9+
10+
const db = new Database('connection-string')
11+
const pubSub = await db.getPubSub() // couples database to pubsub
12+
13+
/* Database methods */
14+
await db.sql`SELECT * FROM users`
15+
db.exec('command')
16+
db.run('command')
17+
db.all('command')
18+
db.each('command')
19+
db.close()
20+
21+
/* PubSub usage */
22+
/** Listen to a table */
23+
pubSub.listen(PUBSUB_ENTITY_TYPE.TABLE, 'users', (error, results, data) => { // note extraneous "data"
24+
console.log(error, results, data)
25+
}, ['extra data'])
26+
27+
/** Listen to a channel */
28+
pubSub.listen(PUBSUB_ENTITY_TYPE.CHANNEL, 'messages', (error, results, data) => {
29+
console.log(error, results, data)
30+
}, ['extra data'])
31+
32+
/** Create a channel */
33+
pubSub.createChannel('messages')
34+
35+
/** Unlisten to a table */
36+
pubSub.unlisten(PUBSUB_ENTITY_TYPE.TABLE, 'users')
37+
38+
/** Remove a channel (not currently exposed) */
39+
// @ts-ignore
40+
pubSub.removeChannel('messages')
41+
42+
/** Notify a channel */
43+
pubSub.notifyChannel('messages', 'my message')
44+
45+
46+
/**
47+
* Developer experience - refactored
48+
* In the refactor, Database still exists and works as before.
49+
*/
50+
51+
import { createClient } from './src/refactor/SQLiteCloudClient'
52+
53+
const client = createClient('connection-string/chinook.db')
54+
55+
// Promise sql query
56+
const { data, error } = await client.sql`SELECT * FROM albums`;
57+
58+
client.defaultDb = 'users'; // helper to set default database for SQL queries
59+
60+
const { data: sessions, error: sessionsError } = await client.sql`SELECT * FROM sessions`;
61+
// or
62+
const result = client.db.exec('SELECT * FROM sessions')
63+
64+
// Weblite
65+
// upload database
66+
const uploadDatabaseResponse = await client.weblite.upload('new_chinook.db', new File([''], 'new_chinook.db'), { replace: false });
67+
68+
// download database
69+
const downloadDatabaseResponse = await client.weblite.download('new_chinook.db');
70+
71+
// delete database
72+
const deleteDatabaseResponse = await client.weblite.delete('new_chinook.db');
73+
74+
// list databases
75+
const listDatabasesResponse = await client.weblite.listDatabases();
76+
77+
// create database
78+
const createDatabaseResponse = await client.weblite.create('new_chinook.db');
79+
80+
// SQLiteCloudFileClient
81+
const createBucketResponse = await client.files.createBucket('myBucket');
82+
const getBucketResponse = await client.files.getBucket('myBucket');
83+
const deleteBucketResponse = await client.files.deleteBucket('myBucket');
84+
const listBucketsResponse = await client.files.listBuckets();
85+
86+
// upload file
87+
const uploadFileResponse = await client.files.upload('myBucket', 'myPath', new File([''], 'myFile.txt'), { contentType: 'text/plain' });
88+
89+
// download file
90+
const downloadFileResponse = await client.files.download('myBucket', 'myPath');
91+
92+
// remove file
93+
const removeFileResponse = await client.files.remove('myBucket', 'myPath');
94+
95+
96+
// SQLiteCloudPubSubClient Refactor
97+
await client.pubSub.create('messages')
98+
await client.pubSub.notify('messages', 'my message')
99+
await client.pubSub.subscribe('messages', (error, results) => {
100+
console.log(error, results)
101+
})
102+
client.pubSub.unsubscribe('messages')
103+
await client.pubSub.delete('messages')
104+
105+
await client.pubSub.listen({ tableName: 'users' }, (error, results) => {
106+
console.log(error, results)
107+
})
108+
109+
await client.pubSub.listen({ tableName: 'users', dbName: 'chinook.sqlite' }, (error, results) => { // note optional dbName
110+
console.log(error, results)
111+
})
112+

src/drivers/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ export const DEFAULT_GLOBAL_OPTIONS = {
1616
headers: DEFAULT_HEADERS
1717
}
1818

19-
export const DEFAULT_WEBLITE_VERSION = 'v2'
20-
export const WEBLITE_PORT = 8090
19+
export const DEFAULT_API_VERSION = 'v2'
20+
export const DEFAULT_API_PORT = 8090

src/drivers/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,6 @@ export type RowsCallback<T = Record<string, any>> = (error: Error | null, rows?:
136136
export type RowCallback<T = Record<string, any>> = (error: Error | null, row?: T) => void
137137
export type RowCountCallback = (error: Error | null, rowCount?: number) => void
138138
export type PubSubCallback<T = any> = (error: Error | null, results?: T, data?: any) => void
139-
export type PubSubRefactorCallback<T = any> = (error: Error | null, results?: T) => void
140139

141140
/**
142141
* Certain responses include arrays with various types of metadata.

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,3 @@ export {
2020
export { SQLiteCloudRowset, SQLiteCloudRow } from './drivers/rowset'
2121
export { parseconnectionstring, validateConfiguration, getInitializationCommands, sanitizeSQLiteIdentifier } from './drivers/utilities'
2222
export * as protocol from './drivers/protocol'
23-
export { createClient } from './SQLiteCloudClient'

src/refactor/SQLiteCloudClient.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Database } from '../drivers/database'
2+
import { Fetch, fetchWithAuth } from './fetch'
3+
import { SQLiteCloudPubSubClient } from './SQLiteCloudPubSubClient'
4+
import { SQLiteCloudWebliteClient } from './SQLiteCloudWebliteClient'
5+
import { SQLiteCloudFileClient } from './SQLiteCloudFileClient'
6+
import { SQLiteCloudCommand } from '../drivers/types'
7+
import { getDefaultDatabase } from './utils'
8+
9+
interface SQLiteCloudClientConfig {
10+
connectionString: string
11+
fetch?: Fetch
12+
}
13+
14+
15+
16+
export class SQLiteCloudClient {
17+
private connectionString: string
18+
private fetch: Fetch
19+
20+
constructor(config: SQLiteCloudClientConfig | string) {
21+
let connectionString: string
22+
let customFetch: Fetch | undefined
23+
24+
if (typeof config === 'string') {
25+
connectionString = config
26+
} else {
27+
connectionString = config.connectionString
28+
customFetch = config.fetch
29+
}
30+
31+
this.connectionString = connectionString
32+
this.fetch = fetchWithAuth(this.connectionString, customFetch)
33+
this.defaultDb = getDefaultDatabase(this.connectionString) ?? ''
34+
}
35+
36+
async sql(sql: TemplateStringsArray | string | SQLiteCloudCommand, ...values: any[]) {
37+
this.db.exec(`USE DATABASE ${this.defaultDb}`)
38+
try {
39+
const result = await this.db.sql(sql, ...values)
40+
return { data: result, error: null }
41+
} catch (error) {
42+
return { error, data: null }
43+
}
44+
}
45+
46+
get pubSub() {
47+
return new SQLiteCloudPubSubClient(this.db)
48+
}
49+
50+
get db() {
51+
return new Database(this.connectionString)
52+
}
53+
54+
get weblite() {
55+
return new SQLiteCloudWebliteClient(this.connectionString, this.fetch)
56+
}
57+
58+
get files() {
59+
return new SQLiteCloudFileClient(this.connectionString, this.fetch)
60+
}
61+
62+
set defaultDb(dbName: string) {
63+
this.defaultDb = dbName
64+
}
65+
}
66+
67+
export function createClient(config: SQLiteCloudClientConfig | string): SQLiteCloudClient {
68+
return new SQLiteCloudClient(config)
69+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { SQLiteCloudError } from "../drivers/types"
2+
import { getAPIUrl } from "./utils"
3+
import { Fetch, fetchWithAuth } from "./fetch"
4+
5+
interface SQLiteCloudFile {
6+
createBucket(bucket: string, path: string): Promise<Response>
7+
getBucket(bucket: string): Promise<any>
8+
deleteBucket(bucket: string): Promise<Response>
9+
listBuckets(): Promise<any>
10+
upload(bucket: string, pathname: string, file: File | Buffer | Blob | string, options: { contentType: string }): Promise<Response>
11+
download(bucket: string, pathname: string): Promise<Blob>
12+
remove(bucket: string, pathName: string): Promise<Response>
13+
list(bucket: string): Promise<any>
14+
}
15+
16+
const FILES_DATABASE = 'files.sqlite'
17+
18+
export class SQLiteCloudFileClient implements SQLiteCloudFile {
19+
private filesUrl: string
20+
private webliteSQLUrl: string
21+
private fetch: Fetch
22+
23+
constructor(connectionString: string, sql?: Fetch) {
24+
this.filesUrl = getAPIUrl(connectionString, 'files')
25+
this.webliteSQLUrl = getAPIUrl(connectionString, 'weblite/sql')
26+
this.fetch = fetchWithAuth(connectionString, fetch)
27+
}
28+
29+
async createBucket(bucket: string) {
30+
const url = `${this.webliteSQLUrl}?sql=USE DATABASE files; INSERT INTO files (Bucket) VALUES ('${bucket}');`
31+
const response = await this.fetch(url, { method: 'POST' })
32+
if (!response.ok) {
33+
throw new SQLiteCloudError(`Failed to create bucket: ${response.statusText}`)
34+
}
35+
return response.json()
36+
}
37+
38+
async getBucket(bucket: string) {
39+
const url = `${this.filesUrl}/${bucket}`
40+
const response = await this.fetch(url, { method: 'GET' })
41+
if (!response.ok) {
42+
throw new SQLiteCloudError(`Failed to get bucket: ${response.statusText}`)
43+
}
44+
45+
return response.json()
46+
}
47+
48+
async deleteBucket(bucket: string) {
49+
const url = `${this.filesUrl}/${bucket}`
50+
const response = await this.fetch(url, { method: 'DELETE' })
51+
if (!response.ok) {
52+
throw new SQLiteCloudError(`Failed to delete bucket: ${response.statusText}`)
53+
}
54+
return response.json()
55+
}
56+
57+
async listBuckets() {
58+
const encodedUrl = encodeURIComponent(`${this.webliteSQLUrl}?sql=USE DATABASE files.sqlite; SELECT * FROM files`)
59+
const response = await this.fetch(encodedUrl)
60+
if (!response.ok) {
61+
throw new SQLiteCloudError(`Failed to list buckets: ${response.statusText}`)
62+
}
63+
return response.json()
64+
}
65+
66+
async upload(bucket: string, pathname: string, file: File | Buffer | Blob | string, options: { contentType: string }) {
67+
const url = `${this.filesUrl}/${bucket}/${pathname}`;
68+
const headers = {
69+
'Content-Type': options?.contentType || 'application/octet-stream'
70+
}
71+
const response = await this.fetch(url, { method: 'POST', body: file, headers })
72+
if (!response.ok) {
73+
throw new SQLiteCloudError(`Failed to upload file: ${response.statusText}`)
74+
}
75+
return response.json()
76+
}
77+
78+
async download(bucket: string, pathname: string) {
79+
const url = `${this.filesUrl}/${bucket}/${pathname}`;
80+
const response = await this.fetch(url, { method: 'GET' })
81+
if (!response.ok) {
82+
throw new SQLiteCloudError(`Failed to download file: ${response.statusText}`)
83+
}
84+
return response.blob()
85+
}
86+
87+
async remove(bucket: string, pathName: string) {
88+
const url = `${this.filesUrl}/${bucket}/${pathName}`
89+
const response = await this.fetch(url, { method: 'DELETE' })
90+
if (!response.ok) {
91+
throw new SQLiteCloudError(`Failed to remove file: ${response.statusText}`)
92+
}
93+
return response.json()
94+
}
95+
96+
async list(bucket: string) {
97+
const encodedUrl = encodeURIComponent(`${this.webliteSQLUrl}?sql=USE DATABASE files.sqlite; SELECT * FROM files WHERE bucket = '${bucket}'`)
98+
const response = await this.fetch(encodedUrl)
99+
if (!response.ok) {
100+
throw new SQLiteCloudError(`Failed to list files: ${response.statusText}`)
101+
}
102+
return response.json()
103+
}
104+
}
105+

0 commit comments

Comments
 (0)