fivebit-client 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AuthClient.d.ts +8 -0
- package/dist/AuthClient.js +10 -0
- package/dist/FivebitClient.d.ts +16 -2
- package/dist/FivebitClient.js +9 -9
- package/dist/QueryClient.d.ts +5 -0
- package/dist/QueryClient.js +34 -13
- package/dist/RealtimeClient.d.ts +13 -3
- package/dist/RealtimeClient.js +64 -26
- package/dist/StorageClient.d.ts +23 -0
- package/dist/StorageClient.js +43 -0
- package/package.json +1 -1
- package/src/AuthClient.ts +10 -0
- package/src/FivebitClient.ts +11 -13
- package/src/QueryClient.ts +37 -33
- package/src/RealtimeClient.ts +65 -26
- package/src/StorageClient.ts +39 -0
package/dist/AuthClient.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export declare class AuthClient {
|
|
|
17
17
|
} | {
|
|
18
18
|
error: string;
|
|
19
19
|
}>;
|
|
20
|
+
signInWithOAuth(provider: 'google' | 'github', credential: {
|
|
21
|
+
idToken?: string;
|
|
22
|
+
code?: string;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
session: AuthSession;
|
|
25
|
+
} | {
|
|
26
|
+
error: string;
|
|
27
|
+
}>;
|
|
20
28
|
signOut(): Promise<void>;
|
|
21
29
|
get token(): string | null;
|
|
22
30
|
}
|
package/dist/AuthClient.js
CHANGED
|
@@ -23,6 +23,16 @@ class AuthClient {
|
|
|
23
23
|
this._token = data.session.token;
|
|
24
24
|
return data;
|
|
25
25
|
}
|
|
26
|
+
async signInWithOAuth(provider, credential) {
|
|
27
|
+
const r = await fetch(`${this.baseUrl}/api/auth/oauth`, {
|
|
28
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
body: JSON.stringify({ provider, ...credential }),
|
|
30
|
+
});
|
|
31
|
+
const data = await r.json();
|
|
32
|
+
if ('session' in data)
|
|
33
|
+
this._token = data.session.token;
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
26
36
|
async signOut() {
|
|
27
37
|
if (this._token) {
|
|
28
38
|
await fetch(`${this.baseUrl}/api/auth/logout`, {
|
package/dist/FivebitClient.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { AuthClient } from './AuthClient';
|
|
2
|
+
import { RealtimeClient } from './RealtimeClient';
|
|
3
|
+
import { StorageClient } from './StorageClient';
|
|
2
4
|
export interface FivebitConfig {
|
|
3
5
|
url: string;
|
|
4
6
|
apiKey?: string;
|
|
@@ -16,8 +18,9 @@ export interface QueryResult<T> {
|
|
|
16
18
|
export declare class FivebitClient {
|
|
17
19
|
config: FivebitConfig;
|
|
18
20
|
auth: AuthClient;
|
|
21
|
+
storage: StorageClient;
|
|
22
|
+
realtime: RealtimeClient;
|
|
19
23
|
private _query;
|
|
20
|
-
private _rt;
|
|
21
24
|
private _session;
|
|
22
25
|
constructor(config: FivebitConfig);
|
|
23
26
|
get session(): AuthSession | null;
|
|
@@ -28,8 +31,19 @@ export declare class FivebitClient {
|
|
|
28
31
|
update(id: number, record: Partial<T>): Promise<QueryResult<T>>;
|
|
29
32
|
delete(id: number): Promise<boolean>;
|
|
30
33
|
query(field: string, value: string, limit?: number): Promise<T[]>;
|
|
34
|
+
count(filter?: string): Promise<number>;
|
|
35
|
+
sum(field: string, filter?: string): Promise<number>;
|
|
36
|
+
avg(field: string, filter?: string): Promise<number>;
|
|
37
|
+
min(field: string): Promise<number>;
|
|
38
|
+
max(field: string): Promise<number>;
|
|
39
|
+
};
|
|
40
|
+
onChanges(table: string, fn: (record: any) => void): () => void;
|
|
41
|
+
channel(name: string): {
|
|
42
|
+
on(fn: (payload: any) => void): void;
|
|
43
|
+
broadcast(payload: any): void;
|
|
44
|
+
presence(userId: number, userName: string): void;
|
|
45
|
+
readonly users: any[];
|
|
31
46
|
};
|
|
32
|
-
onChanges(table: string, fn: (record: any) => void): void;
|
|
33
47
|
close(): void;
|
|
34
48
|
}
|
|
35
49
|
export declare function createClient(config: FivebitConfig): FivebitClient;
|
package/dist/FivebitClient.js
CHANGED
|
@@ -5,23 +5,23 @@ exports.createClient = createClient;
|
|
|
5
5
|
const AuthClient_1 = require("./AuthClient");
|
|
6
6
|
const QueryClient_1 = require("./QueryClient");
|
|
7
7
|
const RealtimeClient_1 = require("./RealtimeClient");
|
|
8
|
+
const StorageClient_1 = require("./StorageClient");
|
|
8
9
|
class FivebitClient {
|
|
9
10
|
constructor(config) {
|
|
10
11
|
this._session = null;
|
|
11
12
|
this.config = config;
|
|
12
13
|
this.auth = new AuthClient_1.AuthClient(config.url);
|
|
13
|
-
|
|
14
|
-
this.
|
|
14
|
+
const getToken = () => this._session?.token || '';
|
|
15
|
+
this._query = new QueryClient_1.QueryClient(config.url, getToken);
|
|
16
|
+
this.storage = new StorageClient_1.StorageClient(config.url, getToken);
|
|
17
|
+
this.realtime = new RealtimeClient_1.RealtimeClient(config.url);
|
|
15
18
|
}
|
|
16
19
|
get session() { return this._session; }
|
|
17
20
|
setSession(s) { this._session = s; }
|
|
18
|
-
from(table) {
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
this._rt.subscribe(table, fn);
|
|
23
|
-
}
|
|
24
|
-
close() { this._rt.close(); }
|
|
21
|
+
from(table) { return this._query.table(table); }
|
|
22
|
+
onChanges(table, fn) { return this.realtime.subscribe(table, fn); }
|
|
23
|
+
channel(name) { return this.realtime.channel(name); }
|
|
24
|
+
close() { this.realtime.close(); }
|
|
25
25
|
}
|
|
26
26
|
exports.FivebitClient = FivebitClient;
|
|
27
27
|
function createClient(config) {
|
package/dist/QueryClient.d.ts
CHANGED
|
@@ -11,5 +11,10 @@ export declare class QueryClient {
|
|
|
11
11
|
update(id: number, record: Partial<T>): Promise<QueryResult<T>>;
|
|
12
12
|
delete(id: number): Promise<boolean>;
|
|
13
13
|
query(field: string, value: string, limit?: number): Promise<T[]>;
|
|
14
|
+
count(filter?: string): Promise<number>;
|
|
15
|
+
sum(field: string, filter?: string): Promise<number>;
|
|
16
|
+
avg(field: string, filter?: string): Promise<number>;
|
|
17
|
+
min(field: string): Promise<number>;
|
|
18
|
+
max(field: string): Promise<number>;
|
|
14
19
|
};
|
|
15
20
|
}
|
package/dist/QueryClient.js
CHANGED
|
@@ -32,9 +32,7 @@ class QueryClient {
|
|
|
32
32
|
return { data, error: null, etag: newEtag };
|
|
33
33
|
},
|
|
34
34
|
async insert(record) {
|
|
35
|
-
const r = await fetch(`${self.baseUrl}/records`, {
|
|
36
|
-
method: 'POST', headers: self.headers(), body: JSON.stringify(record),
|
|
37
|
-
});
|
|
35
|
+
const r = await fetch(`${self.baseUrl}/records`, { method: 'POST', headers: self.headers(), body: JSON.stringify(record) });
|
|
38
36
|
const data = await r.json();
|
|
39
37
|
return { data, error: r.ok ? null : data.error, etag: '' };
|
|
40
38
|
},
|
|
@@ -43,28 +41,51 @@ class QueryClient {
|
|
|
43
41
|
const etag = self.etagCache.get(id);
|
|
44
42
|
if (etag)
|
|
45
43
|
h['If-Match'] = `"${etag}"`;
|
|
46
|
-
const r = await fetch(`${self.baseUrl}/records/${id}`, {
|
|
47
|
-
method: 'PUT', headers: h, body: JSON.stringify(record),
|
|
48
|
-
});
|
|
44
|
+
const r = await fetch(`${self.baseUrl}/records/${id}`, { method: 'PUT', headers: h, body: JSON.stringify(record) });
|
|
49
45
|
if (r.status === 412)
|
|
50
|
-
return { data: null, error: 'Conflict
|
|
46
|
+
return { data: null, error: 'Conflict', etag: '' };
|
|
51
47
|
const data = await r.json();
|
|
52
48
|
return { data, error: null, etag: '' };
|
|
53
49
|
},
|
|
54
50
|
async delete(id) {
|
|
55
|
-
const r = await fetch(`${self.baseUrl}/records/${id}`, {
|
|
56
|
-
method: 'DELETE', headers: self.headers(),
|
|
57
|
-
});
|
|
51
|
+
const r = await fetch(`${self.baseUrl}/records/${id}`, { method: 'DELETE', headers: self.headers() });
|
|
58
52
|
self.etagCache.delete(id);
|
|
59
53
|
return r.ok;
|
|
60
54
|
},
|
|
61
55
|
async query(field, value, limit = 20) {
|
|
62
|
-
const r = await fetch(`${self.baseUrl}/records?field=${field}&value=${value}&limit=${limit}`, {
|
|
63
|
-
headers: self.headers(),
|
|
64
|
-
});
|
|
56
|
+
const r = await fetch(`${self.baseUrl}/records?field=${field}&value=${value}&limit=${limit}`, { headers: self.headers() });
|
|
65
57
|
const data = await r.json();
|
|
66
58
|
return data.results || [];
|
|
67
59
|
},
|
|
60
|
+
// Aggregates
|
|
61
|
+
async count(filter) {
|
|
62
|
+
const q = filter ? `filter=${filter}&aggregate=count` : 'aggregate=count';
|
|
63
|
+
const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
|
|
64
|
+
const d = await r.json();
|
|
65
|
+
return d.count || 0;
|
|
66
|
+
},
|
|
67
|
+
async sum(field, filter) {
|
|
68
|
+
const q = `aggregate=sum:${field}${filter ? '&filter=' + filter : ''}`;
|
|
69
|
+
const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
|
|
70
|
+
const d = await r.json();
|
|
71
|
+
return d[`sum_${field}`] || 0;
|
|
72
|
+
},
|
|
73
|
+
async avg(field, filter) {
|
|
74
|
+
const q = `aggregate=avg:${field}${filter ? '&filter=' + filter : ''}`;
|
|
75
|
+
const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
|
|
76
|
+
const d = await r.json();
|
|
77
|
+
return d[`avg_${field}`] || 0;
|
|
78
|
+
},
|
|
79
|
+
async min(field) {
|
|
80
|
+
const r = await fetch(`${self.baseUrl}/query?aggregate=min:${field}`, { headers: self.headers() });
|
|
81
|
+
const d = await r.json();
|
|
82
|
+
return d[`min_${field}`] || 0;
|
|
83
|
+
},
|
|
84
|
+
async max(field) {
|
|
85
|
+
const r = await fetch(`${self.baseUrl}/query?aggregate=max:${field}`, { headers: self.headers() });
|
|
86
|
+
const d = await r.json();
|
|
87
|
+
return d[`max_${field}`] || 0;
|
|
88
|
+
},
|
|
68
89
|
};
|
|
69
90
|
}
|
|
70
91
|
}
|
package/dist/RealtimeClient.d.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
type ChangeHandler = (record: any) => void;
|
|
2
|
+
type MessageHandler = (payload: any) => void;
|
|
2
3
|
export declare class RealtimeClient {
|
|
3
4
|
private baseUrl;
|
|
4
|
-
private
|
|
5
|
-
private
|
|
5
|
+
private ws;
|
|
6
|
+
private tableHandlers;
|
|
7
|
+
private channelHandlers;
|
|
8
|
+
private presenceCache;
|
|
6
9
|
private reconnectMs;
|
|
10
|
+
private wsUrl;
|
|
7
11
|
constructor(baseUrl: string);
|
|
8
|
-
subscribe(table: string, fn: ChangeHandler): () => void;
|
|
9
12
|
private _connect;
|
|
13
|
+
subscribe(table: string, fn: ChangeHandler): () => void;
|
|
14
|
+
channel(name: string): {
|
|
15
|
+
on(fn: MessageHandler): void;
|
|
16
|
+
broadcast(payload: any): void;
|
|
17
|
+
presence(userId: number, userName: string): void;
|
|
18
|
+
readonly users: any[];
|
|
19
|
+
};
|
|
10
20
|
close(): void;
|
|
11
21
|
}
|
|
12
22
|
export {};
|
package/dist/RealtimeClient.js
CHANGED
|
@@ -3,40 +3,78 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.RealtimeClient = void 0;
|
|
4
4
|
class RealtimeClient {
|
|
5
5
|
constructor(baseUrl) {
|
|
6
|
-
this.
|
|
7
|
-
this.
|
|
6
|
+
this.ws = null;
|
|
7
|
+
this.tableHandlers = new Map();
|
|
8
|
+
this.channelHandlers = new Map();
|
|
9
|
+
this.presenceCache = [];
|
|
8
10
|
this.reconnectMs = 3000;
|
|
9
11
|
this.baseUrl = baseUrl;
|
|
12
|
+
this.wsUrl = baseUrl.replace('http', 'ws') + '/ws';
|
|
13
|
+
this._connect();
|
|
10
14
|
}
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
es.onmessage = (e) => {
|
|
15
|
+
_connect() {
|
|
16
|
+
if (typeof WebSocket === 'undefined')
|
|
17
|
+
return;
|
|
18
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
19
|
+
this.ws.onopen = () => {
|
|
20
|
+
// Re-subscribe to tables
|
|
21
|
+
for (const table of this.tableHandlers.keys()) {
|
|
22
|
+
this.ws.send(JSON.stringify({ type: 'subscribe', table }));
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
this.ws.onmessage = (e) => {
|
|
23
26
|
try {
|
|
24
|
-
const
|
|
25
|
-
|
|
27
|
+
const msg = JSON.parse(e.data);
|
|
28
|
+
if (msg.type === 'change') {
|
|
29
|
+
this.tableHandlers.get(msg.table)?.forEach(fn => fn(msg.event));
|
|
30
|
+
}
|
|
31
|
+
else if (msg.type === 'broadcast') {
|
|
32
|
+
this.channelHandlers.get(msg.channel)?.forEach(fn => fn(msg.payload));
|
|
33
|
+
}
|
|
34
|
+
else if (msg.type === 'presence') {
|
|
35
|
+
this.presenceCache = msg.users || [];
|
|
36
|
+
this.channelHandlers.get(`__presence:${msg.channel || ''}`)?.forEach(fn => fn(msg));
|
|
37
|
+
}
|
|
26
38
|
}
|
|
27
39
|
catch { }
|
|
28
40
|
};
|
|
29
|
-
|
|
30
|
-
es.close();
|
|
31
|
-
setTimeout(() => this._connect(table), this.reconnectMs);
|
|
32
|
-
};
|
|
33
|
-
this.sources.set(table, es);
|
|
41
|
+
this.ws.onclose = () => setTimeout(() => this._connect(), this.reconnectMs);
|
|
34
42
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.
|
|
43
|
+
// Table changes (backward compat + new)
|
|
44
|
+
subscribe(table, fn) {
|
|
45
|
+
if (!this.tableHandlers.has(table))
|
|
46
|
+
this.tableHandlers.set(table, new Set());
|
|
47
|
+
this.tableHandlers.get(table).add(fn);
|
|
48
|
+
if (this.ws?.readyState === 1) {
|
|
49
|
+
this.ws.send(JSON.stringify({ type: 'subscribe', table }));
|
|
50
|
+
}
|
|
51
|
+
return () => this.tableHandlers.get(table)?.delete(fn);
|
|
52
|
+
}
|
|
53
|
+
// Channels (new)
|
|
54
|
+
channel(name) {
|
|
55
|
+
const self = this;
|
|
56
|
+
return {
|
|
57
|
+
on(fn) {
|
|
58
|
+
if (!self.channelHandlers.has(name))
|
|
59
|
+
self.channelHandlers.set(name, new Set());
|
|
60
|
+
self.channelHandlers.get(name).add(fn);
|
|
61
|
+
if (self.ws?.readyState === 1) {
|
|
62
|
+
self.ws.send(JSON.stringify({ type: 'channel', channel: name }));
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
broadcast(payload) {
|
|
66
|
+
if (self.ws?.readyState === 1) {
|
|
67
|
+
self.ws.send(JSON.stringify({ type: 'broadcast', channel: name, payload }));
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
presence(userId, userName) {
|
|
71
|
+
if (self.ws?.readyState === 1) {
|
|
72
|
+
self.ws.send(JSON.stringify({ type: 'presence', userId, name: userName, channel: name }));
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
get users() { return self.presenceCache; },
|
|
76
|
+
};
|
|
40
77
|
}
|
|
78
|
+
close() { this.ws?.close(); }
|
|
41
79
|
}
|
|
42
80
|
exports.RealtimeClient = RealtimeClient;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare class StorageClient {
|
|
2
|
+
private baseUrl;
|
|
3
|
+
private getToken;
|
|
4
|
+
constructor(baseUrl: string, getToken: () => string);
|
|
5
|
+
bucket(name: string): {
|
|
6
|
+
upload(path: string, data: Blob | Buffer | Uint8Array): Promise<{
|
|
7
|
+
path: string;
|
|
8
|
+
hash: string;
|
|
9
|
+
size: number;
|
|
10
|
+
}>;
|
|
11
|
+
download(path: string): Promise<Blob | null>;
|
|
12
|
+
list(opts?: {
|
|
13
|
+
prefix?: string;
|
|
14
|
+
limit?: number;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
path: string;
|
|
17
|
+
size: number;
|
|
18
|
+
hash: string;
|
|
19
|
+
}[]>;
|
|
20
|
+
remove(path: string): Promise<boolean>;
|
|
21
|
+
};
|
|
22
|
+
private _auth;
|
|
23
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StorageClient = void 0;
|
|
4
|
+
class StorageClient {
|
|
5
|
+
constructor(baseUrl, getToken) {
|
|
6
|
+
this.baseUrl = baseUrl;
|
|
7
|
+
this.getToken = getToken;
|
|
8
|
+
}
|
|
9
|
+
bucket(name) {
|
|
10
|
+
const self = this;
|
|
11
|
+
return {
|
|
12
|
+
async upload(path, data) {
|
|
13
|
+
const form = new FormData();
|
|
14
|
+
const blob = data instanceof Blob ? data : new Blob([data]);
|
|
15
|
+
form.append('file', blob, path);
|
|
16
|
+
const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, {
|
|
17
|
+
method: 'POST', headers: self._auth(), body: form,
|
|
18
|
+
});
|
|
19
|
+
return r.json();
|
|
20
|
+
},
|
|
21
|
+
async download(path) {
|
|
22
|
+
const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, { headers: self._auth() });
|
|
23
|
+
return r.ok ? r.blob() : null;
|
|
24
|
+
},
|
|
25
|
+
async list(opts) {
|
|
26
|
+
const prefix = opts?.prefix || '';
|
|
27
|
+
const limit = opts?.limit || 100;
|
|
28
|
+
const r = await fetch(`${self.baseUrl}/storage/${name}?prefix=${prefix}&limit=${limit}`, { headers: self._auth() });
|
|
29
|
+
const d = await r.json();
|
|
30
|
+
return d.files || [];
|
|
31
|
+
},
|
|
32
|
+
async remove(path) {
|
|
33
|
+
const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, { method: 'DELETE', headers: self._auth() });
|
|
34
|
+
return r.ok;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
_auth() {
|
|
39
|
+
const t = this.getToken();
|
|
40
|
+
return t ? { Authorization: `Bearer ${t}` } : {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.StorageClient = StorageClient;
|
package/package.json
CHANGED
package/src/AuthClient.ts
CHANGED
|
@@ -24,6 +24,16 @@ export class AuthClient {
|
|
|
24
24
|
return data;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
async signInWithOAuth(provider: 'google' | 'github', credential: { idToken?: string; code?: string }): Promise<{ session: AuthSession } | { error: string }> {
|
|
28
|
+
const r = await fetch(`${this.baseUrl}/api/auth/oauth`, {
|
|
29
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify({ provider, ...credential }),
|
|
31
|
+
});
|
|
32
|
+
const data = await r.json();
|
|
33
|
+
if ('session' in data) this._token = data.session.token;
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
async signOut(): Promise<void> {
|
|
28
38
|
if (this._token) {
|
|
29
39
|
await fetch(`${this.baseUrl}/api/auth/logout`, {
|
package/src/FivebitClient.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AuthClient } from './AuthClient';
|
|
2
2
|
import { QueryClient } from './QueryClient';
|
|
3
3
|
import { RealtimeClient } from './RealtimeClient';
|
|
4
|
+
import { StorageClient } from './StorageClient';
|
|
4
5
|
|
|
5
6
|
export interface FivebitConfig { url: string; apiKey?: string; }
|
|
6
7
|
export interface AuthSession { userId: number; token: string; expiresAt: number; }
|
|
@@ -9,30 +10,27 @@ export interface QueryResult<T> { data: T | null; error: string | null; etag: st
|
|
|
9
10
|
export class FivebitClient {
|
|
10
11
|
config: FivebitConfig;
|
|
11
12
|
auth: AuthClient;
|
|
13
|
+
storage: StorageClient;
|
|
14
|
+
realtime: RealtimeClient;
|
|
12
15
|
private _query: QueryClient;
|
|
13
|
-
private _rt: RealtimeClient;
|
|
14
16
|
private _session: AuthSession | null = null;
|
|
15
17
|
|
|
16
18
|
constructor(config: FivebitConfig) {
|
|
17
19
|
this.config = config;
|
|
18
20
|
this.auth = new AuthClient(config.url);
|
|
19
|
-
|
|
20
|
-
this.
|
|
21
|
+
const getToken = () => this._session?.token || '';
|
|
22
|
+
this._query = new QueryClient(config.url, getToken);
|
|
23
|
+
this.storage = new StorageClient(config.url, getToken);
|
|
24
|
+
this.realtime = new RealtimeClient(config.url);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
get session(): AuthSession | null { return this._session; }
|
|
24
|
-
|
|
25
28
|
setSession(s: AuthSession | null) { this._session = s; }
|
|
26
29
|
|
|
27
|
-
from<T = any>(table: string) {
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
onChanges(table: string, fn: (record: any) => void) {
|
|
32
|
-
this._rt.subscribe(table, fn);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
close() { this._rt.close(); }
|
|
30
|
+
from<T = any>(table: string) { return this._query.table<T>(table); }
|
|
31
|
+
onChanges(table: string, fn: (record: any) => void) { return this.realtime.subscribe(table, fn); }
|
|
32
|
+
channel(name: string) { return this.realtime.channel(name); }
|
|
33
|
+
close() { this.realtime.close(); }
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
export function createClient(config: FivebitConfig): FivebitClient {
|
package/src/QueryClient.ts
CHANGED
|
@@ -11,8 +11,7 @@ export class QueryClient {
|
|
|
11
11
|
|
|
12
12
|
private headers(): Record<string, string> {
|
|
13
13
|
const h: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
14
|
-
const t = this.getToken();
|
|
15
|
-
if (t) h['Authorization'] = `Bearer ${t}`;
|
|
14
|
+
const t = this.getToken(); if (t) h['Authorization'] = `Bearer ${t}`;
|
|
16
15
|
return h;
|
|
17
16
|
}
|
|
18
17
|
|
|
@@ -20,51 +19,56 @@ export class QueryClient {
|
|
|
20
19
|
const self = this;
|
|
21
20
|
return {
|
|
22
21
|
async getById(id: number): Promise<QueryResult<T>> {
|
|
23
|
-
const h = self.headers();
|
|
24
|
-
const etag = self.etagCache.get(id);
|
|
22
|
+
const h = self.headers(); const etag = self.etagCache.get(id);
|
|
25
23
|
if (etag) h['If-None-Match'] = `"${etag}"`;
|
|
26
24
|
const r = await fetch(`${self.baseUrl}/records/${id}`, { headers: h });
|
|
27
25
|
if (r.status === 304) return { data: null, error: null, etag: etag || '' };
|
|
28
|
-
const data = await r.json();
|
|
29
|
-
const newEtag = r.headers.get('ETag')?.replace(/"/g, '') || '';
|
|
26
|
+
const data = await r.json(); const newEtag = r.headers.get('ETag')?.replace(/"/g, '') || '';
|
|
30
27
|
if (newEtag) self.etagCache.set(id, newEtag);
|
|
31
28
|
return { data, error: null, etag: newEtag };
|
|
32
29
|
},
|
|
33
|
-
|
|
34
30
|
async insert(record: Omit<T, '_id' | '_hash'>): Promise<QueryResult<T>> {
|
|
35
|
-
const r = await fetch(`${self.baseUrl}/records`, {
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
const data = await r.json();
|
|
39
|
-
return { data, error: r.ok ? null : data.error, etag: '' };
|
|
31
|
+
const r = await fetch(`${self.baseUrl}/records`, { method: 'POST', headers: self.headers(), body: JSON.stringify(record) });
|
|
32
|
+
const data = await r.json(); return { data, error: r.ok ? null : data.error, etag: '' };
|
|
40
33
|
},
|
|
41
|
-
|
|
42
34
|
async update(id: number, record: Partial<T>): Promise<QueryResult<T>> {
|
|
43
|
-
const h = self.headers();
|
|
44
|
-
const etag = self.etagCache.get(id);
|
|
35
|
+
const h = self.headers(); const etag = self.etagCache.get(id);
|
|
45
36
|
if (etag) h['If-Match'] = `"${etag}"`;
|
|
46
|
-
const r = await fetch(`${self.baseUrl}/records/${id}`, {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (r.status === 412) return { data: null, error: 'Conflict — record modified', etag: '' };
|
|
50
|
-
const data = await r.json();
|
|
51
|
-
return { data, error: null, etag: '' };
|
|
37
|
+
const r = await fetch(`${self.baseUrl}/records/${id}`, { method: 'PUT', headers: h, body: JSON.stringify(record) });
|
|
38
|
+
if (r.status === 412) return { data: null, error: 'Conflict', etag: '' };
|
|
39
|
+
const data = await r.json(); return { data, error: null, etag: '' };
|
|
52
40
|
},
|
|
53
|
-
|
|
54
41
|
async delete(id: number): Promise<boolean> {
|
|
55
|
-
const r = await fetch(`${self.baseUrl}/records/${id}`, {
|
|
56
|
-
|
|
57
|
-
});
|
|
58
|
-
self.etagCache.delete(id);
|
|
59
|
-
return r.ok;
|
|
42
|
+
const r = await fetch(`${self.baseUrl}/records/${id}`, { method: 'DELETE', headers: self.headers() });
|
|
43
|
+
self.etagCache.delete(id); return r.ok;
|
|
60
44
|
},
|
|
61
|
-
|
|
62
45
|
async query(field: string, value: string, limit = 20): Promise<T[]> {
|
|
63
|
-
const r = await fetch(`${self.baseUrl}/records?field=${field}&value=${value}&limit=${limit}`, {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
46
|
+
const r = await fetch(`${self.baseUrl}/records?field=${field}&value=${value}&limit=${limit}`, { headers: self.headers() });
|
|
47
|
+
const data = await r.json(); return data.results || [];
|
|
48
|
+
},
|
|
49
|
+
// Aggregates
|
|
50
|
+
async count(filter?: string): Promise<number> {
|
|
51
|
+
const q = filter ? `filter=${filter}&aggregate=count` : 'aggregate=count';
|
|
52
|
+
const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
|
|
53
|
+
const d = await r.json(); return d.count || 0;
|
|
54
|
+
},
|
|
55
|
+
async sum(field: string, filter?: string): Promise<number> {
|
|
56
|
+
const q = `aggregate=sum:${field}${filter ? '&filter=' + filter : ''}`;
|
|
57
|
+
const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
|
|
58
|
+
const d = await r.json(); return d[`sum_${field}`] || 0;
|
|
59
|
+
},
|
|
60
|
+
async avg(field: string, filter?: string): Promise<number> {
|
|
61
|
+
const q = `aggregate=avg:${field}${filter ? '&filter=' + filter : ''}`;
|
|
62
|
+
const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
|
|
63
|
+
const d = await r.json(); return d[`avg_${field}`] || 0;
|
|
64
|
+
},
|
|
65
|
+
async min(field: string): Promise<number> {
|
|
66
|
+
const r = await fetch(`${self.baseUrl}/query?aggregate=min:${field}`, { headers: self.headers() });
|
|
67
|
+
const d = await r.json(); return d[`min_${field}`] || 0;
|
|
68
|
+
},
|
|
69
|
+
async max(field: string): Promise<number> {
|
|
70
|
+
const r = await fetch(`${self.baseUrl}/query?aggregate=max:${field}`, { headers: self.headers() });
|
|
71
|
+
const d = await r.json(); return d[`max_${field}`] || 0;
|
|
68
72
|
},
|
|
69
73
|
};
|
|
70
74
|
}
|
package/src/RealtimeClient.ts
CHANGED
|
@@ -1,41 +1,80 @@
|
|
|
1
1
|
type ChangeHandler = (record: any) => void;
|
|
2
|
+
type MessageHandler = (payload: any) => void;
|
|
2
3
|
|
|
3
4
|
export class RealtimeClient {
|
|
4
5
|
private baseUrl: string;
|
|
5
|
-
private
|
|
6
|
-
private
|
|
6
|
+
private ws: WebSocket | null = null;
|
|
7
|
+
private tableHandlers: Map<string, Set<ChangeHandler>> = new Map();
|
|
8
|
+
private channelHandlers: Map<string, Set<MessageHandler>> = new Map();
|
|
9
|
+
private presenceCache: any[] = [];
|
|
7
10
|
private reconnectMs = 3000;
|
|
11
|
+
private wsUrl: string;
|
|
8
12
|
|
|
9
|
-
constructor(baseUrl: string) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
this.handlers.set(table, new Set());
|
|
14
|
-
this._connect(table);
|
|
15
|
-
}
|
|
16
|
-
this.handlers.get(table)!.add(fn);
|
|
17
|
-
return () => this.handlers.get(table)?.delete(fn);
|
|
13
|
+
constructor(baseUrl: string) {
|
|
14
|
+
this.baseUrl = baseUrl;
|
|
15
|
+
this.wsUrl = baseUrl.replace('http', 'ws') + '/ws';
|
|
16
|
+
this._connect();
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
private _connect(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
private _connect(): void {
|
|
20
|
+
if (typeof WebSocket === 'undefined') return;
|
|
21
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
22
|
+
this.ws.onopen = () => {
|
|
23
|
+
// Re-subscribe to tables
|
|
24
|
+
for (const table of this.tableHandlers.keys()) {
|
|
25
|
+
this.ws!.send(JSON.stringify({ type: 'subscribe', table }));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
this.ws.onmessage = (e) => {
|
|
24
29
|
try {
|
|
25
|
-
const
|
|
26
|
-
|
|
30
|
+
const msg = JSON.parse(e.data);
|
|
31
|
+
if (msg.type === 'change') {
|
|
32
|
+
this.tableHandlers.get(msg.table)?.forEach(fn => fn(msg.event));
|
|
33
|
+
} else if (msg.type === 'broadcast') {
|
|
34
|
+
this.channelHandlers.get(msg.channel)?.forEach(fn => fn(msg.payload));
|
|
35
|
+
} else if (msg.type === 'presence') {
|
|
36
|
+
this.presenceCache = msg.users || [];
|
|
37
|
+
this.channelHandlers.get(`__presence:${msg.channel || ''}`)?.forEach(fn => fn(msg));
|
|
38
|
+
}
|
|
27
39
|
} catch {}
|
|
28
40
|
};
|
|
29
|
-
|
|
30
|
-
es.close();
|
|
31
|
-
setTimeout(() => this._connect(table), this.reconnectMs);
|
|
32
|
-
};
|
|
33
|
-
this.sources.set(table, es);
|
|
41
|
+
this.ws.onclose = () => setTimeout(() => this._connect(), this.reconnectMs);
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
this.
|
|
44
|
+
// Table changes (backward compat + new)
|
|
45
|
+
subscribe(table: string, fn: ChangeHandler): () => void {
|
|
46
|
+
if (!this.tableHandlers.has(table)) this.tableHandlers.set(table, new Set());
|
|
47
|
+
this.tableHandlers.get(table)!.add(fn);
|
|
48
|
+
if (this.ws?.readyState === 1) {
|
|
49
|
+
this.ws.send(JSON.stringify({ type: 'subscribe', table }));
|
|
50
|
+
}
|
|
51
|
+
return () => this.tableHandlers.get(table)?.delete(fn);
|
|
40
52
|
}
|
|
53
|
+
|
|
54
|
+
// Channels (new)
|
|
55
|
+
channel(name: string) {
|
|
56
|
+
const self = this;
|
|
57
|
+
return {
|
|
58
|
+
on(fn: MessageHandler) {
|
|
59
|
+
if (!self.channelHandlers.has(name)) self.channelHandlers.set(name, new Set());
|
|
60
|
+
self.channelHandlers.get(name)!.add(fn);
|
|
61
|
+
if (self.ws?.readyState === 1) {
|
|
62
|
+
self.ws.send(JSON.stringify({ type: 'channel', channel: name }));
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
broadcast(payload: any) {
|
|
66
|
+
if (self.ws?.readyState === 1) {
|
|
67
|
+
self.ws.send(JSON.stringify({ type: 'broadcast', channel: name, payload }));
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
presence(userId: number, userName: string) {
|
|
71
|
+
if (self.ws?.readyState === 1) {
|
|
72
|
+
self.ws.send(JSON.stringify({ type: 'presence', userId, name: userName, channel: name }));
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
get users(): any[] { return self.presenceCache; },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
close(): void { this.ws?.close(); }
|
|
41
80
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class StorageClient {
|
|
2
|
+
private baseUrl: string; private getToken: () => string;
|
|
3
|
+
|
|
4
|
+
constructor(baseUrl: string, getToken: () => string) {
|
|
5
|
+
this.baseUrl = baseUrl; this.getToken = getToken;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
bucket(name: string) {
|
|
9
|
+
const self = this;
|
|
10
|
+
return {
|
|
11
|
+
async upload(path: string, data: Blob | Buffer | Uint8Array): Promise<{ path: string; hash: string; size: number }> {
|
|
12
|
+
const form = new FormData();
|
|
13
|
+
const blob = data instanceof Blob ? data : new Blob([data as any]);
|
|
14
|
+
form.append('file', blob, path);
|
|
15
|
+
const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, {
|
|
16
|
+
method: 'POST', headers: self._auth(), body: form,
|
|
17
|
+
});
|
|
18
|
+
return r.json();
|
|
19
|
+
},
|
|
20
|
+
async download(path: string): Promise<Blob | null> {
|
|
21
|
+
const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, { headers: self._auth() });
|
|
22
|
+
return r.ok ? r.blob() : null;
|
|
23
|
+
},
|
|
24
|
+
async list(opts?: { prefix?: string; limit?: number }): Promise<{ path: string; size: number; hash: string }[]> {
|
|
25
|
+
const prefix = opts?.prefix || ''; const limit = opts?.limit || 100;
|
|
26
|
+
const r = await fetch(`${self.baseUrl}/storage/${name}?prefix=${prefix}&limit=${limit}`, { headers: self._auth() });
|
|
27
|
+
const d = await r.json(); return d.files || [];
|
|
28
|
+
},
|
|
29
|
+
async remove(path: string): Promise<boolean> {
|
|
30
|
+
const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, { method: 'DELETE', headers: self._auth() });
|
|
31
|
+
return r.ok;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private _auth(): Record<string, string> {
|
|
37
|
+
const t = this.getToken(); return t ? { Authorization: `Bearer ${t}` } : {};
|
|
38
|
+
}
|
|
39
|
+
}
|