sqlite-hub-client 0.7.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 },
@@ -306,6 +325,136 @@ await db.exec("CREATE INDEX IF NOT EXISTS idx_title ON posts (title)");
306
325
 
307
326
  ---
308
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
+
309
458
  ## Architecture
310
459
 
311
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 {
@@ -47,7 +48,8 @@ export interface UpsertOptions {
47
48
  */
48
49
  export declare class Database {
49
50
  private readonly adapter;
50
- constructor(adapter: IAdapter);
51
+ readonly files?: FileClient;
52
+ constructor(adapter: IAdapter, filesClient?: FileClient);
51
53
  /**
52
54
  * Create a table.
53
55
  *
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
  /**
@@ -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
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.7.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",