openbase-js 0.1.3 → 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.
Files changed (5) hide show
  1. package/index.cjs +229 -15
  2. package/index.d.ts +66 -0
  3. package/index.js +229 -14
  4. package/package.json +1 -1
  5. package/test.js +52 -1
package/index.cjs CHANGED
@@ -6,9 +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
15
  this._single = false;
13
16
  this._operation = 'select';
14
17
  this._insertData = null;
@@ -21,8 +24,58 @@ class QueryBuilder {
21
24
  return this;
22
25
  }
23
26
 
27
+ // ─── Filter operators ──────────────────────────────────────────────────────
28
+
24
29
  eq(column, value) {
25
- this._filters[column] = value;
30
+ this._filters.push({ col: column, op: 'eq', val: value });
31
+ return this;
32
+ }
33
+
34
+ gt(column, value) {
35
+ this._filters.push({ col: column, op: 'gt', val: value });
36
+ return this;
37
+ }
38
+
39
+ lt(column, value) {
40
+ this._filters.push({ col: column, op: 'lt', val: value });
41
+ return this;
42
+ }
43
+
44
+ gte(column, value) {
45
+ this._filters.push({ col: column, op: 'gte', val: value });
46
+ return this;
47
+ }
48
+
49
+ lte(column, value) {
50
+ this._filters.push({ col: column, op: 'lte', val: value });
51
+ return this;
52
+ }
53
+
54
+ like(column, pattern) {
55
+ this._filters.push({ col: column, op: 'like', val: pattern });
56
+ return this;
57
+ }
58
+
59
+ ilike(column, pattern) {
60
+ this._filters.push({ col: column, op: 'ilike', val: pattern });
61
+ return this;
62
+ }
63
+
64
+ in(column, values) {
65
+ this._filters.push({ col: column, op: 'in', val: values });
66
+ return this;
67
+ }
68
+
69
+ is(column, value) {
70
+ this._filters.push({ col: column, op: 'is', val: value });
71
+ return this;
72
+ }
73
+
74
+ // ─── Modifiers ─────────────────────────────────────────────────────────────
75
+
76
+ order(column, { ascending = true } = {}) {
77
+ this._orderCol = column;
78
+ this._orderAsc = ascending;
26
79
  return this;
27
80
  }
28
81
 
@@ -31,12 +84,20 @@ class QueryBuilder {
31
84
  return this;
32
85
  }
33
86
 
87
+ range(from, to) {
88
+ this._limitVal = to - from + 1;
89
+ this._offsetVal = from;
90
+ return this;
91
+ }
92
+
34
93
  single() {
35
94
  this._single = true;
36
95
  this._limitVal = 1;
37
96
  return this;
38
97
  }
39
98
 
99
+ // ─── Mutation operators ────────────────────────────────────────────────────
100
+
40
101
  insert(data) {
41
102
  this._operation = 'insert';
42
103
  this._insertData = data;
@@ -54,16 +115,46 @@ class QueryBuilder {
54
115
  return this;
55
116
  }
56
117
 
118
+ // ─── SQL Builder ───────────────────────────────────────────────────────────
119
+
120
+ _filterToSQL(f) {
121
+ const col = `"${f.col}"`;
122
+ switch (f.op) {
123
+ case 'eq': return typeof f.val === 'string' ? `${col} = '${f.val}'` : `${col} = ${f.val}`;
124
+ case 'gt': return `${col} > ${f.val}`;
125
+ case 'lt': return `${col} < ${f.val}`;
126
+ case 'gte': return `${col} >= ${f.val}`;
127
+ case 'lte': return `${col} <= ${f.val}`;
128
+ case 'like': return `${col} LIKE '${f.val}'`;
129
+ case 'ilike': return `${col} ILIKE '${f.val}'`;
130
+ case 'in': {
131
+ const list = f.val.map(v => typeof v === 'string' ? `'${v}'` : v).join(', ');
132
+ return `${col} IN (${list})`;
133
+ }
134
+ case 'is': {
135
+ if (f.val === null) return `${col} IS NULL`;
136
+ if (f.val === true) return `${col} IS TRUE`;
137
+ if (f.val === false) return `${col} IS FALSE`;
138
+ return `${col} IS NULL`;
139
+ }
140
+ default: return `${col} = '${f.val}'`;
141
+ }
142
+ }
143
+
57
144
  _buildSQL() {
58
145
  let sql = '';
59
146
 
60
147
  if (this._operation === 'select') {
61
148
  sql = `SELECT ${this._columns} FROM "${this._table}"`;
62
- const whereParts = Object.entries(this._filters).map(([col, val]) =>
63
- typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
64
- );
65
- if (whereParts.length) sql += ` WHERE ${whereParts.join(' AND ')}`;
149
+ if (this._filters.length) {
150
+ const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
151
+ sql += ` WHERE ${where}`;
152
+ }
153
+ if (this._orderCol) {
154
+ sql += ` ORDER BY "${this._orderCol}" ${this._orderAsc ? 'ASC' : 'DESC'}`;
155
+ }
66
156
  if (this._limitVal) sql += ` LIMIT ${this._limitVal}`;
157
+ if (this._offsetVal) sql += ` OFFSET ${this._offsetVal}`;
67
158
 
68
159
  } else if (this._operation === 'insert') {
69
160
  const cols = Object.keys(this._insertData).map(c => `"${c}"`).join(', ');
@@ -76,25 +167,27 @@ class QueryBuilder {
76
167
  const setClauses = Object.entries(this._updateData).map(([col, val]) =>
77
168
  typeof val === 'string' ? `"${col}" = '${val.replace(/'/g, "''")}'` : `"${col}" = ${val}`
78
169
  ).join(', ');
79
- const whereParts = Object.entries(this._filters).map(([col, val]) =>
80
- typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
81
- );
82
170
  sql = `UPDATE "${this._table}" SET ${setClauses}`;
83
- if (whereParts.length) sql += ` WHERE ${whereParts.join(' AND ')}`;
171
+ if (this._filters.length) {
172
+ const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
173
+ sql += ` WHERE ${where}`;
174
+ }
84
175
  sql += ` RETURNING *`;
85
176
 
86
177
  } else if (this._operation === 'delete') {
87
- const whereParts = Object.entries(this._filters).map(([col, val]) =>
88
- typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
89
- );
90
178
  sql = `DELETE FROM "${this._table}"`;
91
- if (whereParts.length) sql += ` WHERE ${whereParts.join(' AND ')}`;
179
+ if (this._filters.length) {
180
+ const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
181
+ sql += ` WHERE ${where}`;
182
+ }
92
183
  sql += ` RETURNING *`;
93
184
  }
94
185
 
95
186
  return sql;
96
187
  }
97
188
 
189
+ // ─── Executor ──────────────────────────────────────────────────────────────
190
+
98
191
  async _buildAndRun() {
99
192
  const sql = this._buildSQL();
100
193
  try {
@@ -122,6 +215,127 @@ class QueryBuilder {
122
215
  }
123
216
  }
124
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
+
125
339
  // ─── Client ───────────────────────────────────────────────────────────────────
126
340
 
127
341
  class OpenbaseClient {
@@ -129,6 +343,7 @@ class OpenbaseClient {
129
343
  this._baseUrl = baseUrl.replace(/\/$/, '');
130
344
  this._apiKey = apiKey;
131
345
  this._dbName = dbName;
346
+ this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName);
132
347
  }
133
348
 
134
349
  from(table) {
@@ -152,5 +367,4 @@ if (typeof module !== 'undefined') {
152
367
  if (typeof window !== 'undefined') {
153
368
  window.openbase = { createClient };
154
369
  }
155
-
156
370
  module.exports = { createClient };
package/index.d.ts CHANGED
@@ -1,23 +1,89 @@
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;
15
+ gt(column: string, value: number): this;
16
+ lt(column: string, value: number): this;
17
+ gte(column: string, value: number): this;
18
+ lte(column: string, value: number): this;
19
+ like(column: string, pattern: string): this;
20
+ ilike(column: string, pattern: string): this;
21
+ in(column: string, values: unknown[]): this;
22
+ is(column: string, value: null | boolean): this;
23
+
24
+ // Modifiers
25
+ order(column: string, options?: { ascending?: boolean }): this;
9
26
  limit(n: number): this;
27
+ range(from: number, to: number): this;
10
28
  single(): this;
29
+
30
+ // Mutations
11
31
  insert(data: Partial<T>): this;
12
32
  update(data: Partial<T>): this;
13
33
  delete(): this;
34
+
14
35
  then<R>(
15
36
  resolve: (value: OpenbaseResponse<T[]>) => R,
16
37
  reject?: (reason: unknown) => R
17
38
  ): Promise<R>;
18
39
  }
19
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
+
20
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 */
21
87
  from<T = Record<string, unknown>>(table: string): QueryBuilder<T>;
22
88
  }
23
89
 
package/index.js CHANGED
@@ -6,9 +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
15
  this._single = false;
13
16
  this._operation = 'select';
14
17
  this._insertData = null;
@@ -21,8 +24,58 @@ class QueryBuilder {
21
24
  return this;
22
25
  }
23
26
 
27
+ // ─── Filter operators ──────────────────────────────────────────────────────
28
+
24
29
  eq(column, value) {
25
- this._filters[column] = value;
30
+ this._filters.push({ col: column, op: 'eq', val: value });
31
+ return this;
32
+ }
33
+
34
+ gt(column, value) {
35
+ this._filters.push({ col: column, op: 'gt', val: value });
36
+ return this;
37
+ }
38
+
39
+ lt(column, value) {
40
+ this._filters.push({ col: column, op: 'lt', val: value });
41
+ return this;
42
+ }
43
+
44
+ gte(column, value) {
45
+ this._filters.push({ col: column, op: 'gte', val: value });
46
+ return this;
47
+ }
48
+
49
+ lte(column, value) {
50
+ this._filters.push({ col: column, op: 'lte', val: value });
51
+ return this;
52
+ }
53
+
54
+ like(column, pattern) {
55
+ this._filters.push({ col: column, op: 'like', val: pattern });
56
+ return this;
57
+ }
58
+
59
+ ilike(column, pattern) {
60
+ this._filters.push({ col: column, op: 'ilike', val: pattern });
61
+ return this;
62
+ }
63
+
64
+ in(column, values) {
65
+ this._filters.push({ col: column, op: 'in', val: values });
66
+ return this;
67
+ }
68
+
69
+ is(column, value) {
70
+ this._filters.push({ col: column, op: 'is', val: value });
71
+ return this;
72
+ }
73
+
74
+ // ─── Modifiers ─────────────────────────────────────────────────────────────
75
+
76
+ order(column, { ascending = true } = {}) {
77
+ this._orderCol = column;
78
+ this._orderAsc = ascending;
26
79
  return this;
27
80
  }
28
81
 
@@ -31,12 +84,20 @@ class QueryBuilder {
31
84
  return this;
32
85
  }
33
86
 
87
+ range(from, to) {
88
+ this._limitVal = to - from + 1;
89
+ this._offsetVal = from;
90
+ return this;
91
+ }
92
+
34
93
  single() {
35
94
  this._single = true;
36
95
  this._limitVal = 1;
37
96
  return this;
38
97
  }
39
98
 
99
+ // ─── Mutation operators ────────────────────────────────────────────────────
100
+
40
101
  insert(data) {
41
102
  this._operation = 'insert';
42
103
  this._insertData = data;
@@ -54,16 +115,46 @@ class QueryBuilder {
54
115
  return this;
55
116
  }
56
117
 
118
+ // ─── SQL Builder ───────────────────────────────────────────────────────────
119
+
120
+ _filterToSQL(f) {
121
+ const col = `"${f.col}"`;
122
+ switch (f.op) {
123
+ case 'eq': return typeof f.val === 'string' ? `${col} = '${f.val}'` : `${col} = ${f.val}`;
124
+ case 'gt': return `${col} > ${f.val}`;
125
+ case 'lt': return `${col} < ${f.val}`;
126
+ case 'gte': return `${col} >= ${f.val}`;
127
+ case 'lte': return `${col} <= ${f.val}`;
128
+ case 'like': return `${col} LIKE '${f.val}'`;
129
+ case 'ilike': return `${col} ILIKE '${f.val}'`;
130
+ case 'in': {
131
+ const list = f.val.map(v => typeof v === 'string' ? `'${v}'` : v).join(', ');
132
+ return `${col} IN (${list})`;
133
+ }
134
+ case 'is': {
135
+ if (f.val === null) return `${col} IS NULL`;
136
+ if (f.val === true) return `${col} IS TRUE`;
137
+ if (f.val === false) return `${col} IS FALSE`;
138
+ return `${col} IS NULL`;
139
+ }
140
+ default: return `${col} = '${f.val}'`;
141
+ }
142
+ }
143
+
57
144
  _buildSQL() {
58
145
  let sql = '';
59
146
 
60
147
  if (this._operation === 'select') {
61
148
  sql = `SELECT ${this._columns} FROM "${this._table}"`;
62
- const whereParts = Object.entries(this._filters).map(([col, val]) =>
63
- typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
64
- );
65
- if (whereParts.length) sql += ` WHERE ${whereParts.join(' AND ')}`;
149
+ if (this._filters.length) {
150
+ const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
151
+ sql += ` WHERE ${where}`;
152
+ }
153
+ if (this._orderCol) {
154
+ sql += ` ORDER BY "${this._orderCol}" ${this._orderAsc ? 'ASC' : 'DESC'}`;
155
+ }
66
156
  if (this._limitVal) sql += ` LIMIT ${this._limitVal}`;
157
+ if (this._offsetVal) sql += ` OFFSET ${this._offsetVal}`;
67
158
 
68
159
  } else if (this._operation === 'insert') {
69
160
  const cols = Object.keys(this._insertData).map(c => `"${c}"`).join(', ');
@@ -76,25 +167,27 @@ class QueryBuilder {
76
167
  const setClauses = Object.entries(this._updateData).map(([col, val]) =>
77
168
  typeof val === 'string' ? `"${col}" = '${val.replace(/'/g, "''")}'` : `"${col}" = ${val}`
78
169
  ).join(', ');
79
- const whereParts = Object.entries(this._filters).map(([col, val]) =>
80
- typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
81
- );
82
170
  sql = `UPDATE "${this._table}" SET ${setClauses}`;
83
- if (whereParts.length) sql += ` WHERE ${whereParts.join(' AND ')}`;
171
+ if (this._filters.length) {
172
+ const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
173
+ sql += ` WHERE ${where}`;
174
+ }
84
175
  sql += ` RETURNING *`;
85
176
 
86
177
  } else if (this._operation === 'delete') {
87
- const whereParts = Object.entries(this._filters).map(([col, val]) =>
88
- typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
89
- );
90
178
  sql = `DELETE FROM "${this._table}"`;
91
- if (whereParts.length) sql += ` WHERE ${whereParts.join(' AND ')}`;
179
+ if (this._filters.length) {
180
+ const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
181
+ sql += ` WHERE ${where}`;
182
+ }
92
183
  sql += ` RETURNING *`;
93
184
  }
94
185
 
95
186
  return sql;
96
187
  }
97
188
 
189
+ // ─── Executor ──────────────────────────────────────────────────────────────
190
+
98
191
  async _buildAndRun() {
99
192
  const sql = this._buildSQL();
100
193
  try {
@@ -122,6 +215,127 @@ class QueryBuilder {
122
215
  }
123
216
  }
124
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
+
125
339
  // ─── Client ───────────────────────────────────────────────────────────────────
126
340
 
127
341
  class OpenbaseClient {
@@ -129,6 +343,7 @@ class OpenbaseClient {
129
343
  this._baseUrl = baseUrl.replace(/\/$/, '');
130
344
  this._apiKey = apiKey;
131
345
  this._dbName = dbName;
346
+ this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName);
132
347
  }
133
348
 
134
349
  from(table) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbase-js",
3
- "version": "0.1.3",
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);