@reddb-io/sdk 1.0.7 → 1.1.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
@@ -5,8 +5,9 @@ stdio to a local `red` binary, which is downloaded automatically on install.
5
5
  Works in **Node 18+**, **Bun** and **Deno** (via `npm:` specifier) — same
6
6
  package, no per-runtime fork.
7
7
 
8
- Use this package for application code. If you just want to launch the CLI from
9
- npm, use:
8
+ Use this package when your application should run an embedded local RedDB
9
+ engine. For remote HTTP, gRPC, or RedWire connections, install
10
+ `@reddb-io/client` instead. If you just want to launch the CLI from npm, use:
10
11
 
11
12
  ```bash
12
13
  npx @reddb-io/cli@latest version
@@ -76,10 +77,9 @@ await db.close()
76
77
  |----------------------------|--------------------------------------|
77
78
  | `memory://` | Ephemeral, in-memory database |
78
79
  | `file:///absolute/path` | Embedded engine, persisted to disk |
79
- | `grpc://host:port` | Remote server (planned, not yet) |
80
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.
81
+ Remote URIs such as `http://...`, `red://...`, and `grpc://...` are rejected
82
+ with `EMBEDDED_ONLY`. Use `@reddb-io/client` for those transports.
83
83
 
84
84
  ## API
85
85
 
@@ -102,7 +102,38 @@ const persisted = await connect('file:///tmp/app.rdb')
102
102
  const custom = await connect('memory://', { binary: '/usr/local/bin/red' })
103
103
  ```
104
104
 
105
- ### `db.query(sql) → Promise<{ statement, affected, columns, rows }>`
105
+ ### `db.query(sql, ...params) → Promise<{ statement, affected, columns, rows }>`
106
+
107
+ Bind user values with `$1`, `$2`, ... placeholders. The variadic form is the
108
+ preferred API; the older `db.query(sql, paramsArray)` form remains supported.
109
+
110
+ ```js
111
+ const result = await db.query(
112
+ 'SELECT * FROM users WHERE id = $1 AND name = $2',
113
+ 42,
114
+ 'Alice',
115
+ )
116
+ ```
117
+
118
+ `db.execute(sql, ...params)` is an alias for statements where the affected row
119
+ count is the primary result.
120
+
121
+ `ASK '...'` returns the ASK envelope directly:
122
+
123
+ ```js
124
+ const answer = await db.query("ASK 'why did deploy fail?'")
125
+ answer.answer
126
+ answer.citations
127
+ answer.sources_flat
128
+ answer.validation
129
+ answer.cache_hit
130
+ answer.cost_usd
131
+ ```
132
+
133
+ `ASK '...' STREAM` notifications are not wired over the JS stdio JSON-RPC
134
+ client yet. Use the HTTP streaming API for incremental ASK frames; stdio
135
+ currently supports materialised cursor batching through `query.open` /
136
+ `query.next`, which is separate from ASK token streaming.
106
137
 
107
138
  ### `db.insert(collection, payload) → Promise<{ affected, id? }>`
108
139
 
@@ -179,9 +210,11 @@ bun test/smoke.test.mjs
179
210
  deno run -A test/smoke.test.mjs
180
211
  ```
181
212
 
182
- ## Production deploy
213
+ ## Remote Deploy
183
214
 
184
- When you're ready to point this driver at a production RedDB cluster:
215
+ When you're ready to point JavaScript code at a production RedDB cluster, use
216
+ `@reddb-io/client`. The SDK package is embedded-only and intentionally rejects
217
+ remote URIs.
185
218
 
186
219
  - **Run RedDB with the encrypted vault** so auth state and
187
220
  `red.secret.*` values are protected at rest. See
@@ -192,8 +225,7 @@ When you're ready to point this driver at a production RedDB cluster:
192
225
  - **Track every secret** the driver consumes (bearer tokens, mTLS
193
226
  cert + key, OAuth JWTs) in
194
227
  [`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.
228
+ - **Use TLS** for any traffic crossing the network.
197
229
  - **TLS posture, mTLS, OAuth/JWT and reverse-proxy patterns** are
198
230
  covered in [`docs/security/transport-tls.md`](../../docs/security/transport-tls.md).
199
231
  - See [Policies](../../docs/security/policies.md) for IAM-style authorization.
package/index.d.ts CHANGED
@@ -4,16 +4,6 @@
4
4
  * Hand-written, kept in sync with src/index.js.
5
5
  */
6
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
7
  export type AuthOptions =
18
8
  | { token: string }
19
9
  | { apiKey: string }
@@ -35,13 +25,9 @@ export interface TlsOptions {
35
25
  export interface ConnectOptions {
36
26
  /** Override the path to the `red` binary (defaults to bundled). */
37
27
  binary?: string
38
- /** Authentication credentials (grpc:// only). */
28
+ /** Rejected for embedded SDK connections; use @reddb-io/client remotely. */
39
29
  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
- */
30
+ /** Reserved for remote clients; ignored by the embedded SDK. */
45
31
  tls?: TlsOptions
46
32
  }
47
33
 
@@ -52,14 +38,63 @@ export interface QueryResult {
52
38
  rows: Array<Record<string, unknown>>
53
39
  }
54
40
 
41
+ export type QueryParam =
42
+ | null
43
+ | boolean
44
+ | number
45
+ | string
46
+ | Uint8Array
47
+ | Buffer
48
+ | Date
49
+ | Float32Array
50
+ | Float64Array
51
+ | number[]
52
+ | Record<string, unknown>
53
+
54
+ export interface AskSource {
55
+ urn: string
56
+ payload: string
57
+ }
58
+
59
+ export interface AskCitation {
60
+ marker: number
61
+ urn: string
62
+ }
63
+
64
+ export interface AskValidationItem {
65
+ kind: string
66
+ detail: string
67
+ }
68
+
69
+ export interface AskValidation {
70
+ ok: boolean
71
+ warnings: AskValidationItem[]
72
+ errors: AskValidationItem[]
73
+ }
74
+
75
+ export interface AskQueryResult {
76
+ answer: string
77
+ cache_hit: boolean
78
+ citations: AskCitation[]
79
+ completion_tokens: number
80
+ cost_usd: number
81
+ mode: 'strict' | 'lenient'
82
+ model: string
83
+ prompt_tokens: number
84
+ provider: string
85
+ retry_count: number
86
+ sources_flat: AskSource[]
87
+ validation: AskValidation
88
+ }
89
+
55
90
  export interface InsertResult {
56
91
  affected: number
57
- /** Present when the underlying engine surfaces the inserted entity id. */
58
- id?: string | number
92
+ id: string | number
59
93
  }
60
94
 
61
95
  export interface BulkInsertResult {
62
96
  affected: number
97
+ ids: Array<string | number>
63
98
  }
64
99
 
65
100
  export interface GetResult {
@@ -70,6 +105,13 @@ export interface DeleteResult {
70
105
  affected: number
71
106
  }
72
107
 
108
+ export interface CollectionMeta {
109
+ name: string
110
+ model: string
111
+ capabilities: string[]
112
+ [key: string]: unknown
113
+ }
114
+
73
115
  export interface HealthResult {
74
116
  ok: boolean
75
117
  version: string
@@ -144,6 +186,13 @@ export interface CacheInvalidateResult {
144
186
  removed: number
145
187
  }
146
188
 
189
+ /**
190
+ * Cache client. Requires an HTTP or gRPC / RedWire transport — the
191
+ * underlying `cache.*` RPC methods are not served by the embedded
192
+ * (stdio JSON-RPC) handler. Calls on an unsupported transport throw
193
+ * `RedDBError` with code `UNSUPPORTED_TRANSPORT` before issuing any
194
+ * RPC.
195
+ */
147
196
  export class CacheClient {
148
197
  /** Fetch a cached value. Returns Uint8Array on hit, null on miss. */
149
198
  get(namespace: string, key: string): Promise<Uint8Array | null>
@@ -182,6 +231,8 @@ export class KvClient {
182
231
  value: unknown,
183
232
  options?: { collection?: string; expireMs?: number; tags?: string[] },
184
233
  ): Promise<QueryResult>
234
+ get(key: string, options?: { collection?: string }): Promise<unknown | null>
235
+ getMany(keys: string[], options?: { collection?: string }): Promise<Array<unknown | null>>
185
236
  invalidateTags(tags: string[], options?: { collection?: string }): Promise<number>
186
237
  watch(
187
238
  key: string,
@@ -193,6 +244,32 @@ export class KvClient {
193
244
  ): AsyncIterable<KvWatchEvent>
194
245
  }
195
246
 
247
+ export class QueueClient {
248
+ push(
249
+ queue: string,
250
+ value: unknown,
251
+ options?: { priority?: number },
252
+ ): Promise<QueryResult>
253
+ pop(queue: string, count?: number): Promise<unknown[]>
254
+ peek(queue: string, count?: number): Promise<unknown[]>
255
+ len(queue: string): Promise<number>
256
+ purge(queue: string): Promise<QueryResult>
257
+ }
258
+
259
+ /**
260
+ * Caller-typed SELECT builder. RedDB does not infer `T`; provide it
261
+ * explicitly with `db.from<T>('collection')`.
262
+ */
263
+ export class TypedQueryBuilder<T extends Record<string, unknown> = Record<string, unknown>> {
264
+ select(): TypedQueryBuilder<T>
265
+ select(column: '*'): TypedQueryBuilder<T>
266
+ select<K extends keyof T & string>(...columns: K[]): TypedQueryBuilder<Pick<T, K>>
267
+ select<K extends keyof T & string>(columns: K[]): TypedQueryBuilder<Pick<T, K>>
268
+ where(condition: string, params: QueryParam[]): TypedQueryBuilder<T>
269
+ where(condition: string, ...params: QueryParam[]): TypedQueryBuilder<T>
270
+ run(): Promise<T[]>
271
+ }
272
+
196
273
  export class ConfigClient {
197
274
  put(
198
275
  key: string,
@@ -218,25 +295,42 @@ export class VaultClient {
218
295
  }
219
296
 
220
297
  export class RedDB {
298
+ /** Underlying transport label. connect() returns 'embedded'. */
299
+ readonly transport: string | null
221
300
  readonly cache: CacheClient
301
+ readonly queue: QueueClient
222
302
  readonly kv: KvClient & ((collection?: string) => KvClient)
223
303
  readonly config: (collection?: string) => ConfigClient
224
304
  readonly vault: (collection?: string) => VaultClient
225
305
 
306
+ query(sql: `ASK ${string}`): Promise<AskQueryResult>
226
307
  query(sql: string): Promise<QueryResult>
227
- query(sql: string, params: Array<number | string | null>): Promise<QueryResult>
308
+ query(sql: string, params: QueryParam[]): Promise<QueryResult>
309
+ query(sql: string, ...params: QueryParam[]): Promise<QueryResult>
310
+ execute(sql: string): Promise<QueryResult>
311
+ execute(sql: string, params: QueryParam[]): Promise<QueryResult>
312
+ execute(sql: string, ...params: QueryParam[]): Promise<QueryResult>
228
313
  insert(collection: string, payload: Record<string, unknown>): Promise<InsertResult>
229
314
  bulkInsert(
230
315
  collection: string,
231
316
  payloads: Array<Record<string, unknown>>,
232
317
  ): Promise<BulkInsertResult>
318
+ exists(collection: string): Promise<boolean>
319
+ list(): Promise<CollectionMeta[]>
320
+ /**
321
+ * Caller-typed collection handle. Supply `T`; the SDK does not
322
+ * generate or validate row types at runtime.
323
+ */
324
+ from<T extends Record<string, unknown> = Record<string, unknown>>(
325
+ collection: string,
326
+ ): TypedQueryBuilder<T>
233
327
  get(collection: string, id: string | number): Promise<GetResult>
234
328
  delete(collection: string, id: string | number): Promise<DeleteResult>
235
329
  health(): Promise<HealthResult>
236
330
  version(): Promise<VersionResult>
237
331
 
238
- // Auth surface only meaningful when connected via grpc://.
239
- // Embedded modes will receive 'unknown method' from the bridge.
332
+ // Auth surface is not available through embedded SDK connections.
333
+ // Use @reddb-io/client for remote authenticated servers.
240
334
  login(username: string, password: string): Promise<LoginResult>
241
335
  whoami(): Promise<WhoamiResult>
242
336
  changePassword(currentPassword: string, newPassword: string): Promise<ChangePasswordResult>
@@ -252,32 +346,16 @@ export class RedDB {
252
346
  * Accepted URI schemes:
253
347
  * - `memory://` — ephemeral in-memory database
254
348
  * - `file:///absolute/path` — embedded, persisted to disk
255
- * - `grpc://host:port` — remote server
349
+ *
350
+ * Remote URI schemes throw `EMBEDDED_ONLY`; use @reddb-io/client.
256
351
  */
257
352
  export function connect(uri: string, options?: ConnectOptions): Promise<RedDB>
258
353
 
259
- /**
260
- * Exchange username + password for a bearer token by hitting the
261
- * server's `POST /auth/login` HTTP endpoint. The returned `token`
262
- * can be passed to `connect(uri, { auth: { token } })`.
263
- *
264
- * @example
265
- * import { connect, login } from '@reddb-io/sdk'
266
- * const { token } = await login(
267
- * 'https://reddb.example.com/auth/login',
268
- * { username: 'admin', password: 'secret' },
269
- * )
270
- * const db = await connect('grpc://reddb.example.com:5051', { auth: { token } })
271
- */
272
- export function login(
273
- loginUrl: string,
274
- credentials: { username: string; password: string },
275
- ): Promise<LoginResult>
354
+ export const EMBEDDED_ONLY_MESSAGE: string
276
355
 
277
356
  /**
278
357
  * Translate a connection URI + (optional) auth into argv for
279
- * `red rpc --stdio`. Exported for tests / debug. New code should
280
- * use `parseUri` directly and let `connect` handle dispatch.
358
+ * `red rpc --stdio`. Remote URI schemes throw `EMBEDDED_ONLY`.
281
359
  */
282
360
  export function uriToArgs(
283
361
  uri: string,
@@ -288,7 +366,7 @@ export function uriToArgs(
288
366
  * Parsed `red://` (or legacy) URI. Returned by `parseUri`.
289
367
  */
290
368
  export interface ParsedUri {
291
- kind: 'embedded' | 'http' | 'https' | 'grpc' | 'grpcs' | 'pg'
369
+ kind: 'embedded' | 'http' | 'https' | 'red' | 'reds' | 'grpc' | 'grpcs' | 'pg'
292
370
  host?: string
293
371
  port?: number
294
372
  path?: string
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reddb-io/sdk",
3
- "version": "1.0.7",
4
- "description": "Official RedDB drivertalks the native RedWire TCP protocol (mTLS), HTTP, gRPC bridge, or embedded stdio JSON-RPC. Works in Node 18+, Bun and Deno.",
3
+ "version": "1.1.0",
4
+ "description": "Official embedded RedDB SDKlaunches a local red binary over stdio JSON-RPC. Use @reddb-io/client for remote HTTP, gRPC, and RedWire.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
@@ -45,6 +45,6 @@
45
45
  },
46
46
  "scripts": {
47
47
  "postinstall": "node postinstall.js",
48
- "test": "node --test test/cache.test.mjs && node test/smoke.test.mjs"
48
+ "test": "node --test test/ask.test.mjs test/cache.test.mjs test/db-helpers.test.mjs test/embedded-only.test.mjs test/insert-ids.test.mjs test/kv.test.mjs test/params.test.mjs test/postinstall.test.mjs test/queue.test.mjs test/redwire.params.test.mjs && node test/smoke.test.mjs"
49
49
  }
50
50
  }
package/src/cache.js CHANGED
@@ -6,13 +6,37 @@
6
6
  * flushNamespace routes to the existing POST /admin/blob_cache/flush_namespace.
7
7
  * All others target endpoints planned for a future server release.
8
8
  *
9
+ * Requires an HTTP or gRPC transport. On embedded (stdio JSON-RPC), every
10
+ * method throws `UNSUPPORTED_TRANSPORT` before issuing the RPC call —
11
+ * the engine's stdio handler doesn't implement `cache.*`.
12
+ *
9
13
  * Values are base64-encoded in transit so binary payloads survive JSON.
10
14
  */
11
15
 
16
+ import { RedDBError } from './protocol.js'
17
+
18
+ const UNSUPPORTED_TRANSPORTS = new Set(['embedded'])
19
+
12
20
  export class CacheClient {
13
- /** @param {{ call: Function }} client */
14
- constructor(client) {
21
+ /**
22
+ * @param {{ call: Function }} client
23
+ * @param {string} [transport] Underlying transport label (e.g. 'http',
24
+ * 'grpc', 'embedded'). When the transport doesn't serve `cache.*`,
25
+ * every method throws `UNSUPPORTED_TRANSPORT` before any RPC call.
26
+ */
27
+ constructor(client, transport) {
15
28
  this._client = client
29
+ this._transport = transport ?? null
30
+ }
31
+
32
+ _guard(method) {
33
+ if (this._transport && UNSUPPORTED_TRANSPORTS.has(this._transport)) {
34
+ throw new RedDBError(
35
+ 'UNSUPPORTED_TRANSPORT',
36
+ `cache.${method} is not available on '${this._transport}' transport; `
37
+ + 'use @reddb-io/client for remote cache endpoints.',
38
+ )
39
+ }
16
40
  }
17
41
 
18
42
  /**
@@ -22,6 +46,7 @@ export class CacheClient {
22
46
  * @returns {Promise<Uint8Array | null>}
23
47
  */
24
48
  async get(namespace, key) {
49
+ this._guard('get')
25
50
  const result = await this._client.call('cache.get', { namespace, key })
26
51
  if (result == null || result.value == null) return null
27
52
  return base64ToBytes(result.value)
@@ -39,6 +64,7 @@ export class CacheClient {
39
64
  * @returns {Promise<void>}
40
65
  */
41
66
  async put(namespace, key, value, opts = {}) {
67
+ this._guard('put')
42
68
  const encoded = bytesToBase64(value)
43
69
  await this._client.call('cache.put', {
44
70
  namespace,
@@ -55,6 +81,7 @@ export class CacheClient {
55
81
  * @returns {Promise<'present' | 'absent' | 'maybe'>}
56
82
  */
57
83
  async exists(namespace, key) {
84
+ this._guard('exists')
58
85
  const result = await this._client.call('cache.exists', { namespace, key })
59
86
  return result?.status ?? 'maybe'
60
87
  }
@@ -66,6 +93,7 @@ export class CacheClient {
66
93
  * @returns {Promise<void>}
67
94
  */
68
95
  async invalidate(namespace, key) {
96
+ this._guard('invalidate')
69
97
  await this._client.call('cache.invalidate', { namespace, key })
70
98
  }
71
99
 
@@ -76,6 +104,7 @@ export class CacheClient {
76
104
  * @returns {Promise<number>} Number of entries removed.
77
105
  */
78
106
  async invalidatePrefix(namespace, prefix) {
107
+ this._guard('invalidatePrefix')
79
108
  const result = await this._client.call('cache.invalidate_prefix', { namespace, prefix })
80
109
  return result?.removed ?? 0
81
110
  }
@@ -87,6 +116,7 @@ export class CacheClient {
87
116
  * @returns {Promise<number>} Number of entries removed.
88
117
  */
89
118
  async invalidateTags(namespace, tags) {
119
+ this._guard('invalidateTags')
90
120
  const result = await this._client.call('cache.invalidate_tags', { namespace, tags })
91
121
  return result?.removed ?? 0
92
122
  }
@@ -98,6 +128,7 @@ export class CacheClient {
98
128
  * @returns {Promise<void>}
99
129
  */
100
130
  async flushNamespace(namespace) {
131
+ this._guard('flushNamespace')
101
132
  await this._client.call('cache.flush_namespace', { namespace })
102
133
  }
103
134
  }
@@ -0,0 +1,95 @@
1
+ import { RedDBError } from './protocol.js'
2
+
3
+ export async function listCollections(db) {
4
+ const result = await db.query('SHOW COLLECTIONS')
5
+ return (result.rows ?? []).map(collectionMeta)
6
+ }
7
+
8
+ export async function collectionExists(db, collection) {
9
+ const result = await db.query(`SHOW COLLECTIONS WHERE name = ${sqlString(collection)}`)
10
+ return (result.rows ?? []).some((row) => row.name === String(collection))
11
+ }
12
+
13
+ export class TypedQueryBuilder {
14
+ constructor(db, collection, columns = null, whereClauses = [], params = []) {
15
+ this.db = db
16
+ this.collection = collection
17
+ this.columns = columns
18
+ this.whereClauses = whereClauses
19
+ this.params = params
20
+ }
21
+
22
+ select(...columns) {
23
+ const selected = columns.length === 1 && Array.isArray(columns[0]) ? columns[0] : columns
24
+ const projection = selected.length === 1 && selected[0] === '*' ? null : selected
25
+ return new TypedQueryBuilder(
26
+ this.db,
27
+ this.collection,
28
+ projection != null && projection.length > 0 ? projection : null,
29
+ this.whereClauses,
30
+ this.params,
31
+ )
32
+ }
33
+
34
+ where(condition, ...params) {
35
+ if (typeof condition !== 'string' || condition.trim().length === 0) {
36
+ throw new RedDBError('INVALID_QUERY_BUILDER', 'where() requires a non-empty SQL condition')
37
+ }
38
+ const nextParams = params.length === 1 && Array.isArray(params[0]) ? params[0] : params
39
+ return new TypedQueryBuilder(
40
+ this.db,
41
+ this.collection,
42
+ this.columns,
43
+ [...this.whereClauses, condition.trim()],
44
+ [...this.params, ...nextParams],
45
+ )
46
+ }
47
+
48
+ async run() {
49
+ const projection = this.columns == null
50
+ ? '*'
51
+ : this.columns.map(sqlIdentifierPath).join(', ')
52
+ const where = this.whereClauses.length > 0
53
+ ? ` WHERE ${this.whereClauses.map((clause) => `(${clause})`).join(' AND ')}`
54
+ : ''
55
+ const sql = `SELECT ${projection} FROM ${sqlIdentifierPath(this.collection)}${where}`
56
+ const result = this.params.length > 0
57
+ ? await this.db.query(sql, this.params)
58
+ : await this.db.query(sql)
59
+ const rows = result.rows ?? []
60
+ if (this.columns == null) return rows
61
+ return rows.map((row) => {
62
+ const selected = {}
63
+ for (const column of this.columns) selected[column] = row[column]
64
+ return selected
65
+ })
66
+ }
67
+ }
68
+
69
+ function collectionMeta(row) {
70
+ return {
71
+ ...row,
72
+ name: String(row.name),
73
+ model: String(row.model),
74
+ capabilities: Array.isArray(row.capabilities) ? row.capabilities : [],
75
+ }
76
+ }
77
+
78
+ function sqlIdentifierPath(value) {
79
+ return String(value).split('.').map(sqlIdentifier).join('.')
80
+ }
81
+
82
+ function sqlIdentifier(value) {
83
+ const ident = String(value)
84
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(ident)) {
85
+ throw new RedDBError(
86
+ 'INVALID_IDENTIFIER',
87
+ `invalid SQL identifier "${ident}"`,
88
+ )
89
+ }
90
+ return ident
91
+ }
92
+
93
+ function sqlString(value) {
94
+ return `'${String(value).replace(/'/g, "''")}'`
95
+ }
package/src/http.js CHANGED
@@ -123,9 +123,14 @@ async function parseResponse(response) {
123
123
  const ROUTES = {
124
124
  health: (base) => ({ url: `${base}/health`, init: { method: 'GET' } }),
125
125
  version: (base) => ({ url: `${base}/admin/version`, init: { method: 'GET' } }),
126
- query: (base, { sql }) => ({
126
+ query: (base, { sql, params }) => ({
127
127
  url: `${base}/query`,
128
- init: { method: 'POST', body: JSON.stringify({ query: sql }) },
128
+ init: {
129
+ method: 'POST',
130
+ body: JSON.stringify(
131
+ Array.isArray(params) ? { query: sql, params } : { query: sql },
132
+ ),
133
+ },
129
134
  }),
130
135
  insert: (base, { collection, payload }) => ({
131
136
  url: `${base}/collections/${encodeURIComponent(collection)}/rows`,