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 && / ^ .* s u b r e q u e s t t o U R L .* t i m e d o u t .* $ / . 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 - Z a - z 0 - 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 - Z a - z 0 - 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 - Z a - z 0 - 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+ }
0 commit comments