Skip to content

Commit f99767e

Browse files
committed
add upload, download, fetch, create client
1 parent 1b81eb7 commit f99767e

File tree

10 files changed

+199
-14
lines changed

10 files changed

+199
-14
lines changed

bun.lockb

487 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sqlitecloud/drivers",
3-
"version": "1.0.354",
3+
"version": "1.0.355",
44
"description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients",
55
"main": "./lib/index.js",
66
"types": "./lib/index.d.ts",

src/SQLiteCloudClient.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Database } from './drivers/database';
2+
import { SQLiteCloudError } from './drivers/types';
3+
import { Fetch, fetchWithAuth } from './drivers/fetch';
4+
import { DEFAULT_HEADERS, DEFAULT_WEBLITE_VERSION, WEBLITE_PORT } from './drivers/constants';
5+
6+
interface SQLiteCloudClientConfig {
7+
connectionString: string;
8+
fetch?: Fetch;
9+
}
10+
11+
const parseConnectionString = (connectionString: string) => {
12+
const url = new URL(connectionString);
13+
return {
14+
host: url.hostname,
15+
port: url.port,
16+
database: url.pathname.slice(1),
17+
apiKey: url.searchParams.get('apikey'),
18+
}
19+
}
20+
21+
type UploadOptions = {
22+
replace?: boolean;
23+
}
24+
25+
class SQLiteCloudClient {
26+
// TODO: Add support for custom fetch
27+
private fetch: Fetch
28+
private connectionString: string
29+
private webliteUrl: string
30+
public db: Database
31+
32+
constructor(config: SQLiteCloudClientConfig | string) {
33+
let connectionString: string;
34+
if (typeof config === 'string') {
35+
connectionString = config;
36+
} else {
37+
connectionString = config.connectionString;
38+
}
39+
// TODO: validate connection string
40+
this.connectionString = connectionString;
41+
this.db = new Database(this.connectionString);
42+
43+
const {
44+
host,
45+
apiKey,
46+
} = parseConnectionString(this.connectionString);
47+
48+
if (!apiKey) {
49+
throw new SQLiteCloudError('apiKey is required');
50+
}
51+
52+
this.fetch = fetchWithAuth(this.connectionString);
53+
this.webliteUrl = `https://${host}:${WEBLITE_PORT}/${DEFAULT_WEBLITE_VERSION}/weblite`;
54+
}
55+
56+
async upload(databaseName: string, file: File | Buffer | Blob | string, opts: UploadOptions = {}) {
57+
const url = `${this.webliteUrl}/${databaseName}`;
58+
let body;
59+
if (file instanceof File) {
60+
body = file;
61+
} else if (file instanceof Buffer) {
62+
body = file;
63+
} else if (file instanceof Blob) {
64+
body = file;
65+
} else {
66+
// string
67+
body = new Blob([file]);
68+
}
69+
70+
const headers = {
71+
'Content-Type': 'application/octet-stream',
72+
'X-Client-Info': DEFAULT_HEADERS['X-Client-Info'],
73+
}
74+
75+
const method = opts.replace ? 'PATCH' : 'POST';
76+
77+
const response = await this.fetch(url, { method, body, headers })
78+
79+
if (!response.ok) {
80+
throw new SQLiteCloudError(`Failed to upload database: ${response.statusText}`);
81+
}
82+
83+
return response;
84+
}
85+
86+
async download(databaseName: string) {
87+
const url = `${this.webliteUrl}/${databaseName}`;
88+
const response = await this.fetch(url, { method: 'GET' })
89+
if (!response.ok) {
90+
throw new SQLiteCloudError(`Failed to download database: ${response.statusText}`);
91+
}
92+
const isNode = typeof window === 'undefined';
93+
return isNode ? await response.arrayBuffer() : await response.blob();
94+
}
95+
96+
async delete(databaseName: string) {
97+
const url = `${this.webliteUrl}/${databaseName}`;
98+
const response = await this.fetch(url, { method: 'DELETE' })
99+
if (!response.ok) {
100+
throw new SQLiteCloudError(`Failed to delete database: ${response.statusText}`);
101+
}
102+
return response;
103+
}
104+
105+
async listDatabases() {
106+
const url = `${this.webliteUrl}/databases`;
107+
const response = await this.fetch(url, { method: 'GET' })
108+
if (!response.ok) {
109+
throw new SQLiteCloudError(`Failed to list databases: ${response.statusText}`);
110+
}
111+
return await response.json();
112+
}
113+
}
114+
115+
export function createClient(config: SQLiteCloudClientConfig | string) {
116+
return new SQLiteCloudClient(config);
117+
}

src/drivers/constants.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
const version = '0.0.1'
3+
let JS_ENV = ''
4+
// @ts-ignore
5+
if (typeof Deno !== 'undefined') {
6+
JS_ENV = 'deno'
7+
} else if (typeof document !== 'undefined') {
8+
JS_ENV = 'web'
9+
} else if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
10+
JS_ENV = 'react-native'
11+
} else {
12+
JS_ENV = 'node'
13+
}
14+
15+
export const DEFAULT_HEADERS = { 'X-Client-Info': `sqlitecloud-js-${JS_ENV}/${version}` }
16+
export const DEFAULT_GLOBAL_OPTIONS = {
17+
headers: DEFAULT_HEADERS,
18+
}
19+
20+
export const DEFAULT_WEBLITE_VERSION = 'v2'
21+
export const WEBLITE_PORT = 8090

src/drivers/fetch.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
import nodeFetch, { Headers as NodeFetchHeaders } from 'node-fetch'
3+
4+
export type Fetch = typeof fetch
5+
6+
export const resolveFetch = (customFetch?: Fetch): Fetch => {
7+
let _fetch: Fetch
8+
if (customFetch) {
9+
_fetch = customFetch
10+
} else if (typeof fetch !== 'undefined') {
11+
_fetch = nodeFetch as unknown as Fetch
12+
} else {
13+
_fetch = fetch
14+
}
15+
return (...args: Parameters<Fetch>) => _fetch(...args)
16+
}
17+
18+
export const resolveHeadersConstructor = () => {
19+
if (typeof Headers === 'undefined') {
20+
return NodeFetchHeaders
21+
}
22+
23+
return Headers
24+
}
25+
26+
27+
export const fetchWithAuth = (authorization: string, customFetch?: Fetch): Fetch => {
28+
const fetch = resolveFetch(customFetch);
29+
const HeadersConstructor = resolveHeadersConstructor();
30+
31+
32+
return async (input, init) => {
33+
const headers = new HeadersConstructor(init?.headers);
34+
if (!headers.has('Authorization')) {
35+
headers.set('Authorization', `Bearer ${authorization}`)
36+
}
37+
// @ts-ignore
38+
return fetch(input, { ...init, headers })
39+
}
40+
}

src/drivers/pubsub.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { SQLiteCloudConnection } from './connection'
22
import SQLiteCloudTlsConnection from './connection-tls'
33
import { PubSubCallback } from './types'
44

5-
export enum PUBSUB_ENTITY_TYPE {
6-
TABLE = 'TABLE',
7-
CHANNEL = 'CHANNEL'
5+
interface PubSubOptions {
6+
tableName: string
7+
dbName?: string
88
}
99

10+
1011
/**
1112
* Pub/Sub class to receive changes on database tables or to send messages to channels.
1213
*/
@@ -20,26 +21,31 @@ export class PubSub {
2021
private connectionPubSub: SQLiteCloudConnection
2122

2223
/**
23-
* Listen for a table or channel and start to receive messages to the provided callback.
24-
* @param entityType One of TABLE or CHANNEL'
25-
* @param entityName Name of the table or the channel
24+
* Listen to a channel and start to receive messages to the provided callback.
25+
* @param options Options for the listen operation
2626
* @param callback Callback to be called when a message is received
27-
* @param data Extra data to be passed to the callback
2827
*/
29-
public async listen(entityType: PUBSUB_ENTITY_TYPE, entityName: string, callback: PubSubCallback, data?: any): Promise<any> {
30-
const entity = entityType === 'TABLE' ? 'TABLE ' : ''
3128

32-
const authCommand: string = await this.connection.sql(`LISTEN ${entity}${entityName};`)
29+
public async listen<T>(options: PubSubOptions, callback: PubSubCallback): Promise<T> {
30+
if (options.dbName) {
31+
try {
32+
await this.connection.sql(`USE DATABASE ${options.dbName};`)
33+
} catch (error) {
34+
console.error(error)
35+
}
36+
}
37+
38+
const authCommand: string = await this.connection.sql(`LISTEN TABLE ${options.tableName};`)
3339

3440
return new Promise((resolve, reject) => {
3541
this.connectionPubSub.sendCommands(authCommand, (error, results) => {
3642
if (error) {
37-
callback.call(this, error, null, data)
43+
callback.call(this, error, null)
3844
reject(error)
3945
} else {
4046
// skip results from pubSub auth command
4147
if (results !== 'OK') {
42-
callback.call(this, null, results, data)
48+
callback.call(this, null, results)
4349
}
4450
resolve(results)
4551
}

src/drivers/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export type ResultsCallback<T = any> = (error: Error | null, results?: T) => voi
135135
export type RowsCallback<T = Record<string, any>> = (error: Error | null, rows?: T[]) => void
136136
export type RowCallback<T = Record<string, any>> = (error: Error | null, row?: T) => void
137137
export type RowCountCallback = (error: Error | null, rowCount?: number) => void
138-
export type PubSubCallback<T = any> = (error: Error | null, results?: T, extraData?: T) => void
138+
export type PubSubCallback<T = any> = (error: Error | null, results?: T) => void
139139

140140
/**
141141
* Certain responses include arrays with various types of metadata.

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ 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'

test-db-2.sqlite

8 KB
Binary file not shown.

test-db.sqlite

8 KB
Binary file not shown.

0 commit comments

Comments
 (0)