openbase-js 0.1.7 → 0.1.9

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.js CHANGED
@@ -1,11 +1,13 @@
1
1
  // ─── Query Builder ────────────────────────────────────────────────────────────
2
2
 
3
3
  class QueryBuilder {
4
- constructor(baseUrl, apiKey, dbName, table) {
4
+ constructor(baseUrl, apiKey, dbName, table, anonKey, sessionToken = null) {
5
5
  this._baseUrl = baseUrl;
6
6
  this._apiKey = apiKey;
7
7
  this._dbName = dbName;
8
8
  this._table = table;
9
+ this._anonKey = anonKey;
10
+ this._sessionToken = sessionToken;
9
11
  this._filters = [];
10
12
  this._columns = '*';
11
13
  this._limitVal = null;
@@ -27,7 +29,7 @@ class QueryBuilder {
27
29
  on(event, callback) {
28
30
  if (!this._realtimeSub) {
29
31
  this._realtimeSub = new RealtimeSubscription(
30
- this._baseUrl, this._apiKey, this._dbName, this._table
32
+ this._baseUrl, this._apiKey, this._dbName, this._table, this._anonKey
31
33
  );
32
34
  }
33
35
  this._realtimeSub.on(event, callback);
@@ -37,7 +39,7 @@ class QueryBuilder {
37
39
  subscribe() {
38
40
  if (!this._realtimeSub) {
39
41
  this._realtimeSub = new RealtimeSubscription(
40
- this._baseUrl, this._apiKey, this._dbName, this._table
42
+ this._baseUrl, this._apiKey, this._dbName, this._table, this._anonKey
41
43
  );
42
44
  }
43
45
  return this._realtimeSub.subscribe();
@@ -134,117 +136,154 @@ class QueryBuilder {
134
136
  return this;
135
137
  }
136
138
 
137
- // ─── SQL Builder ───────────────────────────────────────────────────────────
138
-
139
- _filterToSQL(f) {
140
- const col = `"${f.col}"`;
141
- switch (f.op) {
142
- case 'eq': return typeof f.val === 'string' ? `${col} = '${f.val}'` : `${col} = ${f.val}`;
143
- case 'gt': return `${col} > ${f.val}`;
144
- case 'lt': return `${col} < ${f.val}`;
145
- case 'gte': return `${col} >= ${f.val}`;
146
- case 'lte': return `${col} <= ${f.val}`;
147
- case 'like': return `${col} LIKE '${f.val}'`;
148
- case 'ilike': return `${col} ILIKE '${f.val}'`;
149
- case 'in': {
150
- const list = f.val.map(v => typeof v === 'string' ? `'${v}'` : v).join(', ');
151
- return `${col} IN (${list})`;
152
- }
153
- case 'is': {
154
- if (f.val === null) return `${col} IS NULL`;
155
- if (f.val === true) return `${col} IS TRUE`;
156
- if (f.val === false) return `${col} IS FALSE`;
157
- return `${col} IS NULL`;
158
- }
159
- default: return `${col} = '${f.val}'`;
160
- }
139
+ upsert(data, onConflict = 'id') {
140
+ this._operation = 'upsert';
141
+ this._insertData = data;
142
+ this._upsertOnConflict = onConflict;
143
+ return this;
161
144
  }
162
145
 
163
- _buildSQL() {
164
- let sql = '';
146
+ // ─── Executor ──────────────────────────────────────────────────────────────
165
147
 
166
- if (this._operation === 'select') {
167
- sql = `SELECT ${this._columns} FROM "${this._table}"`;
168
- if (this._filters.length) {
169
- const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
170
- sql += ` WHERE ${where}`;
171
- }
172
- if (this._orderCol) {
173
- sql += ` ORDER BY "${this._orderCol}" ${this._orderAsc ? 'ASC' : 'DESC'}`;
174
- }
175
- if (this._limitVal) sql += ` LIMIT ${this._limitVal}`;
176
- if (this._offsetVal) sql += ` OFFSET ${this._offsetVal}`;
177
-
178
- } else if (this._operation === 'insert') {
179
- const cols = Object.keys(this._insertData).map(c => `"${c}"`).join(', ');
180
- const vals = Object.values(this._insertData).map(v =>
181
- typeof v === 'string' ? `'${v.replace(/'/g, "''")}'` : v
182
- ).join(', ');
183
- sql = `INSERT INTO "${this._table}" (${cols}) VALUES (${vals}) RETURNING *`;
184
-
185
- } else if (this._operation === 'update') {
186
- const setClauses = Object.entries(this._updateData).map(([col, val]) =>
187
- typeof val === 'string' ? `"${col}" = '${val.replace(/'/g, "''")}'` : `"${col}" = ${val}`
188
- ).join(', ');
189
- sql = `UPDATE "${this._table}" SET ${setClauses}`;
190
- if (this._filters.length) {
191
- const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
192
- sql += ` WHERE ${where}`;
148
+ async _buildAndRun() {
149
+ try {
150
+ const headers = {
151
+ 'Content-Type': 'application/json',
152
+ 'Authorization': `Bearer ${this._apiKey}`,
153
+ 'X-Anon-Key': this._anonKey,
154
+ };
155
+
156
+ // If we have an active end-user session, use its token instead of the anon key
157
+ if (this._sessionToken) {
158
+ headers['Authorization'] = `Bearer ${this._sessionToken}`;
193
159
  }
194
- sql += ` RETURNING *`;
195
160
 
196
- } else if (this._operation === 'delete') {
197
- sql = `DELETE FROM "${this._table}"`;
198
- if (this._filters.length) {
199
- const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
200
- sql += ` WHERE ${where}`;
201
- }
202
- sql += ` RETURNING *`;
161
+ const body = {
162
+ db_name: this._dbName,
163
+ table: this._table,
164
+ operation: this._operation,
165
+ columns: this._columns,
166
+ filters: this._filters,
167
+ limit: this._limitVal,
168
+ offset: this._offsetVal,
169
+ order_col: this._orderCol,
170
+ order_asc: this._orderAsc,
171
+ data: (this._operation === 'insert' || this._operation === 'upsert') ? this._insertData : this._updateData,
172
+ upsert_on_conflict: this._upsertOnConflict
173
+ };
174
+
175
+ const res = await fetch(`${this._baseUrl}/sdk/query`, {
176
+ method: 'POST',
177
+ headers,
178
+ body: JSON.stringify(body),
179
+ });
180
+
181
+ const json = await res.json();
182
+ const { data, error } = json;
183
+ if (error) return { data: null, error };
184
+ if (this._single) return { data: data[0] || null, error: null };
185
+ return { data, error: null };
186
+ } catch (err) {
187
+ return { data: null, error: err.message };
203
188
  }
189
+ }
204
190
 
205
- return sql;
191
+ then(resolve, reject) {
192
+ return this._buildAndRun().then(resolve, reject);
206
193
  }
194
+ }
207
195
 
208
- // ─── Executor ──────────────────────────────────────────────────────────────
196
+ // ─── Auth Client ───────────────────────────────────────────────────────────────
209
197
 
210
- async _buildAndRun() {
211
- const sql = this._buildSQL();
198
+ class AuthClient {
199
+ constructor(client) {
200
+ this._client = client;
201
+ }
202
+
203
+ async signUp(email, password) {
212
204
  try {
213
- const res = await fetch(`${this._baseUrl}/query`, {
205
+ const res = await fetch(`${this._client._baseUrl}/sdk/auth/signup`, {
214
206
  method: 'POST',
215
207
  headers: {
216
208
  'Content-Type': 'application/json',
217
- 'Authorization': `Bearer ${this._apiKey}`,
209
+ 'Authorization': `Bearer ${this._client._apiKey}`
218
210
  },
219
- body: JSON.stringify({ sql, db_name: this._dbName }),
211
+ body: JSON.stringify({
212
+ db_name: this._client._dbName,
213
+ email,
214
+ password
215
+ }),
220
216
  });
217
+ const json = await res.json();
218
+ if (json.data && json.data.session) {
219
+ this._client._session = json.data.session;
220
+ }
221
+ return json;
222
+ } catch (err) {
223
+ return { data: null, error: err.message };
224
+ }
225
+ }
221
226
 
227
+ async signIn(email, password) {
228
+ try {
229
+ const res = await fetch(`${this._client._baseUrl}/sdk/auth/signin`, {
230
+ method: 'POST',
231
+ headers: {
232
+ 'Content-Type': 'application/json',
233
+ 'Authorization': `Bearer ${this._client._apiKey}`
234
+ },
235
+ body: JSON.stringify({
236
+ db_name: this._client._dbName,
237
+ email,
238
+ password
239
+ }),
240
+ });
222
241
  const json = await res.json();
223
- const { data, error } = json;
224
- if (error) return { data: null, error };
225
- if (this._single) return { data: data[0] || null, error: null };
226
- return { data, error: null };
242
+ if (json.data && json.data.session) {
243
+ this._client._session = json.data.session;
244
+ }
245
+ return json;
227
246
  } catch (err) {
228
247
  return { data: null, error: err.message };
229
248
  }
230
249
  }
231
250
 
232
- then(resolve, reject) {
233
- return this._buildAndRun().then(resolve, reject);
251
+ async getSession() {
252
+ if (!this._client._session) return { data: null, error: null };
253
+ try {
254
+ const res = await fetch(`${this._client._baseUrl}/sdk/auth/session`, {
255
+ headers: {
256
+ 'Authorization': `Bearer ${this._client._session.access_token}`
257
+ }
258
+ });
259
+ return await res.json();
260
+ } catch (err) {
261
+ return { data: null, error: err.message };
262
+ }
263
+ }
264
+
265
+ signOut() {
266
+ this._client._session = null;
267
+ return { data: { ok: true }, error: null };
234
268
  }
235
269
  }
236
270
 
237
271
  // ─── Storage Client ───────────────────────────────────────────────────────────
238
272
 
239
273
  class StorageClient {
240
- constructor(baseUrl, apiKey, dbName) {
241
- this._baseUrl = baseUrl;
242
- this._apiKey = apiKey;
243
- this._dbName = dbName;
274
+ constructor(client) {
275
+ this._client = client;
244
276
  }
245
277
 
246
278
  _headers() {
247
- return { 'Authorization': `Bearer ${this._apiKey}` };
279
+ const headers = {
280
+ 'Authorization': `Bearer ${this._client._apiKey}`,
281
+ 'X-Anon-Key': this._client._anonKey
282
+ };
283
+ if (this._client._session) {
284
+ headers['Authorization'] = `Bearer ${this._client._session.access_token}`;
285
+ }
286
+ return headers;
248
287
  }
249
288
 
250
289
  // Detect if we're running in Node.js
@@ -253,8 +292,6 @@ class StorageClient {
253
292
  }
254
293
 
255
294
  // 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
295
  async _buildFormData(file, filename) {
259
296
  if (this._isNode()) {
260
297
  // Node.js — use form-data package
@@ -273,14 +310,11 @@ class StorageClient {
273
310
  const formData = new FormDataNode();
274
311
 
275
312
  if (typeof file === 'string') {
276
- // file path string — read from disk
277
313
  const resolvedName = filename || path.basename(file);
278
314
  formData.append('file', fs.createReadStream(file), resolvedName);
279
315
  } else if (Buffer.isBuffer(file)) {
280
- // Buffer
281
316
  formData.append('file', file, filename || 'file');
282
317
  } else {
283
- // ReadStream or anything else
284
318
  formData.append('file', file, filename || 'file');
285
319
  }
286
320
 
@@ -294,14 +328,10 @@ class StorageClient {
294
328
  }
295
329
  }
296
330
 
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
331
  async upload(file, filename) {
301
332
  try {
302
333
  const formData = await this._buildFormData(file, filename);
303
334
 
304
- // In Node.js, form-data needs its own headers (includes boundary)
305
335
  const headers = this._isNode()
306
336
  ? { ...this._headers(), ...formData.getHeaders() }
307
337
  : this._headers();
@@ -317,7 +347,6 @@ class StorageClient {
317
347
  }
318
348
  }
319
349
 
320
- // List all files in this project's bucket
321
350
  async list() {
322
351
  try {
323
352
  const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/list`, {
@@ -329,7 +358,6 @@ class StorageClient {
329
358
  }
330
359
  }
331
360
 
332
- // Get a presigned URL valid for 7 days
333
361
  async getUrl(filename) {
334
362
  try {
335
363
  const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/url/${encodeURIComponent(filename)}`, {
@@ -341,7 +369,6 @@ class StorageClient {
341
369
  }
342
370
  }
343
371
 
344
- // Delete a file
345
372
  async remove(filename) {
346
373
  try {
347
374
  const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/${encodeURIComponent(filename)}`, {
@@ -358,12 +385,13 @@ class StorageClient {
358
385
  // ─── Realtime Subscription ────────────────────────────────────────────────────
359
386
 
360
387
  class RealtimeSubscription {
361
- constructor(baseUrl, apiKey, dbName, table) {
388
+ constructor(baseUrl, apiKey, dbName, table, anonKey) {
362
389
  this._baseUrl = baseUrl.replace('http://', 'ws://').replace('https://', 'wss://');
363
390
  this._apiKey = apiKey;
364
391
  this._dbName = dbName;
365
392
  this._table = table;
366
- this._listeners = {}; // { INSERT: [fn], UPDATE: [fn], DELETE: [fn] }
393
+ this._anonKey = anonKey;
394
+ this._listeners = {};
367
395
  this._ws = null;
368
396
  }
369
397
 
@@ -374,7 +402,7 @@ class RealtimeSubscription {
374
402
  }
375
403
 
376
404
  subscribe() {
377
- const url = `${this._baseUrl}/realtime/${this._dbName}/${this._table}?token=${this._apiKey}`;
405
+ const url = `${this._baseUrl}/realtime/${this._dbName}/${this._table}?token=${this._apiKey}&anon_key=${this._anonKey}`;
378
406
  this._ws = new WebSocket(url);
379
407
 
380
408
  this._ws.onmessage = (e) => {
@@ -404,25 +432,35 @@ class RealtimeSubscription {
404
432
  // ─── Client ───────────────────────────────────────────────────────────────────
405
433
 
406
434
  class OpenbaseClient {
407
- constructor(baseUrl, apiKey, dbName) {
435
+ constructor(baseUrl, apiKey, dbName, options = {}) {
408
436
  this._baseUrl = baseUrl.replace(/\/$/, '');
409
437
  this._apiKey = apiKey;
410
438
  this._dbName = dbName;
411
- this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName);
439
+ this._anonKey = options.anonKey || apiKey; // Default to apiKey if anonKey not provided
440
+ this._session = null;
441
+ this.storage = new StorageClient(this);
442
+ this.auth = new AuthClient(this);
412
443
  }
413
444
 
414
445
  from(table) {
415
- return new QueryBuilder(this._baseUrl, this._apiKey, this._dbName, table);
446
+ return new QueryBuilder(
447
+ this._baseUrl,
448
+ this._apiKey,
449
+ this._dbName,
450
+ table,
451
+ this._anonKey,
452
+ this._session ? this._session.access_token : null
453
+ );
416
454
  }
417
455
  }
418
456
 
419
457
  // ─── Factory ──────────────────────────────────────────────────────────────────
420
458
 
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);
459
+ function createClient(baseUrl, apiKey, dbName, options = {}) {
460
+ if (!baseUrl) throw new Error('[openbase] Missing baseUrl.');
461
+ if (!apiKey) throw new Error('[openbase] Missing apiKey.');
462
+ if (!dbName) throw new Error('[openbase] Missing dbName.');
463
+ return new OpenbaseClient(baseUrl, apiKey, dbName, options);
426
464
  }
427
465
 
428
466
  if (typeof module !== 'undefined') {
@@ -431,4 +469,4 @@ if (typeof module !== 'undefined') {
431
469
 
432
470
  if (typeof window !== 'undefined') {
433
471
  window.openbase = { createClient };
434
- }
472
+ }
@@ -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.9",
4
4
  "description": "JavaScript client for Openbase — a self-hosted Supabase alternative",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",