stratus-db 0.2.0 → 0.3.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 +24 -52
package/README.md
CHANGED
|
@@ -51,27 +51,27 @@ of the intended type.
|
|
|
51
51
|
## API
|
|
52
52
|
|
|
53
53
|
All methods return promises; failures reject with `StratusError` (`status`, `error` - e.g.
|
|
54
|
-
`"invalid-query"`, `"conflict"` -, `message`). 429s are retried with backoff (`retries`)
|
|
54
|
+
`"invalid-query"`, `"conflict"` -, `message`). 429s are retried with backoff (`retries`), and so is API
|
|
55
|
+
Gateway's 403 Forbidden for a brand-new key, which works within a minute or two (`newKeyWaitMs`, 120000).
|
|
55
56
|
|
|
56
57
|
- Catalog: `listDbs({deleted})`, `dbByName(name)`, `dbRecord(id)`, `createDb({name, primaryKey})`,
|
|
57
58
|
`renameDb(id, name)`, `deleteDb(id)`.
|
|
58
59
|
- A database: `status(id)`, `transact(id, {tx, retract, delete, txId, match})`, `query(id, q)`,
|
|
59
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
61
|
`awaitIndex(id, att)`, `dropIndex(id, att)`, `history(id, {from, limit})`, `historyAll`,
|
|
61
|
-
`fork(id, {t, name})` (
|
|
62
|
+
`fork(id, {t, name})` (a new database, this one as of t), `excise(id, {rows,
|
|
62
63
|
attributes})` (erasure: rows and {pk, att} out of the state, indexes, history and forks),
|
|
63
64
|
`setHistoryRetention(id, days)` (history kept 30 to 36500 days, or `null` for forever).
|
|
64
|
-
- Keys (
|
|
65
|
+
- Keys (with a key reaching every database): `listKeys()`, `createKey({name, scope})`,
|
|
65
66
|
`revokeKey(keyId)`.
|
|
66
67
|
- Codec: `encode`, `decode` (typed-JSON text), `toJSONValue`, `fromJSONValue`.
|
|
67
68
|
|
|
68
|
-
Queries: docs/query.md ("JSON").
|
|
69
|
-
|
|
70
|
-
resolved through its catalog; `caller` for a local service).
|
|
69
|
+
Queries: docs/query.md ("JSON"). Requests go to `<url>/v1/dbs/<id>/...` with `Authorization:
|
|
70
|
+
Bearer <apiKey>`.
|
|
71
71
|
|
|
72
72
|
## Tests
|
|
73
73
|
|
|
74
74
|
`node --test test/ejson.test.js` (the codec: velojity's own output in
|
|
75
75
|
`clients/fixtures/velojity-ejson.json`, which stratus's client-fixtures-test keeps current), and
|
|
76
|
-
`scripts/test-js-client` from the repo root: the codec, then the client against the
|
|
77
|
-
|
|
76
|
+
`scripts/test-js-client` from the repo root: the codec, then the client against the hosted
|
|
77
|
+
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.3.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;
|
|
@@ -537,28 +534,25 @@ export function connect(opts) {
|
|
|
537
534
|
return new Client(opts);
|
|
538
535
|
}
|
|
539
536
|
|
|
537
|
+
const GATEWAY_FORBIDDEN = /^\s*\{\s*"message"\s*:\s*"Forbidden"\s*\}\s*$/;
|
|
538
|
+
|
|
540
539
|
export class Client {
|
|
541
540
|
/**
|
|
542
541
|
* @param {object} opts
|
|
543
|
-
* @param {string} opts.url the service's base URL
|
|
542
|
+
* @param {string} opts.url the service's base URL
|
|
544
543
|
* @param {string} [opts.apiKey] sent as "Authorization: Bearer <key>"
|
|
545
|
-
* @param {"hosted"|"function-urls"} [opts.addressing]
|
|
546
|
-
* @param {string} [opts.caller] x-stratus-caller, for a local service
|
|
547
544
|
* @param {number} [opts.retries] attempts after a 429 (default 10)
|
|
548
|
-
* @param {number} [opts.
|
|
545
|
+
* @param {number} [opts.newKeyWaitMs] how long to keep retrying API Gateway's own 403 Forbidden, which a
|
|
546
|
+
* brand-new key gets for its first minute or so (default 120000; 0: never)
|
|
549
547
|
* @param {typeof fetch} [opts.fetch] default globalThis.fetch
|
|
550
548
|
*/
|
|
551
549
|
constructor(opts) {
|
|
552
550
|
if (!opts?.url) throw new TypeError("connect needs { url }");
|
|
553
551
|
this.url = trimSlash(opts.url);
|
|
554
|
-
this.addressing = opts.addressing ?? "hosted";
|
|
555
|
-
if (!["hosted", "function-urls"].includes(this.addressing)) throw new TypeError(`addressing: ${this.addressing}`);
|
|
556
552
|
this.apiKey = opts.apiKey;
|
|
557
|
-
this.caller = opts.caller;
|
|
558
553
|
this.retries = opts.retries ?? 10;
|
|
559
|
-
this.
|
|
554
|
+
this.newKeyWaitMs = opts.newKeyWaitMs ?? 120000;
|
|
560
555
|
this.fetch = opts.fetch ?? globalThis.fetch;
|
|
561
|
-
this.urls = { at: 0, byId: new Map() };
|
|
562
556
|
}
|
|
563
557
|
|
|
564
558
|
// -- requests
|
|
@@ -569,8 +563,8 @@ export class Client {
|
|
|
569
563
|
if (IN_NODE) headers["user-agent"] = USER_AGENT;
|
|
570
564
|
if (body !== undefined) headers["content-type"] = "application/json";
|
|
571
565
|
if (this.apiKey) headers.authorization = `Bearer ${this.apiKey}`;
|
|
572
|
-
|
|
573
|
-
for (let attempt = 0; ; attempt++) {
|
|
566
|
+
const started = Date.now();
|
|
567
|
+
for (let attempt = 0, forbidden = 0; ; attempt++) {
|
|
574
568
|
const r = await this.fetch(url, { method, headers, body: body === undefined ? undefined : encode(body) });
|
|
575
569
|
const textBody = await r.text();
|
|
576
570
|
if (r.status === 429 && attempt < this.retries) {
|
|
@@ -578,6 +572,12 @@ export class Client {
|
|
|
578
572
|
await sleep(Math.random() * Math.min(5000, 50 * 2 ** attempt));
|
|
579
573
|
continue;
|
|
580
574
|
}
|
|
575
|
+
// API Gateway's own refusal of a key it hasn't learned yet (a new key, its first minute or so):
|
|
576
|
+
// {"message":"Forbidden"} - the service's own errors always carry "error", and aren't retried
|
|
577
|
+
if (r.status === 403 && GATEWAY_FORBIDDEN.test(textBody) && Date.now() - started < this.newKeyWaitMs) {
|
|
578
|
+
await sleep(Math.min(15000, 1000 * 2 ** forbidden++));
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
581
|
const ct = r.headers.get("content-type") ?? "";
|
|
582
582
|
let value = textBody;
|
|
583
583
|
if (ct.startsWith("application/json") && textBody !== "") {
|
|
@@ -594,42 +594,14 @@ export class Client {
|
|
|
594
594
|
throw new StratusError(r.status, isObj ? b.error : null, isObj ? b.message : String(b ?? ""), b);
|
|
595
595
|
}
|
|
596
596
|
|
|
597
|
-
catalogUrl(path) {
|
|
598
|
-
return this.addressing === "hosted" ? `${this.url}/v1${path}` : `${this.url}${path}`;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
597
|
/** method path body to the catalog: { status, body }. */
|
|
602
598
|
callCatalog(method, path, body) {
|
|
603
|
-
return this.send(this.
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
async resolve() {
|
|
607
|
-
const recs = Client.ok(await this.callCatalog("GET", "/dbs"));
|
|
608
|
-
const byId = new Map();
|
|
609
|
-
for (const r of recs) if (r.url) byId.set(String(r.id), r.url);
|
|
610
|
-
this.urls = { at: Date.now(), byId };
|
|
611
|
-
return byId;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
async dbBase(id) {
|
|
615
|
-
const key = String(id);
|
|
616
|
-
if (this.addressing === "hosted") return `${this.url}/v1/dbs/${encodeURIComponent(key)}`;
|
|
617
|
-
const { at, byId } = this.urls;
|
|
618
|
-
if (byId.has(key) && Date.now() - at < this.resolveMs) return trimSlash(byId.get(key));
|
|
619
|
-
const u = (await this.resolve()).get(key);
|
|
620
|
-
return u === undefined ? null : trimSlash(u);
|
|
599
|
+
return this.send(`${this.url}/v1${path}`, method, body);
|
|
621
600
|
}
|
|
622
601
|
|
|
623
602
|
/** method path body to database id: { status, body }. */
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (base === null) return { status: 404, body: { error: "no-database", message: `No database ${id}` } };
|
|
627
|
-
const r = await this.send(base + path, method, body);
|
|
628
|
-
if (this.addressing === "function-urls" && (r.status === 403 || r.status === 404)) {
|
|
629
|
-
const again = (await this.resolve()).get(String(id));
|
|
630
|
-
if (again !== undefined && trimSlash(again) !== base) return this.send(trimSlash(again) + path, method, body);
|
|
631
|
-
}
|
|
632
|
-
return r;
|
|
603
|
+
callDb(id, method, path, body) {
|
|
604
|
+
return this.send(`${this.url}/v1/dbs/${encodeURIComponent(String(id))}${path}`, method, body);
|
|
633
605
|
}
|
|
634
606
|
|
|
635
607
|
// -- the catalog
|
|
@@ -664,7 +636,7 @@ export class Client {
|
|
|
664
636
|
return Client.ok(await this.callCatalog("DELETE", `/dbs/${encodeURIComponent(String(id))}`));
|
|
665
637
|
}
|
|
666
638
|
|
|
667
|
-
// -- keys (
|
|
639
|
+
// -- keys (an account's keys, managed with a key whose scope is every database)
|
|
668
640
|
|
|
669
641
|
/** The account's keys' records (no secrets). */
|
|
670
642
|
async listKeys() {
|
|
@@ -688,7 +660,7 @@ export class Client {
|
|
|
688
660
|
|
|
689
661
|
/** The database's state: { id, "primary-key", t, "checkpoint-t", indexes, "index-errors", status }. */
|
|
690
662
|
async status(id) {
|
|
691
|
-
return Client.ok(await this.callDb(id, "GET",
|
|
663
|
+
return Client.ok(await this.callDb(id, "GET", "/status"));
|
|
692
664
|
}
|
|
693
665
|
|
|
694
666
|
/**
|
|
@@ -755,7 +727,7 @@ export class Client {
|
|
|
755
727
|
}
|
|
756
728
|
|
|
757
729
|
/**
|
|
758
|
-
* Forks database id
|
|
730
|
+
* Forks database id: a new database, this one as of t (default its latest), recorded
|
|
759
731
|
* in both logs. The fork's record ({ id, "fork-of": { db, t }, ... }).
|
|
760
732
|
*/
|
|
761
733
|
async fork(id, { t, name } = {}) {
|
|
@@ -767,7 +739,7 @@ export class Client {
|
|
|
767
739
|
|
|
768
740
|
/**
|
|
769
741
|
* Excises rows (primary keys) and attributes ({ pk, att }) - erasure: gone from the current
|
|
770
|
-
* state, its indexes and history (and
|
|
742
|
+
* state, its indexes and history (and the database's forks): { t, rows, attributes }.
|
|
771
743
|
*/
|
|
772
744
|
async excise(id, { rows, attributes } = {}) {
|
|
773
745
|
const body = {};
|