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.
- package/README.md +8 -8
- package/package.json +1 -1
- 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})` (
|
|
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 (
|
|
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").
|
|
70
|
-
|
|
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
|
|
78
|
-
|
|
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
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:
|
|
15
|
-
//
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
637
|
-
|
|
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 (
|
|
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",
|
|
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
|
-
/**
|
|
747
|
-
|
|
748
|
-
|
|
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/${
|
|
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 =
|
|
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
|
|
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
|
|
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 = {};
|