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 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
  ```
@@ -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
- constructor(adapter: IAdapter);
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
  *
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub-client",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "High-level SQLite client for sqlite-hub — HTTP adapter included, direct SQLite coming soon",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",