Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jacob/create client #182

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README_Refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
Refactor 12.23.24
- Added SQLiteCloudClient class and createClient function
- Extracted PubSub from Database to SQLiteCloudClient
- Added fetch, customFetch support and fetchWithAuth
- Added Weblite 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
- Added SQLiteCloudVectorClient class and methods for upsert and query

Refactor Summary
- The Plan:
- Improve the usability of the SQLite Cloud platform by consolidating various features
under a single client with one consistent design and interface
The Objective:
- Provide a streamlined and consistent api for discovering, learning and using features on SQLite Cloud
- Improve the visibility of various features on the SQLite Cloud platform by providing explicit namespaces and methods for:
- functions
- file storage
- Pub/Sub
- Vector search
- Weblite (platform-level database management)
- database (core database connection)
- Increase adoption of SQLite Cloud's JS SDK by expanding our documentation.
- Provide a solid architecture for future SDK development.
- Goals:
- Streamline the onboarding and implementation process for building JS apps on SQLite Cloud

Guidelines:
- Use consistent and scalable designs to improve readability, usability and maintainability.

Scope of work:
- refactor new code to improve code smells and readability
- Recap progress.
- packages
- functions:
- Purpose: used to interact with edge functions deployed on the SQLite Cloud platform
- Value: removes need for custom client
- Objective: simplify the onboarding and use of edge functions to increase adoption
- storage:
- Purpose: used to store files, with an emphasis on images and photos
- Value: unifies development experience of handling transactional and non-transactional data
- Objective: simplify the development process
- pubsub:
- Purpose: used to interact with the SQLite Cloud pubsub platform
- Value: removes need for custom client
- Objective: simplify the onboarding and use of pubsub to increase adoption
- write tests for each new class
- Idenitfy protential issues
- Plan refactor with psuedo code
- Implement refactor
- Test refactor

TODO:
- add error handling and logging
- add tests
- add comments
- add documentation


Out of scope:
- Auth module (awaiting auth.js merge)
- Vector search module
Binary file modified bun.lockb
Binary file not shown.
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/packages/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)
})

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sqlitecloud/drivers",
"version": "1.0.354",
"version": "1.0.360",
"description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
22 changes: 22 additions & 0 deletions src/drivers/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const version = '0.0.1'
let JS_ENV = ''
// @ts-ignore
if (typeof Deno !== 'undefined') {
JS_ENV = 'deno'
} else if (typeof document !== 'undefined') {
JS_ENV = 'web'
} else if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
JS_ENV = 'react-native'
} else {
JS_ENV = 'node'
}

export const DEFAULT_HEADERS = {
'X-Client-Info': `sqlitecloud-js-${JS_ENV}/${version}`,
}
export const DEFAULT_GLOBAL_OPTIONS = {
headers: DEFAULT_HEADERS
}

export const DEFAULT_API_VERSION = 'v2'
export const DEFAULT_API_PORT = 8090
26 changes: 13 additions & 13 deletions src/drivers/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { popCallback } from './utilities'
import { ErrorCallback, ResultsCallback, RowCallback, RowsCallback } from './types'
import EventEmitter from 'eventemitter3'
import { isBrowser } from './utilities'
import { PubSub } from './pubsub'
import { Statement } from './statement'
import { PubSub } from './pubsub'

// Uses eventemitter3 instead of node events for browser compatibility
// https://github.com/primus/eventemitter3
Expand Down Expand Up @@ -479,24 +479,24 @@ export class Database extends EventEmitter {
})
})
}

/**
/**
* PubSub class provides a Pub/Sub real-time updates and notifications system to
* allow multiple applications to communicate with each other asynchronously.
* It allows applications to subscribe to tables and receive notifications whenever
* data changes in the database table. It also enables sending messages to anyone
* subscribed to a specific channel.
* @returns {PubSub} A PubSub object
* DEPRECATED: use PubSubClient instead
*/
public async getPubSub(): Promise<PubSub> {
return new Promise((resolve, reject) => {
this.getConnection((error, connection) => {
if (error || !connection) {
reject(error)
} else {
resolve(new PubSub(connection))
}
public async getPubSub(): Promise<PubSub> {
return new Promise((resolve, reject) => {
this.getConnection((error, connection) => {
if (error || !connection) {
reject(error)
} else {
resolve(new PubSub(connection))
}
})
})
})
}
}
}
3 changes: 2 additions & 1 deletion src/drivers/pubsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export class PubSub {
* @param data Extra data to be passed to the callback
*/
public async listen(entityType: PUBSUB_ENTITY_TYPE, entityName: string, callback: PubSubCallback, data?: any): Promise<any> {
const entity = entityType === 'TABLE' ? 'TABLE ' : ''
// should not force user to import and pass in entity type
const entity = entityType === 'TABLE' ? 'TABLE ' : '' // should use PUBSUB_ENTITY_TYPE for check

const authCommand: string = await this.connection.sql(`LISTEN ${entity}${entityName};`)

Expand Down
7 changes: 6 additions & 1 deletion src/drivers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export type ResultsCallback<T = any> = (error: Error | null, results?: T) => voi
export type RowsCallback<T = Record<string, any>> = (error: Error | null, rows?: T[]) => void
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, extraData?: T) => void
export type PubSubCallback<T = any> = (error: Error | null, results?: T, data?: any) => void

/**
* Certain responses include arrays with various types of metadata.
Expand All @@ -160,3 +160,8 @@ export enum SQLiteCloudArrayType {

ARRAY_TYPE_SQLITE_STATUS = 50 // used in sqlite_status
}

export type UploadOptions = {
replace?: boolean
headers?: Record<string, string>
}
105 changes: 105 additions & 0 deletions src/packages/SQLiteCloudClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Database } from '../drivers/database'
import { Fetch, fetchWithAuth } from './utils/fetch'
import { PubSubClient } from './_pubsub/PubSubClient'
import { WebliteClient } from './weblite/WebliteClient'
import { StorageClient } from './_storage/StorageClient'
import { SQLiteCloudDataTypes, SQLiteCloudError } from '../drivers/types'
import { cleanConnectionString } from './utils'
import { FunctionsClient } from './_functions/FunctionsClient'
import { SQLiteCloudClientConfig } from './types'
import { DEFAULT_HEADERS } from '../drivers/constants'

const validateConfig = (config: SQLiteCloudClientConfig | string) => {
if (!(config)) throw new SQLiteCloudError('No configuration provided')
if (typeof config === 'string') {
if (!config.includes('sqlitecloud://')) throw new SQLiteCloudError('Invalid connection string')
}

if (typeof config === 'object') {
if (!config.connectionString) throw new SQLiteCloudError('No connection string provided')
if (!config.connectionString.includes('sqlitecloud://')) throw new SQLiteCloudError('Invalid connection string')
}
}

export class SQLiteCloudClient {
protected connectionString: string
protected fetch: Fetch
protected _globalHeaders: Record<string, string>
protected _db: Database | null
protected _pubSub: PubSubClient | null
protected _weblite: WebliteClient | null

constructor(config: SQLiteCloudClientConfig | string) {
try {
validateConfig(config)
let connectionString: string
let customFetch: Fetch | undefined
let globalHeaders: Record<string, string> = {}

if (typeof config === 'string') {
connectionString = cleanConnectionString(config)
globalHeaders = DEFAULT_HEADERS
} else {
connectionString = config.connectionString
customFetch = config.global?.fetch
globalHeaders = config.global?.headers ? { ...DEFAULT_HEADERS, ...config.global.headers } : DEFAULT_HEADERS
}

this.connectionString = connectionString
this.fetch = fetchWithAuth(this.connectionString, customFetch)
this._globalHeaders = globalHeaders
this._db = null
this._pubSub = null
this._weblite = null

} catch (error) {
throw new SQLiteCloudError('failed to initialize SQLiteCloudClient')
}
}
// Defaults to HTTP API
async sql(sql: TemplateStringsArray | string, ...values: SQLiteCloudDataTypes[]) {
return await this.weblite.sql(sql, ...values)
}

get pubSub() {
if (!this._pubSub) {
this._pubSub = new PubSubClient(this.db.getConfiguration())
}
return this._pubSub
}

get db() {
if (!this._db) {
this._db = new Database(this.connectionString)
}
return this._db
}

get weblite() {
if (!this._weblite) {
this._weblite = new WebliteClient(this.connectionString, {
fetch: this.fetch,
headers: this._globalHeaders
})
}
return this._weblite
}

get files() {
return new StorageClient(this.connectionString, {
customFetch: this.fetch,
headers: this._globalHeaders
})
}

get functions() {
return new FunctionsClient(this.connectionString, {
customFetch: this.fetch,
headers: this._globalHeaders
})
}
}

export function createClient(config: SQLiteCloudClientConfig | string): SQLiteCloudClient {
return new SQLiteCloudClient(config)
}
Loading
Loading