get-db9 0.4.1 → 0.6.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
@@ -1,6 +1,6 @@
1
1
  # get-db9
2
2
 
3
- TypeScript SDK for [pg-tikv](https://github.com/pgtikv/pg-tikv) — instant PostgreSQL-compatible databases on TiKV.
3
+ TypeScript SDK for [db9-backend](https://github.com/c4pt0r/db9-backend) — instant PostgreSQL-compatible databases on TiKV.
4
4
 
5
5
  ## Install
6
6
 
@@ -31,7 +31,7 @@ const db = await instantDatabase({
31
31
 
32
32
  ## Db9 Client
33
33
 
34
- Full typed client for the API (register, databases, SQL, migrations).
34
+ Full typed client for the API databases, SQL, file storage, tokens, migrations, and more.
35
35
 
36
36
  ```typescript
37
37
  import { createDb9Client } from 'get-db9/client';
@@ -57,6 +57,167 @@ await client.databases.applyMigration(db.id, {
57
57
  });
58
58
  ```
59
59
 
60
+ ## File Storage (fs9)
61
+
62
+ Each database includes a built-in file system accessible via **WebSocket**.
63
+ The SDK connects to the fs9 WebSocket server, authenticates, and provides a
64
+ high-level API for file operations.
65
+
66
+ > **Note:** File storage requires a WebSocket implementation. Browsers, Deno,
67
+ > Bun, and Node 21+ have native `WebSocket`. For Node 18–20, install the `ws` package
68
+ > and pass it via the `WebSocket` option.
69
+
70
+ ```typescript
71
+ import { createDb9Client } from 'get-db9/client';
72
+ import WebSocket from 'ws'; // Node 18–20 only
73
+
74
+ const client = createDb9Client({ WebSocket: WebSocket as any });
75
+ const dbId = 'your-database-id';
76
+
77
+ // Write a file (string, ArrayBuffer, or Uint8Array)
78
+ await client.fs.write(dbId, '/data/hello.txt', 'Hello, world!');
79
+
80
+ // Read file as text
81
+ const text = await client.fs.read(dbId, '/data/hello.txt');
82
+
83
+ // Read file as raw bytes (Uint8Array)
84
+ const bytes = await client.fs.readBinary(dbId, '/data/image.png');
85
+
86
+ // List files in a directory
87
+ const files = await client.fs.list(dbId, '/data');
88
+
89
+ // Check if file exists
90
+ const exists = await client.fs.exists(dbId, '/data/hello.txt');
91
+
92
+ // Get file metadata (type, size, mode, mtime)
93
+ const stat = await client.fs.stat(dbId, '/data/hello.txt');
94
+ console.log(stat.type, stat.size); // 'file', 1024
95
+
96
+ // Create directory (recursive)
97
+ await client.fs.mkdir(dbId, '/data/nested/dir');
98
+
99
+ // Append to a file
100
+ await client.fs.append(dbId, '/data/log.txt', 'new line\n');
101
+
102
+ // Rename (move) a file
103
+ await client.fs.rename(dbId, '/data/old.txt', '/data/new.txt');
104
+
105
+ // Delete a file
106
+ await client.fs.remove(dbId, '/data/hello.txt');
107
+
108
+ // Delete a directory recursively
109
+ await client.fs.remove(dbId, '/data/old-dir', { recursive: true });
110
+ ```
111
+
112
+ ### Persistent Connection
113
+
114
+ For multiple operations on the same database, use `fs.connect()` to hold a
115
+ single WebSocket connection and avoid reconnecting per-call:
116
+
117
+ ```typescript
118
+ const fs = await client.fs.connect(dbId);
119
+ try {
120
+ await fs.mkdir('/batch');
121
+ await fs.writeFile('/batch/a.txt', 'file A');
122
+ await fs.writeFile('/batch/b.txt', 'file B');
123
+ const entries = await fs.readdir('/batch');
124
+ console.log(entries); // [{path: '/batch/a.txt', ...}, ...]
125
+ } finally {
126
+ await fs.close();
127
+ }
128
+ ```
129
+
130
+ ## Token Management
131
+
132
+ ```typescript
133
+ const client = createDb9Client();
134
+
135
+ // Create a named API token
136
+ const token = await client.tokens.create({
137
+ name: 'ci-deploy',
138
+ expires_in_days: 90,
139
+ });
140
+ console.log(token.token); // Use this for CI/CD
141
+
142
+ // List all tokens
143
+ const tokens = await client.tokens.list();
144
+
145
+ // Revoke a token
146
+ await client.tokens.revoke(token.id);
147
+ ```
148
+
149
+ ## Database Credentials
150
+
151
+ Retrieve stored admin credentials without resetting the password:
152
+
153
+ ```typescript
154
+ const client = createDb9Client();
155
+ const dbId = 'your-database-id';
156
+
157
+ // Get stored credentials (no password reset)
158
+ const creds = await client.databases.credentials(dbId);
159
+ console.log(creds.admin_user); // admin username
160
+ console.log(creds.admin_password); // current password
161
+ console.log(creds.connection_string); // full connection string
162
+ ```
163
+
164
+ ## Device Authorization (CLI flow)
165
+
166
+ Authorize a CLI or device using the OAuth device code flow:
167
+
168
+ ```typescript
169
+ const client = createDb9Client();
170
+
171
+ // 1. Start device code flow
172
+ const code = await client.deviceAuth.createDeviceCode();
173
+ console.log(`Visit: ${code.verification_uri}`);
174
+ console.log(`Code: ${code.user_code}`);
175
+
176
+ // 2. Poll for authorization (user approves in browser)
177
+ const poll = async () => {
178
+ while (true) {
179
+ const result = await client.deviceAuth.pollDeviceToken({
180
+ device_code: code.device_code,
181
+ });
182
+ if ('token' in result) {
183
+ return result; // { token, expires_at }
184
+ }
185
+ if (result.error === 'access_denied') {
186
+ throw new Error('User denied authorization');
187
+ }
188
+ if (result.error === 'expired_token') {
189
+ throw new Error('Device code expired');
190
+ }
191
+ // authorization_pending — wait and retry
192
+ await new Promise((r) => setTimeout(r, code.interval * 1000));
193
+ }
194
+ };
195
+
196
+ const auth = await poll();
197
+ console.log(auth.token);
198
+ ```
199
+
200
+ ## SQL Error Handling
201
+
202
+ SQL results include structured error details when queries fail:
203
+
204
+ ```typescript
205
+ const result = await client.databases.sql(dbId, 'SELECT * FROM nonexistent');
206
+
207
+ if (result.error) {
208
+ // result.error is a SqlErrorDetail object:
209
+ // {
210
+ // message: "relation \"nonexistent\" does not exist",
211
+ // code: "42P01", // PostgreSQL error code
212
+ // detail: "...", // optional
213
+ // hint: "...", // optional
214
+ // position: 15 // optional cursor position
215
+ // }
216
+ console.log(result.error.message);
217
+ console.log(result.error.code);
218
+ }
219
+ ```
220
+
60
221
  ## Configuration
61
222
 
62
223
  ### instantDatabase options
@@ -69,6 +230,9 @@ await client.databases.applyMigration(db.id, {
69
230
  | `credentialStore` | `CredentialStore` | `FileCredentialStore` | Credential storage |
70
231
  | `seed` | `string` | — | SQL to run after creation |
71
232
  | `seedFile` | `string` | — | SQL file content to run |
233
+ | `timeout` | `number` | — | Request timeout in ms |
234
+ | `maxRetries` | `number` | `3` (max) | Retry count for failed requests |
235
+ | `retryDelay` | `number` | — | Delay between retries in ms |
72
236
 
73
237
  ### Db9 client options
74
238
 
@@ -78,6 +242,11 @@ await client.databases.applyMigration(db.id, {
78
242
  | `token` | `string` | — | Bearer token (optional) |
79
243
  | `fetch` | `FetchFn` | `globalThis.fetch` | Custom fetch |
80
244
  | `credentialStore` | `CredentialStore` | `FileCredentialStore` | Load/save token |
245
+ | `timeout` | `number` | — | Request timeout in ms |
246
+ | `maxRetries` | `number` | `3` (max) | Retry count for failed requests |
247
+ | `retryDelay` | `number` | — | Delay between retries in ms |
248
+ | `WebSocket` | `WebSocketConstructor` | `globalThis.WebSocket` | WebSocket impl for fs operations |
249
+ | `wsPort` | `number` | `5480` | WebSocket port for fs9 server |
81
250
 
82
251
  ## Zero-config client
83
252
 
@@ -107,6 +276,8 @@ try {
107
276
  }
108
277
  ```
109
278
 
279
+ > **Note:** 401 errors are automatically retried with a fresh token for anonymous sessions. You typically won't see `Db9AuthError` unless the refresh itself fails.
280
+
110
281
  ## Credential Storage
111
282
 
112
283
  Credentials are stored in `~/.db9/credentials` (TOML format), shared with the db9 CLI.
@@ -124,6 +295,77 @@ const customStore = new FileCredentialStore('/path/to/credentials');
124
295
  const memStore = new MemoryCredentialStore();
125
296
  ```
126
297
 
298
+ ## API Reference
299
+
300
+ ### `client.auth`
301
+
302
+ | Method | Description |
303
+ |--------|-------------|
304
+ | `register(req)` | Create account with email/password |
305
+ | `login(req)` | Login and get bearer token |
306
+ | `me()` | Get current user profile |
307
+ | `anonymousRegister()` | Register anonymously (auto-called) |
308
+ | `anonymousRefresh(req)` | Refresh anonymous token |
309
+ | `getAnonymousSecret()` | Retrieve anonymous secret for token refresh |
310
+ | `ensureAnonymousSecret()` | Ensure anonymous secret is saved to credential store |
311
+ | `claim(req)` | Claim anonymous account with email/password |
312
+
313
+ ### `client.tokens`
314
+
315
+ | Method | Description |
316
+ |--------|-------------|
317
+ | `create(req)` | Create a named API token (`{ name?, expires_in_days? }`) |
318
+ | `list()` | List all tokens |
319
+ | `revoke(tokenId)` | Revoke a token by ID |
320
+
321
+ ### `client.databases`
322
+
323
+ | Method | Description |
324
+ |--------|-------------|
325
+ | `create(req)` | Create a new database |
326
+ | `list()` | List all databases |
327
+ | `get(id)` | Get database details |
328
+ | `delete(id)` | Delete a database |
329
+ | `resetPassword(id)` | Reset admin password |
330
+ | `credentials(id)` | Get stored admin credentials without resetting |
331
+ | `observability(id)` | Get TPS, latency, connection stats |
332
+ | `sql(id, query)` | Execute SQL query (errors returned as `SqlErrorDetail`) |
333
+ | `sqlFile(id, content)` | Execute SQL from file content |
334
+ | `schema(id)` | Get schema metadata |
335
+ | `dump(id, req?)` | Export schema/data as SQL |
336
+ | `applyMigration(id, req)` | Apply a migration |
337
+ | `listMigrations(id)` | List applied migrations |
338
+ | `branch(id, req)` | Create a database branch |
339
+ | `users.list(id)` | List database users |
340
+ | `users.create(id, req)` | Create database user |
341
+ | `users.delete(id, username)` | Delete database user |
342
+
343
+ ### `client.fs`
344
+
345
+ All methods auto-resolve database credentials and connect via WebSocket.
346
+
347
+ | Method | Description |
348
+ |--------|-------------|
349
+ | `connect(dbId)` | Open a persistent `FsClient` WebSocket connection |
350
+ | `read(dbId, path)` | Read file as text (UTF-8) |
351
+ | `readBinary(dbId, path)` | Read file as `Uint8Array` |
352
+ | `write(dbId, path, content)` | Write file (string, ArrayBuffer, or Uint8Array) |
353
+ | `append(dbId, path, content)` | Append to a file, returns bytes written |
354
+ | `list(dbId, path)` | List directory contents (returns `FileInfo[]`) |
355
+ | `stat(dbId, path)` | Get file metadata (`FileInfo`) |
356
+ | `exists(dbId, path)` | Check if file exists (returns boolean) |
357
+ | `mkdir(dbId, path)` | Create directory recursively |
358
+ | `remove(dbId, path, opts?)` | Remove file or directory (`{ recursive?: boolean }`) |
359
+ | `rename(dbId, old, new)` | Rename (move) a file or directory |
360
+
361
+ ### `client.deviceAuth`
362
+
363
+ | Method | Description |
364
+ |--------|-------------|
365
+ | `createDeviceCode()` | Start OAuth device code flow |
366
+ | `pollDeviceToken(req)` | Poll for token after user authorizes |
367
+ | `verifyDevice(req)` | Submit device verification with email/password |
368
+
127
369
  ## Requirements
128
370
 
129
371
  - Node.js >= 18 (native fetch)