Skip to content

Commit 5b440c9

Browse files
committed
Added EdgeKV helper lib
1 parent ef8b7b8 commit 5b440c9

File tree

2 files changed

+347
-0
lines changed

2 files changed

+347
-0
lines changed
Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
/*
2+
(c) Copyright 2020 Akamai Technologies, Inc. Licensed under Apache 2 license.
3+
Version: 0.6.0
4+
Purpose: Provide a helper class to simplify the interaction with EdgeKV in an EdgeWorker.
5+
Repo: https://github.com/akamai/edgeworkers-examples/tree/master/edgekv/lib
6+
*/
7+
8+
import { TextDecoderStream } from 'text-encode-transform';
9+
import { WritableStream } from 'streams';
10+
import { httpRequest } from 'http-request';
11+
/**
12+
* You must include edgekv_tokens.js in your bundle for this class to function properly.
13+
* edgekv_tokens.js must include all namespaces you are going to use in the bundle.
14+
*/
15+
import { edgekv_access_tokens } from './edgekv_tokens.js';
16+
17+
export class EdgeKV {
18+
#namespace;
19+
#group;
20+
#edgekv_uri;
21+
#token_override;
22+
#num_retries_on_timeout;
23+
#sandbox_id;
24+
#sandbox_fallback;
25+
26+
/**
27+
* Constructor to allow setting default namespace and group
28+
* These defaults can be overriden when making individual GET, PUT, and DELETE operations
29+
* @param {string} [$0.namepsace="default"] the default namespace to use for all GET, PUT, and DELETE operations
30+
* Namespace must be 32 characters or less, consisting of A-Z a-z 0-9 _ or -
31+
* @param {string} [$0.group="default"] the default group to use for all GET, PUT, and DELETE operations
32+
* Group must be 128 characters or less, consisting of A-Z a-z 0-9 _ or -
33+
* @param {number} [$0.num_retries_on_timeout=0] the number of times to retry a GET requests when the sub request times out
34+
* @param {object} [$0.ew_request=null] passes the request object from the EdgeWorkers event handler to enable access to EdgeKV data in sandbox environments
35+
* @param {boolean} [$0.sandbox_fallback=false] whether to fallback to retrieving staging data if the sandbox data does not exist, instead of returning null or the specified default value
36+
*/
37+
constructor(namespace = "default", group = "default") {
38+
if (typeof namespace === "object") {
39+
this.#namespace = namespace.namespace || "default";
40+
this.#group = namespace.group || "default";
41+
this.#edgekv_uri = namespace.edgekv_uri || "https://edgekv.akamai-edge-svcs.net";
42+
this.#token_override = namespace.token_override || null;
43+
this.#num_retries_on_timeout = namespace.num_retries_on_timeout || 0;
44+
this.#sandbox_id = (namespace.ew_request ? (namespace.ew_request.sandboxId || null) : null);
45+
this.#sandbox_fallback = namespace.sandbox_fallback || false;
46+
} else {
47+
this.#namespace = namespace;
48+
this.#group = group;
49+
this.#edgekv_uri = "https://edgekv.akamai-edge-svcs.net";
50+
this.#token_override = null;
51+
this.#num_retries_on_timeout = 0;
52+
this.#sandbox_id = null;
53+
this.#sandbox_fallback = false;
54+
}
55+
}
56+
57+
throwError(failed_reason, status, body) {
58+
throw {
59+
failed: failed_reason,
60+
status: status,
61+
body: body,
62+
toString: function () { return JSON.stringify(this); }
63+
};
64+
}
65+
66+
async requestHandlerTemplate(http_request, handler_200, handler_large_200, error_text, default_value, num_retries_on_timeout) {
67+
try {
68+
let response = await http_request();
69+
switch (response.status) {
70+
case 200:
71+
// need to handle content length > 128000 bytes differently in EdgeWorkers
72+
let contentLength = response.getHeader('Content-Length');
73+
if (!contentLength || contentLength.length == 0 || contentLength[0] >= 128000) {
74+
return handler_large_200(response);
75+
} else {
76+
return handler_200(response);
77+
}
78+
case 404:
79+
return default_value;
80+
default:
81+
let content = "";
82+
try {
83+
content = await response.text();
84+
content = JSON.parse(content);
85+
} catch (error) { }
86+
throw { status: response.status, body: content }; // to be caught in surrounding catch block
87+
}
88+
} catch (error) {
89+
if (num_retries_on_timeout > 0 && /^.*subrequest to URL.*timed out.*$/.test(error.toString())) {
90+
return this.requestHandlerTemplate(http_request, handler_200, handler_large_200, error_text, default_value, num_retries_on_timeout - 1);
91+
}
92+
if (error.hasOwnProperty('status')) {
93+
this.throwError(error_text + " FAILED", error.status, error.body);
94+
}
95+
this.throwError(error_text + " FAILED", 0, error.toString());
96+
}
97+
}
98+
99+
validate({ namespace = null, group = null, item = null }) {
100+
if (!namespace || !/^[A-Za-z0-9_-]{1,32}$/.test(namespace)) {
101+
throw "Namespace is not valid. Must be 32 characters or less, consisting of A-Z a-z 0-9 _ or -";
102+
}
103+
if (!group || !/^[A-Za-z0-9_-]{1,128}$/.test(group)) {
104+
throw "Group is not valid. Must be 128 characters or less, consisting of A-Z a-z 0-9 _ or -";
105+
}
106+
if (!item || !/^[A-Za-z0-9_-]{1,512}$/.test(item)) {
107+
throw "Item is not valid. Must be 512 characters or less, consisting of A-Z a-z 0-9 _ or -";
108+
}
109+
}
110+
111+
getNamespaceToken(namespace) {
112+
if (this.#token_override) {
113+
return this.#token_override;
114+
}
115+
let name = "namespace-" + namespace;
116+
if (!(name in edgekv_access_tokens)) {
117+
throw "MISSING ACCESS TOKEN. No EdgeKV Access Token defined for namespace '" + namespace + "'.";
118+
}
119+
return edgekv_access_tokens[name]["value"];
120+
}
121+
122+
addTimeout(options, timeout) {
123+
if (timeout && (typeof timeout !== 'number' || !isFinite(timeout) || timeout <= 0 || timeout > 1000)) {
124+
throw "Timeout is not valid. Must be a number greater than 0 and less than 1000.";
125+
}
126+
if (timeout) {
127+
options.timeout = timeout;
128+
}
129+
return options;
130+
}
131+
132+
addSandboxId(uri) {
133+
if (this.#sandbox_id) {
134+
uri = uri + "?sandboxId=" + this.#sandbox_id;
135+
if (this.#sandbox_fallback) {
136+
uri = uri + "&sandboxFallback=true";
137+
}
138+
}
139+
return uri;
140+
}
141+
142+
async streamText(response_body) {
143+
let result = "";
144+
await response_body
145+
.pipeThrough(new TextDecoderStream())
146+
.pipeTo(new WritableStream({
147+
write(chunk) {
148+
result += chunk;
149+
}
150+
}), { preventAbort: true });
151+
return result;
152+
}
153+
154+
async streamJson(response_body) {
155+
return JSON.parse(await this.streamText(response_body));
156+
}
157+
158+
putRequest({ namespace = this.#namespace, group = this.#group, item, value, timeout = null } = {}) {
159+
this.validate({ namespace: namespace, group: group, item: item });
160+
let uri = this.#edgekv_uri + "/api/v1/namespaces/" + namespace + "/groups/" + group + "/items/" + item;
161+
return httpRequest(this.addSandboxId(uri), this.addTimeout({
162+
method: "PUT",
163+
body: typeof value === "object" ? JSON.stringify(value) : value,
164+
headers: { "X-Akamai-EdgeDB-Auth": [this.getNamespaceToken(namespace)] }
165+
}, timeout));
166+
}
167+
168+
/**
169+
* async PUT text into an item in the EdgeKV.
170+
* @param {string} [$0.namepsace=this.#namespace] specify a namespace other than the default
171+
* @param {string} [$0.group=this.#group] specify a group other than the default
172+
* @param {string} $0.item item key to put into the EdgeKV
173+
* @param {string} $0.value text value to put into the EdgeKV
174+
* @param {number} [$0.timeout=null] the maximum time, between 1 and 1000 milliseconds, to wait for the response
175+
* @returns {Promise<string>} if the operation was successful, the response from the EdgeKV
176+
* @throws {object} if the operation was not successful,
177+
* an object describing the non-200 response from the EdgeKV: {failed, status, body}
178+
*/
179+
async putText({ namespace = this.#namespace, group = this.#group, item, value, timeout = null } = {}) {
180+
return this.requestHandlerTemplate(
181+
() => this.putRequest({ namespace: namespace, group: group, item: item, value: value, timeout: timeout }),
182+
(response) => response.text(),
183+
(response) => this.streamText(response.body),
184+
"PUT",
185+
null,
186+
0
187+
);
188+
}
189+
190+
/**
191+
* PUT text into an item in the EdgeKV while only waiting for the request to send and not for the response.
192+
* @param {string} [$0.namepsace=this.#namespace] specify a namespace other than the default
193+
* @param {string} [$0.group=this.#group] specify a group other than the default
194+
* @param {string} $0.item item key to put into the EdgeKV
195+
* @param {string} $0.value text value to put into the EdgeKV
196+
* @throws {object} if the operation was not successful at sending the request,
197+
* an object describing the error: {failed, status, body}
198+
*/
199+
putTextNoWait({ namespace = this.#namespace, group = this.#group, item, value } = {}) {
200+
try {
201+
this.putRequest({ namespace: namespace, group: group, item: item, value: value });
202+
} catch (error) {
203+
this.throwError("PUT FAILED", 0, error.toString());
204+
}
205+
}
206+
207+
/**
208+
* async PUT json into an item in the EdgeKV.
209+
* @param {string} [$0.namepsace=this.#namespace] specify a namespace other than the default
210+
* @param {string} [$0.group=this.#group] specify a group other than the default
211+
* @param {string} $0.item item key to put into the EdgeKV
212+
* @param {object} $0.value json value to put into the EdgeKV
213+
* @param {number} [$0.timeout=null] the maximum time, between 1 and 1000 milliseconds, to wait for the response
214+
* @returns {Promise<string>} if the operation was successful, the response from the EdgeKV
215+
* @throws {object} if the operation was not successful,
216+
* an object describing the non-200 response from the EdgeKV: {failed, status, body}
217+
*/
218+
async putJson({ namespace = this.#namespace, group = this.#group, item, value, timeout = null } = {}) {
219+
return this.requestHandlerTemplate(
220+
() => this.putRequest({ namespace: namespace, group: group, item: item, value: JSON.stringify(value), timeout: timeout }),
221+
(response) => response.text(),
222+
(response) => this.streamText(response.body),
223+
"PUT",
224+
null,
225+
0
226+
);
227+
}
228+
229+
/**
230+
* PUT json into an item in the EdgeKV while only waiting for the request to send and not for the response.
231+
* @param {string} [$0.namepsace=this.#namespace] specify a namespace other than the default
232+
* @param {string} [$0.group=this.#group] specify a group other than the default
233+
* @param {string} $0.item item key to put into the EdgeKV
234+
* @param {object} $0.value json value to put into the EdgeKV
235+
* @throws {object} if the operation was not successful at sending the request,
236+
* an object describing the error: {failed, status, body}
237+
*/
238+
putJsonNoWait({ namespace = this.#namespace, group = this.#group, item, value } = {}) {
239+
try {
240+
this.putRequest({ namespace: namespace, group: group, item: item, value: JSON.stringify(value) });
241+
} catch (error) {
242+
this.throwError("PUT FAILED", 0, error.toString());
243+
}
244+
}
245+
246+
getRequest({ namespace = this.#namespace, group = this.#group, item, timeout = null } = {}) {
247+
this.validate({ namespace: namespace, group: group, item: item });
248+
let uri = this.#edgekv_uri + "/api/v1/namespaces/" + namespace + "/groups/" + group + "/items/" + item;
249+
return httpRequest(this.addSandboxId(uri), this.addTimeout({
250+
method: "GET",
251+
headers: { "X-Akamai-EdgeDB-Auth": [this.getNamespaceToken(namespace)] }
252+
}, timeout));
253+
}
254+
255+
/**
256+
* async GET text from an item in the EdgeKV.
257+
* @param {string} [$0.namepsace=this.#namespace] specify a namespace other than the default
258+
* @param {string} [$0.group=this.#group] specify a group other than the default
259+
* @param {string} $0.item item key to get from the EdgeKV
260+
* @param {string} [$0.default_value=null] the default value to return if a 404 response is returned from EdgeKV
261+
* @param {number} [$0.timeout=null] the maximum time, between 1 and 1000 milliseconds, to wait for the response
262+
* @param {number} [$0.num_retries_on_timeout=null] the number of times to retry a requests when the sub request times out
263+
* @returns {Promise<string>} if the operation was successful, the text response from the EdgeKV or the default_value on 404
264+
* @throws {object} if the operation was not successful,
265+
* an object describing the non-200 and non-404 response from the EdgeKV: {failed, status, body}
266+
*/
267+
async getText({ namespace = this.#namespace, group = this.#group, item, default_value = null, timeout = null, num_retries_on_timeout = null } = {}) {
268+
return this.requestHandlerTemplate(
269+
() => this.getRequest({ namespace: namespace, group: group, item: item, timeout: timeout }),
270+
(response) => response.text(),
271+
(response) => this.streamText(response.body),
272+
"GET TEXT",
273+
default_value,
274+
num_retries_on_timeout ?? this.#num_retries_on_timeout
275+
);
276+
}
277+
278+
/**
279+
* async GET json from an item in the EdgeKV.
280+
* @param {string} [$0.namepsace=this.#namespace] specify a namespace other than the default
281+
* @param {string} [$0.group=this.#group] specify a group other than the default
282+
* @param {string} $0.item item key to get from the EdgeKV
283+
* @param {object} [$0.default_value=null] the default value to return if a 404 response is returned from EdgeKV
284+
* @param {number} [$0.timeout=null] the maximum time, between 1 and 1000 milliseconds, to wait for the response
285+
* @param {number} [$0.num_retries_on_timeout=null] the number of times to retry a requests when the sub request times out
286+
* @returns {Promise<object>} if the operation was successful, the json response from the EdgeKV or the default_value on 404
287+
* @throws {object} if the operation was not successful,
288+
* an object describing the non-200 and non-404 response from the EdgeKV: {failed, status, body}
289+
*/
290+
async getJson({ namespace = this.#namespace, group = this.#group, item, default_value = null, timeout = null, num_retries_on_timeout = null } = {}) {
291+
return this.requestHandlerTemplate(
292+
() => this.getRequest({ namespace: namespace, group: group, item: item, timeout: timeout }),
293+
(response) => response.json(),
294+
(response) => this.streamJson(response.body),
295+
"GET JSON",
296+
default_value,
297+
num_retries_on_timeout ?? this.#num_retries_on_timeout
298+
);
299+
}
300+
301+
deleteRequest({ namespace = this.#namespace, group = this.#group, item, timeout = null } = {}) {
302+
this.validate({ namespace: namespace, group: group, item: item });
303+
let uri = this.#edgekv_uri + "/api/v1/namespaces/" + namespace + "/groups/" + group + "/items/" + item;
304+
return httpRequest(this.addSandboxId(uri), this.addTimeout({
305+
method: "DELETE",
306+
headers: { "X-Akamai-EdgeDB-Auth": [this.getNamespaceToken(namespace)] }
307+
}, timeout));
308+
}
309+
310+
/**
311+
* async DELETE an item in the EdgeKV.
312+
* @param {string} [$0.namepsace=this.#namespace] specify a namespace other than the default
313+
* @param {string} [$0.group=this.#group] specify a group other than the default
314+
* @param {string} $0.item item key to delete from the EdgeKV
315+
* @param {number} [$0.timeout=null] the maximum time, between 1 and 1000 milliseconds, to wait for the response
316+
* @returns {Promise<string>} if the operation was successful, the text response from the EdgeKV
317+
* @throws {object} if the operation was not successful,
318+
* an object describing the non-200 response from the EdgeKV: {failed, status, body}
319+
*/
320+
async delete({ namespace = this.#namespace, group = this.#group, item, timeout = null} = {}) {
321+
return this.requestHandlerTemplate(
322+
() => this.deleteRequest({ namespace: namespace, group: group, item: item, timeout: timeout }),
323+
(response) => response.text(),
324+
(response) => this.streamText(response.body),
325+
"DELETE",
326+
null,
327+
0
328+
);
329+
}
330+
331+
/**
332+
* DELETE an item in the EdgeKV while only waiting for the request to send and not for the response.
333+
* @param {string} [$0.namepsace=this.#namespace] specify a namespace other than the default
334+
* @param {string} [$0.group=this.#group] specify a group other than the default
335+
* @param {string} $0.item item key to delete from the EdgeKV
336+
* @throws {object} if the operation was not successful at sending the request,
337+
* an object describing the error: {failed, status, body}
338+
*/
339+
deleteNoWait({ namespace = this.#namespace, group = this.#group, item } = {}) {
340+
try {
341+
this.delete({ namespace: namespace, group: group, item: item });
342+
} catch (error) {
343+
this.throwError("DELETE FAILED", 0, error.toString());
344+
}
345+
}
346+
}

packages/sdk/akamai/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"noImplicitOverride": true,
1111
"allowSyntheticDefaultImports": true,
1212
"sourceMap": true,
13+
"allowJs": true,
1314
"declaration": true,
1415
"declarationMap": true, // enables importers to jump to source
1516
"resolveJsonModule": true,

0 commit comments

Comments
 (0)