Skip to content

Commit d3904cd

Browse files
committed
improve client library, apis and error handling
1 parent 27105e1 commit d3904cd

File tree

7 files changed

+255
-124
lines changed

7 files changed

+255
-124
lines changed

src/packages/SQLiteCloudClient.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Database } from '../drivers/database'
22
import { Fetch, fetchWithAuth } from './utils/fetch'
33
import { PubSubClient } from './pubsub/PubSubClient'
4-
import { SQLiteCloudWebliteClient } from './weblite/SQLiteCloudWebliteClient'
5-
import { StorageClient } from './storage/SQLiteCloudStorageClient'
4+
import { WebliteClient } from './weblite/SQLiteCloudWebliteClient'
5+
import { StorageClient } from './storage/StorageClient'
66
import { SQLiteCloudCommand, SQLiteCloudError } from '../drivers/types'
77
import { cleanConnectionString, getDefaultDatabase } from './utils'
88

@@ -59,11 +59,11 @@ export class SQLiteCloudClient {
5959
}
6060

6161
get weblite() {
62-
return new SQLiteCloudWebliteClient(this.connectionString, this.fetch)
62+
return new WebliteClient(this.connectionString, { customFetch: this.fetch })
6363
}
6464

6565
get files() {
66-
return new StorageClient(this.connectionString, this.fetch)
66+
return new StorageClient(this.connectionString, { customFetch: this.fetch })
6767
}
6868

6969
get functions() {

src/packages/functions/FunctionsClient.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DEFAULT_HEADERS } from '../../drivers/constants'
12
import { SQLiteCloudError } from '../../drivers/types'
23
import { FUNCTIONS_ROOT_PATH } from '../constants'
34
import { getAPIUrl } from '../utils'
@@ -17,20 +18,68 @@ export class FunctionsClient {
1718
) {
1819
this.url = getAPIUrl(connectionString, FUNCTIONS_ROOT_PATH)
1920
this.fetch = resolveFetch(options.customFetch)
20-
this.headers = options.headers ?? {}
21+
this.headers = options.headers ? { ...DEFAULT_HEADERS, ...options.headers } : { ...DEFAULT_HEADERS }
2122
}
2223
// auth token is the full connection string with apikey
2324
setAuth(token: string) {
2425
this.headers.Authorization = `Bearer ${token}`
2526
}
2627

27-
async invoke(functionName: string, args: any[]) {
28+
async invoke(functionId: string, args: any[]) {
2829
try {
29-
// TODO IMPLEMENT
30+
const response = await this.fetch(`${this.url}/${functionId}`, {
31+
method: 'POST',
32+
body: JSON.stringify(args),
33+
headers: this.headers
34+
})
35+
if (!response.ok) {
36+
throw new SQLiteCloudError(`Failed to invoke function: ${response.statusText}`)
37+
}
38+
let responseType = (response.headers.get('Content-Type') ?? 'text/plain').split(';')[0].trim()
39+
let data: any
40+
if (responseType === 'application/json') {
41+
data = await response.json()
42+
} else if (responseType === 'application/octet-stream') {
43+
data = await response.blob()
44+
} else if (responseType === 'text/event-stream') {
45+
data = response
46+
} else if (responseType === 'multipart/form-data') {
47+
data = await response.formData()
48+
} else {
49+
// default to text
50+
data = await response.text()
51+
}
52+
return { data, error: null }
3053
} catch (error) {
31-
throw new SQLiteCloudError(`Failed to invoke function: ${error}`)
54+
return { data: null, error }
3255
}
3356
}
34-
35-
3657
}
58+
59+
/**
60+
if (
61+
functionArgs &&
62+
((headers && !Object.prototype.hasOwnProperty.call(headers, 'Content-Type')) || !headers)
63+
) {
64+
if (
65+
(typeof Blob !== 'undefined' && functionArgs instanceof Blob) ||
66+
functionArgs instanceof ArrayBuffer
67+
) {
68+
// will work for File as File inherits Blob
69+
// also works for ArrayBuffer as it is the same underlying structure as a Blob
70+
_headers['Content-Type'] = 'application/octet-stream'
71+
body = functionArgs
72+
} else if (typeof functionArgs === 'string') {
73+
// plain string
74+
_headers['Content-Type'] = 'text/plain'
75+
body = functionArgs
76+
} else if (typeof FormData !== 'undefined' && functionArgs instanceof FormData) {
77+
// don't set content-type headers
78+
// Request will automatically add the right boundary value
79+
body = functionArgs
80+
} else {
81+
// default, assume this is JSON
82+
_headers['Content-Type'] = 'application/json'
83+
body = JSON.stringify(functionArgs)
84+
}
85+
*/

src/packages/pubsub/PubSubClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export class PubSubClient implements PubSub {
125125
return this.pubSubConnection.sql`NOTIFY ${channelName} ${message};`
126126
}
127127

128+
// DOUBLE CHECK THIS
129+
128130
/**
129131
* Ask the server to close the connection to the database and
130132
* to keep only open the Pub/Sub connection.

src/packages/storage/SQLiteCloudStorageClient.ts

Lines changed: 0 additions & 102 deletions
This file was deleted.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { DEFAULT_HEADERS } from "../../drivers/constants"
2+
import { SQLiteCloudError } from "../../drivers/types"
3+
import { getAPIUrl } from "../utils"
4+
import { Fetch, fetchWithAuth } from "../utils/fetch"
5+
6+
interface StorageResponse {
7+
data: any
8+
error: any
9+
}
10+
11+
interface Storage {
12+
createBucket(bucket: string): Promise<StorageResponse>
13+
getBucket(bucket: string): Promise<StorageResponse>
14+
deleteBucket(bucket: string): Promise<StorageResponse>
15+
listBuckets(): Promise<StorageResponse>
16+
upload(bucket: string, pathname: string, file: File | Buffer | Blob | string, options: { contentType: string }): Promise<StorageResponse>
17+
download(bucket: string, pathname: string): Promise<StorageResponse>
18+
remove(bucket: string, pathName: string): Promise<StorageResponse>
19+
list(bucket: string): Promise<StorageResponse>
20+
}
21+
22+
export class StorageClient implements Storage {
23+
protected filesUrl: string
24+
protected webliteSQLUrl: string
25+
protected headers: Record<string, string>
26+
protected fetch: Fetch
27+
28+
constructor(
29+
connectionString: string,
30+
options: {
31+
customFetch?: Fetch,
32+
headers?: Record<string, string>
33+
} = {}) {
34+
this.filesUrl = getAPIUrl(connectionString, 'files')
35+
this.webliteSQLUrl = getAPIUrl(connectionString, 'weblite/sql')
36+
this.fetch = options.customFetch || fetchWithAuth(connectionString)
37+
this.headers = options.headers ? { ...DEFAULT_HEADERS, ...options.headers } : { ...DEFAULT_HEADERS }
38+
}
39+
40+
async createBucket(bucket: string) {
41+
const sql = `USE DATABASE files; INSERT INTO files (Bucket) VALUES ('${bucket}');`
42+
43+
try {
44+
const response = await this.fetch(this.webliteSQLUrl, {
45+
method: 'POST',
46+
body: JSON.stringify({ sql }),
47+
headers: this.headers
48+
})
49+
50+
if (!response.ok) {
51+
throw new SQLiteCloudError(`Failed to create bucket: ${response.statusText}`)
52+
}
53+
54+
return { data: await response.json(), error: null }
55+
} catch (error) {
56+
return { data: null, error }
57+
}
58+
}
59+
60+
async getBucket(bucket: string) {
61+
const url = `${this.filesUrl}/${bucket}`
62+
const response = await this.fetch(url, { method: 'GET', headers: this.headers })
63+
if (!response.ok) {
64+
throw new SQLiteCloudError(`Failed to get bucket: ${response.statusText}`)
65+
}
66+
67+
return { data: await response.json(), error: null }
68+
}
69+
70+
async deleteBucket(bucket: string) {
71+
const url = `${this.filesUrl}/${bucket}`
72+
try {
73+
const response = await this.fetch(url, { method: 'DELETE', headers: this.headers })
74+
if (!response.ok) {
75+
throw new SQLiteCloudError(`Failed to delete bucket: ${response.statusText}`)
76+
}
77+
return { data: await response.json(), error: null }
78+
} catch (error) {
79+
return { data: null, error }
80+
}
81+
}
82+
83+
async listBuckets() {
84+
const sql = `USE DATABASE files.sqlite; SELECT * FROM files;`
85+
try {
86+
const response = await this.fetch(this.webliteSQLUrl, {
87+
method: 'POST',
88+
body: JSON.stringify({ sql }),
89+
headers: this.headers
90+
})
91+
if (!response.ok) {
92+
throw new SQLiteCloudError(`Failed to list buckets: ${response.statusText}`)
93+
}
94+
return { data: await response.json(), error: null }
95+
} catch (error) {
96+
return {
97+
data: null,
98+
error
99+
}
100+
}
101+
}
102+
103+
async upload(bucket: string, pathname: string, file: File | Buffer | Blob | string, options: { contentType: string }) {
104+
const url = `${this.filesUrl}/${bucket}/${pathname}`;
105+
const headers = {
106+
'Content-Type': options?.contentType || 'application/octet-stream'
107+
}
108+
try {
109+
const response = await this.fetch(url, { method: 'POST', body: file, headers })
110+
if (!response.ok) {
111+
throw new SQLiteCloudError(`Failed to upload file: ${response.statusText}`)
112+
}
113+
return { data: await response.json(), error: null }
114+
} catch (error) {
115+
return { data: null, error }
116+
}
117+
}
118+
119+
async download(bucket: string, pathname: string) {
120+
const url = `${this.filesUrl}/${bucket}/${pathname}`;
121+
try {
122+
const response = await this.fetch(url, { method: 'GET' })
123+
if (!response.ok) {
124+
throw new SQLiteCloudError(`Failed to download file: ${response.statusText}`)
125+
}
126+
let responseType = (response.headers.get('Content-Type') ?? 'text/plain').split(';')[0].trim()
127+
let data: any
128+
if (responseType === 'application/json') {
129+
data = await response.json()
130+
} else if (responseType === 'application/octet-stream') {
131+
data = await response.blob()
132+
} else if (responseType === 'text/event-stream') {
133+
data = response
134+
} else if (responseType === 'multipart/form-data') {
135+
data = await response.formData()
136+
} else {
137+
// default to text
138+
data = await response.text()
139+
}
140+
return { data, error: null }
141+
} catch (error) {
142+
return { data: null, error }
143+
}
144+
}
145+
146+
async remove(bucket: string, pathName: string) {
147+
const url = `${this.filesUrl}/${bucket}/${pathName}`
148+
try {
149+
const response = await this.fetch(url, { method: 'DELETE' })
150+
if (!response.ok) {
151+
throw new SQLiteCloudError(`Failed to remove file: ${response.statusText}`)
152+
}
153+
return { data: response.json(), error: null }
154+
} catch (error) {
155+
return { data: null, error }
156+
}
157+
}
158+
159+
async list(bucket: string) {
160+
const sql = `USE DATABASE files.sqlite; SELECT * FROM files WHERE bucket = '${bucket}'`
161+
try {
162+
const response = await this.fetch(this.webliteSQLUrl, {
163+
method: 'POST',
164+
body: JSON.stringify({ sql }),
165+
headers: this.headers
166+
})
167+
if (!response.ok) {
168+
throw new SQLiteCloudError(`Failed to list files: ${response.statusText}`)
169+
}
170+
return { data: await response.json(), error: null }
171+
} catch (error) {
172+
return { data: null, error }
173+
}
174+
}
175+
}

0 commit comments

Comments
 (0)