@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/dist/index.js CHANGED
@@ -108,20 +108,21 @@ function toWhere(v) {
108
108
 
109
109
  // src/query.ts
110
110
  var Query = class _Query {
111
- constructor(model, state = { clauses: [] }) {
112
- this.model = model;
111
+ constructor(coll, state = { clauses: [] }) {
112
+ this.coll = coll;
113
113
  this.state = state;
114
114
  }
115
- model;
115
+ coll;
116
116
  state;
117
117
  nextCursor = "";
118
- where(w) {
118
+ // Adds conditions, ANDed with the ones already there.
119
+ query(w) {
119
120
  return this.with({ clauses: [...this.state.clauses, toWhere(w)] });
120
121
  }
121
- // A where object built elsewhere, such as from a search form. It is not
122
+ // A query built elsewhere, such as from a search form. It is not
122
123
  // type-checked and is sent as is; the daemon validates it. It is ANDed
123
124
  // with the other clauses.
124
- whereRaw(w) {
125
+ rawQuery(w) {
125
126
  return this.with({ clauses: [...this.state.clauses, w] });
126
127
  }
127
128
  not(w) {
@@ -164,14 +165,15 @@ var Query = class _Query {
164
165
  }
165
166
  return null;
166
167
  }
168
+ // How many documents match (POST /{coll}/_count). limit, skip and sort do
169
+ // not change it.
167
170
  async count() {
168
- await this.model.ready();
169
- const body = { where: this.whereDoc(), count: true, limit: 1 };
170
- const res = await this.model.db.transport.stream("POST", this.model.path("find"), body);
171
- await res.body.dump();
172
- return Number(res.header("x-ndb-total") ?? 0);
171
+ await this.coll.ready();
172
+ const out = await this.coll.db.transport.request("POST", this.coll.path("_count"), { query: this.whereDoc() });
173
+ return out.count;
173
174
  }
174
- async *iterate() {
175
+ // Every row, following the cursor page by page (limit is the page size).
176
+ async *each() {
175
177
  let body = this.body();
176
178
  for (; ; ) {
177
179
  yield* this.rows(body);
@@ -182,16 +184,16 @@ var Query = class _Query {
182
184
  }
183
185
  }
184
186
  async *rows(body) {
185
- await this.model.ready();
186
- const res = await this.model.db.transport.stream("POST", this.model.path("find"), body);
187
+ await this.coll.ready();
188
+ const res = await this.coll.db.transport.stream("POST", this.coll.path("_search"), body);
187
189
  this.nextCursor = res.header("x-ndb-next") ?? "";
188
190
  for await (const raw of res.rows()) {
189
- yield this.model.decode(raw);
191
+ yield this.coll.decode(raw);
190
192
  }
191
193
  }
192
194
  body() {
193
195
  const { fields, sort, limit, skip, after } = this.state;
194
- return { where: this.whereDoc(), fields, sort, limit, skip, after, versions: true };
196
+ return { query: this.whereDoc(), fields, sort, limit, skip, after, versions: true };
195
197
  }
196
198
  whereDoc() {
197
199
  const merged = {};
@@ -206,7 +208,7 @@ var Query = class _Query {
206
208
  return merged;
207
209
  }
208
210
  with(patch) {
209
- return new _Query(this.model, { ...this.state, ...patch });
211
+ return new _Query(this.coll, { ...this.state, ...patch });
210
212
  }
211
213
  };
212
214
 
@@ -220,12 +222,12 @@ async function* watchEvents(lines2, consumer) {
220
222
  if (line.error) {
221
223
  throw makeError(line.error.code, line.error.message);
222
224
  }
223
- yield { consumer, pos: line.pos, op: line.op, _id: line._id ?? "", v: line.v, doc: line.doc };
225
+ yield { consumer, pos: line.pos, op: line.op, _id: line._id ?? "", _v: line._v, doc: line.doc };
224
226
  }
225
227
  }
226
228
 
227
- // src/model.ts
228
- var Model = class {
229
+ // src/collection.ts
230
+ var Collection = class {
229
231
  constructor(db, name, schema) {
230
232
  this.db = db;
231
233
  this.name = name;
@@ -235,12 +237,11 @@ var Model = class {
235
237
  name;
236
238
  schema;
237
239
  ensured;
238
- path(op) {
239
- const base = `/v1/c/${encodeURIComponent(this.name)}`;
240
- return op ? `${base}/${op}` : base;
240
+ // The path of the collection, or of one of its actions ("_search").
241
+ path(action) {
242
+ const base = `/${encodeURIComponent(this.name)}`;
243
+ return action ? `${base}/${action}` : base;
241
244
  }
242
- // A collection pattern ("logs-2026-*") reads many collections and declares
243
- // none: its writes fail with pattern_read_only.
244
245
  get isPattern() {
245
246
  return this.name.includes("*");
246
247
  }
@@ -253,58 +254,72 @@ var Model = class {
253
254
  return this.ensured;
254
255
  }
255
256
  async insert(docs, opts = {}) {
256
- await this.ready();
257
+ if (!Array.isArray(docs) && opts.expireIn === void 0) {
258
+ return this.send("POST", "_doc", docs);
259
+ }
257
260
  const list = Array.isArray(docs) ? docs : [docs];
258
- const out = await this.db.transport.request("POST", this.path("insert"), {
259
- insert: list,
261
+ const out = await this.send("POST", "_bulk", { docs: list, expire_in: opts.expireIn });
262
+ return Array.isArray(docs) ? out.results : out.results[0];
263
+ }
264
+ // Writes a document by its _id: creates it when the _id is free, and
265
+ // merges its fields into the stored document when it exists (the other
266
+ // fields stay), like doc_as_upsert in Elasticsearch. No _id is an insert.
267
+ async upsert(doc, opts = {}) {
268
+ return (await this.upsertAll([doc], opts))[0];
269
+ }
270
+ // upsert for many documents in one transaction, all or nothing
271
+ // (POST /{coll}/_bulk with upsert).
272
+ async upsertAll(docs, opts = {}) {
273
+ const out = await this.send("POST", "_bulk", { docs, expire_in: opts.expireIn, upsert: true });
274
+ return out.results;
275
+ }
276
+ // Applies ops to the document with this _id, in one atomic step
277
+ // (POST /{coll}/_update/{id}). guard adds conditions on the same document,
278
+ // such as { _v: 7 }: matched is 0 when it fails. Matching nothing is not an
279
+ // error.
280
+ async update(id, ops, opts = {}) {
281
+ const out = await this.send("POST", docPath("_update", id), {
282
+ query: opts.guard === void 0 ? void 0 : toWhere(opts.guard),
283
+ update: checkOps(ops),
284
+ upsert: opts.upsert,
260
285
  expire_in: opts.expireIn
261
286
  });
262
- return out.results;
287
+ return this.updated(out);
288
+ }
289
+ // The atomic update by query (POST /{coll}/_update_by_query): the daemon
290
+ // matches and writes in one step on the newest state, so concurrent updates
291
+ // never interleave. Without many it updates the first match; many updates
292
+ // every match and needs _id or an indexed field in the query.
293
+ async updateBy(query, ops, opts = {}) {
294
+ const out = await this.send("POST", "_update_by_query", {
295
+ query: toWhere(query),
296
+ update: checkOps(ops),
297
+ many: opts.many,
298
+ upsert: opts.upsert,
299
+ expire_in: opts.expireIn
300
+ });
301
+ return this.updated(out);
263
302
  }
264
- create(docs, opts = {}) {
265
- return this.insert(docs, opts);
303
+ // Deletes the document with this _id (DELETE /{coll}/_doc/{id}).
304
+ delete(id) {
305
+ return this.send("DELETE", docPath("_doc", id));
266
306
  }
267
- // Atomic update. A string is an _id (POST /{_id}/update); an object is a
268
- // filter (POST /update). The filter and the operators are sent as one body;
269
- // the daemon matches and writes in one step on the newest state, so
270
- // concurrent updates never interleave. A version guard is a filter:
271
- // { _id: 'c1', _v: 7 }. Matching nothing is not an error: matched is 0.
272
- // many does not apply to an _id.
273
- async update(idOrFilter, ops, opts = {}) {
274
- await this.ready();
275
- const byId = typeof idOrFilter === "string";
276
- const body = mergeUpdate(byId ? {} : idOrFilter, ops);
277
- const query = writeQuery({ many: byId ? void 0 : opts.many, upsert: opts.upsert, expire_in: opts.expireIn });
278
- const op = byId ? `${encodeURIComponent(idOrFilter)}/update` : "update";
279
- const out = await this.db.transport.request(
280
- "POST",
281
- this.path(op) + query,
282
- body
283
- );
284
- return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
307
+ // Deletes the first document the query matches, or every match with many
308
+ // (POST /{coll}/_delete_by_query, same rule as updateBy).
309
+ deleteBy(query, opts = {}) {
310
+ return this.send("POST", "_delete_by_query", { query: toWhere(query), many: opts.many });
285
311
  }
286
- // Deletes by _id (a string), or the documents that the filter matches: one,
287
- // or every match with many (same rule as update).
288
- async delete(idOrFilter, opts = {}) {
289
- await this.ready();
290
- if (typeof idOrFilter === "string") {
291
- return this.db.transport.request("DELETE", this.path(encodeURIComponent(idOrFilter)));
292
- }
293
- const filter = idOrFilter;
294
- return this.db.transport.request("POST", this.path("delete") + writeQuery({ many: opts.many }), toWhere(filter));
295
- }
296
- // Removes every document of the collection and keeps its definition and
297
- // indexes. The cost does not depend on the number of documents. Watch
298
- // consumers get one event with op "truncate".
312
+ // Removes every document and keeps the definition and indexes. The cost
313
+ // does not depend on the number of documents. Watch consumers get one event
314
+ // with op "truncate".
299
315
  async truncate() {
300
- await this.ready();
301
- await this.db.transport.request("POST", this.path("truncate") + this.confirm());
316
+ await this.db.transport.request("POST", this.path("_truncate") + this.confirm());
302
317
  }
303
318
  // Removes the collection: its documents, its definition and the positions
304
- // of its watch consumers. The next call on the model declares it again.
319
+ // of its watch consumers. The next call declares it again.
305
320
  async drop() {
306
- await this.db.transport.request("DELETE", this.path() + this.confirm());
307
321
  this.ensured = void 0;
322
+ await this.db.transport.request("DELETE", this.path() + this.confirm());
308
323
  }
309
324
  confirm() {
310
325
  return `?confirm=${encodeURIComponent(this.name)}`;
@@ -313,99 +328,84 @@ var Model = class {
313
328
  // NotFoundError for a missing or expired document, and TtlError when the
314
329
  // schema has no expires field.
315
330
  expire(id, duration) {
316
- return this.setExpiry({ _id: id, in: duration });
331
+ return this.setExpiry(id, { in: duration });
317
332
  }
318
333
  expireAt(id, at) {
319
- return this.setExpiry({ _id: id, at: at.toISOString() });
334
+ return this.setExpiry(id, { at: at.toISOString() });
320
335
  }
321
336
  // Removes the expiry of a document (PERSIST in Redis).
322
- async persist(id) {
323
- await this.ready();
324
- return this.db.transport.request("POST", this.path("persist"), { _id: id });
337
+ persist(id) {
338
+ return this.send("POST", docPath("_persist", id));
325
339
  }
326
340
  // The seconds left (rounded up), -1 when the document never expires, null
327
341
  // when it is missing or expired.
328
342
  async ttl(id) {
329
- await this.ready();
330
- try {
331
- const out = await this.db.transport.request("GET", this.path(`${encodeURIComponent(id)}/ttl`));
332
- return out.ttl;
333
- } catch (err) {
334
- if (err instanceof NotFoundError) {
335
- return null;
336
- }
337
- throw err;
338
- }
343
+ const out = await orNull(this.send("GET", docPath("_ttl", id)));
344
+ return out === null ? null : out.ttl;
339
345
  }
340
- async setExpiry(body) {
341
- await this.ready();
342
- const out = await this.db.transport.request("POST", this.path("expire"), body);
343
- return { _id: out._id, v: out.v, expiresAt: new Date(out.expires_at) };
346
+ async setExpiry(id, body) {
347
+ const out = await this.send("POST", docPath("_expire", id), body);
348
+ return { _id: out._id, _v: out._v, expiresAt: new Date(out.expires_at) };
344
349
  }
345
350
  // The document with this _id, or null.
346
351
  async get(id) {
347
- await this.ready();
348
- try {
349
- const doc = await this.db.transport.request("GET", this.path(encodeURIComponent(id)));
350
- return this.decode(doc);
351
- } catch (err) {
352
- if (err instanceof NotFoundError) {
353
- return null;
354
- }
355
- throw err;
356
- }
357
- }
358
- query() {
359
- return new Query(this);
352
+ const doc = await orNull(this.send("GET", docPath("_doc", id)));
353
+ return doc === null ? null : this.decode(doc);
360
354
  }
361
- where(w) {
362
- return this.query().where(w);
355
+ // A search (POST /{coll}/_search). No query matches every document.
356
+ query(w) {
357
+ const q = new Query(this);
358
+ return w === void 0 ? q : q.query(w);
363
359
  }
364
- whereRaw(w) {
365
- return this.query().whereRaw(w);
360
+ // A query built elsewhere, such as from a search form: it is not
361
+ // type-checked and is sent as is; the daemon validates it.
362
+ rawQuery(w) {
363
+ return new Query(this).rawQuery(w);
366
364
  }
367
365
  not(w) {
368
- return this.query().not(w);
366
+ return new Query(this).not(w);
369
367
  }
370
368
  or(...ws) {
371
- return this.query().or(...ws);
369
+ return new Query(this).or(...ws);
372
370
  }
373
371
  sort(spec) {
374
- return this.query().sort(spec);
372
+ return new Query(this).sort(spec);
375
373
  }
376
374
  limit(n) {
377
- return this.query().limit(n);
375
+ return new Query(this).limit(n);
378
376
  }
379
377
  skip(n) {
380
- return this.query().skip(n);
378
+ return new Query(this).skip(n);
381
379
  }
382
380
  page(n, size) {
383
- return this.query().page(n, size);
381
+ return new Query(this).page(n, size);
384
382
  }
385
383
  after(token) {
386
- return this.query().after(token);
384
+ return new Query(this).after(token);
387
385
  }
388
386
  fields(...names) {
389
- return this.query().fields(...names);
387
+ return new Query(this).fields(...names);
390
388
  }
391
389
  all() {
392
- return this.query().all();
390
+ return new Query(this).all();
393
391
  }
394
- count() {
395
- return this.query().count();
392
+ // How many documents match (POST /{coll}/_count).
393
+ count(w) {
394
+ return this.query(w).count();
396
395
  }
397
396
  first() {
398
- return this.query().first();
397
+ return new Query(this).first();
399
398
  }
400
- iterate() {
401
- return this.query().iterate();
399
+ // Every row, page by page.
400
+ each() {
401
+ return new Query(this).each();
402
402
  }
403
- // Runs a find with a whole body built elsewhere, such as from a search
404
- // form: { where, sort, limit, after, count }. The body is sent as is (the
405
- // daemon validates it); rows are decoded like the rows of where().
406
- async find(body) {
403
+ // Runs a whole search body built elsewhere, such as from a search form:
404
+ // { query, sort, limit, after, count }. The body is sent as is (the daemon
405
+ // validates it); rows are decoded like the rows of query().
406
+ async search(body) {
407
407
  await this.ready();
408
- const res = await this.db.transport.stream("POST", this.path("find"), { ...body, versions: true });
408
+ const res = await this.db.transport.stream("POST", this.path("_search"), { ...body, versions: true });
409
409
  const rows = [];
410
410
  for await (const raw of res.rows()) {
411
411
  rows.push(this.decode(raw));
@@ -414,7 +414,7 @@ var Model = class {
414
414
  }
415
415
  async aggregate(pipeline, opts = {}) {
416
416
  await this.ready();
417
- const res = await this.db.transport.stream("POST", this.path("aggregate"), { pipeline, ...opts });
417
+ const res = await this.db.transport.stream("POST", this.path("_aggregate"), { pipeline, ...opts });
418
418
  const rows = [];
419
419
  for await (const row of res.rows()) {
420
420
  rows.push(row);
@@ -423,14 +423,30 @@ var Model = class {
423
423
  }
424
424
  async *watch(consumer, opts = {}) {
425
425
  await this.ready();
426
- const body = { consumer, from: opts.from, limit: opts.limit, where: opts.where === void 0 ? void 0 : toWhere(opts.where) };
427
- const res = await this.db.transport.stream("POST", this.path("watch"), body, opts.signal);
426
+ const body = { consumer, from: opts.from, limit: opts.limit, query: opts.query === void 0 ? void 0 : toWhere(opts.query) };
427
+ const res = await this.db.transport.stream("POST", this.path("_watch"), body, opts.signal);
428
428
  for await (const ev of watchEvents(res.rows(), consumer)) {
429
429
  yield { ...ev, doc: ev.doc && this.decode(ev.doc) };
430
430
  }
431
431
  }
432
432
  async ack(event) {
433
- await this.db.transport.request("POST", this.path("ack"), { consumer: event.consumer, pos: event.pos });
433
+ await this.send("POST", "_ack", { consumer: event.consumer, pos: event.pos });
434
+ }
435
+ // Rows, indexes, files and bytes (GET /{coll}/_stats).
436
+ stats() {
437
+ return this.send("GET", "_stats");
438
+ }
439
+ // Seals the memtable and merges every segment now (POST /{coll}/_compact).
440
+ compact() {
441
+ return this.send("POST", "_compact");
442
+ }
443
+ // Adds a secondary index and builds it in the background. Returns the
444
+ // indexed fields.
445
+ async ensureIndex(field) {
446
+ return (await this.send("PUT", docPath("_indexes", field))).indexes;
447
+ }
448
+ async dropIndex(field) {
449
+ return (await this.send("DELETE", docPath("_indexes", field))).indexes;
434
450
  }
435
451
  decode(raw) {
436
452
  const out = { ...raw };
@@ -442,6 +458,15 @@ var Model = class {
442
458
  }
443
459
  return out;
444
460
  }
461
+ // Declares the collection on first use, then sends one request to
462
+ // /{coll}/{action}.
463
+ async send(method, action, body) {
464
+ await this.ready();
465
+ return this.db.transport.request(method, this.path(action), body);
466
+ }
467
+ updated(out) {
468
+ return { matched: out.matched, modified: out.modified, doc: out.doc && this.decode(out.doc) };
469
+ }
445
470
  catalog() {
446
471
  const entries = Object.entries(this.schema.fields);
447
472
  return {
@@ -452,26 +477,30 @@ var Model = class {
452
477
  };
453
478
  }
454
479
  };
455
- function writeQuery(params) {
456
- const q = new URLSearchParams();
457
- for (const [name, value] of Object.entries(params)) {
458
- if (value !== void 0 && value !== false) {
459
- q.set(name, String(value));
460
- }
461
- }
462
- const text = q.toString();
463
- return text === "" ? "" : `?${text}`;
480
+ function docPath(action, id) {
481
+ return `${action}/${encodeURIComponent(id)}`;
464
482
  }
465
- function mergeUpdate(filter, ops) {
483
+ function checkOps(ops) {
466
484
  for (const key of Object.keys(ops)) {
467
485
  if (!key.startsWith("$")) {
468
486
  throw new InvalidUpdateError("invalid_update", `update operator "${key}" must start with $ (for example "$set")`);
469
487
  }
470
488
  }
471
- return { ...toWhere(filter), ...ops };
489
+ return ops;
490
+ }
491
+ async function orNull(p) {
492
+ try {
493
+ return await p;
494
+ } catch (err) {
495
+ if (err instanceof NotFoundError) {
496
+ return null;
497
+ }
498
+ throw err;
499
+ }
472
500
  }
473
501
 
474
502
  // src/transport.ts
503
+ import { Readable } from "stream";
475
504
  import { Pool } from "undici";
476
505
 
477
506
  // src/ndjson.ts
@@ -506,9 +535,12 @@ var notConnected = /* @__PURE__ */ new Set(["ECONNREFUSED", "ENOENT"]);
506
535
  var retryBaseMs = 50;
507
536
  var retryCapMs = 1e3;
508
537
  var defaultReadyTimeoutMs = 1e4;
509
- var readOps = /\/(find|count|aggregate|get|stats)$/;
538
+ var readOps = /\/(_search|_count|_aggregate)$/;
539
+ var onBun = typeof globalThis.Bun !== "undefined";
510
540
  var Transport = class {
511
541
  client;
542
+ base;
543
+ socketPath;
512
544
  token;
513
545
  readyTimeout;
514
546
  // addr: unix:///path or tcp://host:port. Default: env NDB_URL.
@@ -518,16 +550,20 @@ var Transport = class {
518
550
  this.token = opts.token || process.env.NDB_TOKEN || void 0;
519
551
  this.readyTimeout = opts.readyTimeout ?? defaultReadyTimeoutMs;
520
552
  if (url.protocol === "unix:" && url.pathname !== "") {
521
- this.client = new Pool("http://ndb", { socketPath: decodeURIComponent(url.pathname), connections: 16 });
553
+ this.base = "http://ndb";
554
+ this.socketPath = decodeURIComponent(url.pathname);
522
555
  } else if (url.protocol === "tcp:" && url.host !== "") {
523
- this.client = new Pool(`http://${url.host}`, { connections: 16 });
556
+ this.base = `http://${url.host}`;
524
557
  } else {
525
558
  throw new TypeError(`ndb: address "${target}" must be unix:///path or tcp://host:port`);
526
559
  }
560
+ if (!onBun) {
561
+ this.client = new Pool(this.base, { socketPath: this.socketPath, connections: 16 });
562
+ }
527
563
  }
528
- async stream(method, path, body, signal) {
564
+ async stream(method, path, body, signal, contentType) {
529
565
  const res = await this.withReadyRetry(method, path, signal, async () => {
530
- const r = await this.send(method, path, body, signal);
566
+ const r = await this.send(method, path, body, signal, contentType);
531
567
  if (r.statusCode >= 300) {
532
568
  throw await apiError(r);
533
569
  }
@@ -541,13 +577,14 @@ var Transport = class {
541
577
  header: (name) => headerOf(res.headers, name)
542
578
  };
543
579
  }
544
- async request(method, path, body) {
545
- const res = await this.stream(method, path, body);
580
+ // contentType sends body as raw bytes (blobs) instead of JSON.
581
+ async request(method, path, body, contentType) {
582
+ const res = await this.stream(method, path, body, void 0, contentType);
546
583
  const text = await res.body.setEncoding("utf8").toArray();
547
584
  return text.length > 0 ? JSON.parse(text.join("")) : void 0;
548
585
  }
549
586
  close() {
550
- return this.client.close();
587
+ return this.client?.close() ?? Promise.resolve();
551
588
  }
552
589
  // Runs fn again while the daemon is not ready, with exponential backoff and
553
590
  // jitter. It repeats only when the request certainly was not processed
@@ -570,31 +607,60 @@ var Transport = class {
570
607
  }
571
608
  }
572
609
  }
573
- headers() {
574
- const headers = { "content-type": "application/json" };
610
+ // Rows come as NDJSON: the SDK reads them one by one (jsonRows).
611
+ headers(contentType = "application/json") {
612
+ const headers = { "content-type": contentType, accept: "application/x-ndjson" };
575
613
  if (this.token) {
576
614
  headers.authorization = `Bearer ${this.token}`;
577
615
  }
578
616
  return headers;
579
617
  }
580
- async send(method, path, body, signal) {
618
+ async send(method, path, body, signal, contentType) {
619
+ const raw = contentType !== void 0;
581
620
  const opts = {
582
621
  method,
583
622
  path,
584
623
  signal,
585
- headers: this.headers(),
586
- body: body === void 0 ? void 0 : JSON.stringify(body)
624
+ headers: this.headers(contentType),
625
+ body: body === void 0 ? void 0 : raw ? body : JSON.stringify(body)
587
626
  };
627
+ const client = this.client;
628
+ if (!client) {
629
+ return this.fetchSend(opts);
630
+ }
588
631
  try {
589
- return await this.client.request(opts);
632
+ return await client.request(opts);
590
633
  } catch (err) {
591
634
  if (!isIdempotent(method, path) || !staleSocket.has(err.code ?? "") || signal?.aborted) {
592
635
  throw err;
593
636
  }
594
- return await this.client.request(opts);
637
+ return await client.request(opts);
595
638
  }
596
639
  }
640
+ // fetchSend is send with fetch (Bun). The answer has the shape of an undici
641
+ // answer: the rest of the SDK reads it the same way.
642
+ async fetchSend(opts) {
643
+ const init = { method: opts.method, headers: opts.headers, body: opts.body, signal: opts.signal, unix: this.socketPath };
644
+ const res = await fetch(this.base + opts.path, init);
645
+ const headers = {};
646
+ res.headers.forEach((value, name) => {
647
+ headers[name] = value;
648
+ });
649
+ return { statusCode: res.status, headers, body: readableOf(res) };
650
+ }
597
651
  };
652
+ function readableOf(res) {
653
+ const stream = res.body ? Readable.fromWeb(res.body) : Readable.from([]);
654
+ return Object.assign(stream, {
655
+ arrayBuffer: async () => {
656
+ const bytes = Buffer.concat(await stream.toArray());
657
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
658
+ },
659
+ dump: async () => {
660
+ stream.resume();
661
+ }
662
+ });
663
+ }
598
664
  function isIdempotent(method, path) {
599
665
  return method === "GET" || readOps.test(path.split("?")[0] ?? path);
600
666
  }
@@ -650,11 +716,37 @@ var Ndb = class {
650
716
  constructor(addr, opts) {
651
717
  this.transport = new Transport(addr, opts);
652
718
  }
653
- model(name, schema) {
654
- return new Model(this, name, schema);
719
+ // A collection of the daemon; see Collection. Nothing is sent until its
720
+ // first call.
721
+ collection(name, schema) {
722
+ return new Collection(this, name, schema);
655
723
  }
724
+ // Resolves when the daemon is ready (GET /_health).
656
725
  async health() {
657
- await this.transport.request("GET", "/health");
726
+ await this.transport.request("GET", "/_health");
727
+ }
728
+ // The catalog: one definition per collection (GET /_collections).
729
+ async collections() {
730
+ return (await this.transport.request("GET", "/_collections")).collections;
731
+ }
732
+ // Every collection, totals, version and uptime (GET /_stats).
733
+ stats() {
734
+ return this.transport.request("GET", "/_stats");
735
+ }
736
+ // Disk use per collection and per part (GET /_size).
737
+ size() {
738
+ return this.transport.request("GET", "/_size");
739
+ }
740
+ // Stores bytes in the blob store (POST /_blobs) and returns the reference
741
+ // to put in a document field.
742
+ putBlob(data, contentType = "application/octet-stream") {
743
+ return this.transport.request("POST", "/_blobs", data, contentType);
744
+ }
745
+ // The bytes of a blob (GET /_blobs/{ref}).
746
+ async getBlob(blob) {
747
+ const ref = typeof blob === "string" ? blob : blob.$blob;
748
+ const res = await this.transport.stream("GET", `/_blobs/${encodeURIComponent(ref)}`);
749
+ return new Uint8Array(await res.body.arrayBuffer());
658
750
  }
659
751
  request(method, path, body) {
660
752
  return this.transport.request(method, path, body);
@@ -702,12 +794,12 @@ function seedCounter() {
702
794
  return r[0] & 2047;
703
795
  }
704
796
  export {
797
+ Collection,
705
798
  CursorError,
706
799
  DuplicateIdError,
707
800
  HistoryTruncatedError,
708
801
  InvalidQueryError,
709
802
  InvalidUpdateError,
710
- Model,
711
803
  Ndb,
712
804
  NdbError,
713
805
  NotFoundError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thadeu/ndb",
3
- "version": "0.4.0",
3
+ "version": "1.1.0",
4
4
  "description": "TypeScript client for the ndb daemon",
5
5
  "keywords": [
6
6
  "ndb",