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/README.md +182 -0
- package/index.cjs +143 -106
- package/index.d.ts +31 -1
- package/index.js +144 -106
- package/openbase_readme.md +115 -0
- package/package.json +1 -1
- package/test.js +109 -39
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
164
|
-
let sql = '';
|
|
146
|
+
// ─── Executor ──────────────────────────────────────────────────────────────
|
|
165
147
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (this.
|
|
176
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
191
|
+
then(resolve, reject) {
|
|
192
|
+
return this._buildAndRun().then(resolve, reject);
|
|
206
193
|
}
|
|
194
|
+
}
|
|
207
195
|
|
|
208
|
-
|
|
196
|
+
// ─── Auth Client ───────────────────────────────────────────────────────────────
|
|
209
197
|
|
|
210
|
-
|
|
211
|
-
|
|
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}/
|
|
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({
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return
|
|
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
|
-
|
|
233
|
-
|
|
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(
|
|
241
|
-
this.
|
|
242
|
-
this._apiKey = apiKey;
|
|
243
|
-
this._dbName = dbName;
|
|
274
|
+
constructor(client) {
|
|
275
|
+
this._client = client;
|
|
244
276
|
}
|
|
245
277
|
|
|
246
278
|
_headers() {
|
|
247
|
-
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
423
|
-
if (!apiKey) throw new Error('[openbase] Missing apiKey.
|
|
424
|
-
if (!dbName) throw new Error('[openbase] Missing dbName.
|
|
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)
|