openbase-js 0.1.7 → 0.1.8

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/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # OpenBase JS SDK
2
+
3
+ The OpenBase JS SDK provides a lightweight, expressive interface for interacting with your OpenBase projects. It handles database queries (PostgreSQL), file storage (S3/MinIO), and realtime subscriptions via a simple, Supabase-like API.
4
+
5
+ ## šŸ“¦ Installation
6
+
7
+ ```bash
8
+ npm install openbase-js
9
+ # or
10
+ yarn add openbase-js
11
+ ```
12
+
13
+ > [!NOTE]
14
+ > If you are using Node.js for file uploads, you will also need `form-data`:
15
+ > `npm install form-data`
16
+
17
+ ## šŸš€ Quick Start
18
+
19
+ Initialize the client with your project URL, API key, and database name.
20
+
21
+ ```javascript
22
+ const { createClient } = require('openbase-js');
23
+
24
+ const openbase = createClient(
25
+ 'http://localhost:3003', // The URL where your OpenBase backend is running
26
+ 'your-anon-key',
27
+ 'your-db-name'
28
+ );
29
+
30
+ // Fetch data
31
+ async function getLeads() {
32
+ const { data, error } = await openbase
33
+ .from('leads')
34
+ .select('*')
35
+ .eq('status', 'active');
36
+
37
+ if (error) console.error(error);
38
+ else console.log(data);
39
+ }
40
+ ```
41
+
42
+ ---
43
+
44
+ ## šŸ“Š Database Operations
45
+
46
+ OpenBase uses a chainable query builder for database operations.
47
+
48
+ ### Basic CRUD
49
+
50
+ #### Select
51
+ ```javascript
52
+ const { data } = await openbase.from('users').select('id, username');
53
+ ```
54
+
55
+ #### Insert
56
+ ```javascript
57
+ const { data } = await openbase.from('users').insert({
58
+ username: 'jdoe',
59
+ email: 'john@example.com'
60
+ });
61
+ ```
62
+
63
+ #### Update
64
+ ```javascript
65
+ const { data } = await openbase.from('users')
66
+ .update({ email: 'newemail@example.com' })
67
+ .eq('id', 1);
68
+ ```
69
+
70
+ #### Delete
71
+ ```javascript
72
+ const { data } = await openbase.from('users')
73
+ .delete()
74
+ .eq('id', 1);
75
+ ```
76
+
77
+ ### šŸ” Detailed Operator Guide
78
+
79
+ Filtering is done using chainable operator methods.
80
+
81
+ | Operator | Method | Description | Example |
82
+ | :--- | :--- | :--- | :--- |
83
+ | `==` | `.eq(col, val)` | Equal to | `.eq('name', 'Alice')` |
84
+ | `>` | `.gt(col, val)` | Greater than | `.gt('age', 21)` |
85
+ | `<` | `.lt(col, val)` | Less than | `.lt('score', 50)` |
86
+ | `>=` | `.gte(col, val)` | Greater than or equal to | `.gte('price', 100)` |
87
+ | `<=` | `.lte(col, val)` | Less than or equal to | `.lte('stock', 5)` |
88
+ | `LIKE` | `.like(col, pattern)` | Case-sensitive pattern match | `.like('name', '%son%')` |
89
+ | `ILIKE` | `.ilike(col, pattern)`| Case-insensitive pattern match | `.ilike('name', '%SON%')` |
90
+ | `IN` | `.in(col, array)` | Contained in array | `.in('role', ['admin', 'mod'])` |
91
+ | `IS` | `.is(col, value)` | Check for `null` or `boolean` | `.is('deleted_at', null)` |
92
+
93
+ ### šŸ› ļø Modifiers
94
+
95
+ * **`.order(column, { ascending: boolean })`**: Sort the results.
96
+ * **`.limit(count)`**: Limit the number of rows returned.
97
+ * **`.range(from, to)`**: Pagination (0-indexed, inclusive).
98
+ * **`.single()`**: Returns a single object instead of an array.
99
+
100
+ ```javascript
101
+ const { data } = await openbase
102
+ .from('posts')
103
+ .select('*')
104
+ .gt('likes', 10)
105
+ .order('created_at', { ascending: false })
106
+ .range(0, 9) // First 10 items
107
+ ```
108
+
109
+ ---
110
+
111
+ ## šŸ“ Storage
112
+
113
+ OpenBase provides built-in S3-compatible storage.
114
+
115
+ ### Uploading Files
116
+
117
+ In the browser, you can pass a `File` or `Blob`. In Node.js, you can pass a file path, `Buffer`, or `ReadStream`.
118
+
119
+ ```javascript
120
+ // Browser
121
+ const file = event.target.files[0];
122
+ const { data, error } = await openbase.storage.upload(file, 'profile.jpg');
123
+
124
+ // Node.js (requires form-data)
125
+ const { data, error } = await openbase.storage.upload('./local-image.png', 'remote-name.png');
126
+ ```
127
+
128
+ ### Listing & Managing Files
129
+
130
+ ```javascript
131
+ // List all files in bucket
132
+ const { data: files } = await openbase.storage.list();
133
+
134
+ // Get a public presigned URL (valid for 7 days)
135
+ const { data: { url } } = await openbase.storage.getUrl('profile.jpg');
136
+
137
+ // Remove a file
138
+ await openbase.storage.remove('profile.jpg');
139
+ ```
140
+
141
+ ---
142
+
143
+ ## šŸ”„ Realtime
144
+
145
+ Subscribe to database changes using WebSockets.
146
+
147
+ ```javascript
148
+ const subscription = openbase
149
+ .from('messages')
150
+ .on('INSERT', (payload) => {
151
+ console.log('New message:', payload.record);
152
+ })
153
+ .subscribe();
154
+
155
+ // To stop listening
156
+ // subscription.unsubscribe();
157
+ ```
158
+
159
+ ---
160
+
161
+ ## āŒØļø TypeScript Support
162
+
163
+ The SDK is written with full TypeScript support. Types are automatically included.
164
+
165
+ ```typescript
166
+ interface Lead {
167
+ id: number;
168
+ company_name: string;
169
+ }
170
+
171
+ const { data } = await openbase.from<Lead>('leads').select('*');
172
+ // data is typed as Lead[] | null
173
+ ```
174
+
175
+ ---
176
+
177
+ ## 🧪 Testing
178
+
179
+ To run the local tests:
180
+ 1. Ensure the OpenBase backend is running.
181
+ 2. Update the credentials in `test.js`.
182
+ 3. Run: `node test.js`
package/index.cjs CHANGED
@@ -1,11 +1,12 @@
1
1
  // ─── Query Builder ────────────────────────────────────────────────────────────
2
2
 
3
3
  class QueryBuilder {
4
- constructor(baseUrl, apiKey, dbName, table) {
4
+ constructor(baseUrl, apiKey, dbName, table, anonKey) {
5
5
  this._baseUrl = baseUrl;
6
6
  this._apiKey = apiKey;
7
7
  this._dbName = dbName;
8
8
  this._table = table;
9
+ this._anonKey = anonKey;
9
10
  this._filters = [];
10
11
  this._columns = '*';
11
12
  this._limitVal = null;
@@ -27,7 +28,7 @@ class QueryBuilder {
27
28
  on(event, callback) {
28
29
  if (!this._realtimeSub) {
29
30
  this._realtimeSub = new RealtimeSubscription(
30
- this._baseUrl, this._apiKey, this._dbName, this._table
31
+ this._baseUrl, this._apiKey, this._dbName, this._table, this._anonKey
31
32
  );
32
33
  }
33
34
  this._realtimeSub.on(event, callback);
@@ -37,7 +38,7 @@ class QueryBuilder {
37
38
  subscribe() {
38
39
  if (!this._realtimeSub) {
39
40
  this._realtimeSub = new RealtimeSubscription(
40
- this._baseUrl, this._apiKey, this._dbName, this._table
41
+ this._baseUrl, this._apiKey, this._dbName, this._table, this._anonKey
41
42
  );
42
43
  }
43
44
  return this._realtimeSub.subscribe();
@@ -215,6 +216,7 @@ class QueryBuilder {
215
216
  headers: {
216
217
  'Content-Type': 'application/json',
217
218
  'Authorization': `Bearer ${this._apiKey}`,
219
+ 'X-Anon-Key': this._anonKey,
218
220
  },
219
221
  body: JSON.stringify({ sql, db_name: this._dbName }),
220
222
  });
@@ -237,14 +239,18 @@ class QueryBuilder {
237
239
  // ─── Storage Client ───────────────────────────────────────────────────────────
238
240
 
239
241
  class StorageClient {
240
- constructor(baseUrl, apiKey, dbName) {
242
+ constructor(baseUrl, apiKey, dbName, anonKey) {
241
243
  this._baseUrl = baseUrl;
242
244
  this._apiKey = apiKey;
243
245
  this._dbName = dbName;
246
+ this._anonKey = anonKey;
244
247
  }
245
248
 
246
249
  _headers() {
247
- return { 'Authorization': `Bearer ${this._apiKey}` };
250
+ return {
251
+ 'Authorization': `Bearer ${this._apiKey}`,
252
+ 'X-Anon-Key': this._anonKey
253
+ };
248
254
  }
249
255
 
250
256
  // Detect if we're running in Node.js
@@ -253,8 +259,6 @@ class StorageClient {
253
259
  }
254
260
 
255
261
  // 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
262
  async _buildFormData(file, filename) {
259
263
  if (this._isNode()) {
260
264
  // Node.js — use form-data package
@@ -273,14 +277,11 @@ class StorageClient {
273
277
  const formData = new FormDataNode();
274
278
 
275
279
  if (typeof file === 'string') {
276
- // file path string — read from disk
277
280
  const resolvedName = filename || path.basename(file);
278
281
  formData.append('file', fs.createReadStream(file), resolvedName);
279
282
  } else if (Buffer.isBuffer(file)) {
280
- // Buffer
281
283
  formData.append('file', file, filename || 'file');
282
284
  } else {
283
- // ReadStream or anything else
284
285
  formData.append('file', file, filename || 'file');
285
286
  }
286
287
 
@@ -294,14 +295,10 @@ class StorageClient {
294
295
  }
295
296
  }
296
297
 
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
298
  async upload(file, filename) {
301
299
  try {
302
300
  const formData = await this._buildFormData(file, filename);
303
301
 
304
- // In Node.js, form-data needs its own headers (includes boundary)
305
302
  const headers = this._isNode()
306
303
  ? { ...this._headers(), ...formData.getHeaders() }
307
304
  : this._headers();
@@ -317,7 +314,6 @@ class StorageClient {
317
314
  }
318
315
  }
319
316
 
320
- // List all files in this project's bucket
321
317
  async list() {
322
318
  try {
323
319
  const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/list`, {
@@ -329,7 +325,6 @@ class StorageClient {
329
325
  }
330
326
  }
331
327
 
332
- // Get a presigned URL valid for 7 days
333
328
  async getUrl(filename) {
334
329
  try {
335
330
  const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/url/${encodeURIComponent(filename)}`, {
@@ -341,7 +336,6 @@ class StorageClient {
341
336
  }
342
337
  }
343
338
 
344
- // Delete a file
345
339
  async remove(filename) {
346
340
  try {
347
341
  const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/${encodeURIComponent(filename)}`, {
@@ -358,12 +352,13 @@ class StorageClient {
358
352
  // ─── Realtime Subscription ────────────────────────────────────────────────────
359
353
 
360
354
  class RealtimeSubscription {
361
- constructor(baseUrl, apiKey, dbName, table) {
355
+ constructor(baseUrl, apiKey, dbName, table, anonKey) {
362
356
  this._baseUrl = baseUrl.replace('http://', 'ws://').replace('https://', 'wss://');
363
357
  this._apiKey = apiKey;
364
358
  this._dbName = dbName;
365
359
  this._table = table;
366
- this._listeners = {}; // { INSERT: [fn], UPDATE: [fn], DELETE: [fn] }
360
+ this._anonKey = anonKey;
361
+ this._listeners = {};
367
362
  this._ws = null;
368
363
  }
369
364
 
@@ -374,7 +369,7 @@ class RealtimeSubscription {
374
369
  }
375
370
 
376
371
  subscribe() {
377
- const url = `${this._baseUrl}/realtime/${this._dbName}/${this._table}?token=${this._apiKey}`;
372
+ const url = `${this._baseUrl}/realtime/${this._dbName}/${this._table}?token=${this._apiKey}&anon_key=${this._anonKey}`;
378
373
  this._ws = new WebSocket(url);
379
374
 
380
375
  this._ws.onmessage = (e) => {
@@ -404,25 +399,26 @@ class RealtimeSubscription {
404
399
  // ─── Client ───────────────────────────────────────────────────────────────────
405
400
 
406
401
  class OpenbaseClient {
407
- constructor(baseUrl, apiKey, dbName) {
402
+ constructor(baseUrl, apiKey, dbName, options = {}) {
408
403
  this._baseUrl = baseUrl.replace(/\/$/, '');
409
404
  this._apiKey = apiKey;
410
405
  this._dbName = dbName;
411
- this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName);
406
+ this._anonKey = options.anonKey || apiKey;
407
+ this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName, this._anonKey);
412
408
  }
413
409
 
414
410
  from(table) {
415
- return new QueryBuilder(this._baseUrl, this._apiKey, this._dbName, table);
411
+ return new QueryBuilder(this._baseUrl, this._apiKey, this._dbName, table, this._anonKey);
416
412
  }
417
413
  }
418
414
 
419
415
  // ─── Factory ──────────────────────────────────────────────────────────────────
420
416
 
421
- function createClient(baseUrl, apiKey, dbName) {
422
- if (!baseUrl) throw new Error('[openbase] Missing baseUrl. Pass your backend URL as the first argument.');
423
- if (!apiKey) throw new Error('[openbase] Missing apiKey. Pass your anon or service key as the second argument.');
424
- if (!dbName) throw new Error('[openbase] Missing dbName. Pass your database name as the third argument.');
425
- return new OpenbaseClient(baseUrl, apiKey, dbName);
417
+ function createClient(baseUrl, apiKey, dbName, options = {}) {
418
+ if (!baseUrl) throw new Error('[openbase] Missing baseUrl.');
419
+ if (!apiKey) throw new Error('[openbase] Missing apiKey.');
420
+ if (!dbName) throw new Error('[openbase] Missing dbName.');
421
+ return new OpenbaseClient(baseUrl, apiKey, dbName, options);
426
422
  }
427
423
 
428
424
  if (typeof module !== 'undefined') {
package/index.d.ts CHANGED
@@ -6,6 +6,14 @@ export interface OpenbaseResponse<T> {
6
6
  error: string | null;
7
7
  }
8
8
 
9
+ export interface OpenbaseClientOptions {
10
+ /**
11
+ * The public Anon Key for your project.
12
+ * If not provided, the 'apiKey' argument will be used as the project identifier.
13
+ */
14
+ anonKey?: string;
15
+ }
16
+
9
17
  export declare class QueryBuilder<T = Record<string, unknown>> {
10
18
  // Column selection
11
19
  select(columns?: string): this;
@@ -98,7 +106,8 @@ export declare class OpenbaseClient {
98
106
  export declare function createClient(
99
107
  baseUrl: string,
100
108
  apiKey: string,
101
- dbName: string
109
+ dbName: string,
110
+ options?: OpenbaseClientOptions
102
111
  ): OpenbaseClient;
103
112
 
104
113
  export declare class RealtimeSubscription {
package/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // ─── Query Builder ────────────────────────────────────────────────────────────
2
2
 
3
3
  class QueryBuilder {
4
- constructor(baseUrl, apiKey, dbName, table) {
4
+ constructor(baseUrl, apiKey, dbName, table, anonKey) {
5
5
  this._baseUrl = baseUrl;
6
6
  this._apiKey = apiKey;
7
7
  this._dbName = dbName;
8
8
  this._table = table;
9
+ this._anonKey = anonKey;
9
10
  this._filters = [];
10
11
  this._columns = '*';
11
12
  this._limitVal = null;
@@ -27,7 +28,7 @@ class QueryBuilder {
27
28
  on(event, callback) {
28
29
  if (!this._realtimeSub) {
29
30
  this._realtimeSub = new RealtimeSubscription(
30
- this._baseUrl, this._apiKey, this._dbName, this._table
31
+ this._baseUrl, this._apiKey, this._dbName, this._table, this._anonKey
31
32
  );
32
33
  }
33
34
  this._realtimeSub.on(event, callback);
@@ -37,7 +38,7 @@ class QueryBuilder {
37
38
  subscribe() {
38
39
  if (!this._realtimeSub) {
39
40
  this._realtimeSub = new RealtimeSubscription(
40
- this._baseUrl, this._apiKey, this._dbName, this._table
41
+ this._baseUrl, this._apiKey, this._dbName, this._table, this._anonKey
41
42
  );
42
43
  }
43
44
  return this._realtimeSub.subscribe();
@@ -196,7 +197,7 @@ class QueryBuilder {
196
197
  } else if (this._operation === 'delete') {
197
198
  sql = `DELETE FROM "${this._table}"`;
198
199
  if (this._filters.length) {
199
- const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
200
+ const where = this._filters.map(f => this._filters.map(f => this._filterToSQL(f)).join(' AND '));
200
201
  sql += ` WHERE ${where}`;
201
202
  }
202
203
  sql += ` RETURNING *`;
@@ -215,6 +216,7 @@ class QueryBuilder {
215
216
  headers: {
216
217
  'Content-Type': 'application/json',
217
218
  'Authorization': `Bearer ${this._apiKey}`,
219
+ 'X-Anon-Key': this._anonKey,
218
220
  },
219
221
  body: JSON.stringify({ sql, db_name: this._dbName }),
220
222
  });
@@ -237,14 +239,18 @@ class QueryBuilder {
237
239
  // ─── Storage Client ───────────────────────────────────────────────────────────
238
240
 
239
241
  class StorageClient {
240
- constructor(baseUrl, apiKey, dbName) {
242
+ constructor(baseUrl, apiKey, dbName, anonKey) {
241
243
  this._baseUrl = baseUrl;
242
244
  this._apiKey = apiKey;
243
245
  this._dbName = dbName;
246
+ this._anonKey = anonKey;
244
247
  }
245
248
 
246
249
  _headers() {
247
- return { 'Authorization': `Bearer ${this._apiKey}` };
250
+ return {
251
+ 'Authorization': `Bearer ${this._apiKey}`,
252
+ 'X-Anon-Key': this._anonKey
253
+ };
248
254
  }
249
255
 
250
256
  // Detect if we're running in Node.js
@@ -253,8 +259,6 @@ class StorageClient {
253
259
  }
254
260
 
255
261
  // 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
262
  async _buildFormData(file, filename) {
259
263
  if (this._isNode()) {
260
264
  // Node.js — use form-data package
@@ -273,14 +277,11 @@ class StorageClient {
273
277
  const formData = new FormDataNode();
274
278
 
275
279
  if (typeof file === 'string') {
276
- // file path string — read from disk
277
280
  const resolvedName = filename || path.basename(file);
278
281
  formData.append('file', fs.createReadStream(file), resolvedName);
279
282
  } else if (Buffer.isBuffer(file)) {
280
- // Buffer
281
283
  formData.append('file', file, filename || 'file');
282
284
  } else {
283
- // ReadStream or anything else
284
285
  formData.append('file', file, filename || 'file');
285
286
  }
286
287
 
@@ -294,14 +295,10 @@ class StorageClient {
294
295
  }
295
296
  }
296
297
 
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
298
  async upload(file, filename) {
301
299
  try {
302
300
  const formData = await this._buildFormData(file, filename);
303
301
 
304
- // In Node.js, form-data needs its own headers (includes boundary)
305
302
  const headers = this._isNode()
306
303
  ? { ...this._headers(), ...formData.getHeaders() }
307
304
  : this._headers();
@@ -317,7 +314,6 @@ class StorageClient {
317
314
  }
318
315
  }
319
316
 
320
- // List all files in this project's bucket
321
317
  async list() {
322
318
  try {
323
319
  const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/list`, {
@@ -329,7 +325,6 @@ class StorageClient {
329
325
  }
330
326
  }
331
327
 
332
- // Get a presigned URL valid for 7 days
333
328
  async getUrl(filename) {
334
329
  try {
335
330
  const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/url/${encodeURIComponent(filename)}`, {
@@ -341,7 +336,6 @@ class StorageClient {
341
336
  }
342
337
  }
343
338
 
344
- // Delete a file
345
339
  async remove(filename) {
346
340
  try {
347
341
  const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/${encodeURIComponent(filename)}`, {
@@ -358,12 +352,13 @@ class StorageClient {
358
352
  // ─── Realtime Subscription ────────────────────────────────────────────────────
359
353
 
360
354
  class RealtimeSubscription {
361
- constructor(baseUrl, apiKey, dbName, table) {
355
+ constructor(baseUrl, apiKey, dbName, table, anonKey) {
362
356
  this._baseUrl = baseUrl.replace('http://', 'ws://').replace('https://', 'wss://');
363
357
  this._apiKey = apiKey;
364
358
  this._dbName = dbName;
365
359
  this._table = table;
366
- this._listeners = {}; // { INSERT: [fn], UPDATE: [fn], DELETE: [fn] }
360
+ this._anonKey = anonKey;
361
+ this._listeners = {};
367
362
  this._ws = null;
368
363
  }
369
364
 
@@ -374,7 +369,7 @@ class RealtimeSubscription {
374
369
  }
375
370
 
376
371
  subscribe() {
377
- const url = `${this._baseUrl}/realtime/${this._dbName}/${this._table}?token=${this._apiKey}`;
372
+ const url = `${this._baseUrl}/realtime/${this._dbName}/${this._table}?token=${this._apiKey}&anon_key=${this._anonKey}`;
378
373
  this._ws = new WebSocket(url);
379
374
 
380
375
  this._ws.onmessage = (e) => {
@@ -404,25 +399,26 @@ class RealtimeSubscription {
404
399
  // ─── Client ───────────────────────────────────────────────────────────────────
405
400
 
406
401
  class OpenbaseClient {
407
- constructor(baseUrl, apiKey, dbName) {
402
+ constructor(baseUrl, apiKey, dbName, options = {}) {
408
403
  this._baseUrl = baseUrl.replace(/\/$/, '');
409
404
  this._apiKey = apiKey;
410
405
  this._dbName = dbName;
411
- this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName);
406
+ this._anonKey = options.anonKey || apiKey; // Default to apiKey if anonKey not provided
407
+ this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName, this._anonKey);
412
408
  }
413
409
 
414
410
  from(table) {
415
- return new QueryBuilder(this._baseUrl, this._apiKey, this._dbName, table);
411
+ return new QueryBuilder(this._baseUrl, this._apiKey, this._dbName, table, this._anonKey);
416
412
  }
417
413
  }
418
414
 
419
415
  // ─── Factory ──────────────────────────────────────────────────────────────────
420
416
 
421
- function createClient(baseUrl, apiKey, dbName) {
422
- if (!baseUrl) throw new Error('[openbase] Missing baseUrl. Pass your backend URL as the first argument.');
423
- if (!apiKey) throw new Error('[openbase] Missing apiKey. Pass your anon or service key as the second argument.');
424
- if (!dbName) throw new Error('[openbase] Missing dbName. Pass your database name as the third argument.');
425
- return new OpenbaseClient(baseUrl, apiKey, dbName);
417
+ function createClient(baseUrl, apiKey, dbName, options = {}) {
418
+ if (!baseUrl) throw new Error('[openbase] Missing baseUrl.');
419
+ if (!apiKey) throw new Error('[openbase] Missing apiKey.');
420
+ if (!dbName) throw new Error('[openbase] Missing dbName.');
421
+ return new OpenbaseClient(baseUrl, apiKey, dbName, options);
426
422
  }
427
423
 
428
424
  if (typeof module !== 'undefined') {
@@ -431,4 +427,4 @@ if (typeof module !== 'undefined') {
431
427
 
432
428
  if (typeof window !== 'undefined') {
433
429
  window.openbase = { createClient };
434
- }
430
+ }
@@ -0,0 +1,115 @@
1
+ # OpenBase
2
+
3
+ OpenBase is an open-source, lightweight alternative to Supabase, designed for rapid development with a focus on PostgreSQL and S3-compatible storage (MinIO). It provides a unified dashboard for managing projects, databases, storage, and monitoring.
4
+
5
+ ## šŸš€ Key Features
6
+
7
+ - **Project Management**: Easily create and manage multiple isolated projects.
8
+ - **Dynamic Database API**: Auto-generated RESTful API for PostgreSQL databases using a custom FastAPI backend.
9
+ - **Integrated S3 Storage**: Built-in file storage powered by MinIO, with project-level isolation.
10
+ - **SQL Editor**: Execute SQL queries directly from the dashboard.
11
+ - **Monitoring & Logs**: Real-time container monitoring and log streaming.
12
+ - **OpenBase SDK**: A lightweight JavaScript/TypeScript SDK for seamless integration into your applications.
13
+
14
+ ## šŸ—ļø Architecture
15
+
16
+ OpenBase is composed of several microservices coordinated via Docker Compose:
17
+
18
+ ```mermaid
19
+ graph TD
20
+ User([User/Developer]) --> Dashboard[React Dashboard :3001]
21
+ Dashboard --> Backend[FastAPI Backend :3003]
22
+ Backend --> Postgres[PostgreSQL :5433]
23
+ Backend --> MinIO[MinIO Storage :9002/9003]
24
+ Backend --> Docker[Docker Socket]
25
+
26
+ App[Your Application] --> SDK[OpenBase SDK]
27
+ SDK --> Backend
28
+ ```
29
+
30
+ ### Services Breakdown:
31
+ - **Dashboard**: React-based UI for project management.
32
+ - **Backend**: FastAPI service handling project creation, database queries, storage management, and log streaming.
33
+ - **PostgreSQL**: Stores both metadata and project-specific data.
34
+ - **MinIO**: S3-compatible object storage.
35
+
36
+ ## 🚦 Getting Started
37
+
38
+ ### Prerequisites
39
+ - [Docker](https://www.docker.com/get-started) and [Docker Compose](https://docs.docker.com/compose/install/)
40
+
41
+ ### Local Setup with Docker (Recommended)
42
+
43
+ 1. **Clone the repository**:
44
+ ```bash
45
+ git clone https://github.com/omkar1344patil/openbase.git
46
+ cd openbase
47
+ ```
48
+
49
+ 2. **Configure Environment**:
50
+ Copy the example environment file (if available) or create a `.env` in the root with your configuration.
51
+ ```bash
52
+ cp .env.example .env
53
+ ```
54
+
55
+ 3. **Start the services**:
56
+ ```bash
57
+ docker compose up --build
58
+ ```
59
+
60
+ 4. **Access the Dashboard**:
61
+ Open [http://localhost:3001](http://localhost:3001) in your browser.
62
+
63
+ ### Development Setup
64
+
65
+ If you wish to run services individually:
66
+
67
+ - **Backend**:
68
+ ```bash
69
+ cd dashboard-backend
70
+ pip install -r requirements.txt
71
+ python main.py
72
+ ```
73
+ - **Dashboard**:
74
+ ```bash
75
+ cd dashboard
76
+ npm install
77
+ npm start
78
+ ```
79
+
80
+ ## āš™ļø Environment Variables
81
+
82
+ Critical environment variables required for the project (defined in `.env`):
83
+
84
+ | Variable | Description | Default |
85
+ |----------|-------------|---------|
86
+ | `POSTGRES_USER` | Admin username for PostgreSQL | `postgres` |
87
+ | `POSTGRES_PASSWORD` | Admin password for PostgreSQL | - |
88
+ | `MINIO_ROOT_USER` | Admin username for MinIO | `minioadmin` |
89
+ | `MINIO_ROOT_PASSWORD` | Admin password for MinIO | `minioadmin` |
90
+ | `ANON_KEY` | Public API key for projects | - |
91
+ | `SERVICE_KEY` | Admin service key | - |
92
+
93
+ ## 🧠 Context for AI Agents
94
+
95
+ OpenBase is designed with a specific philosophy:
96
+ - **PostgREST-like Patterns**: While not using PostgREST directly for all queries, the API follows similar conventions for database interaction.
97
+ - **Project Isolation**: Each project created through the dashboard results in a dedicated PostgreSQL database and a MinIO bucket.
98
+ - **Unified Gateway**: The FastAPI backend acts as the single point of entry for both the dashboard and external applications using the SDK.
99
+
100
+ ### Common Development Tasks:
101
+ - **Adding SDK Operators**: Update the `openbase-js` client logic to support new SQL operators (e.g., `.gt()`, `.like()`).
102
+ - **Extending Monitoring**: Modify `Monitoring.tsx` in the frontend and the corresponding `/monitoring` endpoints in the backend.
103
+ - **Storage Fixes**: Ensure `MINIO_PUBLIC_HOST` is correctly handled for pre-signed URLs to work across both Docker and local environments.
104
+
105
+ ## šŸ› ļø Tech Stack
106
+
107
+ - **Frontend**: React, TypeScript, Tailwind CSS, Lucide Icons, Recharts.
108
+ - **Backend**: Python, FastAPI, Motor (PyMongo - if applicable), Miniopy-async, Psycopg2.
109
+ - **Storage**: MinIO (S3-compatible).
110
+ - **Database**: PostgreSQL 16.
111
+ - **Orchestration**: Docker Compose.
112
+
113
+ ---
114
+
115
+ Built with ā¤ļø by [Omkar Patil](https://github.com/omkar1344patil)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbase-js",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
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
@@ -1,97 +1,115 @@
1
1
  const { createClient } = require('./index');
2
2
 
3
+ /**
4
+ * Openbase Dual-Key Test
5
+ *
6
+ * 1. Authorization Key (JWT or Service Key): Pass as 2nd argument.
7
+ * 2. Identity Key (Anon Key): Pass in the options object.
8
+ */
3
9
  const client = createClient(
4
- 'http://localhost:3002',
5
- 'Yur7U6O5DHzC7Sjtyx_tI7EYM6lpMBO_I9vWWrN4YdQ', // replace with your actual anon key
6
- 'mydb'
10
+ 'http://localhost:3003',
11
+ // In production, this would be your secret JWT
12
+ 'eHUJV8pFtbCh76xiUALc1YMWslyN8QxZgYG5RKySsgE',
13
+ 'mydb',
14
+ {
15
+ // This identifies your project to the backend
16
+ anonKey: 'Yur7U6O5DHzC7Sjtyx_tI7EYM6lpMBO_I9vWWrN4YdQ'
17
+ }
7
18
  );
8
19
 
20
+ // Existing employees: IDs 101–104, salaries 55000–80000
21
+
9
22
  async function test() {
10
23
  console.log('\n── SELECT all ──────────────────────');
11
- const { data, error } = await client.from('leads').select('*');
24
+ const { data, error } = await client.from('employees').select('*');
12
25
  console.log('data:', data);
13
26
  console.log('error:', error);
14
27
 
15
28
  console.log('\n── SELECT with .eq ─────────────────');
16
29
  const { data: single } = await client
17
- .from('leads')
18
- .select('company_name, slug')
19
- .eq('slug', 'google')
30
+ .from('employees')
31
+ .select('firstname, lastname')
32
+ .eq('employeeid', 101)
20
33
  .single();
21
34
  console.log('single:', single);
22
35
 
23
36
  console.log('\n── INSERT ──────────────────────────');
24
- const { data: inserted } = await client.from('leads').insert({
25
- company_name: 'Test Co',
26
- slug: 'test-co',
27
- custom_pitch: 'Hello from SDK',
37
+ const { data: inserted, error: insertErr } = await client.from('employees').insert({
38
+ employeeid: 999,
39
+ firstname: 'Test',
40
+ lastname: 'User',
41
+ hiredate: '2024-01-01',
42
+ salary: 50000,
28
43
  });
29
44
  console.log('inserted:', inserted);
45
+ console.log('insertErr:', insertErr);
30
46
 
31
47
  console.log('\n── UPDATE ──────────────────────────');
32
48
  const { data: updated } = await client
33
- .from('leads')
34
- .update({ custom_pitch: 'Updated via SDK' })
35
- .eq('slug', 'test-co');
49
+ .from('employees')
50
+ .update({ salary: 60000 })
51
+ .eq('employeeid', 999);
36
52
  console.log('updated:', updated);
37
53
 
38
54
  console.log('\n── DELETE ──────────────────────────');
39
55
  const { data: deleted } = await client
40
- .from('leads')
56
+ .from('employees')
41
57
  .delete()
42
- .eq('slug', 'test-co');
58
+ .eq('employeeid', 999);
43
59
  console.log('deleted:', deleted);
44
60
  }
45
61
 
46
62
  async function testOperators() {
47
63
  console.log('\n── GT / LT ─────────────────────────');
48
- const { data: gt } = await client.from('leads').select('*').gt('id', 2);
49
- console.log('gt id > 2:', gt);
64
+ const { data: gt } = await client.from('employees').select('*').gt('salary', 60000);
65
+ console.log('gt salary > 60000:', gt?.map(r => `${r.firstname} (${r.salary})`));
50
66
 
51
67
  console.log('\n── GTE / LTE ───────────────────────');
52
- const { data: lte } = await client.from('leads').select('*').lte('id', 3);
53
- console.log('lte id <= 3:', lte);
68
+ const { data: lte } = await client.from('employees').select('*').lte('salary', 65000);
69
+ console.log('lte salary <= 65000:', lte?.map(r => `${r.firstname} (${r.salary})`));
54
70
 
55
71
  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);
72
+ const { data: like } = await client.from('employees').select('*').like('firstname', '%a%');
73
+ console.log('like firstname %a%:', like?.map(r => r.firstname));
58
74
 
59
75
  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);
76
+ const { data: ilike } = await client.from('employees').select('*').ilike('lastname', '%s%');
77
+ console.log('ilike lastname %s%:', ilike?.map(r => r.lastname));
62
78
 
63
79
  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);
80
+ const { data: inResult } = await client.from('employees').select('*').in('employeeid', [101, 102]);
81
+ console.log('in employeeid [101, 102]:', inResult?.map(r => r.firstname));
66
82
 
67
83
  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);
84
+ const { data: isNull } = await client.from('employees').select('*').is('hiredate', null);
85
+ console.log('is hiredate null:', isNull);
70
86
 
71
87
  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));
88
+ const { data: asc } = await client.from('employees').select('*').order('salary', { ascending: true });
89
+ console.log('order salary asc:', asc?.map(r => r.salary));
74
90
 
75
91
  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));
92
+ const { data: desc } = await client.from('employees').select('*').order('salary', { ascending: false });
93
+ console.log('order salary desc:', desc?.map(r => r.salary));
78
94
 
79
95
  console.log('\n── LIMIT ───────────────────────────');
80
- const { data: limited } = await client.from('leads').select('*').limit(2);
96
+ const { data: limited } = await client.from('employees').select('*').limit(2);
81
97
  console.log('limit 2:', limited?.length, 'rows');
82
98
 
83
99
  console.log('\n── RANGE (pagination) ──────────────');
84
- const { data: page } = await client.from('leads').select('*').range(0, 1);
100
+ const { data: page } = await client.from('employees').select('*').range(0, 1);
85
101
  console.log('range 0-1:', page?.length, 'rows');
86
102
 
87
103
  console.log('\n── CHAINED (gt + order + limit) ────');
88
104
  const { data: chained } = await client
89
- .from('leads')
105
+ .from('employees')
90
106
  .select('*')
91
- .gt('id', 1)
92
- .order('id', { ascending: false })
107
+ .gt('salary', 55000)
108
+ .order('salary', { ascending: false })
93
109
  .limit(3);
94
- console.log('chained:', chained);
110
+ console.log('chained:', chained?.map(r => `${r.firstname} (${r.salary})`));
95
111
  }
96
112
 
97
- testOperators().catch(console.error);
113
+ test()
114
+ .then(() => testOperators())
115
+ .catch(console.error);