openbase-js 0.1.4 → 0.1.5

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,70 @@ 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>;
28
39
  }
29
40
 
41
+ export interface StorageFileInfo {
42
+ name: string;
43
+ size: number;
44
+ content_type: string;
45
+ }
46
+
47
+ export interface StorageListItem {
48
+ name: string;
49
+ size: number;
50
+ last_modified: string;
51
+ }
52
+
53
+ export declare class StorageClient {
54
+ /**
55
+ * Upload a file to this project's storage bucket.
56
+ *
57
+ * Browser: pass a File or Blob (e.g. from <input type="file">)
58
+ * Node.js: pass a file path string, Buffer, or ReadStream
59
+ * Requires the "form-data" package: npm install form-data
60
+ */
61
+ upload(
62
+ file: File | Blob | string | Buffer | ReadStream,
63
+ filename?: string
64
+ ): Promise<OpenbaseResponse<StorageFileInfo>>;
65
+
66
+ /**
67
+ * List all files in this project's storage bucket.
68
+ */
69
+ list(): Promise<OpenbaseResponse<StorageListItem[]>>;
70
+
71
+ /**
72
+ * Get a presigned URL for a file, valid for 7 days.
73
+ */
74
+ getUrl(filename: string): Promise<OpenbaseResponse<{ url: string }>>;
75
+
76
+ /**
77
+ * Delete a file from this project's storage bucket.
78
+ */
79
+ remove(filename: string): Promise<OpenbaseResponse<{ name: string }>>;
80
+ }
81
+
30
82
  export declare class OpenbaseClient {
83
+ /** Storage client for file upload, listing, URLs, and deletion */
84
+ storage: StorageClient;
85
+
86
+ /** Query builder for database operations */
31
87
  from<T = Record<string, unknown>>(table: string): QueryBuilder<T>;
32
88
  }
33
89
 
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;
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbase-js",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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);