1+ /* eslint-disable jsdoc/no-undefined-types */
12/*
23Copyright 2024 Adobe. All rights reserved.
34This file is licensed to you under the Apache License, Version 2.0 (the "License");
@@ -9,16 +10,27 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA
910OF ANY KIND, either express or implied. See the License for the specific language
1011governing permissions and limitations under the License.
1112*/
12- const { codes, logAndThrow } = require ( './StateError' )
13- const utils = require ( './utils' )
1413const cloneDeep = require ( 'lodash.clonedeep' )
1514const logger = require ( '@adobe/aio-lib-core-logging' ) ( '@adobe/aio-lib-state' , { provider : 'debug' } )
1615const { HttpExponentialBackoff } = require ( '@adobe/aio-lib-core-networking' )
1716const url = require ( 'node:url' )
1817const { getCliEnv } = require ( '@adobe/aio-lib-env' )
19- const { REGEX_PATTERN_STORE_KEY , HEADER_KEY_EXPIRES , CUSTOM_ENDPOINT , ENDPOINTS , ALLOWED_REGIONS } = require ( './constants' )
2018const Ajv = require ( 'ajv' )
2119
20+ const { codes, logAndThrow } = require ( './StateError' )
21+ const utils = require ( './utils' )
22+ const {
23+ REGEX_PATTERN_STORE_KEY ,
24+ HEADER_KEY_EXPIRES ,
25+ CUSTOM_ENDPOINT ,
26+ ENDPOINTS ,
27+ ALLOWED_REGIONS ,
28+ MAX_LIST_COUNT_HINT ,
29+ REQUEST_ID_HEADER ,
30+ MIN_LIST_COUNT_HINT ,
31+ REGEX_PATTERN_LIST_KEY_MATCH
32+ } = require ( './constants' )
33+
2234/* *********************************** typedefs *********************************** */
2335
2436/**
@@ -92,20 +104,21 @@ async function handleResponse (response, params) {
92104 }
93105
94106 const copyParams = cloneDeep ( params )
107+ copyParams . requestId = response . headers . get ( REQUEST_ID_HEADER )
108+
95109 switch ( response . status ) {
96110 case 404 :
97- // no exception on 404
98111 return response
99112 case 401 :
100- return logAndThrow ( new codes . ERROR_UNAUTHORIZED ( { messageValues : [ 'underlying DB provider ' ] , sdkDetails : copyParams } ) )
113+ return logAndThrow ( new codes . ERROR_UNAUTHORIZED ( { messageValues : [ 'State service ' ] , sdkDetails : copyParams } ) )
101114 case 403 :
102- return logAndThrow ( new codes . ERROR_BAD_CREDENTIALS ( { messageValues : [ 'underlying DB provider ' ] , sdkDetails : copyParams } ) )
115+ return logAndThrow ( new codes . ERROR_BAD_CREDENTIALS ( { messageValues : [ 'State service ' ] , sdkDetails : copyParams } ) )
103116 case 413 :
104- return logAndThrow ( new codes . ERROR_PAYLOAD_TOO_LARGE ( { messageValues : [ 'underlying DB provider ' ] , sdkDetails : copyParams } ) )
117+ return logAndThrow ( new codes . ERROR_PAYLOAD_TOO_LARGE ( { messageValues : [ 'State service ' ] , sdkDetails : copyParams } ) )
105118 case 429 :
106119 return logAndThrow ( new codes . ERROR_REQUEST_RATE_TOO_HIGH ( { sdkDetails : copyParams } ) )
107120 default : // 500 errors
108- return logAndThrow ( new codes . ERROR_INTERNAL ( { messageValues : [ `unexpected response from provider with status: ${ response . status } body: ${ await response . text ( ) } ` ] , sdkDetails : copyParams } ) )
121+ return logAndThrow ( new codes . ERROR_INTERNAL ( { messageValues : [ `unexpected response from State service with status: ${ response . status } body: ${ await response . text ( ) } ` ] , sdkDetails : copyParams } ) )
109122 }
110123}
111124
@@ -159,18 +172,13 @@ class AdobeState {
159172 * Creates a request url.
160173 *
161174 * @private
162- * @param {string } key the key of the state store
175+ * @param {string } containerURLPath defaults to '' to hit the container
176+ * endpoint, add /data/key to hit the key endpoint
163177 * @param {object } queryObject the query variables to send
164178 * @returns {string } the constructed request url
165179 */
166- createRequestUrl ( key , queryObject = { } ) {
167- let urlString
168-
169- if ( key ) {
170- urlString = `${ this . endpoint } /containers/${ this . namespace } /data/${ key } `
171- } else {
172- urlString = `${ this . endpoint } /containers/${ this . namespace } `
173- }
180+ createRequestUrl ( containerURLPath = '' , queryObject = { } ) {
181+ const urlString = `${ this . endpoint } /containers/${ this . namespace } ${ containerURLPath } `
174182
175183 logger . debug ( 'requestUrl string' , urlString )
176184 const requestUrl = new url . URL ( urlString )
@@ -277,7 +285,7 @@ class AdobeState {
277285 }
278286 }
279287 logger . debug ( 'get' , requestOptions )
280- const promise = this . fetchRetry . exponentialBackoff ( this . createRequestUrl ( key ) , requestOptions )
288+ const promise = this . fetchRetry . exponentialBackoff ( this . createRequestUrl ( `/data/ ${ key } ` ) , requestOptions )
281289 const response = await _wrap ( promise , { key } )
282290 if ( response . ok ) {
283291 // we only expect string values
@@ -334,8 +342,11 @@ class AdobeState {
334342
335343 logger . debug ( 'put' , requestOptions )
336344
337- const promise = this . fetchRetry . exponentialBackoff ( this . createRequestUrl ( key , queryParams ) , requestOptions )
338- await _wrap ( promise , { key, value, ...options } )
345+ const promise = this . fetchRetry . exponentialBackoff (
346+ this . createRequestUrl ( `/data/${ key } ` , queryParams ) ,
347+ requestOptions
348+ )
349+ await _wrap ( promise , { key, value, ...options } , true )
339350 return key
340351 }
341352
@@ -358,7 +369,7 @@ class AdobeState {
358369
359370 logger . debug ( 'delete' , requestOptions )
360371
361- const promise = this . fetchRetry . exponentialBackoff ( this . createRequestUrl ( key ) , requestOptions )
372+ const promise = this . fetchRetry . exponentialBackoff ( this . createRequestUrl ( `/data/ ${ key } ` ) , requestOptions )
362373 const response = await _wrap ( promise , { key } )
363374 if ( response . status === 404 ) {
364375 return null
@@ -423,7 +434,7 @@ class AdobeState {
423434 }
424435 }
425436
426- logger . debug ( 'any ' , requestOptions )
437+ logger . debug ( 'stats ' , requestOptions )
427438
428439 const promise = this . fetchRetry . exponentialBackoff ( this . createRequestUrl ( ) , requestOptions )
429440 const response = await _wrap ( promise , { } )
@@ -433,6 +444,84 @@ class AdobeState {
433444 return response . json ( )
434445 }
435446 }
447+
448+ /**
449+ * List keys, returns an iterator. Every iteration returns a batch of
450+ * approximately `countHint` keys.
451+ * @example
452+ * for await (const { keys } of state.list({ match: 'abc*' })) {
453+ * console.log(keys)
454+ * }
455+ * @param {object } options list options
456+ * @param {string } options.match a glob pattern that supports '*' to filter
457+ * keys.
458+ * @param {number } options.countHint an approximate number on how many items
459+ * to return per iteration. Default: 100, min: 10, max: 1000.
460+ * @returns {AsyncGenerator<{ keys: [] }> } an async generator which yields a
461+ * { keys } object at every iteration.
462+ * @memberof AdobeState
463+ */
464+ list ( options = { } ) {
465+ logger . debug ( 'list' , options )
466+ const requestOptions = {
467+ method : 'GET' ,
468+ headers : {
469+ ...this . getAuthorizationHeaders ( )
470+ }
471+ }
472+ logger . debug ( 'list' , requestOptions )
473+
474+ const queryParams = { }
475+ if ( options . match ) {
476+ queryParams . match = options . match
477+ }
478+ if ( options . countHint ) {
479+ queryParams . countHint = options . countHint
480+ }
481+
482+ if ( queryParams . countHint < MIN_LIST_COUNT_HINT || queryParams . countHint > MAX_LIST_COUNT_HINT ) {
483+ logAndThrow ( new codes . ERROR_BAD_ARGUMENT ( {
484+ messageValues : `'countHint' must be in the [${ MIN_LIST_COUNT_HINT } , ${ MAX_LIST_COUNT_HINT } ] range` ,
485+ sdkDetails : { queryParams }
486+ } ) )
487+ }
488+ const schema = {
489+ type : 'object' ,
490+ properties : {
491+ match : { type : 'string' , pattern : REGEX_PATTERN_LIST_KEY_MATCH } , // this is an important check
492+ countHint : { type : 'integer' }
493+ }
494+ }
495+
496+ const { valid, errors } = validate ( schema , queryParams )
497+ if ( ! valid ) {
498+ logAndThrow ( new codes . ERROR_BAD_ARGUMENT ( {
499+ messageValues : utils . formatAjvErrors ( errors ) ,
500+ sdkDetails : { queryParams, errors }
501+ } ) )
502+ }
503+
504+ const stateInstance = this
505+ return ( async function * iter ( ) {
506+ let cursor = 0
507+
508+ do {
509+ const promise = stateInstance . fetchRetry . exponentialBackoff (
510+ stateInstance . createRequestUrl ( '/data' , { ...queryParams , cursor } ) ,
511+ requestOptions
512+ )
513+ const response = await _wrap ( promise , { ...queryParams , cursor } )
514+ if ( response . status === 404 ) {
515+ yield { keys : [ ] }
516+ return
517+ }
518+ const res = await response . json ( )
519+ cursor = res . cursor
520+
521+ yield { keys : res . keys }
522+ } while ( cursor !== 0 )
523+ } ( ) )
524+ }
436525}
437526
438527module . exports = { AdobeState }
0 commit comments