@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/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
+ }