sqlite-hub-client 0.6.0 → 0.8.0
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 +209 -0
- package/dist/database.d.ts +49 -1
- package/dist/database.js +58 -1
- package/dist/files.d.ts +135 -0
- package/dist/files.js +144 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,25 @@ const db = connect({
|
|
|
20
20
|
db: "my-service",
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
// File upload
|
|
24
|
+
const uploaded = await db.files!.upload({
|
|
25
|
+
file: new Blob(["hello world"], { type: "text/plain" }),
|
|
26
|
+
filename: "hello.txt",
|
|
27
|
+
folderPath: "docs/examples",
|
|
28
|
+
metadata: { source: "docs" },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// File list
|
|
32
|
+
const filePage = await db.files!.list({
|
|
33
|
+
limit: 20,
|
|
34
|
+
offset: 0,
|
|
35
|
+
folderPrefix: "docs",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Presigned browser URL (for third-party dashboards)
|
|
39
|
+
const signed = await db.files!.presign(uploaded.id, { expiresIn: 1800 });
|
|
40
|
+
console.log(signed.url);
|
|
41
|
+
|
|
23
42
|
// Create table
|
|
24
43
|
await db.createTable("users", [
|
|
25
44
|
{ name: "id", type: "INTEGER", primaryKey: true, autoIncrement: true },
|
|
@@ -73,6 +92,26 @@ const active = await db.count("users", { active: 1 });
|
|
|
73
92
|
// Exists check
|
|
74
93
|
const exists = await db.exists("users", { email: "alice@example.com" });
|
|
75
94
|
|
|
95
|
+
// Upsert — INSERT OR REPLACE (relies on PRIMARY KEY / UNIQUE constraints)
|
|
96
|
+
await db.upsert("users", { id: 1, email: "alice@example.com", name: "Alice" });
|
|
97
|
+
|
|
98
|
+
// Upsert — explicit conflict target
|
|
99
|
+
await db.upsert(
|
|
100
|
+
"users",
|
|
101
|
+
{ email: "alice@example.com", name: "Alice" },
|
|
102
|
+
{ conflictColumns: ["email"] }
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Bulk upsert
|
|
106
|
+
await db.upsertMany(
|
|
107
|
+
"users",
|
|
108
|
+
[
|
|
109
|
+
{ email: "alice@example.com", name: "Alice" },
|
|
110
|
+
{ email: "bob@example.com", name: "Bob" },
|
|
111
|
+
],
|
|
112
|
+
{ conflictColumns: ["email"] }
|
|
113
|
+
);
|
|
114
|
+
|
|
76
115
|
// Update
|
|
77
116
|
await db.update("users", { name: "Alice Smith" }, { id: 1 });
|
|
78
117
|
|
|
@@ -168,6 +207,46 @@ await db.insertMany("posts", [
|
|
|
168
207
|
]);
|
|
169
208
|
```
|
|
170
209
|
|
|
210
|
+
#### `db.upsert(table, data, options?)` → `ExecResult`
|
|
211
|
+
|
|
212
|
+
Insert a row or update it on conflict.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// INSERT OR REPLACE — relies on PRIMARY KEY / UNIQUE constraints
|
|
216
|
+
await db.upsert("users", { id: 1, email: "alice@example.com", name: "Alice" });
|
|
217
|
+
|
|
218
|
+
// INSERT … ON CONFLICT(email) DO UPDATE SET name = excluded.name
|
|
219
|
+
await db.upsert(
|
|
220
|
+
"users",
|
|
221
|
+
{ email: "alice@example.com", name: "Alice" },
|
|
222
|
+
{ conflictColumns: ["email"] }
|
|
223
|
+
);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `db.upsertMany(table, rows, options?)` → `ExecResult`
|
|
227
|
+
|
|
228
|
+
Bulk version of `upsert`.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
await db.upsertMany(
|
|
232
|
+
"users",
|
|
233
|
+
[
|
|
234
|
+
{ email: "alice@example.com", name: "Alice" },
|
|
235
|
+
{ email: "bob@example.com", name: "Bob" },
|
|
236
|
+
],
|
|
237
|
+
{ conflictColumns: ["email"] }
|
|
238
|
+
);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**`UpsertOptions` fields:**
|
|
242
|
+
|
|
243
|
+
| Field | Type | Description |
|
|
244
|
+
| ------------------ | ---------- | --------------------------------------------------------------------------------------------------------------- |
|
|
245
|
+
| `conflictColumns` | `string[]` | Conflict-target columns. Omit to use `INSERT OR REPLACE` (relies on PRIMARY KEY / UNIQUE). |
|
|
246
|
+
| `updateColumns` | `string[]` | Columns to overwrite on conflict. Defaults to all columns **not** in `conflictColumns`. Pass `[]` for DO NOTHING. |
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
171
250
|
#### `db.update(table, data, where)` → `ExecResult`
|
|
172
251
|
|
|
173
252
|
```ts
|
|
@@ -246,6 +325,136 @@ await db.exec("CREATE INDEX IF NOT EXISTS idx_title ON posts (title)");
|
|
|
246
325
|
|
|
247
326
|
---
|
|
248
327
|
|
|
328
|
+
### Files
|
|
329
|
+
|
|
330
|
+
#### `db.files.upload(input)`
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const uploaded = await db.files!.upload({
|
|
334
|
+
file: new Blob(["report"], { type: "text/plain" }),
|
|
335
|
+
filename: "report.txt",
|
|
336
|
+
folderPath: "ops/reports",
|
|
337
|
+
conflictMode: "replace", // or "error"
|
|
338
|
+
contentType: "text/plain",
|
|
339
|
+
metadata: { owner: "ops" },
|
|
340
|
+
expiresIn: 3600,
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### `db.files.list(options?)`
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
const files = await db.files!.list({
|
|
348
|
+
limit: 50,
|
|
349
|
+
offset: 0,
|
|
350
|
+
sort: "uploaded_at",
|
|
351
|
+
order: "desc",
|
|
352
|
+
folderPrefix: "ops",
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### `db.files.getMeta(fileId)`
|
|
357
|
+
|
|
358
|
+
```ts
|
|
359
|
+
const meta = await db.files!.getMeta("file-id");
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### `db.files.delete(fileId)`
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
await db.files!.delete("file-id");
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### `db.files.bulkDelete(fileIds)`
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
await db.files!.bulkDelete(["id1", "id2"]);
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### `db.files.presign(fileId, options?)`
|
|
375
|
+
|
|
376
|
+
Returns a signed URL that can be opened directly in browser without bearer token.
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
const signed = await db.files!.presign("file-id", {
|
|
380
|
+
expiresIn: 900,
|
|
381
|
+
disposition: "inline", // or "attachment"
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// e.g. embed in dashboard iframe/image link
|
|
385
|
+
console.log(signed.url);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### `db.files.batchPresign(options)`
|
|
389
|
+
|
|
390
|
+
Generate presigned URLs for multiple files at once. Maximum 100 file IDs per request.
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
const batch = await db.files!.batchPresign({
|
|
394
|
+
fileIds: ["id1", "id2", "id3"],
|
|
395
|
+
expiresIn: 900,
|
|
396
|
+
disposition: "inline",
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Check results
|
|
400
|
+
batch.results.forEach((result) => {
|
|
401
|
+
if (result.error) {
|
|
402
|
+
console.error(`Failed for ${result.file_id}: ${result.error}`);
|
|
403
|
+
} else {
|
|
404
|
+
console.log(`URL for ${result.file_id}: ${result.url}`);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
#### `db.files.createFileAccessToken(options?)`
|
|
410
|
+
|
|
411
|
+
Creates a long-lived access token for file operations. Unlike presigned URLs which are per-file, this token works for all file operations on the database.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const tokenData = await db.files!.createFileAccessToken({
|
|
415
|
+
scope: "files:read",
|
|
416
|
+
expiresIn: 2592000, // 30 days
|
|
417
|
+
description: "Dashboard access",
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Use in query parameter or Authorization header
|
|
421
|
+
const fileUrl = `https://host/api/db/mydb/files/file-id?token=${tokenData.token}`;
|
|
422
|
+
|
|
423
|
+
// Or with Authorization header:
|
|
424
|
+
// Authorization: Bearer <token>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
The token is read-only (`files:read`) and cannot be used for upload/delete operations. Default TTL is 30 days, maximum is 1 year.
|
|
428
|
+
|
|
429
|
+
#### `db.files.revokeFileAccessToken(options)`
|
|
430
|
+
|
|
431
|
+
Revokes a previously issued file access token.
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
// Revoke using the original token string
|
|
435
|
+
await db.files!.revokeFileAccessToken({
|
|
436
|
+
token: tokenData.token,
|
|
437
|
+
reason: "rotated credential",
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Or revoke by token ID + expiresAt if stored separately
|
|
441
|
+
await db.files!.revokeFileAccessToken({
|
|
442
|
+
tokenId: "token-id",
|
|
443
|
+
expiresAt: tokenData.expires_at,
|
|
444
|
+
reason: "compromised",
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### `db.files.getDownloadUrl(fileId)`
|
|
449
|
+
|
|
450
|
+
Returns the authenticated endpoint URL (requires bearer auth unless using presigned query).
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
const url = db.files!.getDownloadUrl("file-id");
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
249
458
|
## Architecture
|
|
250
459
|
|
|
251
460
|
```
|
package/dist/database.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExecResult, IAdapter, RawResult } from "./adapters/types.js";
|
|
2
|
+
import type { FileClient } from "./files.js";
|
|
2
3
|
export type WhereClause = Record<string, unknown>;
|
|
3
4
|
export type OrderDirection = "ASC" | "DESC";
|
|
4
5
|
export interface FindOptions {
|
|
@@ -25,6 +26,21 @@ export interface CreateIndexOptions {
|
|
|
25
26
|
unique?: boolean;
|
|
26
27
|
ifNotExists?: boolean;
|
|
27
28
|
}
|
|
29
|
+
export interface UpsertOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Column(s) that form the conflict target.
|
|
32
|
+
* When provided the statement becomes:
|
|
33
|
+
* INSERT INTO … ON CONFLICT(col1, col2) DO UPDATE SET …
|
|
34
|
+
* When omitted `INSERT OR REPLACE` is used instead (relies on the
|
|
35
|
+
* table's PRIMARY KEY / UNIQUE constraints).
|
|
36
|
+
*/
|
|
37
|
+
conflictColumns?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Columns to update on conflict.
|
|
40
|
+
* Defaults to all columns that are NOT in `conflictColumns`.
|
|
41
|
+
*/
|
|
42
|
+
updateColumns?: string[];
|
|
43
|
+
}
|
|
28
44
|
/**
|
|
29
45
|
* High-level database client.
|
|
30
46
|
* Accepts any `IAdapter` — currently `HttpAdapter`, extensible to direct
|
|
@@ -32,7 +48,8 @@ export interface CreateIndexOptions {
|
|
|
32
48
|
*/
|
|
33
49
|
export declare class Database {
|
|
34
50
|
private readonly adapter;
|
|
35
|
-
|
|
51
|
+
readonly files?: FileClient;
|
|
52
|
+
constructor(adapter: IAdapter, filesClient?: FileClient);
|
|
36
53
|
/**
|
|
37
54
|
* Create a table.
|
|
38
55
|
*
|
|
@@ -76,6 +93,37 @@ export declare class Database {
|
|
|
76
93
|
* ]);
|
|
77
94
|
*/
|
|
78
95
|
insertMany(table: string, rows: Record<string, unknown>[]): Promise<ExecResult>;
|
|
96
|
+
/**
|
|
97
|
+
* Upsert a single row — insert or update on conflict.
|
|
98
|
+
*
|
|
99
|
+
* Without `conflictColumns` the statement uses `INSERT OR REPLACE` which
|
|
100
|
+
* relies on the table's PRIMARY KEY / UNIQUE constraints.
|
|
101
|
+
*
|
|
102
|
+
* With `conflictColumns` it uses the more precise:
|
|
103
|
+
* `INSERT … ON CONFLICT(cols) DO UPDATE SET …`
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* // relies on PRIMARY KEY / UNIQUE constraints
|
|
107
|
+
* await db.upsert("users", { id: 1, email: "a@b.com", name: "Alice" });
|
|
108
|
+
*
|
|
109
|
+
* // explicit conflict target
|
|
110
|
+
* await db.upsert("users", { email: "a@b.com", name: "Alice" }, { conflictColumns: ["email"] });
|
|
111
|
+
*/
|
|
112
|
+
upsert(table: string, data: Record<string, unknown>, options?: UpsertOptions): Promise<ExecResult>;
|
|
113
|
+
/**
|
|
114
|
+
* Upsert multiple rows — insert or update each row on conflict.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* await db.upsertMany(
|
|
118
|
+
* "users",
|
|
119
|
+
* [
|
|
120
|
+
* { email: "a@b.com", name: "Alice" },
|
|
121
|
+
* { email: "b@c.com", name: "Bob" },
|
|
122
|
+
* ],
|
|
123
|
+
* { conflictColumns: ["email"] }
|
|
124
|
+
* );
|
|
125
|
+
*/
|
|
126
|
+
upsertMany(table: string, rows: Record<string, unknown>[], options?: UpsertOptions): Promise<ExecResult>;
|
|
79
127
|
/**
|
|
80
128
|
* Update rows matching `where`.
|
|
81
129
|
*
|
package/dist/database.js
CHANGED
|
@@ -22,8 +22,9 @@ function isExecResult(r) {
|
|
|
22
22
|
* SQLite (via better-sqlite3 or sql.js) without changing business code.
|
|
23
23
|
*/
|
|
24
24
|
export class Database {
|
|
25
|
-
constructor(adapter) {
|
|
25
|
+
constructor(adapter, filesClient) {
|
|
26
26
|
this.adapter = adapter;
|
|
27
|
+
this.files = filesClient;
|
|
27
28
|
}
|
|
28
29
|
// ── Schema ──────────────────────────────────────────────────────────────
|
|
29
30
|
/**
|
|
@@ -114,6 +115,62 @@ export class Database {
|
|
|
114
115
|
const bindings = rows.flatMap((row) => keys.map((k) => row[k]));
|
|
115
116
|
return this._write(`INSERT INTO "${table}" (${cols}) VALUES ${rowPlaceholders}`, bindings);
|
|
116
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Upsert a single row — insert or update on conflict.
|
|
120
|
+
*
|
|
121
|
+
* Without `conflictColumns` the statement uses `INSERT OR REPLACE` which
|
|
122
|
+
* relies on the table's PRIMARY KEY / UNIQUE constraints.
|
|
123
|
+
*
|
|
124
|
+
* With `conflictColumns` it uses the more precise:
|
|
125
|
+
* `INSERT … ON CONFLICT(cols) DO UPDATE SET …`
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* // relies on PRIMARY KEY / UNIQUE constraints
|
|
129
|
+
* await db.upsert("users", { id: 1, email: "a@b.com", name: "Alice" });
|
|
130
|
+
*
|
|
131
|
+
* // explicit conflict target
|
|
132
|
+
* await db.upsert("users", { email: "a@b.com", name: "Alice" }, { conflictColumns: ["email"] });
|
|
133
|
+
*/
|
|
134
|
+
async upsert(table, data, options = {}) {
|
|
135
|
+
return this.upsertMany(table, [data], options);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Upsert multiple rows — insert or update each row on conflict.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* await db.upsertMany(
|
|
142
|
+
* "users",
|
|
143
|
+
* [
|
|
144
|
+
* { email: "a@b.com", name: "Alice" },
|
|
145
|
+
* { email: "b@c.com", name: "Bob" },
|
|
146
|
+
* ],
|
|
147
|
+
* { conflictColumns: ["email"] }
|
|
148
|
+
* );
|
|
149
|
+
*/
|
|
150
|
+
async upsertMany(table, rows, options = {}) {
|
|
151
|
+
if (rows.length === 0)
|
|
152
|
+
return { rowsAffected: 0, lastInsertRowid: null };
|
|
153
|
+
const keys = Object.keys(rows[0]);
|
|
154
|
+
const cols = keys.map((k) => `"${k}"`).join(", ");
|
|
155
|
+
const rowPlaceholders = rows
|
|
156
|
+
.map(() => `(${keys.map(() => "?").join(", ")})`)
|
|
157
|
+
.join(", ");
|
|
158
|
+
const bindings = rows.flatMap((row) => keys.map((k) => row[k]));
|
|
159
|
+
if (options.conflictColumns && options.conflictColumns.length > 0) {
|
|
160
|
+
const conflictCols = options.conflictColumns
|
|
161
|
+
.map((c) => `"${c}"`)
|
|
162
|
+
.join(", ");
|
|
163
|
+
const updateCols = options.updateColumns ??
|
|
164
|
+
keys.filter((k) => !options.conflictColumns.includes(k));
|
|
165
|
+
const conflictClause = updateCols.length === 0
|
|
166
|
+
? `ON CONFLICT(${conflictCols}) DO NOTHING`
|
|
167
|
+
: `ON CONFLICT(${conflictCols}) DO UPDATE SET ${updateCols
|
|
168
|
+
.map((k) => `"${k}" = excluded."${k}"`)
|
|
169
|
+
.join(", ")}`;
|
|
170
|
+
return this._write(`INSERT INTO "${table}" (${cols}) VALUES ${rowPlaceholders} ${conflictClause}`, bindings);
|
|
171
|
+
}
|
|
172
|
+
return this._write(`INSERT OR REPLACE INTO "${table}" (${cols}) VALUES ${rowPlaceholders}`, bindings);
|
|
173
|
+
}
|
|
117
174
|
/**
|
|
118
175
|
* Update rows matching `where`.
|
|
119
176
|
*
|
package/dist/files.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { HttpAdapterOptions } from "./adapters/http.js";
|
|
2
|
+
export interface FileListOptions {
|
|
3
|
+
limit?: number;
|
|
4
|
+
offset?: number;
|
|
5
|
+
sort?: "uploaded_at" | "size_bytes" | "filename" | "folder_path";
|
|
6
|
+
order?: "asc" | "desc";
|
|
7
|
+
folderPrefix?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface StoredFile {
|
|
10
|
+
id: string;
|
|
11
|
+
db_name: string;
|
|
12
|
+
content_hash: string;
|
|
13
|
+
filename: string;
|
|
14
|
+
folder_path: string;
|
|
15
|
+
content_type: string | null;
|
|
16
|
+
size_bytes: number;
|
|
17
|
+
storage_path?: string;
|
|
18
|
+
uploaded_at: string;
|
|
19
|
+
expires_at: string | null;
|
|
20
|
+
metadata: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface FileListResponse {
|
|
23
|
+
files: StoredFile[];
|
|
24
|
+
total: number;
|
|
25
|
+
offset: number;
|
|
26
|
+
limit: number;
|
|
27
|
+
}
|
|
28
|
+
export interface UploadFileInput {
|
|
29
|
+
file: Blob | Uint8Array | ArrayBuffer;
|
|
30
|
+
filename?: string;
|
|
31
|
+
folderPath?: string;
|
|
32
|
+
conflictMode?: "replace" | "error";
|
|
33
|
+
contentType?: string;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
expiresIn?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface UploadFileResponse {
|
|
38
|
+
id: string;
|
|
39
|
+
filename: string;
|
|
40
|
+
folder_path: string;
|
|
41
|
+
size_bytes: number;
|
|
42
|
+
content_type: string | null;
|
|
43
|
+
url: string;
|
|
44
|
+
uploaded_at: string;
|
|
45
|
+
expires_at: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface FileMetaResponse {
|
|
48
|
+
id: string;
|
|
49
|
+
filename: string;
|
|
50
|
+
folder_path: string;
|
|
51
|
+
content_type: string | null;
|
|
52
|
+
size_bytes: number;
|
|
53
|
+
uploaded_at: string;
|
|
54
|
+
expires_at: string | null;
|
|
55
|
+
metadata: Record<string, unknown> | null;
|
|
56
|
+
content_hash: string;
|
|
57
|
+
}
|
|
58
|
+
export interface BulkDeleteFilesResponse {
|
|
59
|
+
deleted: number;
|
|
60
|
+
failed: number;
|
|
61
|
+
}
|
|
62
|
+
export interface PresignFileOptions {
|
|
63
|
+
expiresIn?: number;
|
|
64
|
+
disposition?: "inline" | "attachment";
|
|
65
|
+
}
|
|
66
|
+
export interface PresignFileResponse {
|
|
67
|
+
url: string;
|
|
68
|
+
expires_at: string;
|
|
69
|
+
expires_in: number;
|
|
70
|
+
disposition: "inline" | "attachment";
|
|
71
|
+
token_type: "signed_query";
|
|
72
|
+
}
|
|
73
|
+
export interface BatchPresignOptions {
|
|
74
|
+
fileIds: string[];
|
|
75
|
+
expiresIn?: number;
|
|
76
|
+
disposition?: "inline" | "attachment";
|
|
77
|
+
}
|
|
78
|
+
export interface BatchPresignResult {
|
|
79
|
+
file_id: string;
|
|
80
|
+
url?: string;
|
|
81
|
+
expires_at?: string;
|
|
82
|
+
expires_in?: number;
|
|
83
|
+
disposition?: "inline" | "attachment";
|
|
84
|
+
error?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface BatchPresignResponse {
|
|
87
|
+
results: BatchPresignResult[];
|
|
88
|
+
token_type: "signed_query";
|
|
89
|
+
total: number;
|
|
90
|
+
successful: number;
|
|
91
|
+
failed: number;
|
|
92
|
+
}
|
|
93
|
+
export interface CreateFileAccessTokenOptions {
|
|
94
|
+
scope?: "files:read";
|
|
95
|
+
expiresIn?: number;
|
|
96
|
+
description?: string;
|
|
97
|
+
}
|
|
98
|
+
export interface FileAccessTokenResponse {
|
|
99
|
+
token_id: string;
|
|
100
|
+
token: string;
|
|
101
|
+
token_type: "bearer";
|
|
102
|
+
expires_at: string;
|
|
103
|
+
expires_in: number;
|
|
104
|
+
scope: string;
|
|
105
|
+
description?: string;
|
|
106
|
+
usage: string;
|
|
107
|
+
}
|
|
108
|
+
export interface RevokeFileAccessTokenOptions {
|
|
109
|
+
token?: string;
|
|
110
|
+
tokenId?: string;
|
|
111
|
+
expiresAt?: string;
|
|
112
|
+
reason?: string;
|
|
113
|
+
}
|
|
114
|
+
export interface RevokeFileAccessTokenResponse {
|
|
115
|
+
success: boolean;
|
|
116
|
+
token_id: string;
|
|
117
|
+
revoked_at: string;
|
|
118
|
+
}
|
|
119
|
+
export declare class FileClient {
|
|
120
|
+
private readonly options;
|
|
121
|
+
private readonly baseUrl;
|
|
122
|
+
private readonly basePath;
|
|
123
|
+
constructor(options: HttpAdapterOptions);
|
|
124
|
+
private requestJson;
|
|
125
|
+
list(options?: FileListOptions): Promise<FileListResponse>;
|
|
126
|
+
upload(input: UploadFileInput): Promise<UploadFileResponse>;
|
|
127
|
+
getMeta(fileId: string): Promise<FileMetaResponse>;
|
|
128
|
+
delete(fileId: string): Promise<void>;
|
|
129
|
+
bulkDelete(fileIds: string[]): Promise<BulkDeleteFilesResponse>;
|
|
130
|
+
presign(fileId: string, options?: PresignFileOptions): Promise<PresignFileResponse>;
|
|
131
|
+
batchPresign(options: BatchPresignOptions): Promise<BatchPresignResponse>;
|
|
132
|
+
createFileAccessToken(options?: CreateFileAccessTokenOptions): Promise<FileAccessTokenResponse>;
|
|
133
|
+
revokeFileAccessToken(options: RevokeFileAccessTokenOptions): Promise<RevokeFileAccessTokenResponse>;
|
|
134
|
+
getDownloadUrl(fileId: string): string;
|
|
135
|
+
}
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
function toBlob(file, contentType) {
|
|
2
|
+
if (file instanceof Blob)
|
|
3
|
+
return file;
|
|
4
|
+
if (file instanceof Uint8Array)
|
|
5
|
+
return new Blob([file], { type: contentType });
|
|
6
|
+
return new Blob([new Uint8Array(file)], { type: contentType });
|
|
7
|
+
}
|
|
8
|
+
export class FileClient {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.baseUrl = options.url.replace(/\/$/, "");
|
|
12
|
+
this.basePath = `/api/db/${encodeURIComponent(options.db)}/files`;
|
|
13
|
+
}
|
|
14
|
+
async requestJson(path, init = {}) {
|
|
15
|
+
const headers = new Headers(init.headers ?? {});
|
|
16
|
+
headers.set("Authorization", `Bearer ${this.options.token}`);
|
|
17
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
18
|
+
...init,
|
|
19
|
+
headers,
|
|
20
|
+
});
|
|
21
|
+
if (res.status === 204) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const body = (await res.json().catch(() => null));
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new Error(body?.error ?? `sqlite-hub: HTTP ${res.status} (db: "${this.options.db}")`);
|
|
27
|
+
}
|
|
28
|
+
return body;
|
|
29
|
+
}
|
|
30
|
+
async list(options = {}) {
|
|
31
|
+
const qs = new URLSearchParams();
|
|
32
|
+
if (options.limit !== undefined)
|
|
33
|
+
qs.set("limit", String(options.limit));
|
|
34
|
+
if (options.offset !== undefined)
|
|
35
|
+
qs.set("offset", String(options.offset));
|
|
36
|
+
if (options.sort)
|
|
37
|
+
qs.set("sort", options.sort);
|
|
38
|
+
if (options.order)
|
|
39
|
+
qs.set("order", options.order);
|
|
40
|
+
if (options.folderPrefix)
|
|
41
|
+
qs.set("folder_prefix", options.folderPrefix);
|
|
42
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
43
|
+
return this.requestJson(`${this.basePath}${suffix}`);
|
|
44
|
+
}
|
|
45
|
+
async upload(input) {
|
|
46
|
+
const formData = new FormData();
|
|
47
|
+
const blob = toBlob(input.file, input.contentType);
|
|
48
|
+
const filename = input.filename ?? "file";
|
|
49
|
+
formData.append("file", blob, filename);
|
|
50
|
+
if (input.filename)
|
|
51
|
+
formData.append("filename", input.filename);
|
|
52
|
+
if (input.folderPath)
|
|
53
|
+
formData.append("folder_path", input.folderPath);
|
|
54
|
+
if (input.conflictMode)
|
|
55
|
+
formData.append("conflict_mode", input.conflictMode);
|
|
56
|
+
if (input.contentType)
|
|
57
|
+
formData.append("content_type", input.contentType);
|
|
58
|
+
if (input.metadata)
|
|
59
|
+
formData.append("metadata", JSON.stringify(input.metadata));
|
|
60
|
+
if (input.expiresIn !== undefined)
|
|
61
|
+
formData.append("expires_in", String(input.expiresIn));
|
|
62
|
+
const headers = new Headers({ Authorization: `Bearer ${this.options.token}` });
|
|
63
|
+
const res = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers,
|
|
66
|
+
body: formData,
|
|
67
|
+
});
|
|
68
|
+
const body = (await res.json().catch(() => null));
|
|
69
|
+
if (!res.ok || !body) {
|
|
70
|
+
throw new Error(body?.error ?? `sqlite-hub: HTTP ${res.status} (db: "${this.options.db}")`);
|
|
71
|
+
}
|
|
72
|
+
return body;
|
|
73
|
+
}
|
|
74
|
+
async getMeta(fileId) {
|
|
75
|
+
return this.requestJson(`${this.basePath}/${encodeURIComponent(fileId)}/meta`);
|
|
76
|
+
}
|
|
77
|
+
async delete(fileId) {
|
|
78
|
+
await this.requestJson(`${this.basePath}/${encodeURIComponent(fileId)}`, {
|
|
79
|
+
method: "DELETE",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async bulkDelete(fileIds) {
|
|
83
|
+
return this.requestJson(`${this.basePath}/bulk-delete`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ file_ids: fileIds }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async presign(fileId, options = {}) {
|
|
90
|
+
return this.requestJson(`${this.basePath}/${encodeURIComponent(fileId)}/presign`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
...(options.expiresIn !== undefined ? { expires_in: options.expiresIn } : {}),
|
|
95
|
+
...(options.disposition ? { disposition: options.disposition } : {}),
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async batchPresign(options) {
|
|
100
|
+
return this.requestJson(`${this.basePath}/presign/batch`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
file_ids: options.fileIds,
|
|
105
|
+
...(options.expiresIn !== undefined ? { expires_in: options.expiresIn } : {}),
|
|
106
|
+
...(options.disposition ? { disposition: options.disposition } : {}),
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async createFileAccessToken(options = {}) {
|
|
111
|
+
const tokenPath = `/api/db/${encodeURIComponent(this.options.db)}/tokens/files`;
|
|
112
|
+
return this.requestJson(tokenPath, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { "Content-Type": "application/json" },
|
|
115
|
+
body: JSON.stringify({
|
|
116
|
+
scope: "files:read",
|
|
117
|
+
...(options.expiresIn !== undefined ? { expires_in: options.expiresIn } : {}),
|
|
118
|
+
...(options.description ? { description: options.description } : {}),
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async revokeFileAccessToken(options) {
|
|
123
|
+
const tokenPath = `/api/db/${encodeURIComponent(this.options.db)}/tokens/files/revoke`;
|
|
124
|
+
const payload = {
|
|
125
|
+
...(options.token ? { token: options.token } : {}),
|
|
126
|
+
...(options.reason ? { reason: options.reason } : {}),
|
|
127
|
+
};
|
|
128
|
+
if (!options.token) {
|
|
129
|
+
if (!options.tokenId || !options.expiresAt) {
|
|
130
|
+
throw new Error("tokenId and expiresAt are required when token is not provided");
|
|
131
|
+
}
|
|
132
|
+
payload.token_id = options.tokenId;
|
|
133
|
+
payload.expires_at = options.expiresAt;
|
|
134
|
+
}
|
|
135
|
+
return this.requestJson(tokenPath, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
body: JSON.stringify(payload),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
getDownloadUrl(fileId) {
|
|
142
|
+
return `${this.baseUrl}${this.basePath}/${encodeURIComponent(fileId)}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { Database } from "./database.js";
|
|
2
|
-
export type { ColumnDef, CreateIndexOptions, CreateTableOptions, FindOptions, OrderDirection, WhereClause } from "./database.js";
|
|
2
|
+
export type { ColumnDef, CreateIndexOptions, CreateTableOptions, FindOptions, OrderDirection, UpsertOptions, WhereClause } from "./database.js";
|
|
3
|
+
export { FileClient, } from "./files.js";
|
|
4
|
+
export type { BatchPresignOptions, BatchPresignResponse, BatchPresignResult, BulkDeleteFilesResponse, CreateFileAccessTokenOptions, FileAccessTokenResponse, FileListOptions, FileListResponse, FileMetaResponse, PresignFileOptions, PresignFileResponse, RevokeFileAccessTokenOptions, RevokeFileAccessTokenResponse, StoredFile, UploadFileInput, UploadFileResponse, } from "./files.js";
|
|
3
5
|
export { HttpAdapter } from "./adapters/http.js";
|
|
4
6
|
export type { HttpAdapterOptions } from "./adapters/http.js";
|
|
5
7
|
export type { ColumnHeader, ExecResult, IAdapter, QueryResult, RawResult } from "./adapters/types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { Database } from "./database.js";
|
|
2
|
+
export { FileClient, } from "./files.js";
|
|
2
3
|
export { HttpAdapter } from "./adapters/http.js";
|
|
3
4
|
// ── Convenience factory ──────────────────────────────────────────────────────
|
|
4
5
|
import { HttpAdapter } from "./adapters/http.js";
|
|
5
6
|
import { Database } from "./database.js";
|
|
7
|
+
import { FileClient } from "./files.js";
|
|
6
8
|
/**
|
|
7
9
|
* Create a Database connected to a sqlite-hub service over HTTP.
|
|
8
10
|
*
|
|
@@ -14,6 +16,6 @@ import { Database } from "./database.js";
|
|
|
14
16
|
* });
|
|
15
17
|
*/
|
|
16
18
|
export function connect(options) {
|
|
17
|
-
return new Database(new HttpAdapter(options));
|
|
19
|
+
return new Database(new HttpAdapter(options), new FileClient(options));
|
|
18
20
|
}
|
|
19
21
|
export default connect;
|