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/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,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') {
|
|
@@ -432,4 +470,3 @@ if (typeof module !== 'undefined') {
|
|
|
432
470
|
if (typeof window !== 'undefined') {
|
|
433
471
|
window.openbase = { createClient };
|
|
434
472
|
}
|
|
435
|
-
module.exports = { createClient };
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,31 @@ 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
|
+
|
|
17
|
+
export interface User {
|
|
18
|
+
id: string;
|
|
19
|
+
email: string;
|
|
20
|
+
created_at: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Session {
|
|
24
|
+
access_token: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export declare class AuthClient {
|
|
28
|
+
signUp(email: string, password: string): Promise<OpenbaseResponse<{ user: User, session: Session }>>;
|
|
29
|
+
signIn(email: string, password: string): Promise<OpenbaseResponse<{ user: User, session: Session }>>;
|
|
30
|
+
getSession(): Promise<OpenbaseResponse<{ user: User }>>;
|
|
31
|
+
signOut(): OpenbaseResponse<{ ok: boolean }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
9
34
|
export declare class QueryBuilder<T = Record<string, unknown>> {
|
|
10
35
|
// Column selection
|
|
11
36
|
select(columns?: string): this;
|
|
@@ -31,6 +56,7 @@ export declare class QueryBuilder<T = Record<string, unknown>> {
|
|
|
31
56
|
insert(data: Partial<T>): this;
|
|
32
57
|
update(data: Partial<T>): this;
|
|
33
58
|
delete(): this;
|
|
59
|
+
upsert(data: Partial<T>, onConflict?: string): this;
|
|
34
60
|
|
|
35
61
|
then<R>(
|
|
36
62
|
resolve: (value: OpenbaseResponse<T[]>) => R,
|
|
@@ -91,6 +117,9 @@ export declare class OpenbaseClient {
|
|
|
91
117
|
/** Storage client for file upload, listing, URLs, and deletion */
|
|
92
118
|
storage: StorageClient;
|
|
93
119
|
|
|
120
|
+
/** Auth client for user registration, login, and session management */
|
|
121
|
+
auth: AuthClient;
|
|
122
|
+
|
|
94
123
|
/** Query builder for database operations */
|
|
95
124
|
from<T = Record<string, unknown>>(table: string): QueryBuilder<T>;
|
|
96
125
|
}
|
|
@@ -98,7 +127,8 @@ export declare class OpenbaseClient {
|
|
|
98
127
|
export declare function createClient(
|
|
99
128
|
baseUrl: string,
|
|
100
129
|
apiKey: string,
|
|
101
|
-
dbName: string
|
|
130
|
+
dbName: string,
|
|
131
|
+
options?: OpenbaseClientOptions
|
|
102
132
|
): OpenbaseClient;
|
|
103
133
|
|
|
104
134
|
export declare class RealtimeSubscription {
|