euclid-ndk 0.2.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/LICENSE +201 -0
- package/README.md +603 -0
- package/dist/auth/index.d.ts +18 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +17 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/rfc9421.d.ts +112 -0
- package/dist/auth/rfc9421.d.ts.map +1 -0
- package/dist/auth/rfc9421.js +452 -0
- package/dist/auth/rfc9421.js.map +1 -0
- package/dist/auth/scheme.d.ts +46 -0
- package/dist/auth/scheme.d.ts.map +1 -0
- package/dist/auth/scheme.js +44 -0
- package/dist/auth/scheme.js.map +1 -0
- package/dist/auth/signable.d.ts +37 -0
- package/dist/auth/signable.d.ts.map +1 -0
- package/dist/auth/signable.js +70 -0
- package/dist/auth/signable.js.map +1 -0
- package/dist/auth/sigv4.d.ts +72 -0
- package/dist/auth/sigv4.d.ts.map +1 -0
- package/dist/auth/sigv4.js +227 -0
- package/dist/auth/sigv4.js.map +1 -0
- package/dist/credentials.d.ts +56 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +135 -0
- package/dist/credentials.js.map +1 -0
- package/dist/dto/com.d.ts +101 -0
- package/dist/dto/com.d.ts.map +1 -0
- package/dist/dto/com.js +132 -0
- package/dist/dto/com.js.map +1 -0
- package/dist/dto/eag.d.ts +104 -0
- package/dist/dto/eag.d.ts.map +1 -0
- package/dist/dto/eag.js +70 -0
- package/dist/dto/eag.js.map +1 -0
- package/dist/dto/eam.d.ts +105 -0
- package/dist/dto/eam.d.ts.map +1 -0
- package/dist/dto/eam.js +98 -0
- package/dist/dto/eam.js.map +1 -0
- package/dist/dto/eap.d.ts +88 -0
- package/dist/dto/eap.d.ts.map +1 -0
- package/dist/dto/eap.js +54 -0
- package/dist/dto/eap.js.map +1 -0
- package/dist/dto/ekm.d.ts +109 -0
- package/dist/dto/ekm.d.ts.map +1 -0
- package/dist/dto/ekm.js +78 -0
- package/dist/dto/ekm.js.map +1 -0
- package/dist/dto/ekv.d.ts +73 -0
- package/dist/dto/ekv.d.ts.map +1 -0
- package/dist/dto/ekv.js +45 -0
- package/dist/dto/ekv.js.map +1 -0
- package/dist/dto/ens.d.ts +84 -0
- package/dist/dto/ens.d.ts.map +1 -0
- package/dist/dto/ens.js +72 -0
- package/dist/dto/ens.js.map +1 -0
- package/dist/dto/eqs.d.ts +160 -0
- package/dist/dto/eqs.d.ts.map +1 -0
- package/dist/dto/eqs.js +120 -0
- package/dist/dto/eqs.js.map +1 -0
- package/dist/dto/esm.d.ts +192 -0
- package/dist/dto/esm.d.ts.map +1 -0
- package/dist/dto/esm.js +147 -0
- package/dist/dto/esm.js.map +1 -0
- package/dist/dto/ess.d.ts +49 -0
- package/dist/dto/ess.d.ts.map +1 -0
- package/dist/dto/ess.js +30 -0
- package/dist/dto/ess.js.map +1 -0
- package/dist/dto/json.d.ts +24 -0
- package/dist/dto/json.d.ts.map +1 -0
- package/dist/dto/json.js +47 -0
- package/dist/dto/json.js.map +1 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +73 -0
- package/dist/errors.js.map +1 -0
- package/dist/http/client.d.ts +96 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +258 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/base.d.ts +121 -0
- package/dist/modules/base.d.ts.map +1 -0
- package/dist/modules/base.js +142 -0
- package/dist/modules/base.js.map +1 -0
- package/dist/modules/eag.d.ts +166 -0
- package/dist/modules/eag.d.ts.map +1 -0
- package/dist/modules/eag.js +189 -0
- package/dist/modules/eag.js.map +1 -0
- package/dist/modules/eam.d.ts +314 -0
- package/dist/modules/eam.d.ts.map +1 -0
- package/dist/modules/eam.js +641 -0
- package/dist/modules/eam.js.map +1 -0
- package/dist/modules/eap.d.ts +204 -0
- package/dist/modules/eap.d.ts.map +1 -0
- package/dist/modules/eap.js +218 -0
- package/dist/modules/eap.js.map +1 -0
- package/dist/modules/ekm.d.ts +182 -0
- package/dist/modules/ekm.d.ts.map +1 -0
- package/dist/modules/ekm.js +228 -0
- package/dist/modules/ekm.js.map +1 -0
- package/dist/modules/ekv.d.ts +189 -0
- package/dist/modules/ekv.d.ts.map +1 -0
- package/dist/modules/ekv.js +212 -0
- package/dist/modules/ekv.js.map +1 -0
- package/dist/modules/ens.d.ts +122 -0
- package/dist/modules/ens.d.ts.map +1 -0
- package/dist/modules/ens.js +167 -0
- package/dist/modules/ens.js.map +1 -0
- package/dist/modules/eqs.d.ts +283 -0
- package/dist/modules/eqs.d.ts.map +1 -0
- package/dist/modules/eqs.js +355 -0
- package/dist/modules/eqs.js.map +1 -0
- package/dist/modules/esm.d.ts +358 -0
- package/dist/modules/esm.d.ts.map +1 -0
- package/dist/modules/esm.js +660 -0
- package/dist/modules/esm.js.map +1 -0
- package/dist/modules/ess.d.ts +96 -0
- package/dist/modules/ess.d.ts.map +1 -0
- package/dist/modules/ess.js +110 -0
- package/dist/modules/ess.js.map +1 -0
- package/dist/url.d.ts +31 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +47 -0
- package/dist/url.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EKM - euclid's key management module: encryption keys, and the certificates a deployment serves.
|
|
3
|
+
*
|
|
4
|
+
* One object, {@link EuclidEkm}, built from a session that has already logged in:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const ekm = session.ekm();
|
|
8
|
+
* const key = await ekm.createKey({ description: "customer exports" });
|
|
9
|
+
*
|
|
10
|
+
* const sealed = await ekm.encrypt(key.name, "account 4711");
|
|
11
|
+
* const plain = await ekm.decrypt(key.name, sealed);
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Key material never leaves the server: {@link EuclidEkm.encrypt} and {@link EuclidEkm.decrypt} send the
|
|
15
|
+
* bytes to the key rather than fetching the key to the bytes. That is what makes a key deletable as a
|
|
16
|
+
* unit - and what makes deleting one final, since nothing anywhere else has a copy.
|
|
17
|
+
*
|
|
18
|
+
* A key is named two ways, and they are not interchangeable. `name` is the ID the server minted and is
|
|
19
|
+
* what encrypts, decrypts and is deleted; the ERN is what revokes, describes and tags. Both are on every
|
|
20
|
+
* {@link import("../dto/ekm.js").Key} a listing returns.
|
|
21
|
+
*
|
|
22
|
+
* `encrypt` and `decrypt` carry raw bytes rather than JSON, and present the session's bearer token for
|
|
23
|
+
* the same reason ESM's transfer actions do - see {@link ModuleClient}.
|
|
24
|
+
*/
|
|
25
|
+
import { toPage } from "../dto/eam.js";
|
|
26
|
+
import { toCertificate, toCreateKeyResult, toDeleteCertificateResult, toDeleteKeyResult, toKey, toKeyDescriptionResult, toRevokeKeyResult, } from "../dto/ekm.js";
|
|
27
|
+
import { EuclidServiceError } from "../errors.js";
|
|
28
|
+
import { listPayload, ModuleClient } from "./base.js";
|
|
29
|
+
export const TARGET = "ekm";
|
|
30
|
+
/** The only algorithm the server generates so far; anything else is refused with HTTP 400. */
|
|
31
|
+
export const AES = "AES";
|
|
32
|
+
/**
|
|
33
|
+
* The key length this SDK asks for when the caller does not say. 128 is the other one the server
|
|
34
|
+
* accepts, and what euclid-jdk's no-argument `createKey()` mints; 256 is what euclid itself creates when
|
|
35
|
+
* a bucket asks to be encrypted, which is the better default to inherit.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_KEY_LENGTH = 256;
|
|
38
|
+
/**
|
|
39
|
+
* How long a key scheduled for deletion stays alive by default, in days. The server's own default when
|
|
40
|
+
* the field is left out, restated here because it is the one number in this module that decides whether
|
|
41
|
+
* a mistake can be caught.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_PENDING_WINDOW_DAYS = 7;
|
|
44
|
+
/** The actions that carry raw bytes rather than JSON. */
|
|
45
|
+
export const BYTE_ACTIONS = ["encrypt", "decrypt"];
|
|
46
|
+
/**
|
|
47
|
+
* EKM's operations, on the credentials of the session that created it.
|
|
48
|
+
*
|
|
49
|
+
* Built by {@link EuclidSession.ekm} rather than directly, so that it shares that session's identity,
|
|
50
|
+
* namespace and connection settings - and follows them as they change.
|
|
51
|
+
*/
|
|
52
|
+
export class EuclidEkm extends ModuleClient {
|
|
53
|
+
constructor(session) {
|
|
54
|
+
super(session, { target: TARGET, byteActions: BYTE_ACTIONS });
|
|
55
|
+
}
|
|
56
|
+
// -- keys ------------------------------------------------------------------------------------
|
|
57
|
+
/**
|
|
58
|
+
* Creates a key, and answers with the ID the server minted for it.
|
|
59
|
+
*
|
|
60
|
+
* The description is worth supplying. A key is identified by that generated ID, which says nothing
|
|
61
|
+
* about what the key protects, and a key outlives the reason it was made - so months later this is the
|
|
62
|
+
* only thing that answers whether it can be deleted, and deleting one is not a mistake that can be
|
|
63
|
+
* undone.
|
|
64
|
+
*/
|
|
65
|
+
async createKey(options = {}) {
|
|
66
|
+
return toCreateKeyResult(await this.call("create-key", {
|
|
67
|
+
algorithm: options.algorithm ?? AES,
|
|
68
|
+
length: options.length ?? DEFAULT_KEY_LENGTH,
|
|
69
|
+
description: options.description ?? "",
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
/** One page of keys, and how many exist in total. Never their material. */
|
|
73
|
+
async listKeys(options = {}) {
|
|
74
|
+
return toPage(await this.call("list-keys", listPayload(options, "name")), "keys", toKey);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Schedules a key for deletion, and answers with the date it goes for good.
|
|
78
|
+
*
|
|
79
|
+
* Scheduled rather than immediate, because this is the one action here that cannot be undone by any
|
|
80
|
+
* other: everything the key encrypted - a bucket's objects, a secret's value - becomes unreadable when
|
|
81
|
+
* the date passes, and the window is the only chance anybody gets to notice. A key inside its window
|
|
82
|
+
* still decrypts.
|
|
83
|
+
*
|
|
84
|
+
* Takes the key's ID rather than its ERN, as {@link encrypt} does.
|
|
85
|
+
*/
|
|
86
|
+
async deleteKey(keyId, pendingWindowInDays = DEFAULT_PENDING_WINDOW_DAYS) {
|
|
87
|
+
return toDeleteKeyResult(await this.call("delete-key", { keyId, pendingWindowInDays }));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Stops a key encrypting anything further, without touching what it already wrote.
|
|
91
|
+
*
|
|
92
|
+
* The difference from {@link deleteKey} is that nothing becomes unreadable: a revoked key still
|
|
93
|
+
* decrypts, so this is what to reach for when a key should no longer be used but the data under it is
|
|
94
|
+
* still wanted.
|
|
95
|
+
*
|
|
96
|
+
* Takes the key's ERN rather than its ID.
|
|
97
|
+
*/
|
|
98
|
+
async revokeKey(ern) {
|
|
99
|
+
return toRevokeKeyResult(await this.call("revoke-key", { ern }));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Changes what a key says it is for.
|
|
103
|
+
*
|
|
104
|
+
* Only the description changes: the material, algorithm, length, status and any scheduled deletion are
|
|
105
|
+
* untouched, so describing a key neither prolongs nor shortens its life. An empty string clears the
|
|
106
|
+
* description rather than leaving it alone - otherwise there would be no way to remove one.
|
|
107
|
+
*
|
|
108
|
+
* Takes the key's ERN rather than its ID.
|
|
109
|
+
*/
|
|
110
|
+
async setKeyDescription(ern, description) {
|
|
111
|
+
return toKeyDescriptionResult(await this.call("set-key-description", { ern, description }));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Tags a key. The tag is upserted, so one already there has its value replaced - EKM has no separate
|
|
115
|
+
* set-key-tag action to distinguish the two.
|
|
116
|
+
*/
|
|
117
|
+
async addKeyTag(ern, key, value) {
|
|
118
|
+
await this.call("add-key-tag", { ern, key, value });
|
|
119
|
+
}
|
|
120
|
+
/** Removes a tag from a key. */
|
|
121
|
+
async deleteKeyTag(ern, key) {
|
|
122
|
+
await this.call("delete-key-tag", { ern, key });
|
|
123
|
+
}
|
|
124
|
+
// -- using a key -----------------------------------------------------------------------------
|
|
125
|
+
/**
|
|
126
|
+
* Encrypts bytes with a key the server holds, and answers with `IV || ciphertext || tag`.
|
|
127
|
+
*
|
|
128
|
+
* Those are the exact bytes {@link decrypt} takes back; nothing here needs to be unpacked or
|
|
129
|
+
* re-assembled. Only a key whose status is `AVAILABLE` encrypts - a revoked one, or one scheduled for
|
|
130
|
+
* deletion, is refused with HTTP 403.
|
|
131
|
+
*
|
|
132
|
+
* Takes the key's ID - the `name` {@link createKey} answered with - rather than its ERN.
|
|
133
|
+
*/
|
|
134
|
+
async encrypt(keyId, plaintext) {
|
|
135
|
+
return this.#transform("encrypt", keyId, plaintext);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Decrypts what {@link encrypt} produced.
|
|
139
|
+
*
|
|
140
|
+
* Works for a revoked key and for one scheduled for deletion, right up until its deletion date passes -
|
|
141
|
+
* which is the whole difference between revoking a key and deleting it.
|
|
142
|
+
*/
|
|
143
|
+
async decrypt(keyId, ciphertext) {
|
|
144
|
+
return this.#transform("decrypt", keyId, ciphertext);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* encrypt and decrypt differ only in direction: both send opaque bytes, name their key in a header, and
|
|
148
|
+
* answer with opaque bytes.
|
|
149
|
+
*/
|
|
150
|
+
async #transform(action, keyId, data) {
|
|
151
|
+
const response = await this.postBytes(action, data, { "x-euclid-key-id": keyId });
|
|
152
|
+
if (!response.ok)
|
|
153
|
+
throw new EuclidServiceError(TARGET, action, response.status, response.text);
|
|
154
|
+
return response.content;
|
|
155
|
+
}
|
|
156
|
+
// -- certificates ----------------------------------------------------------------------------
|
|
157
|
+
/**
|
|
158
|
+
* Stores a certificate somebody else issued, together with the private key that proves it.
|
|
159
|
+
*
|
|
160
|
+
* Both halves are required and the server checks them against each other: a certificate stored with a
|
|
161
|
+
* key that is not its own is accepted silently by every step after this one and only shows itself as a
|
|
162
|
+
* handshake that fails for every caller. A mismatch is HTTP 400 here instead.
|
|
163
|
+
*
|
|
164
|
+
* The private key stays with EKM. It goes in and is never handed back - no action returns one.
|
|
165
|
+
*/
|
|
166
|
+
async importCertificate(name, certificatePem, privateKeyPem, description = "") {
|
|
167
|
+
return this.#certificate("import-certificate", {
|
|
168
|
+
name,
|
|
169
|
+
description,
|
|
170
|
+
certificate: certificatePem,
|
|
171
|
+
privateKey: privateKeyPem,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Generates a self-signed certificate, for an installation that has to serve HTTPS before anybody has
|
|
176
|
+
* bought it a real one.
|
|
177
|
+
*
|
|
178
|
+
* Nobody has vouched for the result - {@link import("../dto/ekm.js").Certificate.generated} says so,
|
|
179
|
+
* and a client still has to be told to trust it.
|
|
180
|
+
*/
|
|
181
|
+
async createCertificate(name, options = {}) {
|
|
182
|
+
const payload = {
|
|
183
|
+
name,
|
|
184
|
+
description: options.description ?? "",
|
|
185
|
+
commonName: options.commonName ?? "",
|
|
186
|
+
subjectAltNames: [...(options.subjectAltNames ?? [])],
|
|
187
|
+
};
|
|
188
|
+
// Left out rather than sent as zero, so the server's own defaults - 825 days, 2048 bits - apply.
|
|
189
|
+
if (options.validDays)
|
|
190
|
+
payload["validDays"] = options.validDays;
|
|
191
|
+
if (options.keyBits)
|
|
192
|
+
payload["keyBits"] = options.keyBits;
|
|
193
|
+
return this.#certificate("create-certificate", payload);
|
|
194
|
+
}
|
|
195
|
+
/** One stored certificate, by name. The PEM comes back; the private key does not. */
|
|
196
|
+
async getCertificate(name) {
|
|
197
|
+
return this.#certificate("get-certificate", { name });
|
|
198
|
+
}
|
|
199
|
+
/** One page of certificates, and how many exist in total. */
|
|
200
|
+
async listCertificates(options = {}) {
|
|
201
|
+
const response = await this.call("list-certificates", listPayload(options, "name"));
|
|
202
|
+
return toPage(response, "certificates", toCertificate);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Deletes a certificate, outright and with no grace period.
|
|
206
|
+
*
|
|
207
|
+
* Unlike {@link deleteKey} this needs none: nothing becomes unreadable, because a certificate is
|
|
208
|
+
* public. A listener already serving it keeps the copy it loaded until it is restarted, which is what
|
|
209
|
+
* makes this recoverable - import a replacement under the same name.
|
|
210
|
+
*/
|
|
211
|
+
async deleteCertificate(name) {
|
|
212
|
+
return toDeleteCertificateResult(await this.call("delete-certificate", { name }));
|
|
213
|
+
}
|
|
214
|
+
/** The three actions that answer with one certificate, wrapped in a `certificate` field. */
|
|
215
|
+
async #certificate(action, payload) {
|
|
216
|
+
const response = await this.call(action, payload);
|
|
217
|
+
return toCertificate(response["certificate"]);
|
|
218
|
+
}
|
|
219
|
+
// -- monitoring ------------------------------------------------------------------------------
|
|
220
|
+
/**
|
|
221
|
+
* EKM's own metrics, as the server collects them. Answered unparsed - the shape belongs to the
|
|
222
|
+
* monitoring module rather than to EKM.
|
|
223
|
+
*/
|
|
224
|
+
async metrics() {
|
|
225
|
+
return this.call("get-metrics");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=ekm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ekm.js","sourceRoot":"","sources":["../../src/modules/ekm.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,MAAM,EAAa,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,yBAAyB,EACzB,iBAAiB,EACjB,KAAK,EACL,sBAAsB,EACtB,iBAAiB,GAQlB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAgC,MAAM,WAAW,CAAC;AAGpF,MAAM,CAAC,MAAM,MAAM,GAAG,KAAK,CAAC;AAE5B,8FAA8F;AAC9F,MAAM,CAAC,MAAM,GAAG,GAAG,KAAK,CAAC;AAEzB;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAEtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC;AAE7C,yDAAyD;AACzD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AA6BnD;;;;;GAKG;AACH,MAAM,OAAO,SAAU,SAAQ,YAAY;IACzC,YAAY,OAAsB;QAChC,KAAK,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,+FAA+F;IAE/F;;;;;;;OAOG;IACH,KAAK,CAAC,SAAS,CAAC,UAA4B,EAAE;QAC5C,OAAO,iBAAiB,CACtB,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE;YAC5B,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,GAAG;YACnC,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,kBAAkB;YAC5C,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,EAAE;SACvC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,2EAA2E;IAC3E,KAAK,CAAC,QAAQ,CAAC,UAAuB,EAAE;QACtC,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3F,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,SAAS,CAAC,KAAa,EAAE,mBAAmB,GAAG,2BAA2B;QAC9E,OAAO,iBAAiB,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC,CAAC;IAC1F,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,SAAS,CAAC,GAAW;QACzB,OAAO,iBAAiB,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IACnE,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,iBAAiB,CAAC,GAAW,EAAE,WAAmB;QACtD,OAAO,sBAAsB,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;IAC9F,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,CAAC,GAAW,EAAE,GAAW,EAAE,KAAa;QACrD,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,gCAAgC;IAChC,KAAK,CAAC,YAAY,CAAC,GAAW,EAAE,GAAW;QACzC,MAAM,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,+FAA+F;IAE/F;;;;;;;;OAQG;IACH,KAAK,CAAC,OAAO,CAAC,KAAa,EAAE,SAAgB;QAC3C,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IACtD,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAO,CAAC,KAAa,EAAE,UAA+B;QAC1D,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;IACvD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,MAAc,EAAE,KAAa,EAAE,IAAW;QACzD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC,CAAC;QAClF,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,MAAM,IAAI,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC/F,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC;IAED,+FAA+F;IAE/F;;;;;;;;OAQG;IACH,KAAK,CAAC,iBAAiB,CACrB,IAAY,EACZ,cAAsB,EACtB,aAAqB,EACrB,WAAW,GAAG,EAAE;QAEhB,OAAO,IAAI,CAAC,YAAY,CAAC,oBAAoB,EAAE;YAC7C,IAAI;YACJ,WAAW;YACX,WAAW,EAAE,cAAc;YAC3B,UAAU,EAAE,aAAa;SAC1B,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,iBAAiB,CAAC,IAAY,EAAE,UAAoC,EAAE;QAC1E,MAAM,OAAO,GAA4B;YACvC,IAAI;YACJ,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,EAAE;YACtC,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE;YACpC,eAAe,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC;SACtD,CAAC;QACF,iGAAiG;QACjG,IAAI,OAAO,CAAC,SAAS;YAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC;QAChE,IAAI,OAAO,CAAC,OAAO;YAAE,OAAO,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;QAC1D,OAAO,IAAI,CAAC,YAAY,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC;IAC1D,CAAC;IAED,qFAAqF;IACrF,KAAK,CAAC,cAAc,CAAC,IAAY;QAC/B,OAAO,IAAI,CAAC,YAAY,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,gBAAgB,CAAC,UAAuB,EAAE;QAC9C,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QACpF,OAAO,MAAM,CAAC,QAAQ,EAAE,cAAc,EAAE,aAAa,CAAC,CAAC;IACzD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,iBAAiB,CAAC,IAAY;QAClC,OAAO,yBAAyB,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACpF,CAAC;IAED,4FAA4F;IAC5F,KAAK,CAAC,YAAY,CAAC,MAAc,EAAE,OAAgC;QACjE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAClD,OAAO,aAAa,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,+FAA+F;IAE/F;;;OAGG;IACH,KAAK,CAAC,OAAO;QACX,OAAO,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAClC,CAAC;CACF"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EKV - euclid's key-value store: tables of items, read by key rather than searched.
|
|
3
|
+
*
|
|
4
|
+
* One object, {@link EuclidEkv}, built from a session that has already logged in:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const ekv = session.ekv();
|
|
8
|
+
* await ekv.createTable("sessions", "userId", { sortKey: "startedAt", sortKeyType: KEY_NUMBER });
|
|
9
|
+
*
|
|
10
|
+
* await ekv.putItem("sessions", { userId: "jens", startedAt: 1757462400, host: "laptop" });
|
|
11
|
+
* const recent = await ekv.query("sessions", "jens", { sortOperator: SORT_GE, sortValue: 1757462400 });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A table is keyed on one attribute, or on two: a partition key that identifies an item, and optionally a
|
|
15
|
+
* sort key that orders the items sharing a partition key - which is what makes a partition readable as a
|
|
16
|
+
* range. The key attributes have declared types, and those are what make a range mean what it should: a
|
|
17
|
+
* {@link KEY_NUMBER} sort key orders 2, 9, 10, 100 rather than putting "10" before "9". They cannot be
|
|
18
|
+
* changed after the table is created.
|
|
19
|
+
*
|
|
20
|
+
* An item's other attributes are free-form documents - scalars, arrays, nested objects - and are not
|
|
21
|
+
* declared anywhere. They are also not typed the way a queue message's attributes are: EKV stores what JSON
|
|
22
|
+
* can express, so nothing here takes a {@link import("../dto/com.js").Variant}.
|
|
23
|
+
*/
|
|
24
|
+
import { type Page } from "../dto/eam.js";
|
|
25
|
+
import { type Item, type QueryResult, type ScanResult, type TableDescription } from "../dto/ekv.js";
|
|
26
|
+
import { ModuleClient, type ListOptions } from "./base.js";
|
|
27
|
+
import type { EuclidSession } from "./eam.js";
|
|
28
|
+
export declare const TARGET = "ekv";
|
|
29
|
+
/**
|
|
30
|
+
* The types a key attribute can have. A table's keys are the only attributes with a declared type, and the
|
|
31
|
+
* type is what a comparison is made under.
|
|
32
|
+
*/
|
|
33
|
+
export declare const KEY_STRING = "string";
|
|
34
|
+
export declare const KEY_NUMBER = "number";
|
|
35
|
+
export declare const KEY_BINARY = "binary";
|
|
36
|
+
/**
|
|
37
|
+
* How {@link EuclidEkv.query} narrows by sort key. The default takes the whole partition; the rest need a
|
|
38
|
+
* table that declares a sort key, and asking one that does not is refused with HTTP 400.
|
|
39
|
+
*/
|
|
40
|
+
export declare const WHOLE_PARTITION = "";
|
|
41
|
+
export declare const SORT_EQ = "eq";
|
|
42
|
+
export declare const SORT_LT = "lt";
|
|
43
|
+
export declare const SORT_LE = "le";
|
|
44
|
+
export declare const SORT_GT = "gt";
|
|
45
|
+
export declare const SORT_GE = "ge";
|
|
46
|
+
/** Takes a lower and an upper bound, both inclusive - see {@link EuclidEkv.query}. */
|
|
47
|
+
export declare const SORT_BETWEEN = "between";
|
|
48
|
+
/**
|
|
49
|
+
* The one operator that is not a comparison, and so the one that applies to a string sort key only: a prefix
|
|
50
|
+
* of a number or of a blob is not a thing.
|
|
51
|
+
*/
|
|
52
|
+
export declare const SORT_BEGINS_WITH = "begins-with";
|
|
53
|
+
/** What a table is keyed on, beyond the partition key every table has. */
|
|
54
|
+
export interface CreateTableOptions {
|
|
55
|
+
/** {@link KEY_STRING}, {@link KEY_NUMBER} or {@link KEY_BINARY}. */
|
|
56
|
+
partitionKeyType?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The attribute items sharing a partition key are ordered by, which is what makes a partition readable as
|
|
59
|
+
* a range. Left empty, the table has none and {@link EuclidEkv.query} can only take whole partitions.
|
|
60
|
+
*/
|
|
61
|
+
sortKey?: string;
|
|
62
|
+
/** The sort key's type, which decides what its ordering means. */
|
|
63
|
+
sortKeyType?: string;
|
|
64
|
+
}
|
|
65
|
+
/** How a query narrows a partition, and in what order and quantity it reads it. */
|
|
66
|
+
export interface QueryOptions {
|
|
67
|
+
/**
|
|
68
|
+
* {@link SORT_EQ}, {@link SORT_LT}, {@link SORT_LE}, {@link SORT_GT}, {@link SORT_GE},
|
|
69
|
+
* {@link SORT_BETWEEN} or {@link SORT_BEGINS_WITH}, or {@link WHOLE_PARTITION} for all of it.
|
|
70
|
+
*/
|
|
71
|
+
sortOperator?: string;
|
|
72
|
+
/** What to compare the sort key against - the lower bound for {@link SORT_BETWEEN}. */
|
|
73
|
+
sortValue?: unknown;
|
|
74
|
+
/** The upper bound, for {@link SORT_BETWEEN} only. */
|
|
75
|
+
sortUpper?: unknown;
|
|
76
|
+
/**
|
|
77
|
+
* Whether to read in ascending sort-key order. Always sent, because the server reads an absent flag as
|
|
78
|
+
* descending rather than as "unspecified".
|
|
79
|
+
*/
|
|
80
|
+
forward?: boolean;
|
|
81
|
+
/** The most items to return; 0 means no limit. */
|
|
82
|
+
pageSize?: number;
|
|
83
|
+
/** The zero-based page, applied when `pageSize` is set. */
|
|
84
|
+
pageIndex?: number;
|
|
85
|
+
}
|
|
86
|
+
/** How much of a table a scan reads at a time. */
|
|
87
|
+
export interface ScanOptions {
|
|
88
|
+
pageSize?: number;
|
|
89
|
+
pageIndex?: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* EKV's operations, on the credentials of the session that created it.
|
|
93
|
+
*
|
|
94
|
+
* Built by {@link EuclidSession.ekv} rather than directly, so that it shares that session's identity,
|
|
95
|
+
* namespace and connection settings - and follows them as they change.
|
|
96
|
+
*/
|
|
97
|
+
export declare class EuclidEkv extends ModuleClient {
|
|
98
|
+
constructor(session: EuclidSession);
|
|
99
|
+
/**
|
|
100
|
+
* Creates a table, and answers with it as it was created - with an item count of zero.
|
|
101
|
+
*
|
|
102
|
+
* Refused with HTTP 409 if a table of that name already exists, and with HTTP 400 if a key attribute is
|
|
103
|
+
* empty, starts with `$`, contains `.`, or if the sort key names the same attribute as the partition key.
|
|
104
|
+
*
|
|
105
|
+
* @param name name of the table
|
|
106
|
+
* @param partitionKey the attribute every item is identified by.
|
|
107
|
+
* @param options table options
|
|
108
|
+
*/
|
|
109
|
+
createTable(name: string, partitionKey: string, options?: CreateTableOptions): Promise<TableDescription>;
|
|
110
|
+
/**
|
|
111
|
+
* A table's key, and how many items it holds.
|
|
112
|
+
*
|
|
113
|
+
* The count is counted rather than looked up, so this is not free on a large table.
|
|
114
|
+
*/
|
|
115
|
+
describeTable(name: string): Promise<TableDescription>;
|
|
116
|
+
/** One page of tables, each described as {@link describeTable} would describe it. */
|
|
117
|
+
listTables(options?: ListOptions): Promise<Page<TableDescription>>;
|
|
118
|
+
/**
|
|
119
|
+
* Deletes a table and every item in it, and answers with how many items went with it.
|
|
120
|
+
*
|
|
121
|
+
* There is no confirmation and nothing is kept.
|
|
122
|
+
*/
|
|
123
|
+
deleteTable(name: string): Promise<number>;
|
|
124
|
+
/**
|
|
125
|
+
* Writes an item, replacing whatever was stored under its key.
|
|
126
|
+
*
|
|
127
|
+
* It replaces rather than merges: an item written with two attributes has two attributes afterwards,
|
|
128
|
+
* whatever it had before. So changing one field means reading the item, changing it and writing the whole
|
|
129
|
+
* thing back - which is why {@link import("../dto/ekv.js").Item} keeps the server's timestamps out of its
|
|
130
|
+
* attributes, where they would otherwise be written back as two attributes of the caller's own.
|
|
131
|
+
*
|
|
132
|
+
* The item has to carry the table's key attributes with the types the table declared for them, and no
|
|
133
|
+
* attribute name may be empty, start with `$` or contain `.`.
|
|
134
|
+
*/
|
|
135
|
+
putItem(table: string, item: Record<string, unknown>): Promise<Item>;
|
|
136
|
+
/**
|
|
137
|
+
* Reads one item by its key.
|
|
138
|
+
*
|
|
139
|
+
* The key names the table's key attributes and only those - the partition key alone where the table has no
|
|
140
|
+
* sort key, both where it has one.
|
|
141
|
+
*
|
|
142
|
+
* An item that is not there is HTTP 404, and so a {@link EuclidServiceError} rather than an empty item:
|
|
143
|
+
* "there is no such item" and "here is an item with nothing in it" are different, and a caller should not
|
|
144
|
+
* have to tell them apart. Where a miss is an ordinary outcome, use {@link findItem}.
|
|
145
|
+
*/
|
|
146
|
+
getItem(table: string, key: Record<string, unknown>): Promise<Item>;
|
|
147
|
+
/**
|
|
148
|
+
* The same read as {@link getItem}, answering null rather than throwing when there is no such item.
|
|
149
|
+
*
|
|
150
|
+
* Only a 404 becomes null. A refusal, a malformed key or a table that does not exist still throws, because
|
|
151
|
+
* none of those mean "not there".
|
|
152
|
+
*/
|
|
153
|
+
findItem(table: string, key: Record<string, unknown>): Promise<Item | null>;
|
|
154
|
+
/**
|
|
155
|
+
* Removes one item by its key, and says whether there was one to remove.
|
|
156
|
+
*
|
|
157
|
+
* False rather than an error for a key that names nothing: deleting what is not there has already achieved
|
|
158
|
+
* what the caller asked for.
|
|
159
|
+
*/
|
|
160
|
+
deleteItem(table: string, key: Record<string, unknown>): Promise<boolean>;
|
|
161
|
+
/**
|
|
162
|
+
* Reads the items of one partition, in sort-key order.
|
|
163
|
+
*
|
|
164
|
+
* This is the lookup EKV is for: it addresses a partition by key rather than reading the table. Narrowing
|
|
165
|
+
* by sort key needs a table that declares one, and asking one that does not for anything but
|
|
166
|
+
* {@link WHOLE_PARTITION} is refused with HTTP 400.
|
|
167
|
+
*
|
|
168
|
+
* @param table name of the table
|
|
169
|
+
* @param partitionKey the partition key's value, of the type the table declared for it.
|
|
170
|
+
* @param options table options
|
|
171
|
+
* @throws {Error} if {@link SORT_BETWEEN} was asked for without both bounds, which the server refuses
|
|
172
|
+
* anyway - this just says so before the round trip.
|
|
173
|
+
*/
|
|
174
|
+
query(table: string, partitionKey: unknown, options?: QueryOptions): Promise<QueryResult>;
|
|
175
|
+
/**
|
|
176
|
+
* Reads a table's items without regard to their key.
|
|
177
|
+
*
|
|
178
|
+
* This reads the table rather than an index: fine for a small table or an export, the wrong tool for a
|
|
179
|
+
* lookup - {@link query} is that. A `pageSize` of 0 means no limit, so a scan of a large table with no
|
|
180
|
+
* paging brings all of it back.
|
|
181
|
+
*/
|
|
182
|
+
scan(table: string, options?: ScanOptions): Promise<ScanResult>;
|
|
183
|
+
/**
|
|
184
|
+
* EKV's own metrics, as the server collects them. Answered unparsed - the shape belongs to the monitoring
|
|
185
|
+
* module rather than to EKV.
|
|
186
|
+
*/
|
|
187
|
+
metrics(): Promise<Record<string, unknown>>;
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=ekv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ekv.d.ts","sourceRoot":"","sources":["../../src/modules/ekv.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EKV - euclid's key-value store: tables of items, read by key rather than searched.
|
|
3
|
+
*
|
|
4
|
+
* One object, {@link EuclidEkv}, built from a session that has already logged in:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const ekv = session.ekv();
|
|
8
|
+
* await ekv.createTable("sessions", "userId", { sortKey: "startedAt", sortKeyType: KEY_NUMBER });
|
|
9
|
+
*
|
|
10
|
+
* await ekv.putItem("sessions", { userId: "jens", startedAt: 1757462400, host: "laptop" });
|
|
11
|
+
* const recent = await ekv.query("sessions", "jens", { sortOperator: SORT_GE, sortValue: 1757462400 });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A table is keyed on one attribute, or on two: a partition key that identifies an item, and optionally a
|
|
15
|
+
* sort key that orders the items sharing a partition key - which is what makes a partition readable as a
|
|
16
|
+
* range. The key attributes have declared types, and those are what make a range mean what it should: a
|
|
17
|
+
* {@link KEY_NUMBER} sort key orders 2, 9, 10, 100 rather than putting "10" before "9". They cannot be
|
|
18
|
+
* changed after the table is created.
|
|
19
|
+
*
|
|
20
|
+
* An item's other attributes are free-form documents - scalars, arrays, nested objects - and are not
|
|
21
|
+
* declared anywhere. They are also not typed the way a queue message's attributes are: EKV stores what JSON
|
|
22
|
+
* can express, so nothing here takes a {@link import("../dto/com.js").Variant}.
|
|
23
|
+
*/
|
|
24
|
+
import { toPage } from "../dto/eam.js";
|
|
25
|
+
import { toItem, toQueryResult, toScanResult, toTableDescription, } from "../dto/ekv.js";
|
|
26
|
+
import { EuclidServiceError } from "../errors.js";
|
|
27
|
+
import { listPayload, ModuleClient } from "./base.js";
|
|
28
|
+
export const TARGET = "ekv";
|
|
29
|
+
/**
|
|
30
|
+
* The types a key attribute can have. A table's keys are the only attributes with a declared type, and the
|
|
31
|
+
* type is what a comparison is made under.
|
|
32
|
+
*/
|
|
33
|
+
export const KEY_STRING = "string";
|
|
34
|
+
export const KEY_NUMBER = "number";
|
|
35
|
+
export const KEY_BINARY = "binary";
|
|
36
|
+
/**
|
|
37
|
+
* How {@link EuclidEkv.query} narrows by sort key. The default takes the whole partition; the rest need a
|
|
38
|
+
* table that declares a sort key, and asking one that does not is refused with HTTP 400.
|
|
39
|
+
*/
|
|
40
|
+
export const WHOLE_PARTITION = "";
|
|
41
|
+
export const SORT_EQ = "eq";
|
|
42
|
+
export const SORT_LT = "lt";
|
|
43
|
+
export const SORT_LE = "le";
|
|
44
|
+
export const SORT_GT = "gt";
|
|
45
|
+
export const SORT_GE = "ge";
|
|
46
|
+
/** Takes a lower and an upper bound, both inclusive - see {@link EuclidEkv.query}. */
|
|
47
|
+
export const SORT_BETWEEN = "between";
|
|
48
|
+
/**
|
|
49
|
+
* The one operator that is not a comparison, and so the one that applies to a string sort key only: a prefix
|
|
50
|
+
* of a number or of a blob is not a thing.
|
|
51
|
+
*/
|
|
52
|
+
export const SORT_BEGINS_WITH = "begins-with";
|
|
53
|
+
/** HTTP 404, which is how a read says the item is not there - see {@link EuclidEkv.findItem}. */
|
|
54
|
+
const NOT_FOUND = 404;
|
|
55
|
+
/**
|
|
56
|
+
* EKV's operations, on the credentials of the session that created it.
|
|
57
|
+
*
|
|
58
|
+
* Built by {@link EuclidSession.ekv} rather than directly, so that it shares that session's identity,
|
|
59
|
+
* namespace and connection settings - and follows them as they change.
|
|
60
|
+
*/
|
|
61
|
+
export class EuclidEkv extends ModuleClient {
|
|
62
|
+
constructor(session) {
|
|
63
|
+
super(session, { target: TARGET });
|
|
64
|
+
}
|
|
65
|
+
// -- tables ----------------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Creates a table, and answers with it as it was created - with an item count of zero.
|
|
68
|
+
*
|
|
69
|
+
* Refused with HTTP 409 if a table of that name already exists, and with HTTP 400 if a key attribute is
|
|
70
|
+
* empty, starts with `$`, contains `.`, or if the sort key names the same attribute as the partition key.
|
|
71
|
+
*
|
|
72
|
+
* @param name name of the table
|
|
73
|
+
* @param partitionKey the attribute every item is identified by.
|
|
74
|
+
* @param options table options
|
|
75
|
+
*/
|
|
76
|
+
async createTable(name, partitionKey, options = {}) {
|
|
77
|
+
return toTableDescription(await this.call("create-table", {
|
|
78
|
+
name,
|
|
79
|
+
partitionKey,
|
|
80
|
+
partitionKeyType: options.partitionKeyType ?? KEY_STRING,
|
|
81
|
+
sortKey: options.sortKey ?? "",
|
|
82
|
+
sortKeyType: options.sortKeyType ?? KEY_STRING,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A table's key, and how many items it holds.
|
|
87
|
+
*
|
|
88
|
+
* The count is counted rather than looked up, so this is not free on a large table.
|
|
89
|
+
*/
|
|
90
|
+
async describeTable(name) {
|
|
91
|
+
return toTableDescription(await this.call("describe-table", { name }));
|
|
92
|
+
}
|
|
93
|
+
/** One page of tables, each described as {@link describeTable} would describe it. */
|
|
94
|
+
async listTables(options = {}) {
|
|
95
|
+
const response = await this.call("list-tables", listPayload(options, "name"));
|
|
96
|
+
return toPage(response, "tables", toTableDescription);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Deletes a table and every item in it, and answers with how many items went with it.
|
|
100
|
+
*
|
|
101
|
+
* There is no confirmation and nothing is kept.
|
|
102
|
+
*/
|
|
103
|
+
async deleteTable(name) {
|
|
104
|
+
return this.numberOf("delete-table", { name }, "deletedItems");
|
|
105
|
+
}
|
|
106
|
+
// -- items -----------------------------------------------------------------------------------
|
|
107
|
+
/**
|
|
108
|
+
* Writes an item, replacing whatever was stored under its key.
|
|
109
|
+
*
|
|
110
|
+
* It replaces rather than merges: an item written with two attributes has two attributes afterwards,
|
|
111
|
+
* whatever it had before. So changing one field means reading the item, changing it and writing the whole
|
|
112
|
+
* thing back - which is why {@link import("../dto/ekv.js").Item} keeps the server's timestamps out of its
|
|
113
|
+
* attributes, where they would otherwise be written back as two attributes of the caller's own.
|
|
114
|
+
*
|
|
115
|
+
* The item has to carry the table's key attributes with the types the table declared for them, and no
|
|
116
|
+
* attribute name may be empty, start with `$` or contain `.`.
|
|
117
|
+
*/
|
|
118
|
+
async putItem(table, item) {
|
|
119
|
+
return toItem(await this.call("put-item", { table, item }));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Reads one item by its key.
|
|
123
|
+
*
|
|
124
|
+
* The key names the table's key attributes and only those - the partition key alone where the table has no
|
|
125
|
+
* sort key, both where it has one.
|
|
126
|
+
*
|
|
127
|
+
* An item that is not there is HTTP 404, and so a {@link EuclidServiceError} rather than an empty item:
|
|
128
|
+
* "there is no such item" and "here is an item with nothing in it" are different, and a caller should not
|
|
129
|
+
* have to tell them apart. Where a miss is an ordinary outcome, use {@link findItem}.
|
|
130
|
+
*/
|
|
131
|
+
async getItem(table, key) {
|
|
132
|
+
return toItem(await this.call("get-item", { table, key }));
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The same read as {@link getItem}, answering null rather than throwing when there is no such item.
|
|
136
|
+
*
|
|
137
|
+
* Only a 404 becomes null. A refusal, a malformed key or a table that does not exist still throws, because
|
|
138
|
+
* none of those mean "not there".
|
|
139
|
+
*/
|
|
140
|
+
async findItem(table, key) {
|
|
141
|
+
try {
|
|
142
|
+
return await this.getItem(table, key);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof EuclidServiceError && error.status === NOT_FOUND)
|
|
146
|
+
return null;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Removes one item by its key, and says whether there was one to remove.
|
|
152
|
+
*
|
|
153
|
+
* False rather than an error for a key that names nothing: deleting what is not there has already achieved
|
|
154
|
+
* what the caller asked for.
|
|
155
|
+
*/
|
|
156
|
+
async deleteItem(table, key) {
|
|
157
|
+
const response = await this.call("delete-item", { table, key });
|
|
158
|
+
return response["deleted"] === true;
|
|
159
|
+
}
|
|
160
|
+
// -- reading many ----------------------------------------------------------------------------
|
|
161
|
+
/**
|
|
162
|
+
* Reads the items of one partition, in sort-key order.
|
|
163
|
+
*
|
|
164
|
+
* This is the lookup EKV is for: it addresses a partition by key rather than reading the table. Narrowing
|
|
165
|
+
* by sort key needs a table that declares one, and asking one that does not for anything but
|
|
166
|
+
* {@link WHOLE_PARTITION} is refused with HTTP 400.
|
|
167
|
+
*
|
|
168
|
+
* @param table name of the table
|
|
169
|
+
* @param partitionKey the partition key's value, of the type the table declared for it.
|
|
170
|
+
* @param options table options
|
|
171
|
+
* @throws {Error} if {@link SORT_BETWEEN} was asked for without both bounds, which the server refuses
|
|
172
|
+
* anyway - this just says so before the round trip.
|
|
173
|
+
*/
|
|
174
|
+
async query(table, partitionKey, options = {}) {
|
|
175
|
+
const sortOperator = options.sortOperator ?? WHOLE_PARTITION;
|
|
176
|
+
if (sortOperator === SORT_BETWEEN && (options.sortValue === undefined || options.sortUpper === undefined)) {
|
|
177
|
+
throw new Error("between needs both a sortValue and a sortUpper");
|
|
178
|
+
}
|
|
179
|
+
// Null rather than absent for the two bounds: JSON.stringify drops an undefined field altogether, and
|
|
180
|
+
// the server reads the pair as sent rather than as defaulted.
|
|
181
|
+
return toQueryResult(await this.call("query", {
|
|
182
|
+
table,
|
|
183
|
+
partitionKey,
|
|
184
|
+
sortOperator,
|
|
185
|
+
sortValue: options.sortValue ?? null,
|
|
186
|
+
sortUpper: options.sortUpper ?? null,
|
|
187
|
+
forward: options.forward ?? true,
|
|
188
|
+
pageSize: options.pageSize ?? 0,
|
|
189
|
+
pageIndex: options.pageIndex ?? 0,
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Reads a table's items without regard to their key.
|
|
194
|
+
*
|
|
195
|
+
* This reads the table rather than an index: fine for a small table or an export, the wrong tool for a
|
|
196
|
+
* lookup - {@link query} is that. A `pageSize` of 0 means no limit, so a scan of a large table with no
|
|
197
|
+
* paging brings all of it back.
|
|
198
|
+
*/
|
|
199
|
+
async scan(table, options = {}) {
|
|
200
|
+
const payload = { table, pageSize: options.pageSize ?? 0, pageIndex: options.pageIndex ?? 0 };
|
|
201
|
+
return toScanResult(await this.call("scan", payload));
|
|
202
|
+
}
|
|
203
|
+
// -- monitoring ------------------------------------------------------------------------------
|
|
204
|
+
/**
|
|
205
|
+
* EKV's own metrics, as the server collects them. Answered unparsed - the shape belongs to the monitoring
|
|
206
|
+
* module rather than to EKV.
|
|
207
|
+
*/
|
|
208
|
+
async metrics() {
|
|
209
|
+
return this.call("get-metrics");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=ekv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ekv.js","sourceRoot":"","sources":["../../src/modules/ekv.ts"],"names":[],"mappings":"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"}
|