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 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 = []; // changed: was {} now array of {col, op, val}
9
+ this._filters = [];
10
10
  this._columns = '*';
11
11
  this._limitVal = null;
12
- this._offsetVal = null; // new
13
- this._orderCol = null; // new
14
- this._orderAsc = true; // new
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 = []; // changed: was {} now array of {col, op, val}
9
+ this._filters = [];
10
10
  this._columns = '*';
11
11
  this._limitVal = null;
12
- this._offsetVal = null; // new
13
- this._orderCol = null; // new
14
- this._orderAsc = true; // new
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbase-js",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "JavaScript client for Openbase — a self-hosted Supabase alternative",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/test.js CHANGED
@@ -43,4 +43,55 @@ async function test() {
43
43
  console.log('deleted:', deleted);
44
44
  }
45
45
 
46
- test().catch(console.error);
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);