openbase-js 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs +229 -15
- package/index.d.ts +66 -0
- package/index.js +229 -14
- package/package.json +1 -1
- package/test.js +52 -1
package/index.cjs
CHANGED
|
@@ -6,9 +6,12 @@ class QueryBuilder {
|
|
|
6
6
|
this._apiKey = apiKey;
|
|
7
7
|
this._dbName = dbName;
|
|
8
8
|
this._table = table;
|
|
9
|
-
this._filters =
|
|
9
|
+
this._filters = [];
|
|
10
10
|
this._columns = '*';
|
|
11
11
|
this._limitVal = null;
|
|
12
|
+
this._offsetVal = null;
|
|
13
|
+
this._orderCol = null;
|
|
14
|
+
this._orderAsc = true;
|
|
12
15
|
this._single = false;
|
|
13
16
|
this._operation = 'select';
|
|
14
17
|
this._insertData = null;
|
|
@@ -21,8 +24,58 @@ class QueryBuilder {
|
|
|
21
24
|
return this;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
// ─── Filter operators ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
24
29
|
eq(column, value) {
|
|
25
|
-
this._filters
|
|
30
|
+
this._filters.push({ col: column, op: 'eq', val: value });
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
gt(column, value) {
|
|
35
|
+
this._filters.push({ col: column, op: 'gt', val: value });
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
lt(column, value) {
|
|
40
|
+
this._filters.push({ col: column, op: 'lt', val: value });
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
gte(column, value) {
|
|
45
|
+
this._filters.push({ col: column, op: 'gte', val: value });
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lte(column, value) {
|
|
50
|
+
this._filters.push({ col: column, op: 'lte', val: value });
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
like(column, pattern) {
|
|
55
|
+
this._filters.push({ col: column, op: 'like', val: pattern });
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ilike(column, pattern) {
|
|
60
|
+
this._filters.push({ col: column, op: 'ilike', val: pattern });
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
in(column, values) {
|
|
65
|
+
this._filters.push({ col: column, op: 'in', val: values });
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
is(column, value) {
|
|
70
|
+
this._filters.push({ col: column, op: 'is', val: value });
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Modifiers ─────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
order(column, { ascending = true } = {}) {
|
|
77
|
+
this._orderCol = column;
|
|
78
|
+
this._orderAsc = ascending;
|
|
26
79
|
return this;
|
|
27
80
|
}
|
|
28
81
|
|
|
@@ -31,12 +84,20 @@ class QueryBuilder {
|
|
|
31
84
|
return this;
|
|
32
85
|
}
|
|
33
86
|
|
|
87
|
+
range(from, to) {
|
|
88
|
+
this._limitVal = to - from + 1;
|
|
89
|
+
this._offsetVal = from;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
34
93
|
single() {
|
|
35
94
|
this._single = true;
|
|
36
95
|
this._limitVal = 1;
|
|
37
96
|
return this;
|
|
38
97
|
}
|
|
39
98
|
|
|
99
|
+
// ─── Mutation operators ────────────────────────────────────────────────────
|
|
100
|
+
|
|
40
101
|
insert(data) {
|
|
41
102
|
this._operation = 'insert';
|
|
42
103
|
this._insertData = data;
|
|
@@ -54,16 +115,46 @@ class QueryBuilder {
|
|
|
54
115
|
return this;
|
|
55
116
|
}
|
|
56
117
|
|
|
118
|
+
// ─── SQL Builder ───────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
_filterToSQL(f) {
|
|
121
|
+
const col = `"${f.col}"`;
|
|
122
|
+
switch (f.op) {
|
|
123
|
+
case 'eq': return typeof f.val === 'string' ? `${col} = '${f.val}'` : `${col} = ${f.val}`;
|
|
124
|
+
case 'gt': return `${col} > ${f.val}`;
|
|
125
|
+
case 'lt': return `${col} < ${f.val}`;
|
|
126
|
+
case 'gte': return `${col} >= ${f.val}`;
|
|
127
|
+
case 'lte': return `${col} <= ${f.val}`;
|
|
128
|
+
case 'like': return `${col} LIKE '${f.val}'`;
|
|
129
|
+
case 'ilike': return `${col} ILIKE '${f.val}'`;
|
|
130
|
+
case 'in': {
|
|
131
|
+
const list = f.val.map(v => typeof v === 'string' ? `'${v}'` : v).join(', ');
|
|
132
|
+
return `${col} IN (${list})`;
|
|
133
|
+
}
|
|
134
|
+
case 'is': {
|
|
135
|
+
if (f.val === null) return `${col} IS NULL`;
|
|
136
|
+
if (f.val === true) return `${col} IS TRUE`;
|
|
137
|
+
if (f.val === false) return `${col} IS FALSE`;
|
|
138
|
+
return `${col} IS NULL`;
|
|
139
|
+
}
|
|
140
|
+
default: return `${col} = '${f.val}'`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
57
144
|
_buildSQL() {
|
|
58
145
|
let sql = '';
|
|
59
146
|
|
|
60
147
|
if (this._operation === 'select') {
|
|
61
148
|
sql = `SELECT ${this._columns} FROM "${this._table}"`;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
149
|
+
if (this._filters.length) {
|
|
150
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
151
|
+
sql += ` WHERE ${where}`;
|
|
152
|
+
}
|
|
153
|
+
if (this._orderCol) {
|
|
154
|
+
sql += ` ORDER BY "${this._orderCol}" ${this._orderAsc ? 'ASC' : 'DESC'}`;
|
|
155
|
+
}
|
|
66
156
|
if (this._limitVal) sql += ` LIMIT ${this._limitVal}`;
|
|
157
|
+
if (this._offsetVal) sql += ` OFFSET ${this._offsetVal}`;
|
|
67
158
|
|
|
68
159
|
} else if (this._operation === 'insert') {
|
|
69
160
|
const cols = Object.keys(this._insertData).map(c => `"${c}"`).join(', ');
|
|
@@ -76,25 +167,27 @@ class QueryBuilder {
|
|
|
76
167
|
const setClauses = Object.entries(this._updateData).map(([col, val]) =>
|
|
77
168
|
typeof val === 'string' ? `"${col}" = '${val.replace(/'/g, "''")}'` : `"${col}" = ${val}`
|
|
78
169
|
).join(', ');
|
|
79
|
-
const whereParts = Object.entries(this._filters).map(([col, val]) =>
|
|
80
|
-
typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
|
|
81
|
-
);
|
|
82
170
|
sql = `UPDATE "${this._table}" SET ${setClauses}`;
|
|
83
|
-
if (
|
|
171
|
+
if (this._filters.length) {
|
|
172
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
173
|
+
sql += ` WHERE ${where}`;
|
|
174
|
+
}
|
|
84
175
|
sql += ` RETURNING *`;
|
|
85
176
|
|
|
86
177
|
} else if (this._operation === 'delete') {
|
|
87
|
-
const whereParts = Object.entries(this._filters).map(([col, val]) =>
|
|
88
|
-
typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
|
|
89
|
-
);
|
|
90
178
|
sql = `DELETE FROM "${this._table}"`;
|
|
91
|
-
if (
|
|
179
|
+
if (this._filters.length) {
|
|
180
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
181
|
+
sql += ` WHERE ${where}`;
|
|
182
|
+
}
|
|
92
183
|
sql += ` RETURNING *`;
|
|
93
184
|
}
|
|
94
185
|
|
|
95
186
|
return sql;
|
|
96
187
|
}
|
|
97
188
|
|
|
189
|
+
// ─── Executor ──────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
98
191
|
async _buildAndRun() {
|
|
99
192
|
const sql = this._buildSQL();
|
|
100
193
|
try {
|
|
@@ -122,6 +215,127 @@ class QueryBuilder {
|
|
|
122
215
|
}
|
|
123
216
|
}
|
|
124
217
|
|
|
218
|
+
// ─── Storage Client ───────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
class StorageClient {
|
|
221
|
+
constructor(baseUrl, apiKey, dbName) {
|
|
222
|
+
this._baseUrl = baseUrl;
|
|
223
|
+
this._apiKey = apiKey;
|
|
224
|
+
this._dbName = dbName;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_headers() {
|
|
228
|
+
return { 'Authorization': `Bearer ${this._apiKey}` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Detect if we're running in Node.js
|
|
232
|
+
_isNode() {
|
|
233
|
+
return typeof window === 'undefined' && typeof process !== 'undefined';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Build a FormData object that works in both browser and Node.js
|
|
237
|
+
// In browser: file = File or Blob object
|
|
238
|
+
// In Node.js: file = file path string, Buffer, or ReadStream
|
|
239
|
+
async _buildFormData(file, filename) {
|
|
240
|
+
if (this._isNode()) {
|
|
241
|
+
// Node.js — use form-data package
|
|
242
|
+
let FormDataNode;
|
|
243
|
+
try {
|
|
244
|
+
FormDataNode = require('form-data');
|
|
245
|
+
} catch {
|
|
246
|
+
throw new Error(
|
|
247
|
+
'[openbase] In Node.js, storage.upload() requires the "form-data" package.\n' +
|
|
248
|
+
'Install it with: npm install form-data'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const fs = require('fs');
|
|
253
|
+
const path = require('path');
|
|
254
|
+
const formData = new FormDataNode();
|
|
255
|
+
|
|
256
|
+
if (typeof file === 'string') {
|
|
257
|
+
// file path string — read from disk
|
|
258
|
+
const resolvedName = filename || path.basename(file);
|
|
259
|
+
formData.append('file', fs.createReadStream(file), resolvedName);
|
|
260
|
+
} else if (Buffer.isBuffer(file)) {
|
|
261
|
+
// Buffer
|
|
262
|
+
formData.append('file', file, filename || 'file');
|
|
263
|
+
} else {
|
|
264
|
+
// ReadStream or anything else
|
|
265
|
+
formData.append('file', file, filename || 'file');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return formData;
|
|
269
|
+
|
|
270
|
+
} else {
|
|
271
|
+
// Browser — use native FormData
|
|
272
|
+
const formData = new FormData();
|
|
273
|
+
formData.append('file', file, filename || file.name);
|
|
274
|
+
return formData;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Upload a file
|
|
279
|
+
// Browser: pass a File or Blob (from <input type="file"> or drag-and-drop)
|
|
280
|
+
// Node.js: pass a file path string, Buffer, or ReadStream
|
|
281
|
+
async upload(file, filename) {
|
|
282
|
+
try {
|
|
283
|
+
const formData = await this._buildFormData(file, filename);
|
|
284
|
+
|
|
285
|
+
// In Node.js, form-data needs its own headers (includes boundary)
|
|
286
|
+
const headers = this._isNode()
|
|
287
|
+
? { ...this._headers(), ...formData.getHeaders() }
|
|
288
|
+
: this._headers();
|
|
289
|
+
|
|
290
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/upload`, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers,
|
|
293
|
+
body: formData,
|
|
294
|
+
});
|
|
295
|
+
return await res.json();
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return { data: null, error: err.message };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// List all files in this project's bucket
|
|
302
|
+
async list() {
|
|
303
|
+
try {
|
|
304
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/list`, {
|
|
305
|
+
headers: this._headers(),
|
|
306
|
+
});
|
|
307
|
+
return await res.json();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return { data: null, error: err.message };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Get a presigned URL valid for 7 days
|
|
314
|
+
async getUrl(filename) {
|
|
315
|
+
try {
|
|
316
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/url/${encodeURIComponent(filename)}`, {
|
|
317
|
+
headers: this._headers(),
|
|
318
|
+
});
|
|
319
|
+
return await res.json();
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return { data: null, error: err.message };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Delete a file
|
|
326
|
+
async remove(filename) {
|
|
327
|
+
try {
|
|
328
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/${encodeURIComponent(filename)}`, {
|
|
329
|
+
method: 'DELETE',
|
|
330
|
+
headers: this._headers(),
|
|
331
|
+
});
|
|
332
|
+
return await res.json();
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return { data: null, error: err.message };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
125
339
|
// ─── Client ───────────────────────────────────────────────────────────────────
|
|
126
340
|
|
|
127
341
|
class OpenbaseClient {
|
|
@@ -129,6 +343,7 @@ class OpenbaseClient {
|
|
|
129
343
|
this._baseUrl = baseUrl.replace(/\/$/, '');
|
|
130
344
|
this._apiKey = apiKey;
|
|
131
345
|
this._dbName = dbName;
|
|
346
|
+
this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName);
|
|
132
347
|
}
|
|
133
348
|
|
|
134
349
|
from(table) {
|
|
@@ -152,5 +367,4 @@ if (typeof module !== 'undefined') {
|
|
|
152
367
|
if (typeof window !== 'undefined') {
|
|
153
368
|
window.openbase = { createClient };
|
|
154
369
|
}
|
|
155
|
-
|
|
156
370
|
module.exports = { createClient };
|
package/index.d.ts
CHANGED
|
@@ -1,23 +1,89 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { ReadStream } from 'fs';
|
|
3
|
+
|
|
1
4
|
export interface OpenbaseResponse<T> {
|
|
2
5
|
data: T | null;
|
|
3
6
|
error: string | null;
|
|
4
7
|
}
|
|
5
8
|
|
|
6
9
|
export declare class QueryBuilder<T = Record<string, unknown>> {
|
|
10
|
+
// Column selection
|
|
7
11
|
select(columns?: string): this;
|
|
12
|
+
|
|
13
|
+
// Filter operators
|
|
8
14
|
eq(column: string, value: unknown): this;
|
|
15
|
+
gt(column: string, value: number): this;
|
|
16
|
+
lt(column: string, value: number): this;
|
|
17
|
+
gte(column: string, value: number): this;
|
|
18
|
+
lte(column: string, value: number): this;
|
|
19
|
+
like(column: string, pattern: string): this;
|
|
20
|
+
ilike(column: string, pattern: string): this;
|
|
21
|
+
in(column: string, values: unknown[]): this;
|
|
22
|
+
is(column: string, value: null | boolean): this;
|
|
23
|
+
|
|
24
|
+
// Modifiers
|
|
25
|
+
order(column: string, options?: { ascending?: boolean }): this;
|
|
9
26
|
limit(n: number): this;
|
|
27
|
+
range(from: number, to: number): this;
|
|
10
28
|
single(): this;
|
|
29
|
+
|
|
30
|
+
// Mutations
|
|
11
31
|
insert(data: Partial<T>): this;
|
|
12
32
|
update(data: Partial<T>): this;
|
|
13
33
|
delete(): this;
|
|
34
|
+
|
|
14
35
|
then<R>(
|
|
15
36
|
resolve: (value: OpenbaseResponse<T[]>) => R,
|
|
16
37
|
reject?: (reason: unknown) => R
|
|
17
38
|
): Promise<R>;
|
|
18
39
|
}
|
|
19
40
|
|
|
41
|
+
export interface StorageFileInfo {
|
|
42
|
+
name: string;
|
|
43
|
+
size: number;
|
|
44
|
+
content_type: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface StorageListItem {
|
|
48
|
+
name: string;
|
|
49
|
+
size: number;
|
|
50
|
+
last_modified: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export declare class StorageClient {
|
|
54
|
+
/**
|
|
55
|
+
* Upload a file to this project's storage bucket.
|
|
56
|
+
*
|
|
57
|
+
* Browser: pass a File or Blob (e.g. from <input type="file">)
|
|
58
|
+
* Node.js: pass a file path string, Buffer, or ReadStream
|
|
59
|
+
* Requires the "form-data" package: npm install form-data
|
|
60
|
+
*/
|
|
61
|
+
upload(
|
|
62
|
+
file: File | Blob | string | Buffer | ReadStream,
|
|
63
|
+
filename?: string
|
|
64
|
+
): Promise<OpenbaseResponse<StorageFileInfo>>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* List all files in this project's storage bucket.
|
|
68
|
+
*/
|
|
69
|
+
list(): Promise<OpenbaseResponse<StorageListItem[]>>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get a presigned URL for a file, valid for 7 days.
|
|
73
|
+
*/
|
|
74
|
+
getUrl(filename: string): Promise<OpenbaseResponse<{ url: string }>>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Delete a file from this project's storage bucket.
|
|
78
|
+
*/
|
|
79
|
+
remove(filename: string): Promise<OpenbaseResponse<{ name: string }>>;
|
|
80
|
+
}
|
|
81
|
+
|
|
20
82
|
export declare class OpenbaseClient {
|
|
83
|
+
/** Storage client for file upload, listing, URLs, and deletion */
|
|
84
|
+
storage: StorageClient;
|
|
85
|
+
|
|
86
|
+
/** Query builder for database operations */
|
|
21
87
|
from<T = Record<string, unknown>>(table: string): QueryBuilder<T>;
|
|
22
88
|
}
|
|
23
89
|
|
package/index.js
CHANGED
|
@@ -6,9 +6,12 @@ class QueryBuilder {
|
|
|
6
6
|
this._apiKey = apiKey;
|
|
7
7
|
this._dbName = dbName;
|
|
8
8
|
this._table = table;
|
|
9
|
-
this._filters =
|
|
9
|
+
this._filters = [];
|
|
10
10
|
this._columns = '*';
|
|
11
11
|
this._limitVal = null;
|
|
12
|
+
this._offsetVal = null;
|
|
13
|
+
this._orderCol = null;
|
|
14
|
+
this._orderAsc = true;
|
|
12
15
|
this._single = false;
|
|
13
16
|
this._operation = 'select';
|
|
14
17
|
this._insertData = null;
|
|
@@ -21,8 +24,58 @@ class QueryBuilder {
|
|
|
21
24
|
return this;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
// ─── Filter operators ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
24
29
|
eq(column, value) {
|
|
25
|
-
this._filters
|
|
30
|
+
this._filters.push({ col: column, op: 'eq', val: value });
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
gt(column, value) {
|
|
35
|
+
this._filters.push({ col: column, op: 'gt', val: value });
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
lt(column, value) {
|
|
40
|
+
this._filters.push({ col: column, op: 'lt', val: value });
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
gte(column, value) {
|
|
45
|
+
this._filters.push({ col: column, op: 'gte', val: value });
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lte(column, value) {
|
|
50
|
+
this._filters.push({ col: column, op: 'lte', val: value });
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
like(column, pattern) {
|
|
55
|
+
this._filters.push({ col: column, op: 'like', val: pattern });
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ilike(column, pattern) {
|
|
60
|
+
this._filters.push({ col: column, op: 'ilike', val: pattern });
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
in(column, values) {
|
|
65
|
+
this._filters.push({ col: column, op: 'in', val: values });
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
is(column, value) {
|
|
70
|
+
this._filters.push({ col: column, op: 'is', val: value });
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Modifiers ─────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
order(column, { ascending = true } = {}) {
|
|
77
|
+
this._orderCol = column;
|
|
78
|
+
this._orderAsc = ascending;
|
|
26
79
|
return this;
|
|
27
80
|
}
|
|
28
81
|
|
|
@@ -31,12 +84,20 @@ class QueryBuilder {
|
|
|
31
84
|
return this;
|
|
32
85
|
}
|
|
33
86
|
|
|
87
|
+
range(from, to) {
|
|
88
|
+
this._limitVal = to - from + 1;
|
|
89
|
+
this._offsetVal = from;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
34
93
|
single() {
|
|
35
94
|
this._single = true;
|
|
36
95
|
this._limitVal = 1;
|
|
37
96
|
return this;
|
|
38
97
|
}
|
|
39
98
|
|
|
99
|
+
// ─── Mutation operators ────────────────────────────────────────────────────
|
|
100
|
+
|
|
40
101
|
insert(data) {
|
|
41
102
|
this._operation = 'insert';
|
|
42
103
|
this._insertData = data;
|
|
@@ -54,16 +115,46 @@ class QueryBuilder {
|
|
|
54
115
|
return this;
|
|
55
116
|
}
|
|
56
117
|
|
|
118
|
+
// ─── SQL Builder ───────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
_filterToSQL(f) {
|
|
121
|
+
const col = `"${f.col}"`;
|
|
122
|
+
switch (f.op) {
|
|
123
|
+
case 'eq': return typeof f.val === 'string' ? `${col} = '${f.val}'` : `${col} = ${f.val}`;
|
|
124
|
+
case 'gt': return `${col} > ${f.val}`;
|
|
125
|
+
case 'lt': return `${col} < ${f.val}`;
|
|
126
|
+
case 'gte': return `${col} >= ${f.val}`;
|
|
127
|
+
case 'lte': return `${col} <= ${f.val}`;
|
|
128
|
+
case 'like': return `${col} LIKE '${f.val}'`;
|
|
129
|
+
case 'ilike': return `${col} ILIKE '${f.val}'`;
|
|
130
|
+
case 'in': {
|
|
131
|
+
const list = f.val.map(v => typeof v === 'string' ? `'${v}'` : v).join(', ');
|
|
132
|
+
return `${col} IN (${list})`;
|
|
133
|
+
}
|
|
134
|
+
case 'is': {
|
|
135
|
+
if (f.val === null) return `${col} IS NULL`;
|
|
136
|
+
if (f.val === true) return `${col} IS TRUE`;
|
|
137
|
+
if (f.val === false) return `${col} IS FALSE`;
|
|
138
|
+
return `${col} IS NULL`;
|
|
139
|
+
}
|
|
140
|
+
default: return `${col} = '${f.val}'`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
57
144
|
_buildSQL() {
|
|
58
145
|
let sql = '';
|
|
59
146
|
|
|
60
147
|
if (this._operation === 'select') {
|
|
61
148
|
sql = `SELECT ${this._columns} FROM "${this._table}"`;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
149
|
+
if (this._filters.length) {
|
|
150
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
151
|
+
sql += ` WHERE ${where}`;
|
|
152
|
+
}
|
|
153
|
+
if (this._orderCol) {
|
|
154
|
+
sql += ` ORDER BY "${this._orderCol}" ${this._orderAsc ? 'ASC' : 'DESC'}`;
|
|
155
|
+
}
|
|
66
156
|
if (this._limitVal) sql += ` LIMIT ${this._limitVal}`;
|
|
157
|
+
if (this._offsetVal) sql += ` OFFSET ${this._offsetVal}`;
|
|
67
158
|
|
|
68
159
|
} else if (this._operation === 'insert') {
|
|
69
160
|
const cols = Object.keys(this._insertData).map(c => `"${c}"`).join(', ');
|
|
@@ -76,25 +167,27 @@ class QueryBuilder {
|
|
|
76
167
|
const setClauses = Object.entries(this._updateData).map(([col, val]) =>
|
|
77
168
|
typeof val === 'string' ? `"${col}" = '${val.replace(/'/g, "''")}'` : `"${col}" = ${val}`
|
|
78
169
|
).join(', ');
|
|
79
|
-
const whereParts = Object.entries(this._filters).map(([col, val]) =>
|
|
80
|
-
typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
|
|
81
|
-
);
|
|
82
170
|
sql = `UPDATE "${this._table}" SET ${setClauses}`;
|
|
83
|
-
if (
|
|
171
|
+
if (this._filters.length) {
|
|
172
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
173
|
+
sql += ` WHERE ${where}`;
|
|
174
|
+
}
|
|
84
175
|
sql += ` RETURNING *`;
|
|
85
176
|
|
|
86
177
|
} else if (this._operation === 'delete') {
|
|
87
|
-
const whereParts = Object.entries(this._filters).map(([col, val]) =>
|
|
88
|
-
typeof val === 'string' ? `"${col}" = '${val}'` : `"${col}" = ${val}`
|
|
89
|
-
);
|
|
90
178
|
sql = `DELETE FROM "${this._table}"`;
|
|
91
|
-
if (
|
|
179
|
+
if (this._filters.length) {
|
|
180
|
+
const where = this._filters.map(f => this._filterToSQL(f)).join(' AND ');
|
|
181
|
+
sql += ` WHERE ${where}`;
|
|
182
|
+
}
|
|
92
183
|
sql += ` RETURNING *`;
|
|
93
184
|
}
|
|
94
185
|
|
|
95
186
|
return sql;
|
|
96
187
|
}
|
|
97
188
|
|
|
189
|
+
// ─── Executor ──────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
98
191
|
async _buildAndRun() {
|
|
99
192
|
const sql = this._buildSQL();
|
|
100
193
|
try {
|
|
@@ -122,6 +215,127 @@ class QueryBuilder {
|
|
|
122
215
|
}
|
|
123
216
|
}
|
|
124
217
|
|
|
218
|
+
// ─── Storage Client ───────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
class StorageClient {
|
|
221
|
+
constructor(baseUrl, apiKey, dbName) {
|
|
222
|
+
this._baseUrl = baseUrl;
|
|
223
|
+
this._apiKey = apiKey;
|
|
224
|
+
this._dbName = dbName;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_headers() {
|
|
228
|
+
return { 'Authorization': `Bearer ${this._apiKey}` };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Detect if we're running in Node.js
|
|
232
|
+
_isNode() {
|
|
233
|
+
return typeof window === 'undefined' && typeof process !== 'undefined';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Build a FormData object that works in both browser and Node.js
|
|
237
|
+
// In browser: file = File or Blob object
|
|
238
|
+
// In Node.js: file = file path string, Buffer, or ReadStream
|
|
239
|
+
async _buildFormData(file, filename) {
|
|
240
|
+
if (this._isNode()) {
|
|
241
|
+
// Node.js — use form-data package
|
|
242
|
+
let FormDataNode;
|
|
243
|
+
try {
|
|
244
|
+
FormDataNode = require('form-data');
|
|
245
|
+
} catch {
|
|
246
|
+
throw new Error(
|
|
247
|
+
'[openbase] In Node.js, storage.upload() requires the "form-data" package.\n' +
|
|
248
|
+
'Install it with: npm install form-data'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const fs = require('fs');
|
|
253
|
+
const path = require('path');
|
|
254
|
+
const formData = new FormDataNode();
|
|
255
|
+
|
|
256
|
+
if (typeof file === 'string') {
|
|
257
|
+
// file path string — read from disk
|
|
258
|
+
const resolvedName = filename || path.basename(file);
|
|
259
|
+
formData.append('file', fs.createReadStream(file), resolvedName);
|
|
260
|
+
} else if (Buffer.isBuffer(file)) {
|
|
261
|
+
// Buffer
|
|
262
|
+
formData.append('file', file, filename || 'file');
|
|
263
|
+
} else {
|
|
264
|
+
// ReadStream or anything else
|
|
265
|
+
formData.append('file', file, filename || 'file');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return formData;
|
|
269
|
+
|
|
270
|
+
} else {
|
|
271
|
+
// Browser — use native FormData
|
|
272
|
+
const formData = new FormData();
|
|
273
|
+
formData.append('file', file, filename || file.name);
|
|
274
|
+
return formData;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Upload a file
|
|
279
|
+
// Browser: pass a File or Blob (from <input type="file"> or drag-and-drop)
|
|
280
|
+
// Node.js: pass a file path string, Buffer, or ReadStream
|
|
281
|
+
async upload(file, filename) {
|
|
282
|
+
try {
|
|
283
|
+
const formData = await this._buildFormData(file, filename);
|
|
284
|
+
|
|
285
|
+
// In Node.js, form-data needs its own headers (includes boundary)
|
|
286
|
+
const headers = this._isNode()
|
|
287
|
+
? { ...this._headers(), ...formData.getHeaders() }
|
|
288
|
+
: this._headers();
|
|
289
|
+
|
|
290
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/upload`, {
|
|
291
|
+
method: 'POST',
|
|
292
|
+
headers,
|
|
293
|
+
body: formData,
|
|
294
|
+
});
|
|
295
|
+
return await res.json();
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return { data: null, error: err.message };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// List all files in this project's bucket
|
|
302
|
+
async list() {
|
|
303
|
+
try {
|
|
304
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/list`, {
|
|
305
|
+
headers: this._headers(),
|
|
306
|
+
});
|
|
307
|
+
return await res.json();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return { data: null, error: err.message };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Get a presigned URL valid for 7 days
|
|
314
|
+
async getUrl(filename) {
|
|
315
|
+
try {
|
|
316
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/url/${encodeURIComponent(filename)}`, {
|
|
317
|
+
headers: this._headers(),
|
|
318
|
+
});
|
|
319
|
+
return await res.json();
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return { data: null, error: err.message };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Delete a file
|
|
326
|
+
async remove(filename) {
|
|
327
|
+
try {
|
|
328
|
+
const res = await fetch(`${this._baseUrl}/storage/${this._dbName}/${encodeURIComponent(filename)}`, {
|
|
329
|
+
method: 'DELETE',
|
|
330
|
+
headers: this._headers(),
|
|
331
|
+
});
|
|
332
|
+
return await res.json();
|
|
333
|
+
} catch (err) {
|
|
334
|
+
return { data: null, error: err.message };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
125
339
|
// ─── Client ───────────────────────────────────────────────────────────────────
|
|
126
340
|
|
|
127
341
|
class OpenbaseClient {
|
|
@@ -129,6 +343,7 @@ class OpenbaseClient {
|
|
|
129
343
|
this._baseUrl = baseUrl.replace(/\/$/, '');
|
|
130
344
|
this._apiKey = apiKey;
|
|
131
345
|
this._dbName = dbName;
|
|
346
|
+
this.storage = new StorageClient(this._baseUrl, this._apiKey, this._dbName);
|
|
132
347
|
}
|
|
133
348
|
|
|
134
349
|
from(table) {
|
package/package.json
CHANGED
package/test.js
CHANGED
|
@@ -43,4 +43,55 @@ async function test() {
|
|
|
43
43
|
console.log('deleted:', deleted);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
async function testOperators() {
|
|
47
|
+
console.log('\n── GT / LT ─────────────────────────');
|
|
48
|
+
const { data: gt } = await client.from('leads').select('*').gt('id', 2);
|
|
49
|
+
console.log('gt id > 2:', gt);
|
|
50
|
+
|
|
51
|
+
console.log('\n── GTE / LTE ───────────────────────');
|
|
52
|
+
const { data: lte } = await client.from('leads').select('*').lte('id', 3);
|
|
53
|
+
console.log('lte id <= 3:', lte);
|
|
54
|
+
|
|
55
|
+
console.log('\n── LIKE ────────────────────────────');
|
|
56
|
+
const { data: like } = await client.from('leads').select('*').like('company_name', '%oo%');
|
|
57
|
+
console.log('like company_name %oo%:', like);
|
|
58
|
+
|
|
59
|
+
console.log('\n── ILIKE (case insensitive) ────────');
|
|
60
|
+
const { data: ilike } = await client.from('leads').select('*').ilike('company_name', '%google%');
|
|
61
|
+
console.log('ilike company_name %google%:', ilike);
|
|
62
|
+
|
|
63
|
+
console.log('\n── IN ──────────────────────────────');
|
|
64
|
+
const { data: inResult } = await client.from('leads').select('*').in('slug', ['google', 'meta']);
|
|
65
|
+
console.log('in slug [google, meta]:', inResult);
|
|
66
|
+
|
|
67
|
+
console.log('\n── IS NULL ─────────────────────────');
|
|
68
|
+
const { data: isNull } = await client.from('leads').select('*').is('custom_pitch', null);
|
|
69
|
+
console.log('is custom_pitch null:', isNull);
|
|
70
|
+
|
|
71
|
+
console.log('\n── ORDER ASC ───────────────────────');
|
|
72
|
+
const { data: asc } = await client.from('leads').select('*').order('id', { ascending: true });
|
|
73
|
+
console.log('order id asc:', asc?.map(r => r.id));
|
|
74
|
+
|
|
75
|
+
console.log('\n── ORDER DESC ──────────────────────');
|
|
76
|
+
const { data: desc } = await client.from('leads').select('*').order('id', { ascending: false });
|
|
77
|
+
console.log('order id desc:', desc?.map(r => r.id));
|
|
78
|
+
|
|
79
|
+
console.log('\n── LIMIT ───────────────────────────');
|
|
80
|
+
const { data: limited } = await client.from('leads').select('*').limit(2);
|
|
81
|
+
console.log('limit 2:', limited?.length, 'rows');
|
|
82
|
+
|
|
83
|
+
console.log('\n── RANGE (pagination) ──────────────');
|
|
84
|
+
const { data: page } = await client.from('leads').select('*').range(0, 1);
|
|
85
|
+
console.log('range 0-1:', page?.length, 'rows');
|
|
86
|
+
|
|
87
|
+
console.log('\n── CHAINED (gt + order + limit) ────');
|
|
88
|
+
const { data: chained } = await client
|
|
89
|
+
.from('leads')
|
|
90
|
+
.select('*')
|
|
91
|
+
.gt('id', 1)
|
|
92
|
+
.order('id', { ascending: false })
|
|
93
|
+
.limit(3);
|
|
94
|
+
console.log('chained:', chained);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
testOperators().catch(console.error);
|