Skip to content

Commit

Permalink
refactor weblite
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobprall committed Dec 26, 2024
1 parent 8038b49 commit 3c73865
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 111 deletions.
1 change: 0 additions & 1 deletion src/drivers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ if (typeof Deno !== 'undefined') {

export const DEFAULT_HEADERS = {
'X-Client-Info': `sqlitecloud-js-${JS_ENV}/${version}`,
'Content-Type': 'application/octet-stream'
}
export const DEFAULT_GLOBAL_OPTIONS = {
headers: DEFAULT_HEADERS
Expand Down
1 change: 1 addition & 0 deletions src/drivers/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ export class Database extends EventEmitter {
* 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) => {
Expand Down
88 changes: 48 additions & 40 deletions src/packages/SQLiteCloudClient.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,103 @@
import { Database } from '../drivers/database'
import { Fetch, fetchWithAuth } from './utils/fetch'
import { PubSubClient } from './pubsub/PubSubClient'
import { PubSubClient } from './_pubsub/PubSubClient'
import { WebliteClient } from './weblite/WebliteClient'
import { StorageClient } from './storage/StorageClient'
import { SQLiteCloudCommand, SQLiteCloudError } from '../drivers/types'
import { cleanConnectionString, getDefaultDatabase } from './utils'
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 _defaultDb: string
protected _db: Database
protected _globalHeaders: Record<string, string>
protected _db: Database | null
protected _pubSub: PubSubClient | null
protected _weblite: WebliteClient | null

constructor(config: SQLiteCloudClientConfig | string) {
try {
if (!config) {
throw new SQLiteCloudError('Invalid connection string or config')
}
validateConfig(config)
let connectionString: string
let customFetch: Fetch | undefined
let globalHeaders: Record<string, string> = {}

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

this.connectionString = connectionString
this.fetch = fetchWithAuth(this.connectionString, customFetch)
this.globalHeaders = globalHeaders
this._defaultDb = getDefaultDatabase(this.connectionString) ?? ''
this._db = new Database(this.connectionString)
this._globalHeaders = globalHeaders
this._db = null
this._pubSub = null
this._weblite = null

} catch (error) {
throw new SQLiteCloudError('failed to initialize SQLiteCloudClient')
}
}

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 }
}
// Defaults to HTTP API
async sql(sql: TemplateStringsArray | string, ...values: SQLiteCloudDataTypes[]) {
return await this.weblite.sql(sql, ...values)
}

get pubSub() {
return new PubSubClient(this.db.getConfiguration())
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() {
return new WebliteClient(this.connectionString, {
customFetch: this.fetch,
headers: this.globalHeaders
})
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
headers: this._globalHeaders
})
}

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

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

get defaultDb() {
return this._defaultDb
}
}

export function createClient(config: SQLiteCloudClientConfig | string): SQLiteCloudClient {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Fetch, fetchWithAuth } from "../utils/fetch"
import { Storage } from "../types"

// TODO: add consistent return types

export class StorageClient implements Storage {
protected filesUrl: string
protected webliteSQLUrl: string
Expand All @@ -16,30 +17,33 @@ export class StorageClient implements Storage {
options: {
customFetch?: Fetch,
headers?: Record<string, string>
} = {}) {
} = {
headers: {}
}) {
this.filesUrl = getAPIUrl(connectionString, 'files')
this.webliteSQLUrl = getAPIUrl(connectionString, 'weblite/sql')
this.fetch = options.customFetch || fetchWithAuth(connectionString)
this.headers = options.headers ? { ...DEFAULT_HEADERS, ...options.headers } : { ...DEFAULT_HEADERS }
this.headers = { ...DEFAULT_HEADERS, ...options.headers }
}

async createBucket(bucket: string) {
const sql = `USE DATABASE files; INSERT INTO files (Bucket) VALUES ('${bucket}');`

try {
const response = await this.fetch(this.webliteSQLUrl, {
method: 'POST',
body: JSON.stringify({ sql }),
headers: this.headers
method: 'POST',
body: JSON.stringify({
database: 'files.sqlite',
sql: `INSERT INTO files (Bucket, Pathname, Data) VALUES ('${bucket}', '/', '' );` }
),
headers: this.headers,
})

if (!response.ok) {
throw new SQLiteCloudError(`Failed to create bucket: ${response.statusText}`)
}

return { data: await response.json(), error: null }
return await response.json();
} catch (error) {
return { data: null, error }
return { error, data: null, metadata: null }
}
}

Expand Down
27 changes: 27 additions & 0 deletions src/packages/test/client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CHINOOK_DATABASE_URL } from '../../../test/shared'
import { SQLiteCloudClient } from '../SQLiteCloudClient'

const DEFAULT_TABLE_NAME = 'albums';

const client = new SQLiteCloudClient(CHINOOK_DATABASE_URL)

describe('SQLiteCloudClient test suite', () => {
it('should be able to create a client', () => {
expect(client).toBeDefined()
expect(client).toBeInstanceOf(SQLiteCloudClient)
})

it('should throw errors if no valid params are provided', () => {
expect(() => new SQLiteCloudClient('')).toThrow()
expect(() => new SQLiteCloudClient({ connectionString: '' })).toThrow()
expect(() => new SQLiteCloudClient({ connectionString: 'invalid' })).toThrow()

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical test

The hard-coded value "invalid" is used as
authorization header
.
})

it('should be able to query the database', async () => {
const { data, error } = await client.sql`SELECT * FROM ${DEFAULT_TABLE_NAME}`;

expect(data).toBeDefined()
expect(error).toBeNull()
})

})
31 changes: 25 additions & 6 deletions src/packages/test/storage.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
import { expect } from '@jest/globals'
import { StorageClient } from '../storage/StorageClient'
import { StorageClient } from '../_storage/StorageClient'
import { CHINOOK_DATABASE_URL } from '../../../test/shared'

const TEST_BUCKET_NAME = 'test_bucket'

const storage = new StorageClient(CHINOOK_DATABASE_URL)
const storage = new StorageClient(CHINOOK_DATABASE_URL,
{
headers: {
'Content-Type': 'application/json'
}
}
)

describe('StorageClient', () => {
it('should be able to create a bucket', async () => {
expect(storage).toBeDefined()
const getBucketResponse = await storage.getBucket(TEST_BUCKET_NAME)
console.log(getBucketResponse)

const bucket = await storage.createBucket('test-bucket')

expect(bucket).toBeDefined()
const { data, error } = await storage.createBucket(TEST_BUCKET_NAME)
console.log(data, error)
expect(error).toBeNull()
expect(data).toBeDefined()
})
})

it('should get a bucket', async () => {
expect(storage).toBeDefined()

const { data, error } = await storage.getBucket(TEST_BUCKET_NAME)
console.log(data)
expect(error).toBeNull()
expect(data).toBeDefined()
})


})
1 change: 0 additions & 1 deletion src/packages/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ interface StorageResponse {
* @param list - List all files in a bucket.
*/
interface Storage {
createBucket(bucket: string): Promise<StorageResponse>
getBucket(bucket: string): Promise<StorageResponse>
deleteBucket(bucket: string): Promise<StorageResponse>
listBuckets(): Promise<StorageResponse>
Expand Down
2 changes: 1 addition & 1 deletion src/packages/utils/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const fetchWithAuth = (authorization: string, customFetch?: Fetch): Fetch
const fetch = resolveFetch(customFetch)
const HeadersConstructor = resolveHeadersConstructor()

return async (input, init) => {
return (input, init) => {
const headers = new HeadersConstructor(init?.headers)
if (!headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${authorization}`)
Expand Down
Loading

0 comments on commit 3c73865

Please sign in to comment.