@reddb-io/sdk 1.2.5 → 1.4.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 +147 -19
- package/index.d.ts +32 -0
- package/package.json +2 -2
- package/src/db-helpers.js +1 -1
- package/src/documents.js +6 -2
- package/src/index.js +117 -0
- package/src/kv.js +8 -1
- package/src/queue.js +8 -0
package/README.md
CHANGED
|
@@ -87,9 +87,11 @@ For the SQL/RQL grammar that `db.query()` accepts, see
|
|
|
87
87
|
|
|
88
88
|
## Transactions
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
This driver supports **both** transaction forms from the SDK Helper Spec
|
|
91
|
+
(§7): the imperative `begin` / `commit` / `rollback` trio and the callback
|
|
92
|
+
form.
|
|
93
|
+
|
|
94
|
+
**Callback form** — `db.transaction(callback)` (or `db.tx().run(callback)`):
|
|
93
95
|
|
|
94
96
|
```js
|
|
95
97
|
const userId = await db.transaction(async (tx) => {
|
|
@@ -100,9 +102,28 @@ const userId = await db.transaction(async (tx) => {
|
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
The wrapper sends `BEGIN`, commits when the callback resolves, and rolls back
|
|
103
|
-
when the callback or a `tx.query()` / `tx.insert()` call throws.
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
when the callback or a `tx.query()` / `tx.insert()` call throws.
|
|
106
|
+
|
|
107
|
+
**Imperative form** — `db.tx()` returns a transaction handle:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const tx = db.tx()
|
|
111
|
+
await tx.begin()
|
|
112
|
+
try {
|
|
113
|
+
await db.query("INSERT INTO audit (action) VALUES ('created user')")
|
|
114
|
+
await tx.commit()
|
|
115
|
+
} catch (err) {
|
|
116
|
+
await tx.rollback()
|
|
117
|
+
throw err
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`begin` / `commit` / `rollback` each resolve to a `QueryResult`. A nested
|
|
122
|
+
`tx.run()` (or `db.transaction()`) on the same connection is rejected with
|
|
123
|
+
`INVALID_ARGUMENT` (`NESTED_TX_NOT_SUPPORTED` for the legacy
|
|
124
|
+
`db.transaction()` shortcut) — callers wanting savepoints issue them
|
|
125
|
+
directly via `tx.query()`. Open another `connect()` handle for independent
|
|
126
|
+
concurrent transactions.
|
|
106
127
|
|
|
107
128
|
## Connection URIs
|
|
108
129
|
|
|
@@ -201,8 +222,10 @@ const updated = await db.documents.patch('events', inserted.rid, {
|
|
|
201
222
|
await db.documents.delete('events', inserted.rid)
|
|
202
223
|
```
|
|
203
224
|
|
|
204
|
-
`documents.insert()` creates the document collection when needed. Patch
|
|
205
|
-
|
|
225
|
+
`documents.insert()` creates the document collection when needed. Patch is a
|
|
226
|
+
top-level merge: unrelated fields survive. An empty patch raises
|
|
227
|
+
`INVALID_ARGUMENT`. `documents.delete` returns `{ affected, deleted }` and
|
|
228
|
+
NEVER raises on a missing rid (returns `{ affected: 0, deleted: false }`).
|
|
206
229
|
|
|
207
230
|
### `db.kv(collection?)`
|
|
208
231
|
|
|
@@ -212,26 +235,104 @@ KV helpers preserve exact keys, including namespaced keys with `:`.
|
|
|
212
235
|
await db.query('CREATE KV settings')
|
|
213
236
|
const kv = db.kv('settings')
|
|
214
237
|
|
|
215
|
-
await kv.
|
|
216
|
-
await kv.get('characters:hansel') // 'crumbs'
|
|
238
|
+
await kv.set('characters:hansel', 'crumbs') // `put` is a back-compat alias
|
|
239
|
+
await kv.get('characters:hansel') // 'crumbs' (null when missing)
|
|
217
240
|
await kv.exists('characters:hansel') // { exists: true }
|
|
218
241
|
await kv.list({ prefix: 'characters:' }) // { items: [{ key, value }] }
|
|
219
|
-
await kv.delete('characters:hansel') // { affected }
|
|
242
|
+
await kv.delete('characters:hansel') // { affected, deleted }
|
|
220
243
|
```
|
|
221
244
|
|
|
222
|
-
|
|
245
|
+
A `kv.get` of a missing key returns `null` (never `NOT_FOUND`). `kv.delete`
|
|
246
|
+
returns the `{ affected, deleted }` envelope; deleting a missing key is not an
|
|
247
|
+
error and returns `{ affected: 0, deleted: false }`.
|
|
223
248
|
|
|
224
|
-
|
|
249
|
+
### `db.queues` (alias `db.queue`)
|
|
250
|
+
|
|
251
|
+
Queue helpers cover the embedded FIFO workflow. The spec-canonical namespace
|
|
252
|
+
is the plural `db.queues`; `db.queue` remains as an alias.
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
await db.queues.create('jobs') // CREATE QUEUE IF NOT EXISTS (idempotent)
|
|
256
|
+
await db.queues.push('jobs', { task: 'ship' })
|
|
257
|
+
await db.queues.peek('jobs') // does NOT decrement length
|
|
258
|
+
await db.queues.pop('jobs') // empty queue → [] (never NOT_FOUND)
|
|
259
|
+
await db.queues.len('jobs')
|
|
260
|
+
await db.queues.purge('jobs')
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## SDK Helper Spec conformance
|
|
264
|
+
|
|
265
|
+
This driver implements **SDK Helper Spec v1.0**
|
|
266
|
+
([`docs/spec/sdk-helpers.md`](../../docs/spec/sdk-helpers.md)). The version is
|
|
267
|
+
exposed for cross-driver CI dashboards:
|
|
225
268
|
|
|
226
269
|
```js
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
270
|
+
import { HELPER_SPEC_VERSION } from '@reddb-io/sdk'
|
|
271
|
+
HELPER_SPEC_VERSION // '1.0'
|
|
272
|
+
db.helperSpecVersion // '1.0'
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Return envelopes** (wire field names preserved when serialised to JSON):
|
|
276
|
+
|
|
277
|
+
| Envelope | Fields |
|
|
278
|
+
|---------------------|---------------------------------------------------|
|
|
279
|
+
| `QueryResult` | `statement`, `affected`, `columns`, `rows` |
|
|
280
|
+
| `InsertResult` | `affected` (1), `rid` (`id` legacy alias) |
|
|
281
|
+
| `BulkInsertResult` | `affected`, `rids` in input order (`ids` alias) |
|
|
282
|
+
| `DeleteResult` | `affected`, `deleted` (= `affected > 0`) |
|
|
283
|
+
| `ExistsResult` | `exists` |
|
|
284
|
+
|
|
285
|
+
**Transaction support:** imperative (`db.tx().begin/commit/rollback`) **and**
|
|
286
|
+
callback (`db.transaction(cb)` / `db.tx().run(cb)`). Nested callbacks reject
|
|
287
|
+
with `INVALID_ARGUMENT`.
|
|
288
|
+
|
|
289
|
+
**Case matrix** (spec §12 — ported verbatim in
|
|
290
|
+
`test/conformance.test.mjs`):
|
|
291
|
+
|
|
292
|
+
| Case ID | Status |
|
|
293
|
+
|--------------------------------------|-------------|
|
|
294
|
+
| `meta.spec_version` | supported |
|
|
295
|
+
| `generic.query.no_params` | supported |
|
|
296
|
+
| `generic.query_with.params` | supported |
|
|
297
|
+
| `generic.insert.rid` | supported |
|
|
298
|
+
| `generic.bulk_insert.rids` | supported |
|
|
299
|
+
| `generic.delete` | supported |
|
|
300
|
+
| `documents.crud_nested_patch` | supported |
|
|
301
|
+
| `documents.delete_missing_no_error` | supported |
|
|
302
|
+
| `documents.patch_empty_rejects` | supported |
|
|
303
|
+
| `kv.exact_key_round_trip` | supported |
|
|
304
|
+
| `kv.missing_get_returns_none` | supported |
|
|
305
|
+
| `kv.delete_returns_envelope` | supported |
|
|
306
|
+
| `queues.fifo_peek_pop_len` | supported |
|
|
307
|
+
| `queues.empty_pop_returns_empty` | supported |
|
|
308
|
+
| `queues.purge_resets_len` | supported |
|
|
309
|
+
| `tx.commit_persists` | supported |
|
|
310
|
+
| `tx.rollback_discards` | supported |
|
|
311
|
+
| `errors.invalid_argument.empty_sql` | supported |
|
|
312
|
+
| `errors.not_found.document_get` | supported |
|
|
313
|
+
| `wire.probabilistic.hll_round_trip` | provisional (SQL via `db.query`) |
|
|
314
|
+
| `wire.vectors.sql_round_trip` | reachable via `db.query` (no v1.0 case) |
|
|
315
|
+
| `wire.graph.sql_round_trip` | reachable via `db.query` (no v1.0 case) |
|
|
316
|
+
| `wire.timeseries.sql_round_trip` | reachable via `db.query` (no v1.0 case) |
|
|
317
|
+
|
|
318
|
+
**Out-of-scope in v1.0** (reach via raw `db.query` until v1.1, per spec):
|
|
319
|
+
first-class `vectors.*`, `graph.*`, `timeseries.*`, and `probabilistic.*`
|
|
320
|
+
helpers; KV TTL (`kv.expire`) and gRPC watch; priority queues, consumer
|
|
321
|
+
groups, dead-letter routing; transaction isolation-level arguments and
|
|
322
|
+
cross-shard transactions; JSON Patch / nested / array-positional document
|
|
323
|
+
patches (top-level merge only).
|
|
324
|
+
|
|
325
|
+
Run the conformance harness against a locally built binary:
|
|
326
|
+
|
|
327
|
+
```sh
|
|
328
|
+
cargo build # produces target/debug/red
|
|
329
|
+
node drivers/js/test/conformance.test.mjs
|
|
330
|
+
# or: REDDB_BINARY_PATH=/path/to/red node drivers/js/test/conformance.test.mjs
|
|
233
331
|
```
|
|
234
332
|
|
|
333
|
+
The harness (and the README-examples test) self-skip with exit 0 when no
|
|
334
|
+
binary is present, so `pnpm test` stays green on machines without a build.
|
|
335
|
+
|
|
235
336
|
### `db.health() → Promise<{ ok, version }>`
|
|
236
337
|
|
|
237
338
|
### `db.version() → Promise<{ version, protocol }>`
|
|
@@ -318,3 +419,30 @@ remote URIs.
|
|
|
318
419
|
- **TLS posture, mTLS, OAuth/JWT and reverse-proxy patterns** are
|
|
319
420
|
covered in [`docs/security/transport-tls.md`](../../docs/security/transport-tls.md).
|
|
320
421
|
- See [Policies](../../docs/security/policies.md) for IAM-style authorization.
|
|
422
|
+
|
|
423
|
+
<!-- contract-matrix:begin -->
|
|
424
|
+
## Public-surface support
|
|
425
|
+
|
|
426
|
+
> Generated from [`docs/conformance/public-surface-contract-matrix.json`](/docs/conformance/public-surface-contract-matrix.json) by `scripts/gen-docs-from-matrix.mjs`. Do not edit between the markers by hand — run `node scripts/gen-docs-from-matrix.mjs --write`. The matrix is the source of truth; this block can never claim more than it, and CI (`docs-matrix`) fails on drift.
|
|
427
|
+
>
|
|
428
|
+
> Driver-helper (SDK Helper Spec v1.0) support for every public promise. A helper not marked supported here is not promised by this driver.
|
|
429
|
+
|
|
430
|
+
| Promise | driver_helpers |
|
|
431
|
+
| --- | --- |
|
|
432
|
+
| **PSC-001** — RedDB is one multi-model database (tables, graph, KV, timeseries, probabilistic, vector, queue, documents) backed by a single file. | ✅ supported |
|
|
433
|
+
| **PSC-002** — MATCH supports node, edge, label, property, and LIMIT projections. | ✅ supported |
|
|
434
|
+
| **PSC-003** — GRAPH algorithms accept semantic identifiers, limits, ordering, and return stable rich rows. | ❌ unsupported |
|
|
435
|
+
| **PSC-004** — INSERT creates rows, documents, and native timeseries points. | ✅ supported |
|
|
436
|
+
| **PSC-005** — HLL/SKETCH/FILTER expose write and read commands for cardinality, frequency, and membership. | ⚠️ partial |
|
|
437
|
+
| **PSC-006** — Timeseries stores timestamped metrics with tags and supports query/readback. | ⚠️ partial |
|
|
438
|
+
| **PSC-007** — Documents are first-class: create, read, update, delete, and SQL analytics over JSON. | ✅ supported |
|
|
439
|
+
| **PSC-008** — KV helpers expose get/put/delete; get of a missing key returns null, delete reports affected. | ✅ supported |
|
|
440
|
+
| **PSC-009** — Queue helpers expose create/push/peek/pop/len/purge with FIFO semantics; empty pop is not an error. | ✅ supported |
|
|
441
|
+
| **PSC-010** — Transactions are imperative (begin/commit/rollback) plus a run(callback) form; empty SQL rejects with INVALID_ARGUMENT. | ✅ supported |
|
|
442
|
+
| **PSC-011** — SQL aggregate, projection, expression, and mutation behaviour matches ordinary SQL expectations where advertised. | ✅ supported |
|
|
443
|
+
| **PSC-012** — Server transports expose the same query contract as embedded (HTTP, RedWire, gRPC parity). | ✅ supported |
|
|
444
|
+
| **PSC-013** — Official drivers implement the SDK Helper Spec v1.0 conformance suite (all 22 §12 case IDs). | ✅ supported |
|
|
445
|
+
| **PSC-014** — ASK / SEARCH semantic surfaces return ranked results with stable shape. | ⚠️ partial |
|
|
446
|
+
|
|
447
|
+
_Status legend: ✅ supported · ⚠️ partial (known gaps) · ❌ unsupported._
|
|
448
|
+
<!-- contract-matrix:end -->
|
package/index.d.ts
CHANGED
|
@@ -105,6 +105,8 @@ export interface GetResult {
|
|
|
105
105
|
|
|
106
106
|
export interface DeleteResult {
|
|
107
107
|
affected: number
|
|
108
|
+
/** `affected > 0`. Present on `documents.delete` / `kv.delete` (spec §2.4). */
|
|
109
|
+
deleted?: boolean
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
export interface CollectionMeta {
|
|
@@ -233,6 +235,12 @@ export class KvClient {
|
|
|
233
235
|
value: unknown,
|
|
234
236
|
options?: { collection?: string; expireMs?: number; tags?: string[] },
|
|
235
237
|
): Promise<QueryResult>
|
|
238
|
+
/** Spec-canonical alias for `put` (spec §5.1 `kv.set`). */
|
|
239
|
+
set(
|
|
240
|
+
key: string,
|
|
241
|
+
value: unknown,
|
|
242
|
+
options?: { collection?: string; expireMs?: number; tags?: string[] },
|
|
243
|
+
): Promise<QueryResult>
|
|
236
244
|
get(key: string, options?: { collection?: string }): Promise<unknown | null>
|
|
237
245
|
getMany(keys: string[], options?: { collection?: string }): Promise<Array<unknown | null>>
|
|
238
246
|
exists(key: string, options?: { collection?: string }): Promise<{ exists: boolean }>
|
|
@@ -279,6 +287,8 @@ export class DocumentClient {
|
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
export class QueueClient {
|
|
290
|
+
/** Idempotent CREATE QUEUE IF NOT EXISTS (spec §6.1). */
|
|
291
|
+
create(queue: string): Promise<QueryResult>
|
|
282
292
|
push(
|
|
283
293
|
queue: string,
|
|
284
294
|
value: unknown,
|
|
@@ -290,6 +300,19 @@ export class QueueClient {
|
|
|
290
300
|
purge(queue: string): Promise<QueryResult>
|
|
291
301
|
}
|
|
292
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Spec §7 transaction client returned by `db.tx()`. Imperative
|
|
305
|
+
* `begin`/`commit`/`rollback` each resolve to a `QueryResult`; `run` is the
|
|
306
|
+
* callback form (commit on success, rollback + re-throw on failure). Nested
|
|
307
|
+
* `run` rejects with `INVALID_ARGUMENT`.
|
|
308
|
+
*/
|
|
309
|
+
export class TxClient {
|
|
310
|
+
begin(): Promise<QueryResult>
|
|
311
|
+
commit(): Promise<QueryResult>
|
|
312
|
+
rollback(): Promise<QueryResult>
|
|
313
|
+
run<T>(callback: (tx: RedDBTransaction) => T | Promise<T>): Promise<T>
|
|
314
|
+
}
|
|
315
|
+
|
|
293
316
|
/**
|
|
294
317
|
* Caller-typed SELECT builder. RedDB does not infer `T`; provide it
|
|
295
318
|
* explicitly with `db.from<T>('collection')`.
|
|
@@ -349,8 +372,12 @@ export interface RedDBTransaction {
|
|
|
349
372
|
export class RedDB {
|
|
350
373
|
/** Underlying transport label. connect() returns 'embedded'. */
|
|
351
374
|
readonly transport: string | null
|
|
375
|
+
/** SDK Helper Spec version this driver implements (spec §14). */
|
|
376
|
+
readonly helperSpecVersion: string
|
|
352
377
|
readonly cache: CacheClient
|
|
353
378
|
readonly queue: QueueClient
|
|
379
|
+
/** Spec-canonical plural alias for `queue` (spec §6). */
|
|
380
|
+
readonly queues: QueueClient
|
|
354
381
|
readonly documents: DocumentClient
|
|
355
382
|
readonly kv: KvClient & ((collection?: string) => KvClient)
|
|
356
383
|
readonly config: (collection?: string) => ConfigClient
|
|
@@ -371,6 +398,8 @@ export class RedDB {
|
|
|
371
398
|
transaction<T>(
|
|
372
399
|
callback: (tx: RedDBTransaction) => T | Promise<T>,
|
|
373
400
|
): Promise<T>
|
|
401
|
+
/** Spec §7 transaction handle: imperative begin/commit/rollback + run(). */
|
|
402
|
+
tx(): TxClient
|
|
374
403
|
exists(collection: string): Promise<boolean>
|
|
375
404
|
list(): Promise<CollectionMeta[]>
|
|
376
405
|
/**
|
|
@@ -409,6 +438,9 @@ export function connect(uri: string, options?: ConnectOptions): Promise<RedDB>
|
|
|
409
438
|
|
|
410
439
|
export const EMBEDDED_ONLY_MESSAGE: string
|
|
411
440
|
|
|
441
|
+
/** SDK Helper Spec version implemented by this driver (spec §14). */
|
|
442
|
+
export const HELPER_SPEC_VERSION: string
|
|
443
|
+
|
|
412
444
|
/**
|
|
413
445
|
* Translate a connection URI + (optional) auth into argv for
|
|
414
446
|
* `red rpc --stdio`. Remote URI schemes throw `EMBEDDED_ONLY`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reddb-io/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Official embedded RedDB SDK — launches 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",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"postinstall": "node postinstall.js",
|
|
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 test/transaction.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/helpers.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 test/transaction.test.mjs && node test/smoke.test.mjs && node test/conformance.test.mjs && node test/readme-examples.test.mjs"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/db-helpers.js
CHANGED
|
@@ -50,7 +50,7 @@ export class TypedQueryBuilder {
|
|
|
50
50
|
? '*'
|
|
51
51
|
: this.columns.map(sqlIdentifierPath).join(', ')
|
|
52
52
|
const where = this.whereClauses.length > 0
|
|
53
|
-
? ` WHERE ${this.whereClauses.join(' AND ')}`
|
|
53
|
+
? ` WHERE ${this.whereClauses.map((clause) => `(${clause})`).join(' AND ')}`
|
|
54
54
|
: ''
|
|
55
55
|
const sql = `SELECT ${projection} FROM ${sqlIdentifierPath(this.collection)}${where}`
|
|
56
56
|
const result = this.params.length > 0
|
package/src/documents.js
CHANGED
|
@@ -40,7 +40,10 @@ export class DocumentClient {
|
|
|
40
40
|
validateObject(patch, 'documents.patch patch')
|
|
41
41
|
const entries = Object.entries(patch)
|
|
42
42
|
if (entries.length === 0) {
|
|
43
|
-
|
|
43
|
+
throw new RedDBError(
|
|
44
|
+
'INVALID_ARGUMENT',
|
|
45
|
+
'documents.patch patch must be a non-empty object',
|
|
46
|
+
)
|
|
44
47
|
}
|
|
45
48
|
for (const [field] of entries) {
|
|
46
49
|
if (field.includes('/')) {
|
|
@@ -66,7 +69,8 @@ export class DocumentClient {
|
|
|
66
69
|
|
|
67
70
|
async delete(collection, rid) {
|
|
68
71
|
const result = await this.db.delete(collection, rid)
|
|
69
|
-
|
|
72
|
+
const affected = result.affected ?? 0
|
|
73
|
+
return { affected, deleted: affected > 0 }
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
async ensureCollection(collection) {
|
package/src/index.js
CHANGED
|
@@ -43,6 +43,13 @@ export { parseUri, deriveLoginUrl } from './url.js'
|
|
|
43
43
|
export const EMBEDDED_ONLY_MESSAGE =
|
|
44
44
|
'remote URIs are not supported in @reddb-io/sdk; install @reddb-io/client for grpc/http/red transports'
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* SDK Helper Spec version this driver implements. See
|
|
48
|
+
* `docs/spec/sdk-helpers.md` §14 — every official driver exposes this so
|
|
49
|
+
* cross-driver CI dashboards can assert against it.
|
|
50
|
+
*/
|
|
51
|
+
export const HELPER_SPEC_VERSION = '1.0'
|
|
52
|
+
|
|
46
53
|
const MIN_INSERT_ID_ENGINE_VERSION = '1.0.9'
|
|
47
54
|
const NESTED_TX_NOT_SUPPORTED = 'NESTED_TX_NOT_SUPPORTED'
|
|
48
55
|
|
|
@@ -304,6 +311,92 @@ class TransactionHandle {
|
|
|
304
311
|
}
|
|
305
312
|
}
|
|
306
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Spec §7 transaction client. Returned by `db.tx()`. Exposes the imperative
|
|
316
|
+
* `begin` / `commit` / `rollback` trio (each resolves to a `QueryResult`) plus
|
|
317
|
+
* the optional `run(callback)` form. Transaction state is tracked on the parent
|
|
318
|
+
* `RedDB` so it serialises with `db.transaction()` and nested opens are
|
|
319
|
+
* rejected rather than silently interleaved.
|
|
320
|
+
*/
|
|
321
|
+
export class TxClient {
|
|
322
|
+
constructor(db) {
|
|
323
|
+
this.db = db
|
|
324
|
+
this.active = false
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async begin() {
|
|
328
|
+
if (this.db.inTransaction) {
|
|
329
|
+
throw nestedTransactionError()
|
|
330
|
+
}
|
|
331
|
+
this.db.inTransaction = true
|
|
332
|
+
this.active = true
|
|
333
|
+
try {
|
|
334
|
+
return await this.db.query('BEGIN')
|
|
335
|
+
} catch (err) {
|
|
336
|
+
this.db.inTransaction = false
|
|
337
|
+
this.active = false
|
|
338
|
+
throw err
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async commit() {
|
|
343
|
+
if (!this.active) {
|
|
344
|
+
throw new RedDBError('INVALID_ARGUMENT', 'tx.commit() called without an open transaction')
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
return await this.db.query('COMMIT')
|
|
348
|
+
} finally {
|
|
349
|
+
this.active = false
|
|
350
|
+
this.db.inTransaction = false
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async rollback() {
|
|
355
|
+
if (!this.active) {
|
|
356
|
+
throw new RedDBError('INVALID_ARGUMENT', 'tx.rollback() called without an open transaction')
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
return await this.db.query('ROLLBACK')
|
|
360
|
+
} finally {
|
|
361
|
+
this.active = false
|
|
362
|
+
this.db.inTransaction = false
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Callback form: commit on success, roll back and re-throw on failure.
|
|
368
|
+
* Nested `tx.run` rejects with `INVALID_ARGUMENT` — callers wanting
|
|
369
|
+
* savepoints issue them directly via `tx.query()` (spec §7.2; the README
|
|
370
|
+
* records this choice).
|
|
371
|
+
*/
|
|
372
|
+
async run(callback) {
|
|
373
|
+
if (typeof callback !== 'function') {
|
|
374
|
+
throw new TypeError('tx.run(callback) requires a function')
|
|
375
|
+
}
|
|
376
|
+
if (this.db.inTransaction) {
|
|
377
|
+
throw new RedDBError(
|
|
378
|
+
'INVALID_ARGUMENT',
|
|
379
|
+
'nested tx.run() is not supported; issue savepoints via tx.query() instead',
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
await this.begin()
|
|
383
|
+
try {
|
|
384
|
+
const result = await callback(new TransactionHandle(this.db))
|
|
385
|
+
await this.commit()
|
|
386
|
+
return result
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (this.active) {
|
|
389
|
+
try {
|
|
390
|
+
await this.rollback()
|
|
391
|
+
} catch (rollbackErr) {
|
|
392
|
+
attachRollbackError(err, rollbackErr)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
throw err
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
307
400
|
export class RedDB {
|
|
308
401
|
/**
|
|
309
402
|
* @param {RpcClient} client
|
|
@@ -315,8 +408,12 @@ export class RedDB {
|
|
|
315
408
|
constructor(client, opts = {}) {
|
|
316
409
|
this.client = client
|
|
317
410
|
this.transport = opts.transport ?? null
|
|
411
|
+
this.helperSpecVersion = HELPER_SPEC_VERSION
|
|
318
412
|
this.cache = new CacheClient(client, this.transport)
|
|
319
413
|
this.queue = new QueueClient(client)
|
|
414
|
+
// Spec §6: the canonical namespace is the plural `queues`. `queue` is kept
|
|
415
|
+
// as a back-compat alias to the same handle.
|
|
416
|
+
this.queues = this.queue
|
|
320
417
|
this.documents = new DocumentClient(this)
|
|
321
418
|
const defaultKv = new KvClient(client)
|
|
322
419
|
this.kv = Object.assign((collection = 'kv_default') => new KvClient(client, collection), {
|
|
@@ -341,6 +438,13 @@ export class RedDB {
|
|
|
341
438
|
* Returns `{ statement, affected, columns, rows }`.
|
|
342
439
|
*/
|
|
343
440
|
query(sql, ...params) {
|
|
441
|
+
// Spec §3.1 / §2.5: empty SQL is a caller bug; reject locally before
|
|
442
|
+
// touching the wire.
|
|
443
|
+
if (typeof sql !== 'string' || sql.trim().length === 0) {
|
|
444
|
+
return Promise.reject(
|
|
445
|
+
new RedDBError('INVALID_ARGUMENT', 'query() requires a non-empty SQL string'),
|
|
446
|
+
)
|
|
447
|
+
}
|
|
344
448
|
const wireParams = normalizeQueryParams(params)
|
|
345
449
|
if (wireParams == null) {
|
|
346
450
|
return this.client.call('query', { sql }).then(normalizeResult)
|
|
@@ -361,10 +465,23 @@ export class RedDB {
|
|
|
361
465
|
|
|
362
466
|
/** Insert many rows in one call. Returns `{ affected, rids, ids }`; `ids` is a legacy alias. */
|
|
363
467
|
async bulkInsert(collection, payloads) {
|
|
468
|
+
// Spec §3.4: empty payloads is a no-op returning `{ affected: 0, rids: [] }`.
|
|
469
|
+
if (Array.isArray(payloads) && payloads.length === 0) {
|
|
470
|
+
return { affected: 0, rids: [], ids: [] }
|
|
471
|
+
}
|
|
364
472
|
const result = await this.client.call('bulk_insert', { collection, payloads })
|
|
365
473
|
return requireInsertIds(result, payloads.length)
|
|
366
474
|
}
|
|
367
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Spec §7 transaction handle. `db.tx()` returns a {@link TxClient} exposing
|
|
478
|
+
* imperative `begin` / `commit` / `rollback` plus a `run(callback)` form.
|
|
479
|
+
* `db.transaction(callback)` remains as the original callback-only shortcut.
|
|
480
|
+
*/
|
|
481
|
+
tx() {
|
|
482
|
+
return new TxClient(this)
|
|
483
|
+
}
|
|
484
|
+
|
|
368
485
|
async transaction(callback) {
|
|
369
486
|
if (this.inTransaction) {
|
|
370
487
|
throw nestedTransactionError()
|
package/src/kv.js
CHANGED
|
@@ -17,6 +17,11 @@ export class KvClient {
|
|
|
17
17
|
})
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// Spec-canonical alias for `put` (SDK Helper Spec §5.1 `kv.set`).
|
|
21
|
+
set(key, value, options = {}) {
|
|
22
|
+
return this.put(key, value, options)
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
async get(key, options = {}) {
|
|
21
26
|
const collection = options.collection ?? this.collection
|
|
22
27
|
const result = await this.client.call('query', {
|
|
@@ -40,7 +45,9 @@ export class KvClient {
|
|
|
40
45
|
const result = await this.client.call('query', {
|
|
41
46
|
sql: `KV DELETE ${kvPath(collection, key)}`,
|
|
42
47
|
})
|
|
43
|
-
|
|
48
|
+
const affected = result.affected ?? result.affected_rows ?? 0
|
|
49
|
+
// Spec §5.4 / §2.4 DeleteResult: `deleted` is `affected > 0`.
|
|
50
|
+
return { affected, deleted: affected > 0 }
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
async list(options = {}) {
|
package/src/queue.js
CHANGED
|
@@ -5,6 +5,14 @@ export class QueueClient {
|
|
|
5
5
|
this.client = client
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// Spec §6.1 `queues.create`: idempotent (CREATE QUEUE IF NOT EXISTS) so
|
|
9
|
+
// conformance fixtures can prime a queue the same way the Rust/Go harnesses do.
|
|
10
|
+
create(queue) {
|
|
11
|
+
return this.client.call('query', {
|
|
12
|
+
sql: `CREATE QUEUE IF NOT EXISTS ${queueIdentifier(queue)}`,
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
push(queue, value, options = {}) {
|
|
9
17
|
const priority = options.priority != null ? ` PRIORITY ${queuePriority(options.priority)}` : ''
|
|
10
18
|
return this.client.call('query', {
|