@reddb-io/client 1.7.0 → 1.8.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 +34 -0
- package/index.browser.d.ts +453 -0
- package/index.d.ts +118 -0
- package/package.json +41 -4
- package/src/core/auth.js +102 -0
- package/src/core/embedded-rejection.js +90 -0
- package/src/core/errors.js +20 -0
- package/src/core/index.js +43 -0
- package/src/core/insert-ids.js +45 -0
- package/src/core/ndjson.js +59 -0
- package/src/core/reddb.js +314 -0
- package/src/core/serialization.js +94 -0
- package/src/core/url.js +271 -0
- package/src/embedded-rejection.js +9 -87
- package/src/http.js +191 -0
- package/src/index.browser.js +156 -0
- package/src/index.js +29 -404
- package/src/protocol.js +6 -13
- package/src/queue.js +24 -0
- package/src/redwire.js +186 -0
- package/src/streaming-web.js +450 -0
- package/src/streaming.js +362 -0
- package/src/url.js +9 -268
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @reddb-io/client — browser entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* The browser counterpart to `./index.js`. It builds `connect()` on the
|
|
5
|
+
* transport-agnostic core (`./core/index.js`), dispatching only the
|
|
6
|
+
* browser-reachable wires:
|
|
7
|
+
*
|
|
8
|
+
* - 'http://host:port' — HTTP JSON-RPC over `fetch`
|
|
9
|
+
* - 'https://host:port' — HTTPS JSON-RPC over `fetch`
|
|
10
|
+
*
|
|
11
|
+
* Streaming is the Web-streams implementation (`./streaming-web.js`), so the
|
|
12
|
+
* full transport-agnostic surface works client-side: query, execute, insert,
|
|
13
|
+
* bulkInsert, transactions, the kv/documents/queue/cache/config/vault clients,
|
|
14
|
+
* the typed query builder, and streaming (`stream()` / `inputStream()`).
|
|
15
|
+
*
|
|
16
|
+
* This module imports **neither** the gRPC transport (`./grpc.js`, which pulls
|
|
17
|
+
* `node:http2`) **nor** the RedWire transport (`./redwire.js`) **nor** the
|
|
18
|
+
* `node:stream` streaming impl (`./streaming.js`). No `node:` built-in enters
|
|
19
|
+
* the browser bundle graph through it. A portability guard test
|
|
20
|
+
* (`test/browser-portability.test.mjs`) is the regression net for that.
|
|
21
|
+
*
|
|
22
|
+
* Schemes that need a raw TCP socket or HTTP/2 — `grpc://`, `grpcs://`,
|
|
23
|
+
* `red://`, `reds://`, and `pg` — are not reachable from a browser sandbox.
|
|
24
|
+
* `connect()` rejects them with an actionable error (see
|
|
25
|
+
* `BROWSER_TRANSPORT_UNSUPPORTED` below) instead of crashing the bundler or
|
|
26
|
+
* runtime. Embedded URIs (`memory://`, `file://`, `red:///path`) are rejected
|
|
27
|
+
* with the same wording as the Node entry.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
RedDBError,
|
|
32
|
+
RedDB as CoreRedDB,
|
|
33
|
+
Collection,
|
|
34
|
+
EmbeddedNotSupported,
|
|
35
|
+
EMBEDDED_REJECTION_MESSAGE,
|
|
36
|
+
isEmbeddedUri,
|
|
37
|
+
rejectEmbeddedUri,
|
|
38
|
+
parseUri,
|
|
39
|
+
login,
|
|
40
|
+
mergeAuthFromUri,
|
|
41
|
+
} from './core/index.js'
|
|
42
|
+
import { HttpRpcClient } from './http.js'
|
|
43
|
+
import { createSelectStream, createInputStream } from './streaming-web.js'
|
|
44
|
+
|
|
45
|
+
export { RedDBError, EmbeddedNotSupported, EMBEDDED_REJECTION_MESSAGE, isEmbeddedUri }
|
|
46
|
+
export { RowReadable, RowWritable } from './streaming-web.js'
|
|
47
|
+
export { CacheClient } from './cache.js'
|
|
48
|
+
export { KvClient } from './kv.js'
|
|
49
|
+
export { QueueClient } from './queue.js'
|
|
50
|
+
export { DocumentClient } from './documents.js'
|
|
51
|
+
export { ConfigClient } from './config.js'
|
|
52
|
+
export { VaultClient } from './vault.js'
|
|
53
|
+
export { TypedQueryBuilder } from './db-helpers.js'
|
|
54
|
+
export { parseUri, deriveLoginUrl } from './url.js'
|
|
55
|
+
export { login }
|
|
56
|
+
|
|
57
|
+
// The Web-streams streaming implementation, injected into the core `RedDB` so
|
|
58
|
+
// its `stream()` / `inputStream()` return Web-streams-backed row wrappers. The
|
|
59
|
+
// core itself never statically references Web (or `node:`) streams.
|
|
60
|
+
const WEB_STREAMING = { createSelectStream, createInputStream }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Shared wording for schemes a browser sandbox cannot reach: they need a raw
|
|
64
|
+
* TCP socket (`red(s)://`, `pg`) or HTTP/2 (`grpc(s)://`), neither of which a
|
|
65
|
+
* browser exposes to JavaScript. The remedy is the same in every case — point
|
|
66
|
+
* the client at an HTTP(S) endpoint or gateway in front of the server.
|
|
67
|
+
*/
|
|
68
|
+
function browserTransportError(scheme) {
|
|
69
|
+
return new RedDBError(
|
|
70
|
+
'BROWSER_TRANSPORT_UNSUPPORTED',
|
|
71
|
+
`'${scheme}' connections are not available in the browser: the `
|
|
72
|
+
+ `browser sandbox exposes no raw TCP socket (red://, reds://, pg) or `
|
|
73
|
+
+ `HTTP/2 client (grpc://, grpcs://) to JavaScript. Connect to an `
|
|
74
|
+
+ `HTTP(S) endpoint instead — e.g. 'http://host:port' / `
|
|
75
|
+
+ `'https://host:port' — by running RedDB's HTTP JSON-RPC listener or `
|
|
76
|
+
+ `an HTTP gateway in front of the server, then call `
|
|
77
|
+
+ `connect('https://…').`,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Connect to a remote RedDB instance from a browser.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} uri Connection URI. Only `http(s)://` is reachable from a
|
|
85
|
+
* browser; other schemes raise `BROWSER_TRANSPORT_UNSUPPORTED`.
|
|
86
|
+
* @param {object} [options]
|
|
87
|
+
* @param {object} [options.auth] Authentication credentials.
|
|
88
|
+
* @param {string} [options.auth.token] Bearer / API-key token.
|
|
89
|
+
* @param {string} [options.auth.apiKey] Alias for `token`.
|
|
90
|
+
* @param {string} [options.auth.username] Username for password login.
|
|
91
|
+
* @param {string} [options.auth.password] Password for password login.
|
|
92
|
+
* @param {string} [options.auth.loginUrl] Override URL for the password
|
|
93
|
+
* exchange (defaults to deriving `/auth/login` from `uri`).
|
|
94
|
+
* @returns {Promise<RedDB>}
|
|
95
|
+
*/
|
|
96
|
+
export async function connect(uri, options = {}) {
|
|
97
|
+
// Reject embedded shapes upfront with the same wording the Node entry and
|
|
98
|
+
// the Rust binary use, before the URL parser would map them to kind=embedded.
|
|
99
|
+
rejectEmbeddedUri(uri)
|
|
100
|
+
|
|
101
|
+
const parsed = parseUri(uri)
|
|
102
|
+
|
|
103
|
+
// Belt-and-braces: if the parser still produced an embedded kind, reject it.
|
|
104
|
+
if (parsed.kind === 'embedded') {
|
|
105
|
+
throw new EmbeddedNotSupported(uri)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (parsed.kind === 'http' || parsed.kind === 'https') {
|
|
109
|
+
const merged = mergeAuthFromUri(parsed, options.auth)
|
|
110
|
+
const baseUrl = `${parsed.kind}://${parsed.host}:${parsed.port}`
|
|
111
|
+
let token = merged.token
|
|
112
|
+
if (!token && merged.username && merged.password) {
|
|
113
|
+
const loginUrl = merged.loginUrl ?? `${baseUrl}/auth/login`
|
|
114
|
+
const session = await login(loginUrl, {
|
|
115
|
+
username: merged.username,
|
|
116
|
+
password: merged.password,
|
|
117
|
+
})
|
|
118
|
+
token = session.token
|
|
119
|
+
}
|
|
120
|
+
const client = new HttpRpcClient({ baseUrl, token })
|
|
121
|
+
await client.call('query', { sql: 'SELECT 1' })
|
|
122
|
+
return new RedDB(client)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
parsed.kind === 'grpc'
|
|
127
|
+
|| parsed.kind === 'grpcs'
|
|
128
|
+
|| parsed.kind === 'red'
|
|
129
|
+
|| parsed.kind === 'reds'
|
|
130
|
+
|| parsed.kind === 'pg'
|
|
131
|
+
) {
|
|
132
|
+
throw browserTransportError(parsed.kind)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
throw new RedDBError(
|
|
136
|
+
'UNSUPPORTED_KIND',
|
|
137
|
+
`internal: parsed kind '${parsed.kind}' has no browser transport`,
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Browser connection handle. The full request-shaping surface lives in the
|
|
143
|
+
* transport-agnostic core `RedDB`; this subclass exists only to inject the
|
|
144
|
+
* Web-streams streaming implementation so `stream()` / `inputStream()` return
|
|
145
|
+
* Web-streams-backed row wrappers. The public surface — every method, the
|
|
146
|
+
* `kv`/`config`/`vault` factory shapes, the `cache`/`queue`/`documents`
|
|
147
|
+
* clients — is the core's, unchanged.
|
|
148
|
+
*/
|
|
149
|
+
export class RedDB extends CoreRedDB {
|
|
150
|
+
/** @param {HttpRpcClient} client */
|
|
151
|
+
constructor(client) {
|
|
152
|
+
super(client, WEB_STREAMING)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { Collection }
|
package/src/index.js
CHANGED
|
@@ -26,26 +26,26 @@
|
|
|
26
26
|
* need an embedded engine, install `@reddb-io/sdk` instead.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import { RedDBError } from './protocol.js'
|
|
30
|
-
import { HttpRpcClient } from './http.js'
|
|
31
|
-
import { GrpcRpcClient } from './grpc.js'
|
|
32
|
-
import { connectRedwire } from './redwire.js'
|
|
33
|
-
import { parseUri, deriveLoginUrl } from './url.js'
|
|
34
29
|
import {
|
|
30
|
+
RedDBError,
|
|
31
|
+
RedDB as CoreRedDB,
|
|
32
|
+
Collection,
|
|
35
33
|
EmbeddedNotSupported,
|
|
36
34
|
EMBEDDED_REJECTION_MESSAGE,
|
|
37
35
|
isEmbeddedUri,
|
|
38
36
|
rejectEmbeddedUri,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
37
|
+
parseUri,
|
|
38
|
+
deriveLoginUrl,
|
|
39
|
+
login,
|
|
40
|
+
mergeAuthFromUri,
|
|
41
|
+
} from './core/index.js'
|
|
42
|
+
import { HttpRpcClient } from './http.js'
|
|
43
|
+
import { GrpcRpcClient } from './grpc.js'
|
|
44
|
+
import { connectRedwire } from './redwire.js'
|
|
45
|
+
import { createSelectStream, createInputStream } from './streaming.js'
|
|
47
46
|
|
|
48
47
|
export { RedDBError, EmbeddedNotSupported, EMBEDDED_REJECTION_MESSAGE, isEmbeddedUri }
|
|
48
|
+
export { splitNdjson, RowReadable, RowWritable } from './streaming.js'
|
|
49
49
|
export { CacheClient } from './cache.js'
|
|
50
50
|
export { KvClient } from './kv.js'
|
|
51
51
|
export { QueueClient } from './queue.js'
|
|
@@ -54,9 +54,12 @@ export { ConfigClient } from './config.js'
|
|
|
54
54
|
export { VaultClient } from './vault.js'
|
|
55
55
|
export { TypedQueryBuilder } from './db-helpers.js'
|
|
56
56
|
export { parseUri, deriveLoginUrl } from './url.js'
|
|
57
|
+
export { login }
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
// The `node:stream`-based streaming implementation, injected into the core
|
|
60
|
+
// `RedDB` so its `stream()` / `inputStream()` return Node streams. The core
|
|
61
|
+
// itself never statically references `node:stream`.
|
|
62
|
+
const NODE_STREAMING = { createSelectStream, createInputStream }
|
|
60
63
|
|
|
61
64
|
/**
|
|
62
65
|
* Connect to a remote RedDB instance.
|
|
@@ -158,89 +161,6 @@ export async function connect(uri, options = {}) {
|
|
|
158
161
|
)
|
|
159
162
|
}
|
|
160
163
|
|
|
161
|
-
function serializeParam(value) {
|
|
162
|
-
assertSupportedParam(value)
|
|
163
|
-
if (value instanceof Float32Array || value instanceof Float64Array) {
|
|
164
|
-
return Array.from(value)
|
|
165
|
-
}
|
|
166
|
-
if (value instanceof Date) {
|
|
167
|
-
return { $ts: String(BigInt(value.getTime()) * 1_000_000n) }
|
|
168
|
-
}
|
|
169
|
-
if (value instanceof Uint8Array || (typeof Buffer !== 'undefined' && value instanceof Buffer)) {
|
|
170
|
-
return { $bytes: bytesToBase64(value) }
|
|
171
|
-
}
|
|
172
|
-
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
173
|
-
if (Number.isNaN(value)) return { $float: 'NaN' }
|
|
174
|
-
return { $float: value > 0 ? 'Infinity' : '-Infinity' }
|
|
175
|
-
}
|
|
176
|
-
if (typeof value === 'string' && isUuidString(value)) {
|
|
177
|
-
return { $uuid: value }
|
|
178
|
-
}
|
|
179
|
-
return value
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function assertSupportedParam(value) {
|
|
183
|
-
if (value == null) return
|
|
184
|
-
if (
|
|
185
|
-
typeof value === 'boolean'
|
|
186
|
-
|| typeof value === 'number'
|
|
187
|
-
|| typeof value === 'string'
|
|
188
|
-
) {
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
if (value instanceof Date) {
|
|
192
|
-
if (Number.isNaN(value.getTime())) {
|
|
193
|
-
throw new RedDBError('UNSUPPORTED_PARAM', 'cannot encode invalid Date query parameter')
|
|
194
|
-
}
|
|
195
|
-
return
|
|
196
|
-
}
|
|
197
|
-
if (
|
|
198
|
-
value instanceof Uint8Array
|
|
199
|
-
|| value instanceof Float32Array
|
|
200
|
-
|| value instanceof Float64Array
|
|
201
|
-
|| (typeof Buffer !== 'undefined' && value instanceof Buffer)
|
|
202
|
-
) {
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
if (Array.isArray(value)) {
|
|
206
|
-
if (value.every((item) => typeof item === 'number')) return
|
|
207
|
-
throw new RedDBError(
|
|
208
|
-
'UNSUPPORTED_PARAM',
|
|
209
|
-
'array query parameters must contain only numbers',
|
|
210
|
-
)
|
|
211
|
-
}
|
|
212
|
-
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
throw new RedDBError(
|
|
216
|
-
'UNSUPPORTED_PARAM',
|
|
217
|
-
`cannot encode query parameter of type ${typeof value}`,
|
|
218
|
-
)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function normalizeQueryParams(args) {
|
|
222
|
-
if (args.length === 0) return null
|
|
223
|
-
if (args.length === 1 && Array.isArray(args[0])) return args[0].map(serializeParam)
|
|
224
|
-
return args.map(serializeParam)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function bytesToBase64(value) {
|
|
228
|
-
const bytes = value instanceof Uint8Array
|
|
229
|
-
? value
|
|
230
|
-
: new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
231
|
-
if (typeof Buffer !== 'undefined') {
|
|
232
|
-
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64')
|
|
233
|
-
}
|
|
234
|
-
let text = ''
|
|
235
|
-
for (const byte of bytes) text += String.fromCharCode(byte)
|
|
236
|
-
// eslint-disable-next-line no-undef
|
|
237
|
-
return btoa(text)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function isUuidString(value) {
|
|
241
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
164
|
/**
|
|
245
165
|
* Resolve TLS options for a redwire(s) connection. Source order:
|
|
246
166
|
* 1. caller-supplied `options.tls` object.
|
|
@@ -270,317 +190,22 @@ function buildTlsOpts(parsed, callerTls) {
|
|
|
270
190
|
}
|
|
271
191
|
}
|
|
272
192
|
|
|
273
|
-
function mergeAuthFromUri(parsed, optionAuth) {
|
|
274
|
-
const out = {
|
|
275
|
-
token: parsed.token ?? parsed.apiKey ?? null,
|
|
276
|
-
username: parsed.username ?? null,
|
|
277
|
-
password: parsed.password ?? null,
|
|
278
|
-
loginUrl: parsed.loginUrl ?? null,
|
|
279
|
-
}
|
|
280
|
-
if (optionAuth == null) return out
|
|
281
|
-
if (typeof optionAuth !== 'object') {
|
|
282
|
-
throw new TypeError('options.auth must be an object')
|
|
283
|
-
}
|
|
284
|
-
if (optionAuth.token != null) {
|
|
285
|
-
if (typeof optionAuth.token !== 'string' || optionAuth.token.length === 0) {
|
|
286
|
-
throw new TypeError('options.auth.token must be a non-empty string')
|
|
287
|
-
}
|
|
288
|
-
out.token = optionAuth.token
|
|
289
|
-
}
|
|
290
|
-
if (optionAuth.apiKey != null) {
|
|
291
|
-
if (typeof optionAuth.apiKey !== 'string' || optionAuth.apiKey.length === 0) {
|
|
292
|
-
throw new TypeError('options.auth.apiKey must be a non-empty string')
|
|
293
|
-
}
|
|
294
|
-
out.token = optionAuth.apiKey
|
|
295
|
-
}
|
|
296
|
-
if (optionAuth.username != null) {
|
|
297
|
-
if (typeof optionAuth.username !== 'string' || optionAuth.username.length === 0) {
|
|
298
|
-
throw new TypeError('options.auth.username must be a non-empty string')
|
|
299
|
-
}
|
|
300
|
-
out.username = optionAuth.username
|
|
301
|
-
}
|
|
302
|
-
if (optionAuth.password != null) {
|
|
303
|
-
if (typeof optionAuth.password !== 'string' || optionAuth.password.length === 0) {
|
|
304
|
-
throw new TypeError('options.auth.password must be a non-empty string')
|
|
305
|
-
}
|
|
306
|
-
out.password = optionAuth.password
|
|
307
|
-
}
|
|
308
|
-
if (optionAuth.loginUrl != null) {
|
|
309
|
-
out.loginUrl = optionAuth.loginUrl
|
|
310
|
-
}
|
|
311
|
-
return out
|
|
312
|
-
}
|
|
313
|
-
|
|
314
193
|
/**
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
194
|
+
* Node connection handle. The full request-shaping surface lives in the
|
|
195
|
+
* transport-agnostic core `RedDB`; this subclass exists only to inject the
|
|
196
|
+
* `node:stream`-based streaming implementation so `stream()` / `inputStream()`
|
|
197
|
+
* return Node streams. The public surface — every method, the `kv`/`config`/
|
|
198
|
+
* `vault` factory shapes, the `cache`/`queue`/`documents` clients — is the
|
|
199
|
+
* core's, unchanged.
|
|
318
200
|
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
* @returns {Promise<{ token: string, username: string, role: string, expires_at: number }>}
|
|
201
|
+
* The `Collection` handle (`db.collection(name)`) is re-exported from the
|
|
202
|
+
* core verbatim.
|
|
322
203
|
*/
|
|
323
|
-
export
|
|
324
|
-
if (typeof loginUrl !== 'string' || !loginUrl.startsWith('http')) {
|
|
325
|
-
throw new TypeError("login() requires an http(s):// URL pointing at /auth/login")
|
|
326
|
-
}
|
|
327
|
-
if (typeof username !== 'string' || username.length === 0) {
|
|
328
|
-
throw new TypeError('login() requires a non-empty username')
|
|
329
|
-
}
|
|
330
|
-
if (typeof password !== 'string' || password.length === 0) {
|
|
331
|
-
throw new TypeError('login() requires a non-empty password')
|
|
332
|
-
}
|
|
333
|
-
const response = await fetch(loginUrl, {
|
|
334
|
-
method: 'POST',
|
|
335
|
-
headers: { 'content-type': 'application/json' },
|
|
336
|
-
body: JSON.stringify({ username, password }),
|
|
337
|
-
})
|
|
338
|
-
const body = await response.json().catch(() => ({}))
|
|
339
|
-
if (!response.ok || body.ok === false) {
|
|
340
|
-
const code = body.error_code || `HTTP_${response.status}`
|
|
341
|
-
const message = body.error || `auth/login returned ${response.status}`
|
|
342
|
-
throw new RedDBError(code, message, body)
|
|
343
|
-
}
|
|
344
|
-
if (typeof body.token !== 'string') {
|
|
345
|
-
throw new RedDBError(
|
|
346
|
-
'AUTH_LOGIN_BAD_RESPONSE',
|
|
347
|
-
'auth/login response missing string token',
|
|
348
|
-
body,
|
|
349
|
-
)
|
|
350
|
-
}
|
|
351
|
-
return body
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Connection handle. Methods map 1:1 to JSON-RPC methods on the server.
|
|
356
|
-
* Identical surface to `@reddb-io/sdk`'s `RedDB`, minus the local-spawn
|
|
357
|
-
* lifecycle.
|
|
358
|
-
*/
|
|
359
|
-
class TransactionHandle {
|
|
360
|
-
constructor(db) {
|
|
361
|
-
this.db = db
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
query(sql, ...params) {
|
|
365
|
-
return this.db.query(sql, ...params)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
execute(sql, ...params) {
|
|
369
|
-
return this.db.execute(sql, ...params)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
insert(collection, payload) {
|
|
373
|
-
return this.db.insert(collection, payload)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
bulkInsert(collection, payloads) {
|
|
377
|
-
return this.db.bulkInsert(collection, payloads)
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async transaction() {
|
|
381
|
-
throw nestedTransactionError()
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export class RedDB {
|
|
204
|
+
export class RedDB extends CoreRedDB {
|
|
386
205
|
/** @param {HttpRpcClient | import('./redwire.js').RedWireClient} client */
|
|
387
206
|
constructor(client) {
|
|
388
|
-
|
|
389
|
-
this.cache = new CacheClient(client)
|
|
390
|
-
this.queue = new QueueClient(client)
|
|
391
|
-
this.documents = new DocumentClient(this)
|
|
392
|
-
const defaultKv = new KvClient(client)
|
|
393
|
-
this.kv = Object.assign((collection = 'kv_default') => new KvClient(client, collection), {
|
|
394
|
-
put: defaultKv.put.bind(defaultKv),
|
|
395
|
-
invalidateTags: defaultKv.invalidateTags.bind(defaultKv),
|
|
396
|
-
watch: defaultKv.watch.bind(defaultKv),
|
|
397
|
-
watchPrefix: defaultKv.watchPrefix.bind(defaultKv),
|
|
398
|
-
})
|
|
399
|
-
this.config = (collection = 'red.config') => new ConfigClient(client, collection)
|
|
400
|
-
this.vault = (collection = 'red.vault') => new VaultClient(client, collection)
|
|
401
|
-
this.inTransaction = false
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/** Execute a SQL query. Returns `{ statement, affected, columns, rows }`. */
|
|
405
|
-
query(sql, ...params) {
|
|
406
|
-
const wireParams = normalizeQueryParams(params)
|
|
407
|
-
if (wireParams == null) {
|
|
408
|
-
return this.client.call('query', { sql })
|
|
409
|
-
}
|
|
410
|
-
return this.client.call('query', { sql, params: wireParams })
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** Execute a SQL statement. Alias for `query`, including parameter binding. */
|
|
414
|
-
execute(sql, ...params) {
|
|
415
|
-
return this.query(sql, ...params)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Insert one row. Returns `{ affected, rid, id }`; `id` is a legacy alias for `rid`. */
|
|
419
|
-
async insert(collection, payload) {
|
|
420
|
-
let result = await this.client.call('insert', { collection, payload })
|
|
421
|
-
if (
|
|
422
|
-
result &&
|
|
423
|
-
typeof result === 'object' &&
|
|
424
|
-
!('affected' in result) &&
|
|
425
|
-
('rid' in result || 'id' in result)
|
|
426
|
-
) {
|
|
427
|
-
result = { ...result, affected: 1 }
|
|
428
|
-
}
|
|
429
|
-
return requireInsertId(result, 'insert')
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/** Insert many rows in one call. Returns `{ affected, rids, ids }`; `ids` is a legacy alias. */
|
|
433
|
-
async bulkInsert(collection, payloads) {
|
|
434
|
-
const result = await this.client.call('bulk_insert', { collection, payloads })
|
|
435
|
-
return requireInsertIds(result, payloads.length)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async transaction(callback) {
|
|
439
|
-
if (this.inTransaction) {
|
|
440
|
-
throw nestedTransactionError()
|
|
441
|
-
}
|
|
442
|
-
if (typeof callback !== 'function') {
|
|
443
|
-
throw new TypeError('transaction(callback) requires a function')
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
this.inTransaction = true
|
|
447
|
-
let began = false
|
|
448
|
-
try {
|
|
449
|
-
await this.query('BEGIN')
|
|
450
|
-
began = true
|
|
451
|
-
const result = await callback(new TransactionHandle(this))
|
|
452
|
-
await this.query('COMMIT')
|
|
453
|
-
return result
|
|
454
|
-
} catch (err) {
|
|
455
|
-
if (began) {
|
|
456
|
-
try {
|
|
457
|
-
await this.query('ROLLBACK')
|
|
458
|
-
} catch (rollbackErr) {
|
|
459
|
-
attachRollbackError(err, rollbackErr)
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
throw err
|
|
463
|
-
} finally {
|
|
464
|
-
this.inTransaction = false
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/** Return true when a collection is visible in the catalog. */
|
|
469
|
-
exists(collection) {
|
|
470
|
-
return collectionExists(this, collection)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/** List visible collections using SHOW COLLECTIONS. */
|
|
474
|
-
list() {
|
|
475
|
-
return listCollections(this)
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/** Return a caller-typed query builder for a collection. */
|
|
479
|
-
from(collection) {
|
|
480
|
-
return new TypedQueryBuilder(this, collection)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/** Get an entity by id. Returns `{ entity }` (entity is `null` if not found). */
|
|
484
|
-
get(collection, id) {
|
|
485
|
-
return this.client.call('get', { collection, id: String(id) })
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** Delete an entity by id. Returns `{ affected }`. */
|
|
489
|
-
delete(collection, id) {
|
|
490
|
-
return this.client.call('delete', { collection, id: String(id) })
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/** Probe the server. Returns `{ ok: true, version }`. */
|
|
494
|
-
health() {
|
|
495
|
-
return this.client.call('health', {})
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/** Server version + protocol version. */
|
|
499
|
-
version() {
|
|
500
|
-
return this.client.call('version', {})
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
/** Exchange username + password for a bearer token. */
|
|
504
|
-
login(username, password) {
|
|
505
|
-
return this.client.call('auth.login', { username, password })
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/** Identify the current caller. */
|
|
509
|
-
whoami() {
|
|
510
|
-
return this.client.call('auth.whoami', {})
|
|
207
|
+
super(client, NODE_STREAMING)
|
|
511
208
|
}
|
|
512
|
-
|
|
513
|
-
/** Change the current caller's password. */
|
|
514
|
-
changePassword(currentPassword, newPassword) {
|
|
515
|
-
return this.client.call('auth.change_password', {
|
|
516
|
-
current_password: currentPassword,
|
|
517
|
-
new_password: newPassword,
|
|
518
|
-
})
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/** Mint a long-lived API key. */
|
|
522
|
-
createApiKey({ username, role } = {}) {
|
|
523
|
-
return this.client.call('auth.create_api_key', { username, role })
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/** Revoke an API key by its public id. */
|
|
527
|
-
revokeApiKey(key) {
|
|
528
|
-
return this.client.call('auth.revoke_api_key', { key })
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/** Close the underlying transport. */
|
|
532
|
-
close() {
|
|
533
|
-
return this.client.close()
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function nestedTransactionError() {
|
|
538
|
-
return new RedDBError(
|
|
539
|
-
NESTED_TX_NOT_SUPPORTED,
|
|
540
|
-
`${NESTED_TX_NOT_SUPPORTED}: nested transactions are not supported on one connection`,
|
|
541
|
-
)
|
|
542
209
|
}
|
|
543
210
|
|
|
544
|
-
|
|
545
|
-
if (err && typeof err === 'object') {
|
|
546
|
-
try {
|
|
547
|
-
err.rollbackError = rollbackErr
|
|
548
|
-
} catch {
|
|
549
|
-
// Preserve the original callback/query error even for frozen errors.
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function requireInsertId(result, method) {
|
|
555
|
-
if (!result || typeof result !== 'object' || (result.rid == null && result.id == null)) {
|
|
556
|
-
throw new RedDBError(
|
|
557
|
-
'ENGINE_TOO_OLD',
|
|
558
|
-
`${method}() requires RedDB engine >= ${MIN_INSERT_ID_ENGINE_VERSION} with insert id support`,
|
|
559
|
-
)
|
|
560
|
-
}
|
|
561
|
-
if (result.rid == null) result.rid = result.id
|
|
562
|
-
if (result.id == null) result.id = result.rid
|
|
563
|
-
return result
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function requireInsertIds(result, expected) {
|
|
567
|
-
if (
|
|
568
|
-
!result ||
|
|
569
|
-
typeof result !== 'object' ||
|
|
570
|
-
(!Array.isArray(result.rids) && !Array.isArray(result.ids))
|
|
571
|
-
) {
|
|
572
|
-
throw new RedDBError(
|
|
573
|
-
'ENGINE_TOO_OLD',
|
|
574
|
-
`bulkInsert() requires RedDB engine >= ${MIN_INSERT_ID_ENGINE_VERSION} with bulk insert id support`,
|
|
575
|
-
)
|
|
576
|
-
}
|
|
577
|
-
if (!Array.isArray(result.rids)) result.rids = result.ids
|
|
578
|
-
if (!Array.isArray(result.ids)) result.ids = result.rids
|
|
579
|
-
if (result.rids.length !== expected) {
|
|
580
|
-
throw new RedDBError(
|
|
581
|
-
'INVALID_RESPONSE',
|
|
582
|
-
`bulkInsert() expected ${expected} rids, got ${result.rids.length}`,
|
|
583
|
-
)
|
|
584
|
-
}
|
|
585
|
-
return result
|
|
586
|
-
}
|
|
211
|
+
export { Collection }
|
package/src/protocol.js
CHANGED
|
@@ -8,23 +8,16 @@
|
|
|
8
8
|
* Spec: PLAN_DRIVERS.md, "Spec do protocolo stdio".
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { RedDBError } from './core/errors.js'
|
|
12
|
+
|
|
13
|
+
// Re-exported from the transport-agnostic core so existing
|
|
14
|
+
// `import { RedDBError } from './protocol.js'` call sites keep working.
|
|
15
|
+
export { RedDBError }
|
|
16
|
+
|
|
11
17
|
const NEWLINE = 0x0a // '\n'
|
|
12
18
|
const encoder = new TextEncoder()
|
|
13
19
|
const decoder = new TextDecoder('utf-8')
|
|
14
20
|
|
|
15
|
-
/**
|
|
16
|
-
* RedDB-shaped error. Drivers in other languages should expose an
|
|
17
|
-
* equivalent class with the same `code` field.
|
|
18
|
-
*/
|
|
19
|
-
export class RedDBError extends Error {
|
|
20
|
-
constructor(code, message, data) {
|
|
21
|
-
super(message)
|
|
22
|
-
this.name = 'RedDBError'
|
|
23
|
-
this.code = code
|
|
24
|
-
this.data = data ?? null
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
export class RpcClient {
|
|
29
22
|
/** @param {import('./spawn.js').RedProcess} child */
|
|
30
23
|
constructor(child) {
|
package/src/queue.js
CHANGED
|
@@ -38,6 +38,30 @@ export class QueueClient {
|
|
|
38
38
|
sql: `QUEUE PURGE ${queueIdentifier(queue)}`,
|
|
39
39
|
})
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
// Live `QUEUE READ … WAIT <ms>` helper (PRD #718 / #725). Same
|
|
43
|
+
// contract as drivers/js: required `waitMs`, timeout returns empty
|
|
44
|
+
// array, cancellation/cap rejection surface as transport errors.
|
|
45
|
+
async readWait(queue, consumer, options = {}) {
|
|
46
|
+
const sql = buildQueueReadWaitSql(queue, consumer, options)
|
|
47
|
+
const result = await this.client.call('query', { sql })
|
|
48
|
+
return queuePayloads(result)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildQueueReadWaitSql(queue, consumer, options) {
|
|
53
|
+
const { waitMs, group = null, count = null } = options ?? {}
|
|
54
|
+
if (!Number.isInteger(waitMs) || waitMs < 0) {
|
|
55
|
+
throw new RedDBError(
|
|
56
|
+
'INVALID_QUEUE_WAIT',
|
|
57
|
+
'queue readWait requires an explicit non-negative integer waitMs (no infinite wait)',
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
const q = queueIdentifier(queue)
|
|
61
|
+
const c = queueIdentifier(consumer)
|
|
62
|
+
const g = group != null ? ` GROUP ${queueIdentifier(group)}` : ''
|
|
63
|
+
const n = count != null ? queueCount(count) : ''
|
|
64
|
+
return `QUEUE READ ${q}${g} CONSUMER ${c}${n} WAIT ${waitMs}ms`
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
function queueIdentifier(value) {
|