@reddb-io/sdk 1.0.1
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/LICENSE +661 -0
- package/README.md +199 -0
- package/index.d.ts +362 -0
- package/package.json +50 -0
- package/postinstall.js +86 -0
- package/src/binary.js +66 -0
- package/src/cache.js +137 -0
- package/src/cli.js +25 -0
- package/src/config.js +66 -0
- package/src/http.js +200 -0
- package/src/index.js +432 -0
- package/src/internal/asset-fetcher/asset-name.js +37 -0
- package/src/internal/asset-fetcher/checksum.js +23 -0
- package/src/internal/asset-fetcher/download.js +89 -0
- package/src/internal/asset-fetcher/index.js +52 -0
- package/src/internal/bin-resolver/index.js +57 -0
- package/src/internal/version-compare/index.js +163 -0
- package/src/kv.js +70 -0
- package/src/protocol.js +157 -0
- package/src/redwire.js +723 -0
- package/src/spawn.js +177 -0
- package/src/url.js +271 -0
- package/src/vault.js +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# @reddb-io/sdk
|
|
2
|
+
|
|
3
|
+
Official RedDB SDK for JavaScript and TypeScript. Speaks JSON-RPC 2.0 over
|
|
4
|
+
stdio to a local `red` binary, which is downloaded automatically on install.
|
|
5
|
+
Works in **Node 18+**, **Bun** and **Deno** (via `npm:` specifier) — same
|
|
6
|
+
package, no per-runtime fork.
|
|
7
|
+
|
|
8
|
+
Use this package for application code. If you just want to launch the CLI from
|
|
9
|
+
npm, use:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @reddb-io/cli@latest version
|
|
13
|
+
npx @reddb-io/cli@latest server --http-bind 127.0.0.1:8080 --path ./data.rdb
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @reddb-io/sdk
|
|
20
|
+
# or
|
|
21
|
+
npm install @reddb-io/sdk
|
|
22
|
+
# or
|
|
23
|
+
bun add @reddb-io/sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
In Deno:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { connect } from 'npm:@reddb-io/sdk'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `postinstall` script downloads the matching `red` binary from GitHub
|
|
33
|
+
Releases into `node_modules/@reddb-io/sdk/bin/`. If your environment blocks
|
|
34
|
+
postinstall scripts or has no network, set `REDDB_BINARY_PATH=/path/to/red`
|
|
35
|
+
and the driver will use that instead.
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { connect } from '@reddb-io/sdk'
|
|
41
|
+
|
|
42
|
+
const db = await connect('memory://') // ephemeral
|
|
43
|
+
// or: await connect('file:///var/lib/reddb/data.rdb') // persisted
|
|
44
|
+
|
|
45
|
+
await db.insert('users', { name: 'Alice', age: 30 })
|
|
46
|
+
await db.bulkInsert('users', [{ name: 'Bob' }, { name: 'Carol' }])
|
|
47
|
+
|
|
48
|
+
const result = await db.query('SELECT * FROM users')
|
|
49
|
+
console.log(result.rows)
|
|
50
|
+
|
|
51
|
+
await db.close()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
TypeScript is the same API:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { connect, RedDBError } from '@reddb-io/sdk'
|
|
58
|
+
|
|
59
|
+
const db = await connect('file:///var/lib/reddb/data.rdb')
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const result = await db.query('SELECT * FROM users LIMIT 10')
|
|
63
|
+
console.log(result.rows)
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (err instanceof RedDBError) {
|
|
66
|
+
console.error(err.code, err.message)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await db.close()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Connection URIs
|
|
74
|
+
|
|
75
|
+
| URI | Mode |
|
|
76
|
+
|----------------------------|--------------------------------------|
|
|
77
|
+
| `memory://` | Ephemeral, in-memory database |
|
|
78
|
+
| `file:///absolute/path` | Embedded engine, persisted to disk |
|
|
79
|
+
| `grpc://host:port` | Remote server (planned, not yet) |
|
|
80
|
+
|
|
81
|
+
`grpc://` is not supported by the JS driver yet — the binary needs the
|
|
82
|
+
`--connect` flag wired up first. See `PLAN_DRIVERS.md` in the repo root.
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
### `connect(uri, options?) → Promise<RedDB>`
|
|
87
|
+
|
|
88
|
+
Spawns `red rpc --stdio` with arguments derived from the URI, attaches a
|
|
89
|
+
JSON-RPC client to its stdin/stdout, then returns a connection handle.
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
- `binary` — override the `red` binary path. Defaults to `bin/red` next to the
|
|
93
|
+
package, or `REDDB_BINARY_PATH` env var if set.
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
import { connect } from '@reddb-io/sdk'
|
|
99
|
+
|
|
100
|
+
const db = await connect('memory://')
|
|
101
|
+
const persisted = await connect('file:///tmp/app.rdb')
|
|
102
|
+
const custom = await connect('memory://', { binary: '/usr/local/bin/red' })
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `db.query(sql) → Promise<{ statement, affected, columns, rows }>`
|
|
106
|
+
|
|
107
|
+
### `db.insert(collection, payload) → Promise<{ affected, id? }>`
|
|
108
|
+
|
|
109
|
+
### `db.bulkInsert(collection, payloads) → Promise<{ affected }>`
|
|
110
|
+
|
|
111
|
+
### `db.get(collection, id) → Promise<{ entity }>`
|
|
112
|
+
|
|
113
|
+
### `db.delete(collection, id) → Promise<{ affected }>`
|
|
114
|
+
|
|
115
|
+
### `db.health() → Promise<{ ok, version }>`
|
|
116
|
+
|
|
117
|
+
### `db.version() → Promise<{ version, protocol }>`
|
|
118
|
+
|
|
119
|
+
### `db.close() → Promise<void>`
|
|
120
|
+
|
|
121
|
+
Sends `close` to the binary, waits for it to exit. Calls after `close()` reject
|
|
122
|
+
with `RedDBError('CLIENT_CLOSED', ...)`.
|
|
123
|
+
|
|
124
|
+
## Errors
|
|
125
|
+
|
|
126
|
+
All RPC failures throw `RedDBError`:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
import { RedDBError } from '@reddb-io/sdk'
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await db.query('NOT VALID SQL')
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err instanceof RedDBError) {
|
|
135
|
+
console.error(err.code) // 'QUERY_ERROR'
|
|
136
|
+
console.error(err.message) // server-provided detail
|
|
137
|
+
console.error(err.data) // optional structured data
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Stable error codes:
|
|
143
|
+
|
|
144
|
+
| code | when |
|
|
145
|
+
|-------------------|------------------------------------------------------------|
|
|
146
|
+
| `PARSE_ERROR` | Server got malformed JSON (driver bug, please report) |
|
|
147
|
+
| `INVALID_REQUEST` | Missing field or unknown method |
|
|
148
|
+
| `INVALID_PARAMS` | params didn't match the method schema |
|
|
149
|
+
| `QUERY_ERROR` | SQL parse, type or constraint error |
|
|
150
|
+
| `NOT_FOUND` | Entity / collection does not exist |
|
|
151
|
+
| `INTERNAL_ERROR` | Server caught a panic |
|
|
152
|
+
| `CLIENT_CLOSED` | Driver-side: call after `close()` or unexpected EOF |
|
|
153
|
+
| `UNSUPPORTED_SCHEME` | URI scheme not yet supported by the driver |
|
|
154
|
+
|
|
155
|
+
## Limits
|
|
156
|
+
|
|
157
|
+
- **stdio IPC overhead.** Each call is a JSON serialize, write, parse, read
|
|
158
|
+
round-trip. For most apps (web servers, scripts, ETL) this is invisible.
|
|
159
|
+
For very high-throughput single-process ingestion, use the embedded Rust
|
|
160
|
+
crate `reddb` directly.
|
|
161
|
+
- **No edge runtimes.** Cloudflare Workers, Vercel Edge and the browser have
|
|
162
|
+
no subprocess support. If you need RedDB there, wait for the planned HTTP
|
|
163
|
+
transport (see `PLAN_DRIVERS.md`).
|
|
164
|
+
- **One process per connection.** No connection pooling yet. If you need
|
|
165
|
+
concurrent independent transactions, open multiple `connect()` handles.
|
|
166
|
+
|
|
167
|
+
## Testing locally
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
cargo build --bin red # at repo root
|
|
171
|
+
cd drivers/js
|
|
172
|
+
node test/smoke.test.mjs
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Same test runs in Bun and Deno:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
bun test/smoke.test.mjs
|
|
179
|
+
deno run -A test/smoke.test.mjs
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Production deploy
|
|
183
|
+
|
|
184
|
+
When you're ready to point this driver at a production RedDB cluster:
|
|
185
|
+
|
|
186
|
+
- **Run RedDB with the encrypted vault** so auth state and
|
|
187
|
+
`red.secret.*` values are protected at rest. See
|
|
188
|
+
[`docs/security/vault.md`](../../docs/security/vault.md).
|
|
189
|
+
- **Use Docker secrets or your cloud secret manager** to inject the
|
|
190
|
+
certificate — never bake it into an image. See
|
|
191
|
+
[`docs/getting-started/docker.md`](../../docs/getting-started/docker.md).
|
|
192
|
+
- **Track every secret** the driver consumes (bearer tokens, mTLS
|
|
193
|
+
cert + key, OAuth JWTs) in
|
|
194
|
+
[`docs/operations/secrets.md`](../../docs/operations/secrets.md).
|
|
195
|
+
- **Use `reds://` (TLS)** or `red://...?tls=true` for any traffic
|
|
196
|
+
crossing the network — never plain `red://` outside localhost.
|
|
197
|
+
- **TLS posture, mTLS, OAuth/JWT and reverse-proxy patterns** are
|
|
198
|
+
covered in [`docs/security/transport-tls.md`](../../docs/security/transport-tls.md).
|
|
199
|
+
- See [Policies](../../docs/security/policies.md) for IAM-style authorization.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedDB JavaScript driver — TypeScript definitions.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written, kept in sync with src/index.js.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Authentication credentials. Only meaningful for `grpc://` URIs;
|
|
9
|
+
* embedded modes (memory://, file://) inherit the caller's
|
|
10
|
+
* filesystem privileges and reject auth options at the boundary.
|
|
11
|
+
*
|
|
12
|
+
* The shape is `{ token }` (or its `{ apiKey }` alias). For
|
|
13
|
+
* username/password login, mint a token first via the standalone
|
|
14
|
+
* `login(httpUrl, { username, password })` helper, then pass it
|
|
15
|
+
* here — the gRPC surface does not currently bridge `auth.login`.
|
|
16
|
+
*/
|
|
17
|
+
export type AuthOptions =
|
|
18
|
+
| { token: string }
|
|
19
|
+
| { apiKey: string }
|
|
20
|
+
| { username: string; password: string; loginUrl?: string }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* TLS / mTLS configuration for `redwire(s)://` connections.
|
|
24
|
+
* `ca` / `cert` / `key` accept a filesystem path or a PEM string
|
|
25
|
+
* starting with `-----BEGIN`.
|
|
26
|
+
*/
|
|
27
|
+
export interface TlsOptions {
|
|
28
|
+
ca?: string | Uint8Array
|
|
29
|
+
cert?: string | Uint8Array
|
|
30
|
+
key?: string | Uint8Array
|
|
31
|
+
servername?: string
|
|
32
|
+
rejectUnauthorized?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ConnectOptions {
|
|
36
|
+
/** Override the path to the `red` binary (defaults to bundled). */
|
|
37
|
+
binary?: string
|
|
38
|
+
/** Authentication credentials (grpc:// only). */
|
|
39
|
+
auth?: AuthOptions
|
|
40
|
+
/**
|
|
41
|
+
* TLS for `redwire(s)://` connections. URL params (`tls=true`,
|
|
42
|
+
* `cert=`, `key=`, `ca=`) feed the same field; this option
|
|
43
|
+
* always wins.
|
|
44
|
+
*/
|
|
45
|
+
tls?: TlsOptions
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface QueryResult {
|
|
49
|
+
statement: string
|
|
50
|
+
affected: number
|
|
51
|
+
columns: string[]
|
|
52
|
+
rows: Array<Record<string, unknown>>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface InsertResult {
|
|
56
|
+
affected: number
|
|
57
|
+
/** Present when the underlying engine surfaces the inserted entity id. */
|
|
58
|
+
id?: string | number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BulkInsertResult {
|
|
62
|
+
affected: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface GetResult {
|
|
66
|
+
entity: Record<string, unknown> | null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface DeleteResult {
|
|
70
|
+
affected: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface HealthResult {
|
|
74
|
+
ok: boolean
|
|
75
|
+
version: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface VersionResult {
|
|
79
|
+
version: string
|
|
80
|
+
protocol: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type Role = 'read' | 'write' | 'admin'
|
|
84
|
+
|
|
85
|
+
export interface LoginResult {
|
|
86
|
+
token: string
|
|
87
|
+
username: string
|
|
88
|
+
role: Role
|
|
89
|
+
expires_at: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface WhoamiResult {
|
|
93
|
+
username: string
|
|
94
|
+
role: Role
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CreateApiKeyResult {
|
|
98
|
+
key: string
|
|
99
|
+
role: Role
|
|
100
|
+
created_at: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ChangePasswordResult {
|
|
104
|
+
ok: true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface RevokeApiKeyResult {
|
|
108
|
+
ok: true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class RedDBError extends Error {
|
|
112
|
+
readonly name: 'RedDBError'
|
|
113
|
+
readonly code: string
|
|
114
|
+
readonly data: unknown
|
|
115
|
+
constructor(code: string, message: string, data?: unknown)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Cache API
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/** Options for cache.put(). Maps to Rust BlobCachePolicy fields. */
|
|
123
|
+
export interface CachePutOptions {
|
|
124
|
+
/** Entry TTL in milliseconds. Omit to use the namespace default. */
|
|
125
|
+
ttl_ms?: number
|
|
126
|
+
/** Tags for group invalidation via cache.invalidateTags(). */
|
|
127
|
+
tags?: string[]
|
|
128
|
+
/** Extended TTL policy (idle eviction, stale-while-revalidate, jitter). */
|
|
129
|
+
policy?: {
|
|
130
|
+
idle_evict_ms?: number
|
|
131
|
+
stale_while_revalidate_ms?: number
|
|
132
|
+
jitter_ms?: number
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface CacheGetResult {
|
|
137
|
+
/** Raw bytes of the cached value. null when not found. */
|
|
138
|
+
value: Uint8Array | null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type CacheExistsStatus = 'present' | 'absent' | 'maybe'
|
|
142
|
+
|
|
143
|
+
export interface CacheInvalidateResult {
|
|
144
|
+
removed: number
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export class CacheClient {
|
|
148
|
+
/** Fetch a cached value. Returns Uint8Array on hit, null on miss. */
|
|
149
|
+
get(namespace: string, key: string): Promise<Uint8Array | null>
|
|
150
|
+
/** Store a value in the cache. */
|
|
151
|
+
put(
|
|
152
|
+
namespace: string,
|
|
153
|
+
key: string,
|
|
154
|
+
value: Uint8Array | Buffer | string,
|
|
155
|
+
opts?: CachePutOptions,
|
|
156
|
+
): Promise<void>
|
|
157
|
+
/** Check whether a key exists. */
|
|
158
|
+
exists(namespace: string, key: string): Promise<CacheExistsStatus>
|
|
159
|
+
/** Remove a single entry. */
|
|
160
|
+
invalidate(namespace: string, key: string): Promise<void>
|
|
161
|
+
/** Remove all entries whose key starts with prefix. Returns count removed. */
|
|
162
|
+
invalidatePrefix(namespace: string, prefix: string): Promise<number>
|
|
163
|
+
/** Remove all entries tagged with any of the given tags. Returns count removed. */
|
|
164
|
+
invalidateTags(namespace: string, tags: string[]): Promise<number>
|
|
165
|
+
/** Remove all entries in a namespace (routes to POST /admin/blob_cache/flush_namespace). */
|
|
166
|
+
flushNamespace(namespace: string): Promise<void>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface KvWatchEvent {
|
|
170
|
+
key: string
|
|
171
|
+
op: 'insert' | 'update' | 'delete'
|
|
172
|
+
before: unknown
|
|
173
|
+
after: unknown
|
|
174
|
+
lsn: number
|
|
175
|
+
committed_at: number
|
|
176
|
+
dropped_event_count: number
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export class KvClient {
|
|
180
|
+
put(
|
|
181
|
+
key: string,
|
|
182
|
+
value: unknown,
|
|
183
|
+
options?: { collection?: string; expireMs?: number; tags?: string[] },
|
|
184
|
+
): Promise<QueryResult>
|
|
185
|
+
invalidateTags(tags: string[], options?: { collection?: string }): Promise<number>
|
|
186
|
+
watch(
|
|
187
|
+
key: string,
|
|
188
|
+
options?: { collection?: string; sinceLsn?: number; limit?: number },
|
|
189
|
+
): AsyncIterable<KvWatchEvent>
|
|
190
|
+
watchPrefix(
|
|
191
|
+
prefix: string,
|
|
192
|
+
options?: { collection?: string; sinceLsn?: number; limit?: number },
|
|
193
|
+
): AsyncIterable<KvWatchEvent>
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export class ConfigClient {
|
|
197
|
+
put(
|
|
198
|
+
key: string,
|
|
199
|
+
value: unknown,
|
|
200
|
+
options?: {
|
|
201
|
+
collection?: string
|
|
202
|
+
tags?: string[]
|
|
203
|
+
secretRef?: { collection: string; key: string }
|
|
204
|
+
},
|
|
205
|
+
): Promise<QueryResult>
|
|
206
|
+
get(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
207
|
+
resolve(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export class VaultClient {
|
|
211
|
+
put(
|
|
212
|
+
key: string,
|
|
213
|
+
value: unknown,
|
|
214
|
+
options?: { collection?: string; tags?: string[] },
|
|
215
|
+
): Promise<QueryResult>
|
|
216
|
+
get(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
217
|
+
unseal(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export class RedDB {
|
|
221
|
+
readonly cache: CacheClient
|
|
222
|
+
readonly kv: KvClient & ((collection?: string) => KvClient)
|
|
223
|
+
readonly config: (collection?: string) => ConfigClient
|
|
224
|
+
readonly vault: (collection?: string) => VaultClient
|
|
225
|
+
|
|
226
|
+
query(sql: string): Promise<QueryResult>
|
|
227
|
+
insert(collection: string, payload: Record<string, unknown>): Promise<InsertResult>
|
|
228
|
+
bulkInsert(
|
|
229
|
+
collection: string,
|
|
230
|
+
payloads: Array<Record<string, unknown>>,
|
|
231
|
+
): Promise<BulkInsertResult>
|
|
232
|
+
get(collection: string, id: string | number): Promise<GetResult>
|
|
233
|
+
delete(collection: string, id: string | number): Promise<DeleteResult>
|
|
234
|
+
health(): Promise<HealthResult>
|
|
235
|
+
version(): Promise<VersionResult>
|
|
236
|
+
|
|
237
|
+
// Auth surface — only meaningful when connected via grpc://.
|
|
238
|
+
// Embedded modes will receive 'unknown method' from the bridge.
|
|
239
|
+
login(username: string, password: string): Promise<LoginResult>
|
|
240
|
+
whoami(): Promise<WhoamiResult>
|
|
241
|
+
changePassword(currentPassword: string, newPassword: string): Promise<ChangePasswordResult>
|
|
242
|
+
createApiKey(opts?: { username?: string; role?: Role }): Promise<CreateApiKeyResult>
|
|
243
|
+
revokeApiKey(key: string): Promise<RevokeApiKeyResult>
|
|
244
|
+
|
|
245
|
+
close(): Promise<void>
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Connect to a RedDB instance.
|
|
250
|
+
*
|
|
251
|
+
* Accepted URI schemes:
|
|
252
|
+
* - `memory://` — ephemeral in-memory database
|
|
253
|
+
* - `file:///absolute/path` — embedded, persisted to disk
|
|
254
|
+
* - `grpc://host:port` — remote server
|
|
255
|
+
*/
|
|
256
|
+
export function connect(uri: string, options?: ConnectOptions): Promise<RedDB>
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Exchange username + password for a bearer token by hitting the
|
|
260
|
+
* server's `POST /auth/login` HTTP endpoint. The returned `token`
|
|
261
|
+
* can be passed to `connect(uri, { auth: { token } })`.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* import { connect, login } from '@reddb-io/sdk'
|
|
265
|
+
* const { token } = await login(
|
|
266
|
+
* 'https://reddb.example.com/auth/login',
|
|
267
|
+
* { username: 'admin', password: 'secret' },
|
|
268
|
+
* )
|
|
269
|
+
* const db = await connect('grpc://reddb.example.com:5051', { auth: { token } })
|
|
270
|
+
*/
|
|
271
|
+
export function login(
|
|
272
|
+
loginUrl: string,
|
|
273
|
+
credentials: { username: string; password: string },
|
|
274
|
+
): Promise<LoginResult>
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Translate a connection URI + (optional) auth into argv for
|
|
278
|
+
* `red rpc --stdio`. Exported for tests / debug. New code should
|
|
279
|
+
* use `parseUri` directly and let `connect` handle dispatch.
|
|
280
|
+
*/
|
|
281
|
+
export function uriToArgs(
|
|
282
|
+
uri: string,
|
|
283
|
+
auth?: { kind: 'token'; token: string } | null,
|
|
284
|
+
): string[]
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Parsed `red://` (or legacy) URI. Returned by `parseUri`.
|
|
288
|
+
*/
|
|
289
|
+
export interface ParsedUri {
|
|
290
|
+
kind: 'embedded' | 'http' | 'https' | 'grpc' | 'grpcs' | 'pg'
|
|
291
|
+
host?: string
|
|
292
|
+
port?: number
|
|
293
|
+
path?: string
|
|
294
|
+
username?: string
|
|
295
|
+
password?: string
|
|
296
|
+
token?: string
|
|
297
|
+
apiKey?: string
|
|
298
|
+
loginUrl?: string
|
|
299
|
+
params?: URLSearchParams
|
|
300
|
+
originalUri: string
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Parse any URI (red://, memory://, file://, grpc://, http(s)://) into a normalised shape. */
|
|
304
|
+
export function parseUri(uri: string): ParsedUri
|
|
305
|
+
|
|
306
|
+
/** Derive the HTTP `/auth/login` URL from a parsed URI. */
|
|
307
|
+
export function deriveLoginUrl(parsed: ParsedUri): string
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------
|
|
310
|
+
// RedWire native TCP transport (drivers/js/src/redwire.js)
|
|
311
|
+
// ---------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
/** RedWire frame kinds. Numeric values are the wire-stable spec. */
|
|
314
|
+
export const MessageKind: Readonly<{
|
|
315
|
+
Query: 0x01
|
|
316
|
+
Result: 0x02
|
|
317
|
+
Error: 0x03
|
|
318
|
+
BulkInsert: 0x04
|
|
319
|
+
BulkOk: 0x05
|
|
320
|
+
Hello: 0x10
|
|
321
|
+
HelloAck: 0x11
|
|
322
|
+
AuthRequest: 0x12
|
|
323
|
+
AuthResponse: 0x13
|
|
324
|
+
AuthOk: 0x14
|
|
325
|
+
AuthFail: 0x15
|
|
326
|
+
Bye: 0x16
|
|
327
|
+
Ping: 0x17
|
|
328
|
+
Pong: 0x18
|
|
329
|
+
}>
|
|
330
|
+
|
|
331
|
+
export type RedWireAuth =
|
|
332
|
+
| { kind: 'anonymous' }
|
|
333
|
+
| { kind: 'bearer'; token: string }
|
|
334
|
+
|
|
335
|
+
export interface RedWireConnectOptions {
|
|
336
|
+
host: string
|
|
337
|
+
port: number
|
|
338
|
+
auth?: RedWireAuth
|
|
339
|
+
clientName?: string
|
|
340
|
+
/** When set, wraps the socket in TLS (mTLS via cert + key). */
|
|
341
|
+
tls?: TlsOptions
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Open a RedWire connection. Speaks the binary protocol directly via
|
|
346
|
+
* a TCP socket — no spawn, no fetch. Returned client matches the
|
|
347
|
+
* `RpcClient` / `HttpRpcClient` surface so it slots into the
|
|
348
|
+
* existing `RedDB` class.
|
|
349
|
+
*/
|
|
350
|
+
export function connectRedwire(opts: RedWireConnectOptions): Promise<RedWireClient>
|
|
351
|
+
|
|
352
|
+
export class RedWireClient {
|
|
353
|
+
/**
|
|
354
|
+
* Generic RPC entry. Routes:
|
|
355
|
+
* - 'query' → Query frame (SQL string)
|
|
356
|
+
* - 'insert' → BulkInsert frame, single-row shape
|
|
357
|
+
* - 'bulk_insert' → BulkInsert frame, array shape
|
|
358
|
+
* - 'health' / 'version' → Ping frame
|
|
359
|
+
*/
|
|
360
|
+
call(method: string, params?: Record<string, unknown>): Promise<unknown>
|
|
361
|
+
close(): Promise<void>
|
|
362
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reddb-io/sdk",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Official RedDB driver — talks the native RedWire TCP protocol (mTLS), HTTP, gRPC bridge, or embedded stdio JSON-RPC. Works in Node 18+, Bun and Deno.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./src/index.js",
|
|
10
|
+
"types": "./index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"src/",
|
|
16
|
+
"index.d.ts",
|
|
17
|
+
"postinstall.js",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"reddb",
|
|
26
|
+
"database",
|
|
27
|
+
"client",
|
|
28
|
+
"driver",
|
|
29
|
+
"json-rpc",
|
|
30
|
+
"multi-model",
|
|
31
|
+
"vector",
|
|
32
|
+
"graph",
|
|
33
|
+
"document",
|
|
34
|
+
"key-value"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://github.com/reddb-io/reddb",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/reddb-io/reddb.git",
|
|
41
|
+
"directory": "drivers/js"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/reddb-io/reddb/issues"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"postinstall": "node postinstall.js",
|
|
48
|
+
"test": "node --test test/cache.test.mjs && node test/smoke.test.mjs"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postinstall.js — download the matching `red` binary from GitHub Releases.
|
|
3
|
+
*
|
|
4
|
+
* Behavior:
|
|
5
|
+
* - Resolves `red-<platform>-<arch>` from `process.platform` + `process.arch`
|
|
6
|
+
* via the vendored asset fetcher.
|
|
7
|
+
* - Targets the GitHub release matching this package's version.
|
|
8
|
+
* - Drops the binary at `<package>/bin/red[.exe]` and chmods +x on Unix.
|
|
9
|
+
* - On any failure, prints a warning to stderr but exits 0 — `npm install`
|
|
10
|
+
* never breaks because of this script. The user gets a clear error
|
|
11
|
+
* later when they call `connect()` if the binary isn't present.
|
|
12
|
+
*
|
|
13
|
+
* Override hooks (env vars):
|
|
14
|
+
* REDDB_SKIP_POSTINSTALL=1 do nothing
|
|
15
|
+
* REDDB_POSTINSTALL_VERSION=… pull a different release tag
|
|
16
|
+
* REDDB_POSTINSTALL_REPO=… pull from a fork (default: reddb-io/reddb)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
import { dirname, join } from 'node:path'
|
|
22
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'
|
|
23
|
+
|
|
24
|
+
import { fetchReleaseAsset } from './src/internal/asset-fetcher/index.js'
|
|
25
|
+
|
|
26
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const require = createRequire(import.meta.url)
|
|
28
|
+
const pkg = require('./package.json')
|
|
29
|
+
|
|
30
|
+
const DEFAULT_REPO = 'reddb-io/reddb'
|
|
31
|
+
|
|
32
|
+
if (process.env.REDDB_SKIP_POSTINSTALL === '1') {
|
|
33
|
+
process.stdout.write('reddb: postinstall skipped (REDDB_SKIP_POSTINSTALL=1)\n')
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main().catch((err) => {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
`reddb: postinstall could not download the binary (${err.message}).\n` +
|
|
40
|
+
` The package will still install. To use the driver you can:\n` +
|
|
41
|
+
` - set REDDB_BINARY_PATH=/path/to/red\n` +
|
|
42
|
+
` - or install the binary manually from https://github.com/${DEFAULT_REPO}/releases\n`,
|
|
43
|
+
)
|
|
44
|
+
process.exit(0)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const repo = process.env.REDDB_POSTINSTALL_REPO || DEFAULT_REPO
|
|
49
|
+
const tag = process.env.REDDB_POSTINSTALL_VERSION
|
|
50
|
+
? normalizeTag(process.env.REDDB_POSTINSTALL_VERSION)
|
|
51
|
+
: `v${pkg.version}`
|
|
52
|
+
|
|
53
|
+
const binDir = join(HERE, 'bin')
|
|
54
|
+
const binaryPath = join(binDir, defaultBinaryName())
|
|
55
|
+
|
|
56
|
+
if (existsSync(binaryPath)) {
|
|
57
|
+
process.stdout.write(`reddb: binary already present at ${binaryPath}\n`)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
`reddb: downloading red ${tag} for ${process.platform}/${process.arch} from ${repo}\n`,
|
|
63
|
+
)
|
|
64
|
+
const body = await fetchReleaseAsset({
|
|
65
|
+
repo,
|
|
66
|
+
tag,
|
|
67
|
+
platform: process.platform,
|
|
68
|
+
arch: process.arch,
|
|
69
|
+
binName: 'red',
|
|
70
|
+
})
|
|
71
|
+
mkdirSync(binDir, { recursive: true })
|
|
72
|
+
writeFileSync(binaryPath, body)
|
|
73
|
+
if (process.platform !== 'win32') {
|
|
74
|
+
chmodSync(binaryPath, 0o755)
|
|
75
|
+
}
|
|
76
|
+
process.stdout.write(`reddb: installed binary at ${binaryPath}\n`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function defaultBinaryName() {
|
|
80
|
+
return process.platform === 'win32' ? 'red.exe' : 'red'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeTag(value) {
|
|
84
|
+
const v = String(value).trim()
|
|
85
|
+
return v.startsWith('v') ? v : `v${v}`
|
|
86
|
+
}
|