Skip to content

Commit

Permalink
improve client library, apis and error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobprall committed Dec 25, 2024
1 parent 27105e1 commit d3904cd
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 124 deletions.
8 changes: 4 additions & 4 deletions src/packages/SQLiteCloudClient.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Database } from '../drivers/database'
import { Fetch, fetchWithAuth } from './utils/fetch'
import { PubSubClient } from './pubsub/PubSubClient'
import { SQLiteCloudWebliteClient } from './weblite/SQLiteCloudWebliteClient'
import { StorageClient } from './storage/SQLiteCloudStorageClient'
import { WebliteClient } from './weblite/SQLiteCloudWebliteClient'
import { StorageClient } from './storage/StorageClient'
import { SQLiteCloudCommand, SQLiteCloudError } from '../drivers/types'
import { cleanConnectionString, getDefaultDatabase } from './utils'

Expand Down Expand Up @@ -59,11 +59,11 @@ export class SQLiteCloudClient {
}

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

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

get functions() {
Expand Down
61 changes: 55 additions & 6 deletions src/packages/functions/FunctionsClient.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_HEADERS } from '../../drivers/constants'
import { SQLiteCloudError } from '../../drivers/types'
import { FUNCTIONS_ROOT_PATH } from '../constants'
import { getAPIUrl } from '../utils'
Expand All @@ -17,20 +18,68 @@ export class FunctionsClient {
) {
this.url = getAPIUrl(connectionString, FUNCTIONS_ROOT_PATH)
this.fetch = resolveFetch(options.customFetch)
this.headers = options.headers ?? {}
this.headers = options.headers ? { ...DEFAULT_HEADERS, ...options.headers } : { ...DEFAULT_HEADERS }
}
// auth token is the full connection string with apikey
setAuth(token: string) {
this.headers.Authorization = `Bearer ${token}`
}

async invoke(functionName: string, args: any[]) {
async invoke(functionId: string, args: any[]) {
try {
// TODO IMPLEMENT
const response = await this.fetch(`${this.url}/${functionId}`, {
method: 'POST',
body: JSON.stringify(args),
headers: this.headers
})
if (!response.ok) {
throw new SQLiteCloudError(`Failed to invoke function: ${response.statusText}`)
}
let responseType = (response.headers.get('Content-Type') ?? 'text/plain').split(';')[0].trim()
let data: any
if (responseType === 'application/json') {
data = await response.json()
} else if (responseType === 'application/octet-stream') {
data = await response.blob()
} else if (responseType === 'text/event-stream') {
data = response
} else if (responseType === 'multipart/form-data') {
data = await response.formData()
} else {
// default to text
data = await response.text()
}
return { data, error: null }
} catch (error) {
throw new SQLiteCloudError(`Failed to invoke function: ${error}`)
return { data: null, error }
}
}


}

/**
if (
functionArgs &&
((headers && !Object.prototype.hasOwnProperty.call(headers, 'Content-Type')) || !headers)
) {
if (
(typeof Blob !== 'undefined' && functionArgs instanceof Blob) ||
functionArgs instanceof ArrayBuffer
) {
// will work for File as File inherits Blob
// also works for ArrayBuffer as it is the same underlying structure as a Blob
_headers['Content-Type'] = 'application/octet-stream'
body = functionArgs
} else if (typeof functionArgs === 'string') {
// plain string
_headers['Content-Type'] = 'text/plain'
body = functionArgs
} else if (typeof FormData !== 'undefined' && functionArgs instanceof FormData) {
// don't set content-type headers
// Request will automatically add the right boundary value
body = functionArgs
} else {
// default, assume this is JSON
_headers['Content-Type'] = 'application/json'
body = JSON.stringify(functionArgs)
}
*/
2 changes: 2 additions & 0 deletions src/packages/pubsub/PubSubClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export class PubSubClient implements PubSub {
return this.pubSubConnection.sql`NOTIFY ${channelName} ${message};`
}

// DOUBLE CHECK THIS

/**
* Ask the server to close the connection to the database and
* to keep only open the Pub/Sub connection.
Expand Down
102 changes: 0 additions & 102 deletions src/packages/storage/SQLiteCloudStorageClient.ts

This file was deleted.

175 changes: 175 additions & 0 deletions src/packages/storage/StorageClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { DEFAULT_HEADERS } from "../../drivers/constants"
import { SQLiteCloudError } from "../../drivers/types"
import { getAPIUrl } from "../utils"
import { Fetch, fetchWithAuth } from "../utils/fetch"

interface StorageResponse {
data: any
error: any
}

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

export class StorageClient implements Storage {
protected filesUrl: string
protected webliteSQLUrl: string
protected headers: Record<string, string>
protected fetch: Fetch

constructor(
connectionString: string,
options: {
customFetch?: Fetch,
headers?: Record<string, string>
} = {}) {
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 }
}

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
})

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

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

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

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

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

async listBuckets() {
const sql = `USE DATABASE files.sqlite; SELECT * FROM files;`
try {
const response = await this.fetch(this.webliteSQLUrl, {
method: 'POST',
body: JSON.stringify({ sql }),
headers: this.headers
})
if (!response.ok) {
throw new SQLiteCloudError(`Failed to list buckets: ${response.statusText}`)
}
return { data: await response.json(), error: null }
} catch (error) {
return {
data: null,
error
}
}
}

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'
}
try {
const response = await this.fetch(url, { method: 'POST', body: file, headers })
if (!response.ok) {
throw new SQLiteCloudError(`Failed to upload file: ${response.statusText}`)
}
return { data: await response.json(), error: null }
} catch (error) {
return { data: null, error }
}
}

async download(bucket: string, pathname: string) {
const url = `${this.filesUrl}/${bucket}/${pathname}`;
try {
const response = await this.fetch(url, { method: 'GET' })
if (!response.ok) {
throw new SQLiteCloudError(`Failed to download file: ${response.statusText}`)
}
let responseType = (response.headers.get('Content-Type') ?? 'text/plain').split(';')[0].trim()
let data: any
if (responseType === 'application/json') {
data = await response.json()
} else if (responseType === 'application/octet-stream') {
data = await response.blob()
} else if (responseType === 'text/event-stream') {
data = response
} else if (responseType === 'multipart/form-data') {
data = await response.formData()
} else {
// default to text
data = await response.text()
}
return { data, error: null }
} catch (error) {
return { data: null, error }
}
}

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

async list(bucket: string) {
const sql = `USE DATABASE files.sqlite; SELECT * FROM files WHERE bucket = '${bucket}'`
try {
const response = await this.fetch(this.webliteSQLUrl, {
method: 'POST',
body: JSON.stringify({ sql }),
headers: this.headers
})
if (!response.ok) {
throw new SQLiteCloudError(`Failed to list files: ${response.statusText}`)
}
return { data: await response.json(), error: null }
} catch (error) {
return { data: null, error }
}
}
}
Loading

0 comments on commit d3904cd

Please sign in to comment.