@reddb-io/client 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 +12 -1
- package/index.d.ts +64 -2
- package/package.json +1 -1
- package/src/db-helpers.js +95 -0
- package/src/grpc.js +435 -0
- package/src/http.js +7 -2
- package/src/index.js +179 -15
- package/src/kv.js +23 -1
- package/src/queue.js +78 -0
- package/src/redwire.js +320 -7
package/src/index.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import { RedDBError } from './protocol.js'
|
|
30
30
|
import { HttpRpcClient } from './http.js'
|
|
31
|
+
import { GrpcRpcClient } from './grpc.js'
|
|
31
32
|
import { connectRedwire } from './redwire.js'
|
|
32
33
|
import { parseUri, deriveLoginUrl } from './url.js'
|
|
33
34
|
import {
|
|
@@ -38,16 +39,22 @@ import {
|
|
|
38
39
|
} from './embedded-rejection.js'
|
|
39
40
|
import { CacheClient } from './cache.js'
|
|
40
41
|
import { KvClient } from './kv.js'
|
|
42
|
+
import { QueueClient } from './queue.js'
|
|
41
43
|
import { ConfigClient } from './config.js'
|
|
42
44
|
import { VaultClient } from './vault.js'
|
|
45
|
+
import { TypedQueryBuilder, collectionExists, listCollections } from './db-helpers.js'
|
|
43
46
|
|
|
44
47
|
export { RedDBError, EmbeddedNotSupported, EMBEDDED_REJECTION_MESSAGE, isEmbeddedUri }
|
|
45
48
|
export { CacheClient } from './cache.js'
|
|
46
49
|
export { KvClient } from './kv.js'
|
|
50
|
+
export { QueueClient } from './queue.js'
|
|
47
51
|
export { ConfigClient } from './config.js'
|
|
48
52
|
export { VaultClient } from './vault.js'
|
|
53
|
+
export { TypedQueryBuilder } from './db-helpers.js'
|
|
49
54
|
export { parseUri, deriveLoginUrl } from './url.js'
|
|
50
55
|
|
|
56
|
+
const MIN_INSERT_ID_ENGINE_VERSION = '1.0.9'
|
|
57
|
+
|
|
51
58
|
/**
|
|
52
59
|
* Connect to a remote RedDB instance.
|
|
53
60
|
*
|
|
@@ -90,16 +97,29 @@ export async function connect(uri, options = {}) {
|
|
|
90
97
|
token = session.token
|
|
91
98
|
}
|
|
92
99
|
const client = new HttpRpcClient({ baseUrl, token })
|
|
93
|
-
await client.call('
|
|
100
|
+
await client.call('query', { sql: 'SELECT 1' })
|
|
94
101
|
return new RedDB(client)
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
if (parsed.kind === 'grpc' || parsed.kind === 'grpcs') {
|
|
105
|
+
let token = merged.token
|
|
106
|
+
if (!token && merged.username && merged.password) {
|
|
107
|
+
const loginUrl = merged.loginUrl ?? deriveLoginUrl(parsed)
|
|
108
|
+
const session = await login(loginUrl, {
|
|
109
|
+
username: merged.username,
|
|
110
|
+
password: merged.password,
|
|
111
|
+
})
|
|
112
|
+
token = session.token
|
|
113
|
+
}
|
|
114
|
+
const scheme = parsed.kind === 'grpcs' ? 'https' : 'http'
|
|
115
|
+
const client = new GrpcRpcClient({
|
|
116
|
+
baseUrl: `${scheme}://${parsed.host}:${parsed.port}`,
|
|
117
|
+
token,
|
|
118
|
+
})
|
|
119
|
+
return new RedDB(client)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (parsed.kind === 'red' || parsed.kind === 'reds') {
|
|
103
123
|
let token = merged.token
|
|
104
124
|
if (!token && merged.username && merged.password) {
|
|
105
125
|
const loginUrl = merged.loginUrl ?? deriveLoginUrl(parsed)
|
|
@@ -135,6 +155,89 @@ export async function connect(uri, options = {}) {
|
|
|
135
155
|
)
|
|
136
156
|
}
|
|
137
157
|
|
|
158
|
+
function serializeParam(value) {
|
|
159
|
+
assertSupportedParam(value)
|
|
160
|
+
if (value instanceof Float32Array || value instanceof Float64Array) {
|
|
161
|
+
return Array.from(value)
|
|
162
|
+
}
|
|
163
|
+
if (value instanceof Date) {
|
|
164
|
+
return { $ts: String(BigInt(value.getTime()) * 1_000_000n) }
|
|
165
|
+
}
|
|
166
|
+
if (value instanceof Uint8Array || (typeof Buffer !== 'undefined' && value instanceof Buffer)) {
|
|
167
|
+
return { $bytes: bytesToBase64(value) }
|
|
168
|
+
}
|
|
169
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
170
|
+
if (Number.isNaN(value)) return { $float: 'NaN' }
|
|
171
|
+
return { $float: value > 0 ? 'Infinity' : '-Infinity' }
|
|
172
|
+
}
|
|
173
|
+
if (typeof value === 'string' && isUuidString(value)) {
|
|
174
|
+
return { $uuid: value }
|
|
175
|
+
}
|
|
176
|
+
return value
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function assertSupportedParam(value) {
|
|
180
|
+
if (value == null) return
|
|
181
|
+
if (
|
|
182
|
+
typeof value === 'boolean'
|
|
183
|
+
|| typeof value === 'number'
|
|
184
|
+
|| typeof value === 'string'
|
|
185
|
+
) {
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (value instanceof Date) {
|
|
189
|
+
if (Number.isNaN(value.getTime())) {
|
|
190
|
+
throw new RedDBError('UNSUPPORTED_PARAM', 'cannot encode invalid Date query parameter')
|
|
191
|
+
}
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
value instanceof Uint8Array
|
|
196
|
+
|| value instanceof Float32Array
|
|
197
|
+
|| value instanceof Float64Array
|
|
198
|
+
|| (typeof Buffer !== 'undefined' && value instanceof Buffer)
|
|
199
|
+
) {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(value)) {
|
|
203
|
+
if (value.every((item) => typeof item === 'number')) return
|
|
204
|
+
throw new RedDBError(
|
|
205
|
+
'UNSUPPORTED_PARAM',
|
|
206
|
+
'array query parameters must contain only numbers',
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
if (typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
throw new RedDBError(
|
|
213
|
+
'UNSUPPORTED_PARAM',
|
|
214
|
+
`cannot encode query parameter of type ${typeof value}`,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeQueryParams(args) {
|
|
219
|
+
if (args.length === 0) return null
|
|
220
|
+
if (args.length === 1 && Array.isArray(args[0])) return args[0].map(serializeParam)
|
|
221
|
+
return args.map(serializeParam)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function bytesToBase64(value) {
|
|
225
|
+
const bytes = value instanceof Uint8Array
|
|
226
|
+
? value
|
|
227
|
+
: new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
228
|
+
if (typeof Buffer !== 'undefined') {
|
|
229
|
+
return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64')
|
|
230
|
+
}
|
|
231
|
+
let text = ''
|
|
232
|
+
for (const byte of bytes) text += String.fromCharCode(byte)
|
|
233
|
+
// eslint-disable-next-line no-undef
|
|
234
|
+
return btoa(text)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isUuidString(value) {
|
|
238
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)
|
|
239
|
+
}
|
|
240
|
+
|
|
138
241
|
/**
|
|
139
242
|
* Resolve TLS options for a redwire(s) connection. Source order:
|
|
140
243
|
* 1. caller-supplied `options.tls` object.
|
|
@@ -255,6 +358,7 @@ export class RedDB {
|
|
|
255
358
|
constructor(client) {
|
|
256
359
|
this.client = client
|
|
257
360
|
this.cache = new CacheClient(client)
|
|
361
|
+
this.queue = new QueueClient(client)
|
|
258
362
|
const defaultKv = new KvClient(client)
|
|
259
363
|
this.kv = Object.assign((collection = 'kv_default') => new KvClient(client, collection), {
|
|
260
364
|
put: defaultKv.put.bind(defaultKv),
|
|
@@ -267,18 +371,52 @@ export class RedDB {
|
|
|
267
371
|
}
|
|
268
372
|
|
|
269
373
|
/** Execute a SQL query. Returns `{ statement, affected, columns, rows }`. */
|
|
270
|
-
query(sql) {
|
|
271
|
-
|
|
374
|
+
query(sql, ...params) {
|
|
375
|
+
const wireParams = normalizeQueryParams(params)
|
|
376
|
+
if (wireParams == null) {
|
|
377
|
+
return this.client.call('query', { sql })
|
|
378
|
+
}
|
|
379
|
+
return this.client.call('query', { sql, params: wireParams })
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Execute a SQL statement. Alias for `query`, including parameter binding. */
|
|
383
|
+
execute(sql, ...params) {
|
|
384
|
+
return this.query(sql, ...params)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Insert one row. Returns `{ affected, id }`. */
|
|
388
|
+
async insert(collection, payload) {
|
|
389
|
+
let result = await this.client.call('insert', { collection, payload })
|
|
390
|
+
if (
|
|
391
|
+
result &&
|
|
392
|
+
typeof result === 'object' &&
|
|
393
|
+
!('affected' in result) &&
|
|
394
|
+
'id' in result
|
|
395
|
+
) {
|
|
396
|
+
result = { ...result, affected: 1 }
|
|
397
|
+
}
|
|
398
|
+
return requireInsertId(result, 'insert')
|
|
272
399
|
}
|
|
273
400
|
|
|
274
|
-
/** Insert one
|
|
275
|
-
|
|
276
|
-
|
|
401
|
+
/** Insert many rows in one call. Returns `{ affected, ids }`. */
|
|
402
|
+
async bulkInsert(collection, payloads) {
|
|
403
|
+
const result = await this.client.call('bulk_insert', { collection, payloads })
|
|
404
|
+
return requireInsertIds(result, payloads.length)
|
|
277
405
|
}
|
|
278
406
|
|
|
279
|
-
/**
|
|
280
|
-
|
|
281
|
-
return this
|
|
407
|
+
/** Return true when a collection is visible in the catalog. */
|
|
408
|
+
exists(collection) {
|
|
409
|
+
return collectionExists(this, collection)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** List visible collections using SHOW COLLECTIONS. */
|
|
413
|
+
list() {
|
|
414
|
+
return listCollections(this)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Return a caller-typed query builder for a collection. */
|
|
418
|
+
from(collection) {
|
|
419
|
+
return new TypedQueryBuilder(this, collection)
|
|
282
420
|
}
|
|
283
421
|
|
|
284
422
|
/** Get an entity by id. Returns `{ entity }` (entity is `null` if not found). */
|
|
@@ -334,3 +472,29 @@ export class RedDB {
|
|
|
334
472
|
return this.client.close()
|
|
335
473
|
}
|
|
336
474
|
}
|
|
475
|
+
|
|
476
|
+
function requireInsertId(result, method) {
|
|
477
|
+
if (!result || typeof result !== 'object' || result.id == null) {
|
|
478
|
+
throw new RedDBError(
|
|
479
|
+
'ENGINE_TOO_OLD',
|
|
480
|
+
`${method}() requires RedDB engine >= ${MIN_INSERT_ID_ENGINE_VERSION} with insert id support`,
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
return result
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function requireInsertIds(result, expected) {
|
|
487
|
+
if (!result || typeof result !== 'object' || !Array.isArray(result.ids)) {
|
|
488
|
+
throw new RedDBError(
|
|
489
|
+
'ENGINE_TOO_OLD',
|
|
490
|
+
`bulkInsert() requires RedDB engine >= ${MIN_INSERT_ID_ENGINE_VERSION} with bulk insert id support`,
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
if (result.ids.length !== expected) {
|
|
494
|
+
throw new RedDBError(
|
|
495
|
+
'INVALID_RESPONSE',
|
|
496
|
+
`bulkInsert() expected ${expected} ids, got ${result.ids.length}`,
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
return result
|
|
500
|
+
}
|
package/src/kv.js
CHANGED
|
@@ -17,6 +17,20 @@ export class KvClient {
|
|
|
17
17
|
})
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
async get(key, options = {}) {
|
|
21
|
+
const collection = options.collection ?? this.collection
|
|
22
|
+
const result = await this.client.call('query', {
|
|
23
|
+
sql: `KV GET ${kvPath(collection, key)}`,
|
|
24
|
+
})
|
|
25
|
+
return result?.rows?.[0]?.value ?? null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async getMany(keys, options = {}) {
|
|
29
|
+
const values = []
|
|
30
|
+
for (const key of keys) values.push(await this.get(key, options))
|
|
31
|
+
return values
|
|
32
|
+
}
|
|
33
|
+
|
|
20
34
|
async invalidateTags(tags, options = {}) {
|
|
21
35
|
const collection = options.collection ?? this.collection
|
|
22
36
|
const result = await this.client.call('query', {
|
|
@@ -56,7 +70,15 @@ function kvPath(collection, key) {
|
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
function kvIdentifier(value) {
|
|
59
|
-
|
|
73
|
+
const ident = String(value)
|
|
74
|
+
const invalid = ident.match(/[^A-Za-z0-9_]/)
|
|
75
|
+
if (invalid) {
|
|
76
|
+
throw new RedDBError(
|
|
77
|
+
'INVALID_KV_KEY',
|
|
78
|
+
`invalid KV key "${ident}": character "${invalid[0]}" is not supported`,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
return ident
|
|
60
82
|
}
|
|
61
83
|
|
|
62
84
|
function kvValueLiteral(value) {
|
package/src/queue.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { RedDBError } from './protocol.js'
|
|
2
|
+
|
|
3
|
+
export class QueueClient {
|
|
4
|
+
constructor(client) {
|
|
5
|
+
this.client = client
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
push(queue, value, options = {}) {
|
|
9
|
+
const priority = options.priority != null ? ` PRIORITY ${queuePriority(options.priority)}` : ''
|
|
10
|
+
return this.client.call('query', {
|
|
11
|
+
sql: `QUEUE PUSH ${queueIdentifier(queue)} ${queueValueLiteral(value)}${priority}`,
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async pop(queue, count) {
|
|
16
|
+
const result = await this.client.call('query', {
|
|
17
|
+
sql: `QUEUE POP ${queueIdentifier(queue)}${queueCount(count)}`,
|
|
18
|
+
})
|
|
19
|
+
return queuePayloads(result)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async peek(queue, count) {
|
|
23
|
+
const result = await this.client.call('query', {
|
|
24
|
+
sql: `QUEUE PEEK ${queueIdentifier(queue)}${queueCount(count)}`,
|
|
25
|
+
})
|
|
26
|
+
return queuePayloads(result)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async len(queue) {
|
|
30
|
+
const result = await this.client.call('query', {
|
|
31
|
+
sql: `QUEUE LEN ${queueIdentifier(queue)}`,
|
|
32
|
+
})
|
|
33
|
+
return Number(result?.rows?.[0]?.len ?? 0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
purge(queue) {
|
|
37
|
+
return this.client.call('query', {
|
|
38
|
+
sql: `QUEUE PURGE ${queueIdentifier(queue)}`,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function queueIdentifier(value) {
|
|
44
|
+
const ident = String(value)
|
|
45
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(ident)) {
|
|
46
|
+
throw new RedDBError(
|
|
47
|
+
'INVALID_QUEUE_NAME',
|
|
48
|
+
`invalid queue name "${ident}": expected an SQL identifier`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
return ident
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function queueCount(count) {
|
|
55
|
+
if (count == null) return ''
|
|
56
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
57
|
+
throw new RedDBError('INVALID_QUEUE_COUNT', 'queue count must be a non-negative integer')
|
|
58
|
+
}
|
|
59
|
+
return ` COUNT ${count}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function queuePriority(priority) {
|
|
63
|
+
if (!Number.isInteger(priority)) {
|
|
64
|
+
throw new RedDBError('INVALID_QUEUE_PRIORITY', 'queue priority must be an integer')
|
|
65
|
+
}
|
|
66
|
+
return String(priority)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function queueValueLiteral(value) {
|
|
70
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
|
71
|
+
if (value == null) return 'NULL'
|
|
72
|
+
if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`
|
|
73
|
+
return JSON.stringify(value)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function queuePayloads(result) {
|
|
77
|
+
return Array.isArray(result?.rows) ? result.rows.map((row) => row.payload) : []
|
|
78
|
+
}
|