@thadeu/ndb 0.4.0 → 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 +103 -49
- package/dist/index.cjs +248 -156
- package/dist/index.d.cts +56 -29
- package/dist/index.d.ts +56 -29
- package/dist/index.js +247 -155
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,12 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
Collection: () => Collection,
|
|
23
24
|
CursorError: () => CursorError,
|
|
24
25
|
DuplicateIdError: () => DuplicateIdError,
|
|
25
26
|
HistoryTruncatedError: () => HistoryTruncatedError,
|
|
26
27
|
InvalidQueryError: () => InvalidQueryError,
|
|
27
28
|
InvalidUpdateError: () => InvalidUpdateError,
|
|
28
|
-
Model: () => Model,
|
|
29
29
|
Ndb: () => Ndb,
|
|
30
30
|
NdbError: () => NdbError,
|
|
31
31
|
NotFoundError: () => NotFoundError,
|
|
@@ -150,20 +150,21 @@ function toWhere(v) {
|
|
|
150
150
|
|
|
151
151
|
// src/query.ts
|
|
152
152
|
var Query = class _Query {
|
|
153
|
-
constructor(
|
|
154
|
-
this.
|
|
153
|
+
constructor(coll, state = { clauses: [] }) {
|
|
154
|
+
this.coll = coll;
|
|
155
155
|
this.state = state;
|
|
156
156
|
}
|
|
157
|
-
|
|
157
|
+
coll;
|
|
158
158
|
state;
|
|
159
159
|
nextCursor = "";
|
|
160
|
-
|
|
160
|
+
// Adds conditions, ANDed with the ones already there.
|
|
161
|
+
query(w) {
|
|
161
162
|
return this.with({ clauses: [...this.state.clauses, toWhere(w)] });
|
|
162
163
|
}
|
|
163
|
-
// A
|
|
164
|
+
// A query built elsewhere, such as from a search form. It is not
|
|
164
165
|
// type-checked and is sent as is; the daemon validates it. It is ANDed
|
|
165
166
|
// with the other clauses.
|
|
166
|
-
|
|
167
|
+
rawQuery(w) {
|
|
167
168
|
return this.with({ clauses: [...this.state.clauses, w] });
|
|
168
169
|
}
|
|
169
170
|
not(w) {
|
|
@@ -206,14 +207,15 @@ var Query = class _Query {
|
|
|
206
207
|
}
|
|
207
208
|
return null;
|
|
208
209
|
}
|
|
210
|
+
// How many documents match (POST /{coll}/_count). limit, skip and sort do
|
|
211
|
+
// not change it.
|
|
209
212
|
async count() {
|
|
210
|
-
await this.
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
await res.body.dump();
|
|
214
|
-
return Number(res.header("x-ndb-total") ?? 0);
|
|
213
|
+
await this.coll.ready();
|
|
214
|
+
const out = await this.coll.db.transport.request("POST", this.coll.path("_count"), { query: this.whereDoc() });
|
|
215
|
+
return out.count;
|
|
215
216
|
}
|
|
216
|
-
|
|
217
|
+
// Every row, following the cursor page by page (limit is the page size).
|
|
218
|
+
async *each() {
|
|
217
219
|
let body = this.body();
|
|
218
220
|
for (; ; ) {
|
|
219
221
|
yield* this.rows(body);
|
|
@@ -224,16 +226,16 @@ var Query = class _Query {
|
|
|
224
226
|
}
|
|
225
227
|
}
|
|
226
228
|
async *rows(body) {
|
|
227
|
-
await this.
|
|
228
|
-
const res = await this.
|
|
229
|
+
await this.coll.ready();
|
|
230
|
+
const res = await this.coll.db.transport.stream("POST", this.coll.path("_search"), body);
|
|
229
231
|
this.nextCursor = res.header("x-ndb-next") ?? "";
|
|
230
232
|
for await (const raw of res.rows()) {
|
|
231
|
-
yield this.
|
|
233
|
+
yield this.coll.decode(raw);
|
|
232
234
|
}
|
|
233
235
|
}
|
|
234
236
|
body() {
|
|
235
237
|
const { fields, sort, limit, skip, after } = this.state;
|
|
236
|
-
return {
|
|
238
|
+
return { query: this.whereDoc(), fields, sort, limit, skip, after, versions: true };
|
|
237
239
|
}
|
|
238
240
|
whereDoc() {
|
|
239
241
|
const merged = {};
|
|
@@ -248,7 +250,7 @@ var Query = class _Query {
|
|
|
248
250
|
return merged;
|
|
249
251
|
}
|
|
250
252
|
with(patch) {
|
|
251
|
-
return new _Query(this.
|
|
253
|
+
return new _Query(this.coll, { ...this.state, ...patch });
|
|
252
254
|
}
|
|
253
255
|
};
|
|
254
256
|
|
|
@@ -262,12 +264,12 @@ async function* watchEvents(lines2, consumer) {
|
|
|
262
264
|
if (line.error) {
|
|
263
265
|
throw makeError(line.error.code, line.error.message);
|
|
264
266
|
}
|
|
265
|
-
yield { consumer, pos: line.pos, op: line.op, _id: line._id ?? "",
|
|
267
|
+
yield { consumer, pos: line.pos, op: line.op, _id: line._id ?? "", _v: line._v, doc: line.doc };
|
|
266
268
|
}
|
|
267
269
|
}
|
|
268
270
|
|
|
269
|
-
// src/
|
|
270
|
-
var
|
|
271
|
+
// src/collection.ts
|
|
272
|
+
var Collection = class {
|
|
271
273
|
constructor(db, name, schema) {
|
|
272
274
|
this.db = db;
|
|
273
275
|
this.name = name;
|
|
@@ -277,12 +279,11 @@ var Model = class {
|
|
|
277
279
|
name;
|
|
278
280
|
schema;
|
|
279
281
|
ensured;
|
|
280
|
-
path(
|
|
281
|
-
|
|
282
|
-
|
|
282
|
+
// The path of the collection, or of one of its actions ("_search").
|
|
283
|
+
path(action) {
|
|
284
|
+
const base = `/${encodeURIComponent(this.name)}`;
|
|
285
|
+
return action ? `${base}/${action}` : base;
|
|
283
286
|
}
|
|
284
|
-
// A collection pattern ("logs-2026-*") reads many collections and declares
|
|
285
|
-
// none: its writes fail with pattern_read_only.
|
|
286
287
|
get isPattern() {
|
|
287
288
|
return this.name.includes("*");
|
|
288
289
|
}
|
|
@@ -295,58 +296,72 @@ var Model = class {
|
|
|
295
296
|
return this.ensured;
|
|
296
297
|
}
|
|
297
298
|
async insert(docs, opts = {}) {
|
|
298
|
-
|
|
299
|
+
if (!Array.isArray(docs) && opts.expireIn === void 0) {
|
|
300
|
+
return this.send("POST", "_doc", docs);
|
|
301
|
+
}
|
|
299
302
|
const list = Array.isArray(docs) ? docs : [docs];
|
|
300
|
-
const out = await this.
|
|
301
|
-
|
|
303
|
+
const out = await this.send("POST", "_bulk", { docs: list, expire_in: opts.expireIn });
|
|
304
|
+
return Array.isArray(docs) ? out.results : out.results[0];
|
|
305
|
+
}
|
|
306
|
+
// Writes a document by its _id: creates it when the _id is free, and
|
|
307
|
+
// merges its fields into the stored document when it exists (the other
|
|
308
|
+
// fields stay), like doc_as_upsert in Elasticsearch. No _id is an insert.
|
|
309
|
+
async upsert(doc, opts = {}) {
|
|
310
|
+
return (await this.upsertAll([doc], opts))[0];
|
|
311
|
+
}
|
|
312
|
+
// upsert for many documents in one transaction, all or nothing
|
|
313
|
+
// (POST /{coll}/_bulk with upsert).
|
|
314
|
+
async upsertAll(docs, opts = {}) {
|
|
315
|
+
const out = await this.send("POST", "_bulk", { docs, expire_in: opts.expireIn, upsert: true });
|
|
316
|
+
return out.results;
|
|
317
|
+
}
|
|
318
|
+
// Applies ops to the document with this _id, in one atomic step
|
|
319
|
+
// (POST /{coll}/_update/{id}). guard adds conditions on the same document,
|
|
320
|
+
// such as { _v: 7 }: matched is 0 when it fails. Matching nothing is not an
|
|
321
|
+
// error.
|
|
322
|
+
async update(id, ops, opts = {}) {
|
|
323
|
+
const out = await this.send("POST", docPath("_update", id), {
|
|
324
|
+
query: opts.guard === void 0 ? void 0 : toWhere(opts.guard),
|
|
325
|
+
update: checkOps(ops),
|
|
326
|
+
upsert: opts.upsert,
|
|
302
327
|
expire_in: opts.expireIn
|
|
303
328
|
});
|
|
304
|
-
return out
|
|
329
|
+
return this.updated(out);
|
|
330
|
+
}
|
|
331
|
+
// The atomic update by query (POST /{coll}/_update_by_query): the daemon
|
|
332
|
+
// matches and writes in one step on the newest state, so concurrent updates
|
|
333
|
+
// never interleave. Without many it updates the first match; many updates
|
|
334
|
+
// every match and needs _id or an indexed field in the query.
|
|
335
|
+
async updateBy(query, ops, opts = {}) {
|
|
336
|
+
const out = await this.send("POST", "_update_by_query", {
|
|
337
|
+
query: toWhere(query),
|
|
338
|
+
update: checkOps(ops),
|
|
339
|
+
many: opts.many,
|
|
340
|
+
upsert: opts.upsert,
|
|
341
|
+
expire_in: opts.expireIn
|
|
342
|
+
});
|
|
343
|
+
return this.updated(out);
|
|
305
344
|
}
|
|
306
|
-
|
|
307
|
-
|
|
345
|
+
// Deletes the document with this _id (DELETE /{coll}/_doc/{id}).
|
|
346
|
+
delete(id) {
|
|
347
|
+
return this.send("DELETE", docPath("_doc", id));
|
|
308
348
|
}
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// { _id: 'c1', _v: 7 }. Matching nothing is not an error: matched is 0.
|
|
314
|
-
// many does not apply to an _id.
|
|
315
|
-
async update(idOrFilter, ops, opts = {}) {
|
|
316
|
-
await this.ready();
|
|
317
|
-
const byId = typeof idOrFilter === "string";
|
|
318
|
-
const body = mergeUpdate(byId ? {} : idOrFilter, ops);
|
|
319
|
-
const query = writeQuery({ many: byId ? void 0 : opts.many, upsert: opts.upsert, expire_in: opts.expireIn });
|
|
320
|
-
const op = byId ? `${encodeURIComponent(idOrFilter)}/update` : "update";
|
|
321
|
-
const out = await this.db.transport.request(
|
|
322
|
-
"POST",
|
|
323
|
-
this.path(op) + query,
|
|
324
|
-
body
|
|
325
|
-
);
|
|
326
|
-
return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
|
|
349
|
+
// Deletes the first document the query matches, or every match with many
|
|
350
|
+
// (POST /{coll}/_delete_by_query, same rule as updateBy).
|
|
351
|
+
deleteBy(query, opts = {}) {
|
|
352
|
+
return this.send("POST", "_delete_by_query", { query: toWhere(query), many: opts.many });
|
|
327
353
|
}
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
await this.ready();
|
|
332
|
-
if (typeof idOrFilter === "string") {
|
|
333
|
-
return this.db.transport.request("DELETE", this.path(encodeURIComponent(idOrFilter)));
|
|
334
|
-
}
|
|
335
|
-
const filter = idOrFilter;
|
|
336
|
-
return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
|
|
337
|
-
}
|
|
338
|
-
// Removes every document of the collection and keeps its definition and
|
|
339
|
-
// indexes. The cost does not depend on the number of documents. Watch
|
|
340
|
-
// consumers get one event with op "truncate".
|
|
354
|
+
// Removes every document and keeps the definition and indexes. The cost
|
|
355
|
+
// does not depend on the number of documents. Watch consumers get one event
|
|
356
|
+
// with op "truncate".
|
|
341
357
|
async truncate() {
|
|
342
|
-
await this.
|
|
343
|
-
await this.db.transport.request("POST", this.path("truncate") + this.confirm());
|
|
358
|
+
await this.db.transport.request("POST", this.path("_truncate") + this.confirm());
|
|
344
359
|
}
|
|
345
360
|
// Removes the collection: its documents, its definition and the positions
|
|
346
|
-
// of its watch consumers. The next call
|
|
361
|
+
// of its watch consumers. The next call declares it again.
|
|
347
362
|
async drop() {
|
|
348
|
-
await this.db.transport.request("DELETE", this.path() + this.confirm());
|
|
349
363
|
this.ensured = void 0;
|
|
364
|
+
await this.db.transport.request("DELETE", this.path() + this.confirm());
|
|
350
365
|
}
|
|
351
366
|
confirm() {
|
|
352
367
|
return `?confirm=${encodeURIComponent(this.name)}`;
|
|
@@ -355,99 +370,84 @@ var Model = class {
|
|
|
355
370
|
// NotFoundError for a missing or expired document, and TtlError when the
|
|
356
371
|
// schema has no expires field.
|
|
357
372
|
expire(id, duration) {
|
|
358
|
-
return this.setExpiry(
|
|
373
|
+
return this.setExpiry(id, { in: duration });
|
|
359
374
|
}
|
|
360
375
|
expireAt(id, at) {
|
|
361
|
-
return this.setExpiry(
|
|
376
|
+
return this.setExpiry(id, { at: at.toISOString() });
|
|
362
377
|
}
|
|
363
378
|
// Removes the expiry of a document (PERSIST in Redis).
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return this.db.transport.request("POST", this.path("persist"), { _id: id });
|
|
379
|
+
persist(id) {
|
|
380
|
+
return this.send("POST", docPath("_persist", id));
|
|
367
381
|
}
|
|
368
382
|
// The seconds left (rounded up), -1 when the document never expires, null
|
|
369
383
|
// when it is missing or expired.
|
|
370
384
|
async ttl(id) {
|
|
371
|
-
await this.
|
|
372
|
-
|
|
373
|
-
const out = await this.db.transport.request("GET", this.path(`${encodeURIComponent(id)}/ttl`));
|
|
374
|
-
return out.ttl;
|
|
375
|
-
} catch (err) {
|
|
376
|
-
if (err instanceof NotFoundError) {
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
throw err;
|
|
380
|
-
}
|
|
385
|
+
const out = await orNull(this.send("GET", docPath("_ttl", id)));
|
|
386
|
+
return out === null ? null : out.ttl;
|
|
381
387
|
}
|
|
382
|
-
async setExpiry(body) {
|
|
383
|
-
await this.
|
|
384
|
-
|
|
385
|
-
return { _id: out._id, v: out.v, expiresAt: new Date(out.expires_at) };
|
|
388
|
+
async setExpiry(id, body) {
|
|
389
|
+
const out = await this.send("POST", docPath("_expire", id), body);
|
|
390
|
+
return { _id: out._id, _v: out._v, expiresAt: new Date(out.expires_at) };
|
|
386
391
|
}
|
|
387
392
|
// The document with this _id, or null.
|
|
388
393
|
async get(id) {
|
|
389
|
-
await this.
|
|
390
|
-
|
|
391
|
-
const doc = await this.db.transport.request("GET", this.path(encodeURIComponent(id)));
|
|
392
|
-
return this.decode(doc);
|
|
393
|
-
} catch (err) {
|
|
394
|
-
if (err instanceof NotFoundError) {
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
397
|
-
throw err;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
query() {
|
|
401
|
-
return new Query(this);
|
|
394
|
+
const doc = await orNull(this.send("GET", docPath("_doc", id)));
|
|
395
|
+
return doc === null ? null : this.decode(doc);
|
|
402
396
|
}
|
|
403
|
-
|
|
404
|
-
|
|
397
|
+
// A search (POST /{coll}/_search). No query matches every document.
|
|
398
|
+
query(w) {
|
|
399
|
+
const q = new Query(this);
|
|
400
|
+
return w === void 0 ? q : q.query(w);
|
|
405
401
|
}
|
|
406
|
-
|
|
407
|
-
|
|
402
|
+
// A query built elsewhere, such as from a search form: it is not
|
|
403
|
+
// type-checked and is sent as is; the daemon validates it.
|
|
404
|
+
rawQuery(w) {
|
|
405
|
+
return new Query(this).rawQuery(w);
|
|
408
406
|
}
|
|
409
407
|
not(w) {
|
|
410
|
-
return this
|
|
408
|
+
return new Query(this).not(w);
|
|
411
409
|
}
|
|
412
410
|
or(...ws) {
|
|
413
|
-
return this
|
|
411
|
+
return new Query(this).or(...ws);
|
|
414
412
|
}
|
|
415
413
|
sort(spec) {
|
|
416
|
-
return this
|
|
414
|
+
return new Query(this).sort(spec);
|
|
417
415
|
}
|
|
418
416
|
limit(n) {
|
|
419
|
-
return this
|
|
417
|
+
return new Query(this).limit(n);
|
|
420
418
|
}
|
|
421
419
|
skip(n) {
|
|
422
|
-
return this
|
|
420
|
+
return new Query(this).skip(n);
|
|
423
421
|
}
|
|
424
422
|
page(n, size) {
|
|
425
|
-
return this
|
|
423
|
+
return new Query(this).page(n, size);
|
|
426
424
|
}
|
|
427
425
|
after(token) {
|
|
428
|
-
return this
|
|
426
|
+
return new Query(this).after(token);
|
|
429
427
|
}
|
|
430
428
|
fields(...names) {
|
|
431
|
-
return this
|
|
429
|
+
return new Query(this).fields(...names);
|
|
432
430
|
}
|
|
433
431
|
all() {
|
|
434
|
-
return this
|
|
432
|
+
return new Query(this).all();
|
|
435
433
|
}
|
|
436
|
-
|
|
437
|
-
|
|
434
|
+
// How many documents match (POST /{coll}/_count).
|
|
435
|
+
count(w) {
|
|
436
|
+
return this.query(w).count();
|
|
438
437
|
}
|
|
439
438
|
first() {
|
|
440
|
-
return this
|
|
439
|
+
return new Query(this).first();
|
|
441
440
|
}
|
|
442
|
-
|
|
443
|
-
|
|
441
|
+
// Every row, page by page.
|
|
442
|
+
each() {
|
|
443
|
+
return new Query(this).each();
|
|
444
444
|
}
|
|
445
|
-
// Runs a
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
async
|
|
445
|
+
// Runs a whole search body built elsewhere, such as from a search form:
|
|
446
|
+
// { query, sort, limit, after, count }. The body is sent as is (the daemon
|
|
447
|
+
// validates it); rows are decoded like the rows of query().
|
|
448
|
+
async search(body) {
|
|
449
449
|
await this.ready();
|
|
450
|
-
const res = await this.db.transport.stream("POST", this.path("
|
|
450
|
+
const res = await this.db.transport.stream("POST", this.path("_search"), { ...body, versions: true });
|
|
451
451
|
const rows = [];
|
|
452
452
|
for await (const raw of res.rows()) {
|
|
453
453
|
rows.push(this.decode(raw));
|
|
@@ -456,7 +456,7 @@ var Model = class {
|
|
|
456
456
|
}
|
|
457
457
|
async aggregate(pipeline, opts = {}) {
|
|
458
458
|
await this.ready();
|
|
459
|
-
const res = await this.db.transport.stream("POST", this.path("
|
|
459
|
+
const res = await this.db.transport.stream("POST", this.path("_aggregate"), { pipeline, ...opts });
|
|
460
460
|
const rows = [];
|
|
461
461
|
for await (const row of res.rows()) {
|
|
462
462
|
rows.push(row);
|
|
@@ -465,14 +465,30 @@ var Model = class {
|
|
|
465
465
|
}
|
|
466
466
|
async *watch(consumer, opts = {}) {
|
|
467
467
|
await this.ready();
|
|
468
|
-
const body = { consumer, from: opts.from, limit: opts.limit,
|
|
469
|
-
const res = await this.db.transport.stream("POST", this.path("
|
|
468
|
+
const body = { consumer, from: opts.from, limit: opts.limit, query: opts.query === void 0 ? void 0 : toWhere(opts.query) };
|
|
469
|
+
const res = await this.db.transport.stream("POST", this.path("_watch"), body, opts.signal);
|
|
470
470
|
for await (const ev of watchEvents(res.rows(), consumer)) {
|
|
471
471
|
yield { ...ev, doc: ev.doc && this.decode(ev.doc) };
|
|
472
472
|
}
|
|
473
473
|
}
|
|
474
474
|
async ack(event) {
|
|
475
|
-
await this.
|
|
475
|
+
await this.send("POST", "_ack", { consumer: event.consumer, pos: event.pos });
|
|
476
|
+
}
|
|
477
|
+
// Rows, indexes, files and bytes (GET /{coll}/_stats).
|
|
478
|
+
stats() {
|
|
479
|
+
return this.send("GET", "_stats");
|
|
480
|
+
}
|
|
481
|
+
// Seals the memtable and merges every segment now (POST /{coll}/_compact).
|
|
482
|
+
compact() {
|
|
483
|
+
return this.send("POST", "_compact");
|
|
484
|
+
}
|
|
485
|
+
// Adds a secondary index and builds it in the background. Returns the
|
|
486
|
+
// indexed fields.
|
|
487
|
+
async ensureIndex(field) {
|
|
488
|
+
return (await this.send("PUT", docPath("_indexes", field))).indexes;
|
|
489
|
+
}
|
|
490
|
+
async dropIndex(field) {
|
|
491
|
+
return (await this.send("DELETE", docPath("_indexes", field))).indexes;
|
|
476
492
|
}
|
|
477
493
|
decode(raw) {
|
|
478
494
|
const out = { ...raw };
|
|
@@ -484,6 +500,15 @@ var Model = class {
|
|
|
484
500
|
}
|
|
485
501
|
return out;
|
|
486
502
|
}
|
|
503
|
+
// Declares the collection on first use, then sends one request to
|
|
504
|
+
// /{coll}/{action}.
|
|
505
|
+
async send(method, action, body) {
|
|
506
|
+
await this.ready();
|
|
507
|
+
return this.db.transport.request(method, this.path(action), body);
|
|
508
|
+
}
|
|
509
|
+
updated(out) {
|
|
510
|
+
return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
|
|
511
|
+
}
|
|
487
512
|
catalog() {
|
|
488
513
|
const entries = Object.entries(this.schema.fields);
|
|
489
514
|
return {
|
|
@@ -494,26 +519,30 @@ var Model = class {
|
|
|
494
519
|
};
|
|
495
520
|
}
|
|
496
521
|
};
|
|
497
|
-
function
|
|
498
|
-
|
|
499
|
-
for (const [name, value] of Object.entries(params)) {
|
|
500
|
-
if (value !== void 0 && value !== false) {
|
|
501
|
-
q.set(name, String(value));
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
const text = q.toString();
|
|
505
|
-
return text === "" ? "" : `?${text}`;
|
|
522
|
+
function docPath(action, id) {
|
|
523
|
+
return `${action}/${encodeURIComponent(id)}`;
|
|
506
524
|
}
|
|
507
|
-
function
|
|
525
|
+
function checkOps(ops) {
|
|
508
526
|
for (const key of Object.keys(ops)) {
|
|
509
527
|
if (!key.startsWith("$")) {
|
|
510
528
|
throw new InvalidUpdateError("invalid_update", `update operator "${key}" must start with $ (for example "$set")`);
|
|
511
529
|
}
|
|
512
530
|
}
|
|
513
|
-
return
|
|
531
|
+
return ops;
|
|
532
|
+
}
|
|
533
|
+
async function orNull(p) {
|
|
534
|
+
try {
|
|
535
|
+
return await p;
|
|
536
|
+
} catch (err) {
|
|
537
|
+
if (err instanceof NotFoundError) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
throw err;
|
|
541
|
+
}
|
|
514
542
|
}
|
|
515
543
|
|
|
516
544
|
// src/transport.ts
|
|
545
|
+
var import_node_stream = require("stream");
|
|
517
546
|
var import_undici = require("undici");
|
|
518
547
|
|
|
519
548
|
// src/ndjson.ts
|
|
@@ -548,9 +577,12 @@ var notConnected = /* @__PURE__ */ new Set(["ECONNREFUSED", "ENOENT"]);
|
|
|
548
577
|
var retryBaseMs = 50;
|
|
549
578
|
var retryCapMs = 1e3;
|
|
550
579
|
var defaultReadyTimeoutMs = 1e4;
|
|
551
|
-
var readOps = /\/(
|
|
580
|
+
var readOps = /\/(_search|_count|_aggregate)$/;
|
|
581
|
+
var onBun = typeof globalThis.Bun !== "undefined";
|
|
552
582
|
var Transport = class {
|
|
553
583
|
client;
|
|
584
|
+
base;
|
|
585
|
+
socketPath;
|
|
554
586
|
token;
|
|
555
587
|
readyTimeout;
|
|
556
588
|
// addr: unix:///path or tcp://host:port. Default: env NDB_URL.
|
|
@@ -560,16 +592,20 @@ var Transport = class {
|
|
|
560
592
|
this.token = opts.token || process.env.NDB_TOKEN || void 0;
|
|
561
593
|
this.readyTimeout = opts.readyTimeout ?? defaultReadyTimeoutMs;
|
|
562
594
|
if (url.protocol === "unix:" && url.pathname !== "") {
|
|
563
|
-
this.
|
|
595
|
+
this.base = "http://ndb";
|
|
596
|
+
this.socketPath = decodeURIComponent(url.pathname);
|
|
564
597
|
} else if (url.protocol === "tcp:" && url.host !== "") {
|
|
565
|
-
this.
|
|
598
|
+
this.base = `http://${url.host}`;
|
|
566
599
|
} else {
|
|
567
600
|
throw new TypeError(`ndb: address "${target}" must be unix:///path or tcp://host:port`);
|
|
568
601
|
}
|
|
602
|
+
if (!onBun) {
|
|
603
|
+
this.client = new import_undici.Pool(this.base, { socketPath: this.socketPath, connections: 16 });
|
|
604
|
+
}
|
|
569
605
|
}
|
|
570
|
-
async stream(method, path, body, signal) {
|
|
606
|
+
async stream(method, path, body, signal, contentType) {
|
|
571
607
|
const res = await this.withReadyRetry(method, path, signal, async () => {
|
|
572
|
-
const r = await this.send(method, path, body, signal);
|
|
608
|
+
const r = await this.send(method, path, body, signal, contentType);
|
|
573
609
|
if (r.statusCode >= 300) {
|
|
574
610
|
throw await apiError(r);
|
|
575
611
|
}
|
|
@@ -583,13 +619,14 @@ var Transport = class {
|
|
|
583
619
|
header: (name) => headerOf(res.headers, name)
|
|
584
620
|
};
|
|
585
621
|
}
|
|
586
|
-
|
|
587
|
-
|
|
622
|
+
// contentType sends body as raw bytes (blobs) instead of JSON.
|
|
623
|
+
async request(method, path, body, contentType) {
|
|
624
|
+
const res = await this.stream(method, path, body, void 0, contentType);
|
|
588
625
|
const text = await res.body.setEncoding("utf8").toArray();
|
|
589
626
|
return text.length > 0 ? JSON.parse(text.join("")) : void 0;
|
|
590
627
|
}
|
|
591
628
|
close() {
|
|
592
|
-
return this.client
|
|
629
|
+
return this.client?.close() ?? Promise.resolve();
|
|
593
630
|
}
|
|
594
631
|
// Runs fn again while the daemon is not ready, with exponential backoff and
|
|
595
632
|
// jitter. It repeats only when the request certainly was not processed
|
|
@@ -612,31 +649,60 @@ var Transport = class {
|
|
|
612
649
|
}
|
|
613
650
|
}
|
|
614
651
|
}
|
|
615
|
-
|
|
616
|
-
|
|
652
|
+
// Rows come as NDJSON: the SDK reads them one by one (jsonRows).
|
|
653
|
+
headers(contentType = "application/json") {
|
|
654
|
+
const headers = { "content-type": contentType, accept: "application/x-ndjson" };
|
|
617
655
|
if (this.token) {
|
|
618
656
|
headers.authorization = `Bearer ${this.token}`;
|
|
619
657
|
}
|
|
620
658
|
return headers;
|
|
621
659
|
}
|
|
622
|
-
async send(method, path, body, signal) {
|
|
660
|
+
async send(method, path, body, signal, contentType) {
|
|
661
|
+
const raw = contentType !== void 0;
|
|
623
662
|
const opts = {
|
|
624
663
|
method,
|
|
625
664
|
path,
|
|
626
665
|
signal,
|
|
627
|
-
headers: this.headers(),
|
|
628
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
666
|
+
headers: this.headers(contentType),
|
|
667
|
+
body: body === void 0 ? void 0 : raw ? body : JSON.stringify(body)
|
|
629
668
|
};
|
|
669
|
+
const client = this.client;
|
|
670
|
+
if (!client) {
|
|
671
|
+
return this.fetchSend(opts);
|
|
672
|
+
}
|
|
630
673
|
try {
|
|
631
|
-
return await
|
|
674
|
+
return await client.request(opts);
|
|
632
675
|
} catch (err) {
|
|
633
676
|
if (!isIdempotent(method, path) || !staleSocket.has(err.code ?? "") || signal?.aborted) {
|
|
634
677
|
throw err;
|
|
635
678
|
}
|
|
636
|
-
return await
|
|
679
|
+
return await client.request(opts);
|
|
637
680
|
}
|
|
638
681
|
}
|
|
682
|
+
// fetchSend is send with fetch (Bun). The answer has the shape of an undici
|
|
683
|
+
// answer: the rest of the SDK reads it the same way.
|
|
684
|
+
async fetchSend(opts) {
|
|
685
|
+
const init = { method: opts.method, headers: opts.headers, body: opts.body, signal: opts.signal, unix: this.socketPath };
|
|
686
|
+
const res = await fetch(this.base + opts.path, init);
|
|
687
|
+
const headers = {};
|
|
688
|
+
res.headers.forEach((value, name) => {
|
|
689
|
+
headers[name] = value;
|
|
690
|
+
});
|
|
691
|
+
return { statusCode: res.status, headers, body: readableOf(res) };
|
|
692
|
+
}
|
|
639
693
|
};
|
|
694
|
+
function readableOf(res) {
|
|
695
|
+
const stream = res.body ? import_node_stream.Readable.fromWeb(res.body) : import_node_stream.Readable.from([]);
|
|
696
|
+
return Object.assign(stream, {
|
|
697
|
+
arrayBuffer: async () => {
|
|
698
|
+
const bytes = Buffer.concat(await stream.toArray());
|
|
699
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
700
|
+
},
|
|
701
|
+
dump: async () => {
|
|
702
|
+
stream.resume();
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
640
706
|
function isIdempotent(method, path) {
|
|
641
707
|
return method === "GET" || readOps.test(path.split("?")[0] ?? path);
|
|
642
708
|
}
|
|
@@ -692,11 +758,37 @@ var Ndb = class {
|
|
|
692
758
|
constructor(addr, opts) {
|
|
693
759
|
this.transport = new Transport(addr, opts);
|
|
694
760
|
}
|
|
695
|
-
|
|
696
|
-
|
|
761
|
+
// A collection of the daemon; see Collection. Nothing is sent until its
|
|
762
|
+
// first call.
|
|
763
|
+
collection(name, schema) {
|
|
764
|
+
return new Collection(this, name, schema);
|
|
697
765
|
}
|
|
766
|
+
// Resolves when the daemon is ready (GET /_health).
|
|
698
767
|
async health() {
|
|
699
|
-
await this.transport.request("GET", "/
|
|
768
|
+
await this.transport.request("GET", "/_health");
|
|
769
|
+
}
|
|
770
|
+
// The catalog: one definition per collection (GET /_collections).
|
|
771
|
+
async collections() {
|
|
772
|
+
return (await this.transport.request("GET", "/_collections")).collections;
|
|
773
|
+
}
|
|
774
|
+
// Every collection, totals, version and uptime (GET /_stats).
|
|
775
|
+
stats() {
|
|
776
|
+
return this.transport.request("GET", "/_stats");
|
|
777
|
+
}
|
|
778
|
+
// Disk use per collection and per part (GET /_size).
|
|
779
|
+
size() {
|
|
780
|
+
return this.transport.request("GET", "/_size");
|
|
781
|
+
}
|
|
782
|
+
// Stores bytes in the blob store (POST /_blobs) and returns the reference
|
|
783
|
+
// to put in a document field.
|
|
784
|
+
putBlob(data, contentType = "application/octet-stream") {
|
|
785
|
+
return this.transport.request("POST", "/_blobs", data, contentType);
|
|
786
|
+
}
|
|
787
|
+
// The bytes of a blob (GET /_blobs/{ref}).
|
|
788
|
+
async getBlob(blob) {
|
|
789
|
+
const ref = typeof blob === "string" ? blob : blob.$blob;
|
|
790
|
+
const res = await this.transport.stream("GET", `/_blobs/${encodeURIComponent(ref)}`);
|
|
791
|
+
return new Uint8Array(await res.body.arrayBuffer());
|
|
700
792
|
}
|
|
701
793
|
request(method, path, body) {
|
|
702
794
|
return this.transport.request(method, path, body);
|
|
@@ -745,12 +837,12 @@ function seedCounter() {
|
|
|
745
837
|
}
|
|
746
838
|
// Annotate the CommonJS export names for ESM import in node:
|
|
747
839
|
0 && (module.exports = {
|
|
840
|
+
Collection,
|
|
748
841
|
CursorError,
|
|
749
842
|
DuplicateIdError,
|
|
750
843
|
HistoryTruncatedError,
|
|
751
844
|
InvalidQueryError,
|
|
752
845
|
InvalidUpdateError,
|
|
753
|
-
Model,
|
|
754
846
|
Ndb,
|
|
755
847
|
NdbError,
|
|
756
848
|
NotFoundError,
|