@reddb-io/client 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 +97 -0
- package/index.d.ts +223 -0
- package/package.json +49 -0
- package/postinstall.js +97 -0
- package/src/cache.js +137 -0
- package/src/config.js +66 -0
- package/src/embedded-rejection.js +90 -0
- package/src/http.js +200 -0
- package/src/index.js +336 -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/kv.js +70 -0
- package/src/protocol.js +157 -0
- package/src/redwire.js +723 -0
- package/src/url.js +271 -0
- package/src/vault.js +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# @reddb-io/client
|
|
2
|
+
|
|
3
|
+
Thin **remote-only** RedDB driver for JavaScript and TypeScript. Speaks
|
|
4
|
+
RedWire (TCP + mTLS), gRPC, and HTTP straight to a remote RedDB
|
|
5
|
+
server. Ships the `red_client` thin binary for an ad-hoc REPL — about
|
|
6
|
+
10x smaller than `@reddb-io/sdk`.
|
|
7
|
+
|
|
8
|
+
> Embedded engines (`memory://`, `file:///path`) are intentionally
|
|
9
|
+
> rejected by this package. Use [`@reddb-io/sdk`](../js) instead if you
|
|
10
|
+
> need an in-process database.
|
|
11
|
+
|
|
12
|
+
## When to use this package
|
|
13
|
+
|
|
14
|
+
| You want… | Install |
|
|
15
|
+
| ------------------------------------------------- | -------------------- |
|
|
16
|
+
| Connect to a running RedDB server (most apps) | `@reddb-io/client` |
|
|
17
|
+
| Same, plus the ability to spin up a local engine | `@reddb-io/sdk` |
|
|
18
|
+
| The CLI launcher (`reddb-cli`) | `@reddb-io/cli` |
|
|
19
|
+
|
|
20
|
+
All three packages stay version-locked.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add @reddb-io/client
|
|
26
|
+
# or
|
|
27
|
+
npm install @reddb-io/client
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The `postinstall` script downloads the matching `red_client` binary
|
|
31
|
+
from GitHub Releases into `node_modules/@reddb-io/client/bin/`. If your
|
|
32
|
+
environment blocks postinstall scripts or has no network, set
|
|
33
|
+
`REDDB_CLIENT_BIN=/path/to/red_client` to point at a copy you've placed
|
|
34
|
+
manually. The driver itself does **not** need the binary for `connect()`
|
|
35
|
+
— it speaks the wire protocols directly from JS.
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { connect } from '@reddb-io/client'
|
|
41
|
+
|
|
42
|
+
const db = await connect('red://reddb.example.com:5050', {
|
|
43
|
+
auth: { token: process.env.REDDB_TOKEN },
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await db.insert('users', { name: 'Alice' })
|
|
47
|
+
const result = await db.query('SELECT * FROM users LIMIT 10')
|
|
48
|
+
console.log(result.rows)
|
|
49
|
+
|
|
50
|
+
await db.close()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Accepted URI schemes
|
|
54
|
+
|
|
55
|
+
| Scheme | Transport | Default port |
|
|
56
|
+
| -------------- | ----------------------------- | ------------ |
|
|
57
|
+
| `red://` | RedWire (TCP) | 5050 |
|
|
58
|
+
| `reds://` | RedWire over TLS | 5050 |
|
|
59
|
+
| `grpc://` | gRPC | 5055 |
|
|
60
|
+
| `grpcs://` | gRPC over TLS | 5056 |
|
|
61
|
+
| `http://` | HTTP JSON | 8080 |
|
|
62
|
+
| `https://` | HTTPS JSON | 8443 |
|
|
63
|
+
|
|
64
|
+
## Rejected URI schemes
|
|
65
|
+
|
|
66
|
+
`memory://`, `memory:`, `file:///abs/path`, `red://`, `red:///path`,
|
|
67
|
+
`red://:memory`, `red://:memory:` — all throw `EmbeddedNotSupported`
|
|
68
|
+
with the same wording as the underlying `red_client` binary:
|
|
69
|
+
|
|
70
|
+
> embedded schemes (memory:// / file://) are not supported. Use the
|
|
71
|
+
> full `red` binary for in-memory or file-backed engines.
|
|
72
|
+
|
|
73
|
+
## Authentication
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
// Bearer / API key
|
|
77
|
+
await connect('red://host:5050', { auth: { token: 'sk-abc' } })
|
|
78
|
+
|
|
79
|
+
// or via the URI:
|
|
80
|
+
await connect('red://host:5050?token=sk-abc')
|
|
81
|
+
|
|
82
|
+
// Username + password (driver calls /auth/login first):
|
|
83
|
+
await connect('red://user:pass@host:5050')
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Environment overrides
|
|
87
|
+
|
|
88
|
+
| Variable | Effect |
|
|
89
|
+
| ---------------------------- | ----------------------------------------------------- |
|
|
90
|
+
| `REDDB_CLIENT_BIN` | Override path to `red_client` for spawn-style helpers |
|
|
91
|
+
| `REDDB_SKIP_POSTINSTALL=1` | Don't download the binary on install |
|
|
92
|
+
| `REDDB_POSTINSTALL_VERSION` | Pull a specific release tag |
|
|
93
|
+
| `REDDB_POSTINSTALL_REPO` | Pull from a fork (default `reddb-io/reddb`) |
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @reddb-io/client — TypeScript definitions for the thin remote-only
|
|
3
|
+
* RedDB driver. Hand-written, kept in sync with src/index.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type AuthOptions =
|
|
7
|
+
| { token: string }
|
|
8
|
+
| { apiKey: string }
|
|
9
|
+
| { username: string; password: string; loginUrl?: string }
|
|
10
|
+
|
|
11
|
+
export interface TlsOptions {
|
|
12
|
+
ca?: string | Uint8Array
|
|
13
|
+
cert?: string | Uint8Array
|
|
14
|
+
key?: string | Uint8Array
|
|
15
|
+
servername?: string
|
|
16
|
+
rejectUnauthorized?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ConnectOptions {
|
|
20
|
+
/** Authentication credentials for remote transports. */
|
|
21
|
+
auth?: AuthOptions
|
|
22
|
+
/** TLS options for `reds://` / `grpcs://` connections. */
|
|
23
|
+
tls?: TlsOptions
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface QueryResult {
|
|
27
|
+
statement: string
|
|
28
|
+
affected: number
|
|
29
|
+
columns: string[]
|
|
30
|
+
rows: Array<Record<string, unknown>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface InsertResult { affected: number; id?: string | number }
|
|
34
|
+
export interface BulkInsertResult { affected: number }
|
|
35
|
+
export interface GetResult { entity: Record<string, unknown> | null }
|
|
36
|
+
export interface DeleteResult { affected: number }
|
|
37
|
+
export interface HealthResult { ok: boolean; version: string }
|
|
38
|
+
export interface VersionResult { version: string; protocol: string }
|
|
39
|
+
|
|
40
|
+
export type Role = 'read' | 'write' | 'admin'
|
|
41
|
+
|
|
42
|
+
export interface LoginResult {
|
|
43
|
+
token: string
|
|
44
|
+
username: string
|
|
45
|
+
role: Role
|
|
46
|
+
expires_at: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WhoamiResult { username: string; role: Role }
|
|
50
|
+
export interface CreateApiKeyResult { key: string; role: Role; created_at: number }
|
|
51
|
+
export interface ChangePasswordResult { ok: true }
|
|
52
|
+
export interface RevokeApiKeyResult { ok: true }
|
|
53
|
+
|
|
54
|
+
export class RedDBError extends Error {
|
|
55
|
+
readonly name: 'RedDBError'
|
|
56
|
+
readonly code: string
|
|
57
|
+
readonly data: unknown
|
|
58
|
+
constructor(code: string, message: string, data?: unknown)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Cache API
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export interface CachePutOptions {
|
|
66
|
+
ttl_ms?: number
|
|
67
|
+
tags?: string[]
|
|
68
|
+
policy?: {
|
|
69
|
+
idle_evict_ms?: number
|
|
70
|
+
stale_while_revalidate_ms?: number
|
|
71
|
+
jitter_ms?: number
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type CacheExistsStatus = 'present' | 'absent' | 'maybe'
|
|
76
|
+
|
|
77
|
+
export class CacheClient {
|
|
78
|
+
get(namespace: string, key: string): Promise<Uint8Array | null>
|
|
79
|
+
put(
|
|
80
|
+
namespace: string,
|
|
81
|
+
key: string,
|
|
82
|
+
value: Uint8Array | Buffer | string,
|
|
83
|
+
opts?: CachePutOptions,
|
|
84
|
+
): Promise<void>
|
|
85
|
+
exists(namespace: string, key: string): Promise<CacheExistsStatus>
|
|
86
|
+
invalidate(namespace: string, key: string): Promise<void>
|
|
87
|
+
invalidatePrefix(namespace: string, prefix: string): Promise<number>
|
|
88
|
+
invalidateTags(namespace: string, tags: string[]): Promise<number>
|
|
89
|
+
flushNamespace(namespace: string): Promise<void>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface KvWatchEvent {
|
|
93
|
+
key: string
|
|
94
|
+
op: 'insert' | 'update' | 'delete'
|
|
95
|
+
before: unknown
|
|
96
|
+
after: unknown
|
|
97
|
+
lsn: number
|
|
98
|
+
committed_at: number
|
|
99
|
+
dropped_event_count: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export class KvClient {
|
|
103
|
+
put(
|
|
104
|
+
key: string,
|
|
105
|
+
value: unknown,
|
|
106
|
+
options?: { collection?: string; expireMs?: number; tags?: string[] },
|
|
107
|
+
): Promise<QueryResult>
|
|
108
|
+
invalidateTags(tags: string[], options?: { collection?: string }): Promise<number>
|
|
109
|
+
watch(
|
|
110
|
+
key: string,
|
|
111
|
+
options?: { collection?: string; sinceLsn?: number; limit?: number },
|
|
112
|
+
): AsyncIterable<KvWatchEvent>
|
|
113
|
+
watchPrefix(
|
|
114
|
+
prefix: string,
|
|
115
|
+
options?: { collection?: string; sinceLsn?: number; limit?: number },
|
|
116
|
+
): AsyncIterable<KvWatchEvent>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class ConfigClient {
|
|
120
|
+
put(
|
|
121
|
+
key: string,
|
|
122
|
+
value: unknown,
|
|
123
|
+
options?: {
|
|
124
|
+
collection?: string
|
|
125
|
+
tags?: string[]
|
|
126
|
+
secretRef?: { collection: string; key: string }
|
|
127
|
+
},
|
|
128
|
+
): Promise<QueryResult>
|
|
129
|
+
get(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
130
|
+
resolve(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class VaultClient {
|
|
134
|
+
put(
|
|
135
|
+
key: string,
|
|
136
|
+
value: unknown,
|
|
137
|
+
options?: { collection?: string; tags?: string[] },
|
|
138
|
+
): Promise<QueryResult>
|
|
139
|
+
get(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
140
|
+
unseal(key: string, options?: { collection?: string }): Promise<QueryResult>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Specialised error thrown when an embedded URI is passed to the
|
|
145
|
+
* thin client. Always has `code === 'EmbeddedNotSupported'`. Use
|
|
146
|
+
* `@reddb-io/sdk` instead for in-memory or file-backed engines.
|
|
147
|
+
*/
|
|
148
|
+
export class EmbeddedNotSupported extends RedDBError {
|
|
149
|
+
readonly name: 'EmbeddedNotSupported'
|
|
150
|
+
readonly code: 'EmbeddedNotSupported'
|
|
151
|
+
readonly uri: string
|
|
152
|
+
constructor(uri: string)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const EMBEDDED_REJECTION_MESSAGE: string
|
|
156
|
+
|
|
157
|
+
/** Returns true when `uri` selects the embedded engine. */
|
|
158
|
+
export function isEmbeddedUri(uri: string): boolean
|
|
159
|
+
|
|
160
|
+
export class RedDB {
|
|
161
|
+
readonly cache: CacheClient
|
|
162
|
+
readonly kv: KvClient & ((collection?: string) => KvClient)
|
|
163
|
+
readonly config: (collection?: string) => ConfigClient
|
|
164
|
+
readonly vault: (collection?: string) => VaultClient
|
|
165
|
+
|
|
166
|
+
query(sql: string): Promise<QueryResult>
|
|
167
|
+
insert(collection: string, payload: Record<string, unknown>): Promise<InsertResult>
|
|
168
|
+
bulkInsert(
|
|
169
|
+
collection: string,
|
|
170
|
+
payloads: Array<Record<string, unknown>>,
|
|
171
|
+
): Promise<BulkInsertResult>
|
|
172
|
+
get(collection: string, id: string | number): Promise<GetResult>
|
|
173
|
+
delete(collection: string, id: string | number): Promise<DeleteResult>
|
|
174
|
+
health(): Promise<HealthResult>
|
|
175
|
+
version(): Promise<VersionResult>
|
|
176
|
+
|
|
177
|
+
login(username: string, password: string): Promise<LoginResult>
|
|
178
|
+
whoami(): Promise<WhoamiResult>
|
|
179
|
+
changePassword(currentPassword: string, newPassword: string): Promise<ChangePasswordResult>
|
|
180
|
+
createApiKey(opts?: { username?: string; role?: Role }): Promise<CreateApiKeyResult>
|
|
181
|
+
revokeApiKey(key: string): Promise<RevokeApiKeyResult>
|
|
182
|
+
|
|
183
|
+
close(): Promise<void>
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Connect to a remote RedDB instance.
|
|
188
|
+
*
|
|
189
|
+
* Accepted URI schemes:
|
|
190
|
+
* - `red://host:port` — RedWire TCP (default)
|
|
191
|
+
* - `reds://host:port` — RedWire over TLS
|
|
192
|
+
* - `grpc://host:port` — gRPC
|
|
193
|
+
* - `grpcs://host:port` — gRPC over TLS
|
|
194
|
+
* - `http://host:port` — HTTP JSON
|
|
195
|
+
* - `https://host:port` — HTTPS JSON
|
|
196
|
+
*
|
|
197
|
+
* Embedded URIs (`memory://`, `memory:`, `file:///path`, `red:///`,
|
|
198
|
+
* `red://:memory[:]`) throw `EmbeddedNotSupported`.
|
|
199
|
+
*/
|
|
200
|
+
export function connect(uri: string, options?: ConnectOptions): Promise<RedDB>
|
|
201
|
+
|
|
202
|
+
/** Exchange username + password for a bearer token via /auth/login. */
|
|
203
|
+
export function login(
|
|
204
|
+
loginUrl: string,
|
|
205
|
+
credentials: { username: string; password: string },
|
|
206
|
+
): Promise<LoginResult>
|
|
207
|
+
|
|
208
|
+
export interface ParsedUri {
|
|
209
|
+
kind: 'embedded' | 'http' | 'https' | 'red' | 'reds' | 'grpc' | 'grpcs' | 'pg'
|
|
210
|
+
host?: string
|
|
211
|
+
port?: number
|
|
212
|
+
path?: string
|
|
213
|
+
username?: string
|
|
214
|
+
password?: string
|
|
215
|
+
token?: string
|
|
216
|
+
apiKey?: string
|
|
217
|
+
loginUrl?: string
|
|
218
|
+
params?: URLSearchParams
|
|
219
|
+
originalUri: string
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function parseUri(uri: string): ParsedUri
|
|
223
|
+
export function deriveLoginUrl(parsed: ParsedUri): string
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reddb-io/client",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Thin remote-only RedDB driver. Downloads the `red_client` binary on install. Speaks RedWire/gRPC/HTTP. Embedded URIs (memory://, file://, red:///path) are rejected — use @reddb-io/sdk for those.",
|
|
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
|
+
"remote",
|
|
30
|
+
"thin-client",
|
|
31
|
+
"redwire",
|
|
32
|
+
"grpc",
|
|
33
|
+
"http"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"homepage": "https://github.com/reddb-io/reddb",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/reddb-io/reddb.git",
|
|
40
|
+
"directory": "drivers/js-client"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/reddb-io/reddb/issues"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"postinstall": "node postinstall.js",
|
|
47
|
+
"test": "node --test test/*.test.mjs"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* postinstall.js — download the matching `red_client` binary from
|
|
3
|
+
* GitHub Releases.
|
|
4
|
+
*
|
|
5
|
+
* Behaviour:
|
|
6
|
+
* - Resolves `red_client-<platform>-<arch>` from process.platform +
|
|
7
|
+
* process.arch via the vendored asset fetcher.
|
|
8
|
+
* - Targets the GitHub release matching this package's version.
|
|
9
|
+
* - Drops the binary at `<package>/bin/red_client[.exe]` and chmods
|
|
10
|
+
* +x on Unix.
|
|
11
|
+
* - On any failure, prints a warning to stderr but exits 0 — npm
|
|
12
|
+
* install never breaks because of this script. The user gets a
|
|
13
|
+
* clear error later when they call `connect()` if the binary is
|
|
14
|
+
* missing AND the runtime needs to spawn it. (The remote-only
|
|
15
|
+
* transports in @reddb-io/client don't actually need the binary
|
|
16
|
+
* for `connect()` itself; the binary is provided as a CLI helper
|
|
17
|
+
* for ad-hoc REPL / one-shot SQL.)
|
|
18
|
+
*
|
|
19
|
+
* Override hooks (env vars):
|
|
20
|
+
* REDDB_SKIP_POSTINSTALL=1 do nothing
|
|
21
|
+
* REDDB_POSTINSTALL_VERSION=… pull a different release tag
|
|
22
|
+
* REDDB_POSTINSTALL_REPO=… pull from a fork (default: reddb-io/reddb)
|
|
23
|
+
* REDDB_CLIENT_BIN=/path runtime override consulted by callers
|
|
24
|
+
* that spawn the binary; postinstall
|
|
25
|
+
* still downloads to the package dir
|
|
26
|
+
* unless REDDB_SKIP_POSTINSTALL=1.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { createRequire } from 'node:module'
|
|
30
|
+
import { fileURLToPath } from 'node:url'
|
|
31
|
+
import { dirname, join } from 'node:path'
|
|
32
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs'
|
|
33
|
+
|
|
34
|
+
import { fetchReleaseAsset } from './src/internal/asset-fetcher/index.js'
|
|
35
|
+
|
|
36
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
37
|
+
const require = createRequire(import.meta.url)
|
|
38
|
+
const pkg = require('./package.json')
|
|
39
|
+
|
|
40
|
+
const DEFAULT_REPO = 'reddb-io/reddb'
|
|
41
|
+
const BIN_NAME = 'red_client'
|
|
42
|
+
|
|
43
|
+
if (process.env.REDDB_SKIP_POSTINSTALL === '1') {
|
|
44
|
+
process.stdout.write('@reddb-io/client: postinstall skipped (REDDB_SKIP_POSTINSTALL=1)\n')
|
|
45
|
+
process.exit(0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main().catch((err) => {
|
|
49
|
+
process.stderr.write(
|
|
50
|
+
`@reddb-io/client: postinstall could not download red_client (${err.message}).\n`
|
|
51
|
+
+ ` The package will still install. To use the binary you can:\n`
|
|
52
|
+
+ ` - set REDDB_CLIENT_BIN=/path/to/red_client\n`
|
|
53
|
+
+ ` - or install it manually from https://github.com/${DEFAULT_REPO}/releases\n`,
|
|
54
|
+
)
|
|
55
|
+
process.exit(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
async function main() {
|
|
59
|
+
const repo = process.env.REDDB_POSTINSTALL_REPO || DEFAULT_REPO
|
|
60
|
+
const tag = process.env.REDDB_POSTINSTALL_VERSION
|
|
61
|
+
? normalizeTag(process.env.REDDB_POSTINSTALL_VERSION)
|
|
62
|
+
: `v${pkg.version}`
|
|
63
|
+
|
|
64
|
+
const binDir = join(HERE, 'bin')
|
|
65
|
+
const binaryPath = join(binDir, defaultBinaryName())
|
|
66
|
+
|
|
67
|
+
if (existsSync(binaryPath)) {
|
|
68
|
+
process.stdout.write(`@reddb-io/client: binary already present at ${binaryPath}\n`)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
process.stdout.write(
|
|
73
|
+
`@reddb-io/client: downloading ${BIN_NAME} ${tag} for ${process.platform}/${process.arch} from ${repo}\n`,
|
|
74
|
+
)
|
|
75
|
+
const body = await fetchReleaseAsset({
|
|
76
|
+
repo,
|
|
77
|
+
tag,
|
|
78
|
+
platform: process.platform,
|
|
79
|
+
arch: process.arch,
|
|
80
|
+
binName: BIN_NAME,
|
|
81
|
+
})
|
|
82
|
+
mkdirSync(binDir, { recursive: true })
|
|
83
|
+
writeFileSync(binaryPath, body)
|
|
84
|
+
if (process.platform !== 'win32') {
|
|
85
|
+
chmodSync(binaryPath, 0o755)
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(`@reddb-io/client: installed binary at ${binaryPath}\n`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function defaultBinaryName() {
|
|
91
|
+
return process.platform === 'win32' ? `${BIN_NAME}.exe` : BIN_NAME
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeTag(value) {
|
|
95
|
+
const v = String(value).trim()
|
|
96
|
+
return v.startsWith('v') ? v : `v${v}`
|
|
97
|
+
}
|
package/src/cache.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache client — exposes cache.{get,put,exists,invalidate,invalidatePrefix,
|
|
3
|
+
* invalidateTags,flushNamespace} via the underlying HTTP transport.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: These methods require server-side HTTP endpoints under /cache/ns/*.
|
|
6
|
+
* flushNamespace routes to the existing POST /admin/blob_cache/flush_namespace.
|
|
7
|
+
* All others target endpoints planned for a future server release.
|
|
8
|
+
*
|
|
9
|
+
* Values are base64-encoded in transit so binary payloads survive JSON.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class CacheClient {
|
|
13
|
+
/** @param {{ call: Function }} client */
|
|
14
|
+
constructor(client) {
|
|
15
|
+
this._client = client
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fetch a cached value. Returns a Uint8Array on hit, null on miss.
|
|
20
|
+
* @param {string} namespace
|
|
21
|
+
* @param {string} key
|
|
22
|
+
* @returns {Promise<Uint8Array | null>}
|
|
23
|
+
*/
|
|
24
|
+
async get(namespace, key) {
|
|
25
|
+
const result = await this._client.call('cache.get', { namespace, key })
|
|
26
|
+
if (result == null || result.value == null) return null
|
|
27
|
+
return base64ToBytes(result.value)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Store a value in the cache.
|
|
32
|
+
* @param {string} namespace
|
|
33
|
+
* @param {string} key
|
|
34
|
+
* @param {Uint8Array | Buffer | string} value String is UTF-8 encoded.
|
|
35
|
+
* @param {object} [opts]
|
|
36
|
+
* @param {number} [opts.ttl_ms]
|
|
37
|
+
* @param {string[]} [opts.tags]
|
|
38
|
+
* @param {object} [opts.policy]
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
async put(namespace, key, value, opts = {}) {
|
|
42
|
+
const encoded = bytesToBase64(value)
|
|
43
|
+
await this._client.call('cache.put', {
|
|
44
|
+
namespace,
|
|
45
|
+
key,
|
|
46
|
+
value: encoded,
|
|
47
|
+
...opts,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check whether a key is present.
|
|
53
|
+
* @param {string} namespace
|
|
54
|
+
* @param {string} key
|
|
55
|
+
* @returns {Promise<'present' | 'absent' | 'maybe'>}
|
|
56
|
+
*/
|
|
57
|
+
async exists(namespace, key) {
|
|
58
|
+
const result = await this._client.call('cache.exists', { namespace, key })
|
|
59
|
+
return result?.status ?? 'maybe'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Remove a single entry.
|
|
64
|
+
* @param {string} namespace
|
|
65
|
+
* @param {string} key
|
|
66
|
+
* @returns {Promise<void>}
|
|
67
|
+
*/
|
|
68
|
+
async invalidate(namespace, key) {
|
|
69
|
+
await this._client.call('cache.invalidate', { namespace, key })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remove all entries whose key starts with `prefix`.
|
|
74
|
+
* @param {string} namespace
|
|
75
|
+
* @param {string} prefix
|
|
76
|
+
* @returns {Promise<number>} Number of entries removed.
|
|
77
|
+
*/
|
|
78
|
+
async invalidatePrefix(namespace, prefix) {
|
|
79
|
+
const result = await this._client.call('cache.invalidate_prefix', { namespace, prefix })
|
|
80
|
+
return result?.removed ?? 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove all entries tagged with any of the given tags.
|
|
85
|
+
* @param {string} namespace
|
|
86
|
+
* @param {string[]} tags
|
|
87
|
+
* @returns {Promise<number>} Number of entries removed.
|
|
88
|
+
*/
|
|
89
|
+
async invalidateTags(namespace, tags) {
|
|
90
|
+
const result = await this._client.call('cache.invalidate_tags', { namespace, tags })
|
|
91
|
+
return result?.removed ?? 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Remove all entries in a namespace.
|
|
96
|
+
* Routes to POST /admin/blob_cache/flush_namespace (live endpoint).
|
|
97
|
+
* @param {string} namespace
|
|
98
|
+
* @returns {Promise<void>}
|
|
99
|
+
*/
|
|
100
|
+
async flushNamespace(namespace) {
|
|
101
|
+
await this._client.call('cache.flush_namespace', { namespace })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Helpers
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
function bytesToBase64(value) {
|
|
110
|
+
if (typeof value === 'string') {
|
|
111
|
+
const bytes = new TextEncoder().encode(value)
|
|
112
|
+
return bufToBase64(bytes)
|
|
113
|
+
}
|
|
114
|
+
if (value instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))) {
|
|
115
|
+
return bufToBase64(value)
|
|
116
|
+
}
|
|
117
|
+
throw new TypeError('cache value must be a string, Uint8Array, or Buffer')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function bufToBase64(bytes) {
|
|
121
|
+
if (typeof Buffer !== 'undefined') {
|
|
122
|
+
return Buffer.from(bytes).toString('base64')
|
|
123
|
+
}
|
|
124
|
+
let bin = ''
|
|
125
|
+
for (const b of bytes) bin += String.fromCharCode(b)
|
|
126
|
+
return btoa(bin)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function base64ToBytes(b64) {
|
|
130
|
+
if (typeof Buffer !== 'undefined') {
|
|
131
|
+
return new Uint8Array(Buffer.from(b64, 'base64'))
|
|
132
|
+
}
|
|
133
|
+
const bin = atob(b64)
|
|
134
|
+
const out = new Uint8Array(bin.length)
|
|
135
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
136
|
+
return out
|
|
137
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export class ConfigClient {
|
|
2
|
+
constructor(client, collection = 'red.config') {
|
|
3
|
+
this.client = client
|
|
4
|
+
this.collection = collection
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
put(key, value, options = {}) {
|
|
8
|
+
rejectVolatileOptions(options, 'config')
|
|
9
|
+
const collection = options.collection ?? this.collection
|
|
10
|
+
const tags = Array.isArray(options.tags) && options.tags.length > 0
|
|
11
|
+
? ` TAGS [${options.tags.map(keyedStringLiteral).join(', ')}]`
|
|
12
|
+
: ''
|
|
13
|
+
return this.client.call('query', {
|
|
14
|
+
sql: `PUT CONFIG ${keyedIdentifier(collection)} ${keyedIdentifier(key)} = ${configValueLiteral(value, options)}${tags}`,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(key, options = {}) {
|
|
19
|
+
const collection = options.collection ?? this.collection
|
|
20
|
+
return this.client.call('query', {
|
|
21
|
+
sql: `GET CONFIG ${keyedIdentifier(collection)} ${keyedIdentifier(key)}`,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
resolve(key, options = {}) {
|
|
26
|
+
const collection = options.collection ?? this.collection
|
|
27
|
+
return this.client.call('query', {
|
|
28
|
+
sql: `RESOLVE CONFIG ${keyedIdentifier(collection)} ${keyedIdentifier(key)}`,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function configValueLiteral(value, options) {
|
|
34
|
+
if (options.secretRef) {
|
|
35
|
+
const { collection, key } = options.secretRef
|
|
36
|
+
return `SECRET_REF(vault, ${keyedIdentifier(collection)}.${keyedIdentifier(key)})`
|
|
37
|
+
}
|
|
38
|
+
return keyedValueLiteral(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rejectVolatileOptions(options, domain) {
|
|
42
|
+
for (const field of ['ttl', 'ttlMs', 'ttl_ms', 'expireMs', 'expire_ms', 'expiresAt']) {
|
|
43
|
+
if (options[field] != null) {
|
|
44
|
+
throw new TypeError(`${domain} does not support TTL or expiration options`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function keyedIdentifier(value) {
|
|
50
|
+
const out = String(value)
|
|
51
|
+
if (!/^[A-Za-z0-9_.]+$/.test(out)) {
|
|
52
|
+
throw new TypeError('keyed collection and key names must use letters, numbers, underscores, or dots')
|
|
53
|
+
}
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function keyedValueLiteral(value) {
|
|
58
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
|
59
|
+
if (value == null) return 'NULL'
|
|
60
|
+
if (Array.isArray(value) || typeof value === 'object') return JSON.stringify(value)
|
|
61
|
+
return keyedStringLiteral(value)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function keyedStringLiteral(value) {
|
|
65
|
+
return `'${String(value).replace(/'/g, "''")}'`
|
|
66
|
+
}
|