openbase-js 0.1.4 → 0.1.6
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/index.cjs +126 -7
- package/index.d.ts +76 -2
- package/index.js +191 -7
- package/package.json +1 -1
- package/test.js +52 -1
package/index.cjs
CHANGED
|
@@ -6,12 +6,12 @@ class QueryBuilder {
|
|
|
6
6
|
this._apiKey = apiKey;
|
|
7
7
|
this._dbName = dbName;
|
|
8
8
|
this._table = table;
|
|
9
|
-
this._filters = [];
|
|
9
|
+
this._filters = [];
|
|
10
10
|
this._columns = '*';
|
|
11
11
|
this._limitVal = null;
|
|
12
|
-
this._offsetVal = null;
|
|
13
|
-
this._orderCol = null;
|
|
14
|
-
this._orderAsc = true;
|
|
12
|
+
this._offsetVal = null;
|
|
13
|
+
this._orderCol = null;
|
|
14
|
+
this._orderAsc = true;
|
|
15
15
|
this._single = false;
|
|
16
16
|
this._operation = 'select';
|
|
17
17
|
this._insertData = null;
|
|
@@ -67,7 +67,6 @@ class QueryBuilder {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
is(column, value) {
|
|
70
|
-
// value should be null, true, or false
|
|
71
70
|
this._filters.push({ col: column, op: 'is', val: value });
|
|
72
71
|
return this;
|
|
73
72
|
}
|
|
@@ -86,7 +85,6 @@ class QueryBuilder {
|
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
range(from, to) {
|
|
89
|
-
// e.g. range(0, 9) => LIMIT 10 OFFSET 0
|
|
90
88
|
this._limitVal = to - from + 1;
|
|
91
89
|
this._offsetVal = from;
|
|
92
90
|
return this;
|
|
@@ -148,7 +146,6 @@ class QueryBuilder {
|
|
|
148
146
|
|
|
149
147
|
if (this._operation === 'select') {
|
|
150
148
|
sql = `SELECT ${this._columns} FROM "${this._table}"`;
|
|
151
|
-
|
|
152
149
|
if (this._filters.length) {
|
|
153
150
|
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
154
151
|
sql += ` WHERE ${where}`;
|
|
@@ -218,6 +215,127 @@ class QueryBuilder {
|
|
|
218
215
|
}
|
|
219
216
|
}
|
|
220
217
|
|
|
218
|
+
// ─── Storage Client ───────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
class StorageClient {
|
|
221
|
+
constructor(baseUrl, apiKey, dbName) {
|
|
222
|
+
this._baseUrl = baseUrl;
|
|
223
|
+
this._apiKey = apiKey;
|
|
224
|
+
this._dbName = dbName;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_headers() {
|
|
228
|
+
return { 'Authorization': `Bearer ${this._apiKey}` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Detect if we're running in Node.js
|
|
232
|
+
_isNode() {
|
|
233
|
+
return typeof window === 'undefined' && typeof process !== 'undefined';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Build a FormData object that works in both browser and Node.js
|
|
237
|
+
// In browser: file = File or Blob object
|
|
238
|
+
// In Node.js: file = file path string, Buffer, or ReadStream
|
|
239
|
+
async _buildFormData(file, filename) {
|
|
240
|
+
if (this._isNode()) {
|
|
241
|
+
// Node.js — use form-data package
|
|
242
|
+
let FormDataNode;
|
|
243
|
+
try {
|
|
244
|
+
FormDataNode = require('form-data');
|
|
245
|
+
} catch {
|
|
246
|
+
throw new Error(
|
|
247
|
+
'[openbase] In Node.js, storage.upload() requires the "form-data" package.\n' +
|
|
248
|
+
'Install it with: npm install form-data'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const fs = require('fs');
|
|
253
|
+
const path = require('path');
|
|
254
|
+
const formData = new FormDataNode();
|
|
255
|
+
|
|
256
|
+
if (typeof file === 'string') {
|
|
257
|
+
// file path string — read from disk
|
|
258
|
+
const resolvedName = filename || path.basename(file);
|
|
259
|
+
formData.append('file', fs.createReadStream(file), resolvedName);
|
|
260
|
+
} else if (Buffer.isBuffer(file)) {
|
|
261
|
+
// Buffer
|
|
262
|
+
formData.append('file', file, filename || 'file');
|
|
263
|
+
} else {
|
|
264
|
+
// ReadStream or anything else
|
|
265
|
+
formData.append('file', file, filename || 'file');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return formData;
|
|
269
|
+
|
|
270
|
+
} else {
|
|
271
|
+
// Browser — use native FormData
|
|
272
|
+
const formData = new FormData();
|
|
273
|
+
formData.append('file', file, filename || file.name);
|
|
274
|
+
return formData;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Upload a file
|
|
279
|
+
// Browser: pass a File or Blob (from <input type="file"> or drag-and-drop)
|
|
280
|
+
// Node.js: pass a file path string, Buffer, or ReadStream
|
|
281
|
+
async upload(file, filename) {
|
|
282
|
+
try {
|
|
283
|
+
const formData = await this._buildFormData(file, filename);
|
|
284
|
+
|
|
285
|
+
// In Node.js, form-data needs its own headers (includes boundary)
|
|
286
|
+
const headers = this._isNode()
|
|
287
|
+
? { ...this._headers(), ...formData.getHeaders() }
|
|
288
|
+
: this._headers();
|
|
289
|
+
|
|
290
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/upload`, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers,
|
|
293
|
+
body: formData,
|
|
294
|
+
});
|
|
295
|
+
return await res.json();
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return { data: null, error: err.message };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// List all files in this project's bucket
|
|
302
|
+
async list() {
|
|
303
|
+
try {
|
|
304
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/list`, {
|
|
305
|
+
headers: this._headers(),
|
|
306
|
+
});
|
|
307
|
+
return await res.json();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return { data: null, error: err.message };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Get a presigned URL valid for 7 days
|
|
314
|
+
async getUrl(filename) {
|
|
315
|
+
try {
|
|
316
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/url/${encodeURIComponent(filename)}`, {
|
|
317
|
+
headers: this._headers(),
|
|
318
|
+
});
|
|
319
|
+
return await res.json();
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return { data: null, error: err.message };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Delete a file
|
|
326
|
+
async remove(filename) {
|
|
327
|
+
try {
|
|
328
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/${encodeURIComponent(filename)}`, {
|
|
329
|
+
method: 'DELETE',
|
|
330
|
+
headers: this._headers(),
|
|
331
|
+
});
|
|
332
|
+
return await res.json();
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return { data: null, error: err.message };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
221
339
|
// ─── Client ───────────────────────────────────────────────────────────────────
|
|
222
340
|
|
|
223
341
|
class OpenbaseClient {
|
|
@@ -225,6 +343,7 @@ class OpenbaseClient {
|
|
|
225
343
|
this._baseUrl = baseUrl.replace(/\/$/, '');
|
|
226
344
|
this._apiKey = apiKey;
|
|
227
345
|
this._dbName = dbName;
|
|
346
|
+
this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName);
|
|
228
347
|
}
|
|
229
348
|
|
|
230
349
|
from(table) {
|
package/index.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { ReadStream } from 'fs';
|
|
3
|
+
|
|
1
4
|
export interface OpenbaseResponse<T> {
|
|
2
5
|
data: T | null;
|
|
3
6
|
error: string | null;
|
|
4
7
|
}
|
|
5
8
|
|
|
6
9
|
export declare class QueryBuilder<T = Record<string, unknown>> {
|
|
10
|
+
// Column selection
|
|
7
11
|
select(columns?: string): this;
|
|
12
|
+
|
|
13
|
+
// Filter operators
|
|
8
14
|
eq(column: string, value: unknown): this;
|
|
9
15
|
gt(column: string, value: number): this;
|
|
10
16
|
lt(column: string, value: number): this;
|
|
@@ -14,20 +20,78 @@ export declare class QueryBuilder<T = Record<string, unknown>> {
|
|
|
14
20
|
ilike(column: string, pattern: string): this;
|
|
15
21
|
in(column: string, values: unknown[]): this;
|
|
16
22
|
is(column: string, value: null | boolean): this;
|
|
23
|
+
|
|
24
|
+
// Modifiers
|
|
17
25
|
order(column: string, options?: { ascending?: boolean }): this;
|
|
18
|
-
range(from: number, to: number): this;
|
|
19
26
|
limit(n: number): this;
|
|
27
|
+
range(from: number, to: number): this;
|
|
20
28
|
single(): this;
|
|
29
|
+
|
|
30
|
+
// Mutations
|
|
21
31
|
insert(data: Partial<T>): this;
|
|
22
32
|
update(data: Partial<T>): this;
|
|
23
33
|
delete(): this;
|
|
34
|
+
|
|
24
35
|
then<R>(
|
|
25
36
|
resolve: (value: OpenbaseResponse<T[]>) => R,
|
|
26
37
|
reject?: (reason: unknown) => R
|
|
27
38
|
): Promise<R>;
|
|
39
|
+
|
|
40
|
+
on(event: 'INSERT' | 'UPDATE' | 'DELETE', callback: (payload: {
|
|
41
|
+
event: string;
|
|
42
|
+
table: string;
|
|
43
|
+
record: Record<string, unknown>;
|
|
44
|
+
}) => void): this;
|
|
45
|
+
subscribe(): RealtimeSubscription;
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface StorageFileInfo {
|
|
50
|
+
name: string;
|
|
51
|
+
size: number;
|
|
52
|
+
content_type: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StorageListItem {
|
|
56
|
+
name: string;
|
|
57
|
+
size: number;
|
|
58
|
+
last_modified: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export declare class StorageClient {
|
|
62
|
+
/**
|
|
63
|
+
* Upload a file to this project's storage bucket.
|
|
64
|
+
*
|
|
65
|
+
* Browser: pass a File or Blob (e.g. from <input type="file">)
|
|
66
|
+
* Node.js: pass a file path string, Buffer, or ReadStream
|
|
67
|
+
* Requires the "form-data" package: npm install form-data
|
|
68
|
+
*/
|
|
69
|
+
upload(
|
|
70
|
+
file: File | Blob | string | Buffer | ReadStream,
|
|
71
|
+
filename?: string
|
|
72
|
+
): Promise<OpenbaseResponse<StorageFileInfo>>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* List all files in this project's storage bucket.
|
|
76
|
+
*/
|
|
77
|
+
list(): Promise<OpenbaseResponse<StorageListItem[]>>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get a presigned URL for a file, valid for 7 days.
|
|
81
|
+
*/
|
|
82
|
+
getUrl(filename: string): Promise<OpenbaseResponse<{ url: string }>>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Delete a file from this project's storage bucket.
|
|
86
|
+
*/
|
|
87
|
+
remove(filename: string): Promise<OpenbaseResponse<{ name: string }>>;
|
|
28
88
|
}
|
|
29
89
|
|
|
30
90
|
export declare class OpenbaseClient {
|
|
91
|
+
/** Storage client for file upload, listing, URLs, and deletion */
|
|
92
|
+
storage: StorageClient;
|
|
93
|
+
|
|
94
|
+
/** Query builder for database operations */
|
|
31
95
|
from<T = Record<string, unknown>>(table: string): QueryBuilder<T>;
|
|
32
96
|
}
|
|
33
97
|
|
|
@@ -35,4 +99,14 @@ export declare function createClient(
|
|
|
35
99
|
baseUrl: string,
|
|
36
100
|
apiKey: string,
|
|
37
101
|
dbName: string
|
|
38
|
-
): OpenbaseClient;
|
|
102
|
+
): OpenbaseClient;
|
|
103
|
+
|
|
104
|
+
export declare class RealtimeSubscription {
|
|
105
|
+
on(event: 'INSERT' | 'UPDATE' | 'DELETE', callback: (payload: {
|
|
106
|
+
event: string;
|
|
107
|
+
table: string;
|
|
108
|
+
record: Record<string, unknown>;
|
|
109
|
+
}) => void): this;
|
|
110
|
+
subscribe(): this;
|
|
111
|
+
unsubscribe(): void;
|
|
112
|
+
}
|
package/index.js
CHANGED
|
@@ -6,12 +6,12 @@ class QueryBuilder {
|
|
|
6
6
|
this._apiKey = apiKey;
|
|
7
7
|
this._dbName = dbName;
|
|
8
8
|
this._table = table;
|
|
9
|
-
this._filters = [];
|
|
9
|
+
this._filters = [];
|
|
10
10
|
this._columns = '*';
|
|
11
11
|
this._limitVal = null;
|
|
12
|
-
this._offsetVal = null;
|
|
13
|
-
this._orderCol = null;
|
|
14
|
-
this._orderAsc = true;
|
|
12
|
+
this._offsetVal = null;
|
|
13
|
+
this._orderCol = null;
|
|
14
|
+
this._orderAsc = true;
|
|
15
15
|
this._single = false;
|
|
16
16
|
this._operation = 'select';
|
|
17
17
|
this._insertData = null;
|
|
@@ -24,6 +24,25 @@ class QueryBuilder {
|
|
|
24
24
|
return this;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
on(event, callback) {
|
|
28
|
+
if (!this._realtimeSub) {
|
|
29
|
+
this._realtimeSub = new RealtimeSubscription(
|
|
30
|
+
this._baseUrl, this._apiKey, this._dbName, this._table
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
this._realtimeSub.on(event, callback);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
subscribe() {
|
|
38
|
+
if (!this._realtimeSub) {
|
|
39
|
+
this._realtimeSub = new RealtimeSubscription(
|
|
40
|
+
this._baseUrl, this._apiKey, this._dbName, this._table
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return this._realtimeSub.subscribe();
|
|
44
|
+
}
|
|
45
|
+
|
|
27
46
|
// ─── Filter operators ──────────────────────────────────────────────────────
|
|
28
47
|
|
|
29
48
|
eq(column, value) {
|
|
@@ -67,7 +86,6 @@ class QueryBuilder {
|
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
is(column, value) {
|
|
70
|
-
// value should be null, true, or false
|
|
71
89
|
this._filters.push({ col: column, op: 'is', val: value });
|
|
72
90
|
return this;
|
|
73
91
|
}
|
|
@@ -86,7 +104,6 @@ class QueryBuilder {
|
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
range(from, to) {
|
|
89
|
-
// e.g. range(0, 9) => LIMIT 10 OFFSET 0
|
|
90
107
|
this._limitVal = to - from + 1;
|
|
91
108
|
this._offsetVal = from;
|
|
92
109
|
return this;
|
|
@@ -148,7 +165,6 @@ class QueryBuilder {
|
|
|
148
165
|
|
|
149
166
|
if (this._operation === 'select') {
|
|
150
167
|
sql = `SELECT ${this._columns} FROM "${this._table}"`;
|
|
151
|
-
|
|
152
168
|
if (this._filters.length) {
|
|
153
169
|
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
154
170
|
sql += ` WHERE ${where}`;
|
|
@@ -218,6 +234,173 @@ class QueryBuilder {
|
|
|
218
234
|
}
|
|
219
235
|
}
|
|
220
236
|
|
|
237
|
+
// ─── Storage Client ───────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
class StorageClient {
|
|
240
|
+
constructor(baseUrl, apiKey, dbName) {
|
|
241
|
+
this._baseUrl = baseUrl;
|
|
242
|
+
this._apiKey = apiKey;
|
|
243
|
+
this._dbName = dbName;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
_headers() {
|
|
247
|
+
return { 'Authorization': `Bearer ${this._apiKey}` };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Detect if we're running in Node.js
|
|
251
|
+
_isNode() {
|
|
252
|
+
return typeof window === 'undefined' && typeof process !== 'undefined';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Build a FormData object that works in both browser and Node.js
|
|
256
|
+
// In browser: file = File or Blob object
|
|
257
|
+
// In Node.js: file = file path string, Buffer, or ReadStream
|
|
258
|
+
async _buildFormData(file, filename) {
|
|
259
|
+
if (this._isNode()) {
|
|
260
|
+
// Node.js — use form-data package
|
|
261
|
+
let FormDataNode;
|
|
262
|
+
try {
|
|
263
|
+
FormDataNode = require('form-data');
|
|
264
|
+
} catch {
|
|
265
|
+
throw new Error(
|
|
266
|
+
'[openbase] In Node.js, storage.upload() requires the "form-data" package.\n' +
|
|
267
|
+
'Install it with: npm install form-data'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const fs = require('fs');
|
|
272
|
+
const path = require('path');
|
|
273
|
+
const formData = new FormDataNode();
|
|
274
|
+
|
|
275
|
+
if (typeof file === 'string') {
|
|
276
|
+
// file path string — read from disk
|
|
277
|
+
const resolvedName = filename || path.basename(file);
|
|
278
|
+
formData.append('file', fs.createReadStream(file), resolvedName);
|
|
279
|
+
} else if (Buffer.isBuffer(file)) {
|
|
280
|
+
// Buffer
|
|
281
|
+
formData.append('file', file, filename || 'file');
|
|
282
|
+
} else {
|
|
283
|
+
// ReadStream or anything else
|
|
284
|
+
formData.append('file', file, filename || 'file');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return formData;
|
|
288
|
+
|
|
289
|
+
} else {
|
|
290
|
+
// Browser — use native FormData
|
|
291
|
+
const formData = new FormData();
|
|
292
|
+
formData.append('file', file, filename || file.name);
|
|
293
|
+
return formData;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Upload a file
|
|
298
|
+
// Browser: pass a File or Blob (from <input type="file"> or drag-and-drop)
|
|
299
|
+
// Node.js: pass a file path string, Buffer, or ReadStream
|
|
300
|
+
async upload(file, filename) {
|
|
301
|
+
try {
|
|
302
|
+
const formData = await this._buildFormData(file, filename);
|
|
303
|
+
|
|
304
|
+
// In Node.js, form-data needs its own headers (includes boundary)
|
|
305
|
+
const headers = this._isNode()
|
|
306
|
+
? { ...this._headers(), ...formData.getHeaders() }
|
|
307
|
+
: this._headers();
|
|
308
|
+
|
|
309
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/upload`, {
|
|
310
|
+
method: 'POST',
|
|
311
|
+
headers,
|
|
312
|
+
body: formData,
|
|
313
|
+
});
|
|
314
|
+
return await res.json();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
return { data: null, error: err.message };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// List all files in this project's bucket
|
|
321
|
+
async list() {
|
|
322
|
+
try {
|
|
323
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/list`, {
|
|
324
|
+
headers: this._headers(),
|
|
325
|
+
});
|
|
326
|
+
return await res.json();
|
|
327
|
+
} catch (err) {
|
|
328
|
+
return { data: null, error: err.message };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Get a presigned URL valid for 7 days
|
|
333
|
+
async getUrl(filename) {
|
|
334
|
+
try {
|
|
335
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/url/${encodeURIComponent(filename)}`, {
|
|
336
|
+
headers: this._headers(),
|
|
337
|
+
});
|
|
338
|
+
return await res.json();
|
|
339
|
+
} catch (err) {
|
|
340
|
+
return { data: null, error: err.message };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Delete a file
|
|
345
|
+
async remove(filename) {
|
|
346
|
+
try {
|
|
347
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/${encodeURIComponent(filename)}`, {
|
|
348
|
+
method: 'DELETE',
|
|
349
|
+
headers: this._headers(),
|
|
350
|
+
});
|
|
351
|
+
return await res.json();
|
|
352
|
+
} catch (err) {
|
|
353
|
+
return { data: null, error: err.message };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─── Realtime Subscription ────────────────────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
class RealtimeSubscription {
|
|
361
|
+
constructor(baseUrl, apiKey, dbName, table) {
|
|
362
|
+
this._baseUrl = baseUrl.replace('http://', 'ws://').replace('https://', 'wss://');
|
|
363
|
+
this._apiKey = apiKey;
|
|
364
|
+
this._dbName = dbName;
|
|
365
|
+
this._table = table;
|
|
366
|
+
this._listeners = {}; // { INSERT: [fn], UPDATE: [fn], DELETE: [fn] }
|
|
367
|
+
this._ws = null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
on(event, callback) {
|
|
371
|
+
if (!this._listeners[event]) this._listeners[event] = [];
|
|
372
|
+
this._listeners[event].push(callback);
|
|
373
|
+
return this;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
subscribe() {
|
|
377
|
+
const url = `${this._baseUrl}/realtime/${this._dbName}/${this._table}?token=${this._apiKey}`;
|
|
378
|
+
this._ws = new WebSocket(url);
|
|
379
|
+
|
|
380
|
+
this._ws.onmessage = (e) => {
|
|
381
|
+
try {
|
|
382
|
+
const payload = JSON.parse(e.data);
|
|
383
|
+
if (payload.event === 'PING') return;
|
|
384
|
+
const handlers = this._listeners[payload.event] || [];
|
|
385
|
+
handlers.forEach(fn => fn(payload));
|
|
386
|
+
} catch { }
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
this._ws.onerror = (err) => {
|
|
390
|
+
console.error('[openbase] Realtime error:', err);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return this;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
unsubscribe() {
|
|
397
|
+
if (this._ws) {
|
|
398
|
+
this._ws.close();
|
|
399
|
+
this._ws = null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
221
404
|
// ─── Client ───────────────────────────────────────────────────────────────────
|
|
222
405
|
|
|
223
406
|
class OpenbaseClient {
|
|
@@ -225,6 +408,7 @@ class OpenbaseClient {
|
|
|
225
408
|
this._baseUrl = baseUrl.replace(/\/$/, '');
|
|
226
409
|
this._apiKey = apiKey;
|
|
227
410
|
this._dbName = dbName;
|
|
411
|
+
this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName);
|
|
228
412
|
}
|
|
229
413
|
|
|
230
414
|
from(table) {
|
package/package.json
CHANGED
package/test.js
CHANGED
|
@@ -43,4 +43,55 @@ async function test() {
|
|
|
43
43
|
console.log('deleted:', deleted);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
async function testOperators() {
|
|
47
|
+
console.log('\n── GT / LT ─────────────────────────');
|
|
48
|
+
const { data: gt } = await client.from('leads').select('*').gt('id', 2);
|
|
49
|
+
console.log('gt id > 2:', gt);
|
|
50
|
+
|
|
51
|
+
console.log('\n── GTE / LTE ───────────────────────');
|
|
52
|
+
const { data: lte } = await client.from('leads').select('*').lte('id', 3);
|
|
53
|
+
console.log('lte id <= 3:', lte);
|
|
54
|
+
|
|
55
|
+
console.log('\n── LIKE ────────────────────────────');
|
|
56
|
+
const { data: like } = await client.from('leads').select('*').like('company_name', '%oo%');
|
|
57
|
+
console.log('like company_name %oo%:', like);
|
|
58
|
+
|
|
59
|
+
console.log('\n── ILIKE (case insensitive) ────────');
|
|
60
|
+
const { data: ilike } = await client.from('leads').select('*').ilike('company_name', '%google%');
|
|
61
|
+
console.log('ilike company_name %google%:', ilike);
|
|
62
|
+
|
|
63
|
+
console.log('\n── IN ──────────────────────────────');
|
|
64
|
+
const { data: inResult } = await client.from('leads').select('*').in('slug', ['google', 'meta']);
|
|
65
|
+
console.log('in slug [google, meta]:', inResult);
|
|
66
|
+
|
|
67
|
+
console.log('\n── IS NULL ─────────────────────────');
|
|
68
|
+
const { data: isNull } = await client.from('leads').select('*').is('custom_pitch', null);
|
|
69
|
+
console.log('is custom_pitch null:', isNull);
|
|
70
|
+
|
|
71
|
+
console.log('\n── ORDER ASC ───────────────────────');
|
|
72
|
+
const { data: asc } = await client.from('leads').select('*').order('id', { ascending: true });
|
|
73
|
+
console.log('order id asc:', asc?.map(r => r.id));
|
|
74
|
+
|
|
75
|
+
console.log('\n── ORDER DESC ──────────────────────');
|
|
76
|
+
const { data: desc } = await client.from('leads').select('*').order('id', { ascending: false });
|
|
77
|
+
console.log('order id desc:', desc?.map(r => r.id));
|
|
78
|
+
|
|
79
|
+
console.log('\n── LIMIT ───────────────────────────');
|
|
80
|
+
const { data: limited } = await client.from('leads').select('*').limit(2);
|
|
81
|
+
console.log('limit 2:', limited?.length, 'rows');
|
|
82
|
+
|
|
83
|
+
console.log('\n── RANGE (pagination) ──────────────');
|
|
84
|
+
const { data: page } = await client.from('leads').select('*').range(0, 1);
|
|
85
|
+
console.log('range 0-1:', page?.length, 'rows');
|
|
86
|
+
|
|
87
|
+
console.log('\n── CHAINED (gt + order + limit) ────');
|
|
88
|
+
const { data: chained } = await client
|
|
89
|
+
.from('leads')
|
|
90
|
+
.select('*')
|
|
91
|
+
.gt('id', 1)
|
|
92
|
+
.order('id', { ascending: false })
|
|
93
|
+
.limit(3);
|
|
94
|
+
console.log('chained:', chained);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
testOperators().catch(console.error);
|