stratus-db 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -8
  2. package/package.json +1 -1
  3. package/stratus-db.js +33 -56
package/README.md CHANGED
@@ -57,22 +57,22 @@ Gateway's 403 Forbidden for a brand-new key, which works within a minute or two
57
57
  - Catalog: `listDbs({deleted})`, `dbByName(name)`, `dbRecord(id)`, `createDb({name, primaryKey})`,
58
58
  `renameDb(id, name)`, `deleteDb(id)`.
59
59
  - A database: `status(id)`, `transact(id, {tx, retract, delete, txId, match})`, `query(id, q)`,
60
- `queryAll(id, q)` (async iterator), `pull(id, pks, {asOf})` (and `asOf: t` in a query: the database as it stood after t), `requestIndex(id, att)`,
60
+ `queryAll(id, q)` (async iterator), `pull(id, pks, {asOf})` (and `asOf: t` in a query: the database as it stood after t), `requestIndex(id, att, {required})` (att an attribute, or an array of 2 to 8 for a
61
+ multi-attribute index: queried by tuples, `["=", ["a", "b"], [x, y]]`; docs/query.md),
61
62
  `awaitIndex(id, att)`, `dropIndex(id, att)`, `history(id, {from, limit})`, `historyAll`,
62
- `fork(id, {t, name})` (hosted: a new database, this one as of t), `excise(id, {rows,
63
+ `fork(id, {t, name})` (a new database, this one as of t), `excise(id, {rows,
63
64
  attributes})` (erasure: rows and {pk, att} out of the state, indexes, history and forks),
64
65
  `setHistoryRetention(id, days)` (history kept 30 to 36500 days, or `null` for forever).
65
- - Keys (hosted, with a key reaching every database): `listKeys()`, `createKey({name, scope})`,
66
+ - Keys (with a key reaching every database): `listKeys()`, `createKey({name, scope})`,
66
67
  `revokeKey(keyId)`.
67
68
  - Codec: `encode`, `decode` (typed-JSON text), `toJSONValue`, `fromJSONValue`.
68
69
 
69
- Queries: docs/query.md ("JSON"). Addressing: `"hosted"` (default: `<url>/v1/dbs/<id>/...`,
70
- `Authorization: Bearer <apiKey>`) or `"function-urls"` (the internal service's Function URLs,
71
- resolved through its catalog; `caller` for a local service).
70
+ Queries: docs/query.md ("JSON"). Requests go to `<url>/v1/dbs/<id>/...` with `Authorization:
71
+ Bearer <apiKey>`.
72
72
 
73
73
  ## Tests
74
74
 
75
75
  `node --test test/ejson.test.js` (the codec: velojity's own output in
76
76
  `clients/fixtures/velojity-ejson.json`, which stratus's client-fixtures-test keeps current), and
77
- `scripts/test-js-client` from the repo root: the codec, then the client against the local
78
- service.
77
+ `scripts/test-js-client` from the repo root: the codec, then the client against the hosted
78
+ product run locally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stratus-db",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "The stratus-db service from JavaScript: typed JSON over fetch, no dependencies.",
5
5
  "homepage": "https://stratus-db.com",
6
6
  "license": "Apache-2.0",
package/stratus-db.js CHANGED
@@ -11,14 +11,11 @@
11
11
  // await s.transact(id, { tx: [{ "order/id": squuid(), "order/status": kw("status/new") }] });
12
12
  // const { rows } = await s.query(id, { where: ["=", "order/status", kw("status/new")] });
13
13
  //
14
- // Addressing: "hosted" (the default) - the catalog at <url>/v1/dbs, a database at
15
- // <url>/v1/dbs/<id>/..., the API key as "Authorization: Bearer <key>". "function-urls" - the
16
- // internal service: the catalog at url, each database at the Function URL its catalog record
17
- // names (looked up, cached for resolveMs, and again when one answers 403 or 404); for testing
18
- // against a local service, opts.caller names the caller (x-stratus-caller).
14
+ // Addressing: the catalog at <url>/v1/dbs, a database at <url>/v1/dbs/<id>/..., the API key as
15
+ // "Authorization: Bearer <key>".
19
16
 
20
17
  /** The client's version: package.json's, which release-js.yml checks against it. */
21
- export const VERSION = "0.2.1";
18
+ export const VERSION = "0.4.0";
22
19
 
23
20
  const USER_AGENT = `stratus-db-js/${VERSION}`;
24
21
  const IN_NODE = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
@@ -532,6 +529,15 @@ function queryString(params) {
532
529
 
533
530
  function attName(att) { return att instanceof Keyword ? att.fullName : String(att).replace(/^:/, ""); }
534
531
 
532
+ // An index's name: an attribute's, or a multi-attribute index's (an array) joined by commas -
533
+ // how the status keys it, and the DELETE path names it.
534
+ function indexName(att) { return Array.isArray(att) ? att.map(attName).join(",") : attName(att); }
535
+
536
+ // The DELETE path of an index: each name's parts encoded, its slashes and commas kept.
537
+ function indexPath(att) {
538
+ return indexName(att).split(",").map((n) => n.split("/").map(encodeURIComponent).join("/")).join(",");
539
+ }
540
+
535
541
  /** A client of the stratus service at opts.url (see the module's comment for opts). */
536
542
  export function connect(opts) {
537
543
  return new Client(opts);
@@ -542,28 +548,20 @@ const GATEWAY_FORBIDDEN = /^\s*\{\s*"message"\s*:\s*"Forbidden"\s*\}\s*$/;
542
548
  export class Client {
543
549
  /**
544
550
  * @param {object} opts
545
- * @param {string} opts.url the service's base URL (hosted), or the catalog's URL (function-urls)
551
+ * @param {string} opts.url the service's base URL
546
552
  * @param {string} [opts.apiKey] sent as "Authorization: Bearer <key>"
547
- * @param {"hosted"|"function-urls"} [opts.addressing]
548
- * @param {string} [opts.caller] x-stratus-caller, for a local service
549
553
  * @param {number} [opts.retries] attempts after a 429 (default 10)
550
554
  * @param {number} [opts.newKeyWaitMs] how long to keep retrying API Gateway's own 403 Forbidden, which a
551
555
  * brand-new key gets for its first minute or so (default 120000; 0: never)
552
- * @param {number} [opts.resolveMs] function-urls: how long a database's URL is cached (5 minutes)
553
556
  * @param {typeof fetch} [opts.fetch] default globalThis.fetch
554
557
  */
555
558
  constructor(opts) {
556
559
  if (!opts?.url) throw new TypeError("connect needs { url }");
557
560
  this.url = trimSlash(opts.url);
558
- this.addressing = opts.addressing ?? "hosted";
559
- if (!["hosted", "function-urls"].includes(this.addressing)) throw new TypeError(`addressing: ${this.addressing}`);
560
561
  this.apiKey = opts.apiKey;
561
- this.caller = opts.caller;
562
562
  this.retries = opts.retries ?? 10;
563
563
  this.newKeyWaitMs = opts.newKeyWaitMs ?? 120000;
564
- this.resolveMs = opts.resolveMs ?? 300000;
565
564
  this.fetch = opts.fetch ?? globalThis.fetch;
566
- this.urls = { at: 0, byId: new Map() };
567
565
  }
568
566
 
569
567
  // -- requests
@@ -574,7 +572,6 @@ export class Client {
574
572
  if (IN_NODE) headers["user-agent"] = USER_AGENT;
575
573
  if (body !== undefined) headers["content-type"] = "application/json";
576
574
  if (this.apiKey) headers.authorization = `Bearer ${this.apiKey}`;
577
- if (this.caller) headers["x-stratus-caller"] = this.caller;
578
575
  const started = Date.now();
579
576
  for (let attempt = 0, forbidden = 0; ; attempt++) {
580
577
  const r = await this.fetch(url, { method, headers, body: body === undefined ? undefined : encode(body) });
@@ -606,42 +603,14 @@ export class Client {
606
603
  throw new StratusError(r.status, isObj ? b.error : null, isObj ? b.message : String(b ?? ""), b);
607
604
  }
608
605
 
609
- catalogUrl(path) {
610
- return this.addressing === "hosted" ? `${this.url}/v1${path}` : `${this.url}${path}`;
611
- }
612
-
613
606
  /** method path body to the catalog: { status, body }. */
614
607
  callCatalog(method, path, body) {
615
- return this.send(this.catalogUrl(path), method, body);
616
- }
617
-
618
- async resolve() {
619
- const recs = Client.ok(await this.callCatalog("GET", "/dbs"));
620
- const byId = new Map();
621
- for (const r of recs) if (r.url) byId.set(String(r.id), r.url);
622
- this.urls = { at: Date.now(), byId };
623
- return byId;
624
- }
625
-
626
- async dbBase(id) {
627
- const key = String(id);
628
- if (this.addressing === "hosted") return `${this.url}/v1/dbs/${encodeURIComponent(key)}`;
629
- const { at, byId } = this.urls;
630
- if (byId.has(key) && Date.now() - at < this.resolveMs) return trimSlash(byId.get(key));
631
- const u = (await this.resolve()).get(key);
632
- return u === undefined ? null : trimSlash(u);
608
+ return this.send(`${this.url}/v1${path}`, method, body);
633
609
  }
634
610
 
635
611
  /** method path body to database id: { status, body }. */
636
- async callDb(id, method, path, body) {
637
- const base = await this.dbBase(id);
638
- if (base === null) return { status: 404, body: { error: "no-database", message: `No database ${id}` } };
639
- const r = await this.send(base + path, method, body);
640
- if (this.addressing === "function-urls" && (r.status === 403 || r.status === 404)) {
641
- const again = (await this.resolve()).get(String(id));
642
- if (again !== undefined && trimSlash(again) !== base) return this.send(trimSlash(again) + path, method, body);
643
- }
644
- return r;
612
+ callDb(id, method, path, body) {
613
+ return this.send(`${this.url}/v1/dbs/${encodeURIComponent(String(id))}${path}`, method, body);
645
614
  }
646
615
 
647
616
  // -- the catalog
@@ -676,7 +645,7 @@ export class Client {
676
645
  return Client.ok(await this.callCatalog("DELETE", `/dbs/${encodeURIComponent(String(id))}`));
677
646
  }
678
647
 
679
- // -- keys (hosted: an account's keys, managed with a key whose scope is every database)
648
+ // -- keys (an account's keys, managed with a key whose scope is every database)
680
649
 
681
650
  /** The account's keys' records (no secrets). */
682
651
  async listKeys() {
@@ -700,7 +669,7 @@ export class Client {
700
669
 
701
670
  /** The database's state: { id, "primary-key", t, "checkpoint-t", indexes, "index-errors", status }. */
702
671
  async status(id) {
703
- return Client.ok(await this.callDb(id, "GET", this.addressing === "hosted" ? "/status" : "/"));
672
+ return Client.ok(await this.callDb(id, "GET", "/status"));
704
673
  }
705
674
 
706
675
  /**
@@ -743,18 +712,26 @@ export class Client {
743
712
  return (Client.ok(await this.callDb(id, "POST", "/pull", asOf === undefined ? { pks } : { pks, "as-of": asOf }))).rows;
744
713
  }
745
714
 
746
- /** Requests a value index on att: { att, state } ("building" until a checkpoint builds it). */
747
- async requestIndex(id, att) {
748
- return Client.ok(await this.callDb(id, "POST", "/indexes", { att: attName(att) }));
715
+ /**
716
+ * Requests a value index on att: { att, state } ("building" until a checkpoint builds it).
717
+ * att is an attribute, or an array of 2 to 8 for a multi-attribute index (its rows keyed by
718
+ * the tuple of their values; queried by tuple predicates), with { required }: the attributes
719
+ * a row needs a value of to be in it.
720
+ */
721
+ async requestIndex(id, att, { required } = {}) {
722
+ const body = { att: Array.isArray(att) ? att.map(attName) : attName(att) };
723
+ if (required !== undefined) body.required = required.map(attName);
724
+ return Client.ok(await this.callDb(id, "POST", "/indexes", body));
749
725
  }
750
726
 
727
+ /** Drops att's index (an attribute, or a multi-attribute index's array). */
751
728
  async dropIndex(id, att) {
752
- return Client.ok(await this.callDb(id, "DELETE", `/indexes/${attName(att).split("/").map(encodeURIComponent).join("/")}`));
729
+ return Client.ok(await this.callDb(id, "DELETE", `/indexes/${indexPath(att)}`));
753
730
  }
754
731
 
755
732
  /** Waits until att's index is ready (rejects if it failed, or after timeoutMs). */
756
733
  async awaitIndex(id, att, { timeoutMs = 60000, intervalMs = 200 } = {}) {
757
- const name = attName(att);
734
+ const name = indexName(att);
758
735
  const deadline = Date.now() + timeoutMs;
759
736
  for (;;) {
760
737
  const s = await this.status(id);
@@ -767,7 +744,7 @@ export class Client {
767
744
  }
768
745
 
769
746
  /**
770
- * Forks database id (hosted): a new database, this one as of t (default its latest), recorded
747
+ * Forks database id: a new database, this one as of t (default its latest), recorded
771
748
  * in both logs. The fork's record ({ id, "fork-of": { db, t }, ... }).
772
749
  */
773
750
  async fork(id, { t, name } = {}) {
@@ -779,7 +756,7 @@ export class Client {
779
756
 
780
757
  /**
781
758
  * Excises rows (primary keys) and attributes ({ pk, att }) - erasure: gone from the current
782
- * state, its indexes and history (and, hosted, the database's forks): { t, rows, attributes }.
759
+ * state, its indexes and history (and the database's forks): { t, rows, attributes }.
783
760
  */
784
761
  async excise(id, { rows, attributes } = {}) {
785
762
  const body = {};