Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ACNA-2585 - library configuration & networking #138

Merged
merged 15 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,20 @@
"NodeJS": true
},
"plugins": ["jest"],
"extends": ["standard", "plugin:jsdoc/recommended", "plugin:jest/recommended"]
"extends": ["standard", "plugin:jsdoc/recommended", "plugin:jest/recommended"],
"settings": {
"jsdoc": {
"ignorePrivate": true
}
},
"rules": {
"jsdoc/tag-lines": [
// The Error level should be `error`, `warn`, or `off` (or 2, 1, or 0)
"error",
"never",
{
"startLines": null
}
]
}
}
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ npm install @adobe/aio-lib-state
```js
const stateLib = require('@adobe/aio-lib-state')

// init when running in an Adobe I/O Runtime action (OpenWhisk) (uses env vars __OW_API_KEY and __OW_NAMESPACE automatically)
// init when running in an Adobe I/O Runtime action (OpenWhisk) (uses env vars __OW_API_KEY and __OW_NAMESPACE automatically. default region is 'amer')
const state = await stateLib.init()
// set an explicit region
const state2 = await stateLib.init({ region: 'apac' })

// get
const res = await state.get('key') // res = { value, expiration }
Expand Down
25 changes: 13 additions & 12 deletions doc/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ Creates or updates a state key-value pair
**Kind**: instance method of [<code>AdobeState</code>](#AdobeState)
**Returns**: <code>Promise.&lt;string&gt;</code> - key

| Param | Type | Default | Description |
| --- | --- | --- | --- |
| key | <code>string</code> | | state key identifier |
| value | <code>string</code> | | state value |
| [options] | [<code>AdobeStatePutOptions</code>](#AdobeStatePutOptions) | <code>{}</code> | put options |
| Param | Type | Description |
| --- | --- | --- |
| key | <code>string</code> | state key identifier |
| value | <code>string</code> | state value |
| [options] | [<code>AdobeStatePutOptions</code>](#AdobeStatePutOptions) | put options |

<a name="AdobeState+delete"></a>

Expand Down Expand Up @@ -138,10 +138,10 @@ OpenWhisk credentials can also be read from environment variables `__OW_NAMESPAC
**Kind**: global function
**Returns**: [<code>Promise.&lt;AdobeState&gt;</code>](#AdobeState) - An AdobeState instance

| Param | Type | Default | Description |
| --- | --- | --- | --- |
| [config] | <code>object</code> | <code>{}</code> | used to init the sdk |
| [config.ow] | [<code>OpenWhiskCredentials</code>](#OpenWhiskCredentials) | | [OpenWhiskCredentials](#OpenWhiskCredentials). Set those if you want to use ootb credentials to access the state management service. OpenWhisk namespace and auth can also be passed through environment variables: `__OW_NAMESPACE` and `__OW_API_KEY` |
| Param | Type | Description |
| --- | --- | --- |
| [config] | <code>object</code> | used to init the sdk |
| [config.ow] | [<code>OpenWhiskCredentials</code>](#OpenWhiskCredentials) | [OpenWhiskCredentials](#OpenWhiskCredentials). Set those if you want to use ootb credentials to access the state management service. OpenWhisk namespace and auth can also be passed through environment variables: `__OW_NAMESPACE` and `__OW_API_KEY` |

<a name="AdobeStateCredentials"></a>

Expand All @@ -155,6 +155,7 @@ AdobeStateCredentials
| --- | --- | --- |
| namespace | <code>string</code> | the state store namespace |
| apikey | <code>string</code> | the state store api key |
| region | <code>&#x27;amer&#x27;</code> \| <code>&#x27;apac&#x27;</code> \| <code>&#x27;emea&#x27;</code> | the region for the Adobe State Store. defaults to 'amer' |

<a name="AdobeStatePutOptions"></a>

Expand All @@ -166,7 +167,7 @@ AdobeState put options

| Name | Type | Description |
| --- | --- | --- |
| ttl | <code>number</code> | time-to-live for key-value pair in seconds, defaults to 24 hours (86400s). Set to < 0 for no expiry. A value of 0 sets default. |
| ttl | <code>number</code> | time-to-live for key-value pair in seconds, defaults to 24 hours (86400s). Set to < 0 for max ttl of one year. A value of 0 sets default. |
shazron marked this conversation as resolved.
Show resolved Hide resolved

<a name="AdobeStateGetReturnValue"></a>

Expand All @@ -178,8 +179,8 @@ AdobeState get return object

| Name | Type | Description |
| --- | --- | --- |
| expiration | <code>string</code> \| <code>null</code> | ISO date string of expiration time for the key-value pair, if the ttl is infinite expiration=null |
| value | <code>any</code> | the value set by put |
| expiration | <code>string</code> | the ISO-8601 date string of the expiration time for the key-value pair |
| value | <code>string</code> | the value set by put |

<a name="OpenWhiskCredentials"></a>

Expand Down
17 changes: 12 additions & 5 deletions e2e/e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('e2e tests using OpenWhisk credentials (as env vars)', () => {

expect(await state.get(testKey)).toEqual(undefined)
expect(await state.put(testKey, testValue)).toEqual(testKey)
expect(await state.get(testKey)).toEqual(expect.objectContaining({ value: testValue }))
expect(await state.get(testKey)).toEqual(expect.objectContaining({ value: testValue, expiration: expect.any(String) }))
expect(await state.delete(testKey)).toEqual(testKey)
expect(await state.get(testKey)).toEqual(undefined)
expect(await state.any()).toEqual(false)
Expand All @@ -93,14 +93,21 @@ describe('e2e tests using OpenWhisk credentials (as env vars)', () => {
expect(resTime).toBeLessThanOrEqual(new Date(Date.now() + 86400000).getTime()) // 86400000 ms = 1 day
expect(resTime).toBeGreaterThanOrEqual(new Date(Date.now() + 86400000 - 10000).getTime()) // give more or less 10 seconds clock skew + request time

// 2. test max ttl
// 2. test ttl = 0 (should default to default ttl of 1 day)
expect(await state.put(testKey, testValue, { ttl: 0 })).toEqual(testKey)
res = await state.get(testKey)
resTime = new Date(res.expiration).getTime()
expect(resTime).toBeLessThanOrEqual(new Date(Date.now() + 86400000).getTime()) // 86400000 ms = 1 day
expect(resTime).toBeGreaterThanOrEqual(new Date(Date.now() + 86400000 - 10000).getTime()) // give more or less 10 seconds clock skew + request time

// 3. test max ttl
const nowPlus365Days = new Date(MAX_TTL_SECONDS).getTime()
expect(await state.put(testKey, testValue, { ttl: -1 })).toEqual(testKey)
res = await state.get(testKey)
resTime = new Date(res.expiration).getTime()
expect(resTime).toBeGreaterThanOrEqual(nowPlus365Days)

// 3. test that after ttl object is deleted
// 4. test that after ttl object is deleted
expect(await state.put(testKey, testValue, { ttl: 2 })).toEqual(testKey)
res = await state.get(testKey)
expect(new Date(res.expiration).getTime()).toBeLessThanOrEqual(new Date(Date.now() + 2000).getTime())
Expand All @@ -111,8 +118,8 @@ describe('e2e tests using OpenWhisk credentials (as env vars)', () => {
test('throw error when get/put with invalid keys', async () => {
const invalidKey = 'some/invalid/key'
const state = await initStateEnv()
await expect(state.put(invalidKey, 'testValue')).rejects.toThrow('[AdobeStateLib:ERROR_BAD_ARGUMENT] invalid key and/or value')
await expect(state.get(invalidKey)).rejects.toThrow('[AdobeStateLib:ERROR_BAD_ARGUMENT] invalid key')
await expect(state.put(invalidKey, 'testValue')).rejects.toThrow('[AdobeStateLib:ERROR_BAD_ARGUMENT] /key must match pattern "^[a-zA-Z0-9-_-]{1,1024}$"')
await expect(state.get(invalidKey)).rejects.toThrow('[AdobeStateLib:ERROR_BAD_ARGUMENT] /key must match pattern "^[a-zA-Z0-9-_-]{1,1024}$"')
})

test('isolation tests: get, write, delete on same key for two namespaces do not interfere', async () => {
Expand Down
49 changes: 34 additions & 15 deletions lib/AdobeState.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const logger = require('@adobe/aio-lib-core-logging')('@adobe/aio-lib-state', {
const { HttpExponentialBackoff } = require('@adobe/aio-lib-core-networking')
const url = require('node:url')
const { getCliEnv } = require('@adobe/aio-lib-env')
const { ADOBE_STATE_STORE_ENDPOINT, REGEX_PATTERN_STORE_KEY } = require('./constants')
const { ADOBE_STATE_STORE_ENDPOINT, REGEX_PATTERN_STORE_KEY, API_VERSION, ADOBE_STATE_STORE_REGIONS, HEADER_KEY_EXPIRES } = require('./constants')
const Ajv = require('ajv')

/* *********************************** typedefs *********************************** */
Expand All @@ -28,14 +28,15 @@ const Ajv = require('ajv')
* @type {object}
* @property {string} namespace the state store namespace
* @property {string} apikey the state store api key
* @property {('amer'|'apac'|'emea')} region the region for the Adobe State Store. defaults to 'amer'
*/

/**
* AdobeState put options
*
* @typedef AdobeStatePutOptions
* @type {object}
* @property {number} ttl time-to-live for key-value pair in seconds, defaults to 24 hours (86400s). Set to < 0 for no expiry. A
* @property {number} ttl time-to-live for key-value pair in seconds, defaults to 24 hours (86400s). Set to < 0 for max ttl of one year. A
* value of 0 sets default.
*/

Expand All @@ -44,9 +45,8 @@ const Ajv = require('ajv')
*
* @typedef AdobeStateGetReturnValue
* @type {object}
* @property {string|null} expiration ISO date string of expiration time for the key-value pair, if the ttl is infinite
* expiration=null
* @property {any} value the value set by put
* @property {string} expiration the ISO-8601 date string of the expiration time for the key-value pair
* @property {string} value the value set by put
*/

/* *********************************** helpers *********************************** */
Expand Down Expand Up @@ -118,13 +118,16 @@ class AdobeState {
* @private
* @param {string} namespace the namespace for the Adobe State Store
* @param {string} apikey the apikey for the Adobe State Store
* @param {('amer'|'apac'|'emea')} [region] the region for the Adobe State Store. defaults to 'amer'
*/
constructor (namespace, apikey) {
constructor (namespace, apikey, region) {
/** @private */
this.namespace = namespace
/** @private */
this.apikey = apikey
/** @private */
this.region = region
/** @private */
this.endpoint = ADOBE_STATE_STORE_ENDPOINT[getCliEnv()]
/** @private */
this.fetchRetry = new HttpExponentialBackoff()
Expand All @@ -139,14 +142,19 @@ class AdobeState {
* @returns {string} the constructed request url
*/
createRequestUrl (key, queryObject = {}) {
let requestUrl
const isLocal = this.endpoint.startsWith('localhost') || this.endpoint.startsWith('127.0.0.1')
const protocol = isLocal ? 'http' : 'https'
const regionSubdomain = isLocal ? '' : `${this.region}.`
let urlString

if (key) {
requestUrl = new url.URL(`${this.endpoint}/v1/containers/${this.namespace}/data/${key}`)
urlString = `${protocol}://${regionSubdomain}${this.endpoint}/${API_VERSION}/containers/${this.namespace}/data/${key}`
} else {
requestUrl = new url.URL(`${this.endpoint}/v1/containers/${this.namespace}`)
urlString = `${protocol}://${regionSubdomain}${this.endpoint}/${API_VERSION}/containers/${this.namespace}`
}

logger.debug('requestUrl string', urlString)
const requestUrl = new url.URL(urlString)
// add the query params
requestUrl.search = (new url.URLSearchParams(queryObject)).toString()
return requestUrl.toString()
Expand Down Expand Up @@ -184,9 +192,17 @@ class AdobeState {
const cloned = utils.withHiddenFields(credentials, ['apikey'])
logger.debug(`init AdobeState with ${JSON.stringify(cloned, null, 2)}`)

if (!credentials.region) {
credentials.region = ADOBE_STATE_STORE_REGIONS.at(0) // first item is the default
}

const schema = {
type: 'object',
properties: {
region: {
type: 'string',
enum: ADOBE_STATE_STORE_REGIONS
},
apikey: { type: 'string' },
namespace: { type: 'string' }
},
Expand All @@ -196,12 +212,12 @@ class AdobeState {
const { valid, errors } = validate(schema, credentials)
if (!valid) {
logAndThrow(new codes.ERROR_BAD_ARGUMENT({
messageValues: ['apikey and/or namespace is missing', JSON.stringify(errors, null, 2)],
messageValues: utils.formatAjvErrors(errors),
sdkDetails: cloned
}))
}

return new AdobeState(credentials.namespace, credentials.apikey)
return new AdobeState(credentials.namespace, credentials.apikey, credentials.region)
}

/* **************************** ADOBE STATE STORE OPERATORS ***************************** */
Expand Down Expand Up @@ -230,7 +246,7 @@ class AdobeState {
const { valid, errors } = validate(schema, { key })
if (!valid) {
logAndThrow(new codes.ERROR_BAD_ARGUMENT({
messageValues: ['invalid key', JSON.stringify(errors, null, 2)],
messageValues: utils.formatAjvErrors(errors),
sdkDetails: { key, errors }
}))
}
Expand All @@ -246,7 +262,10 @@ class AdobeState {
const response = await _wrap(promise, { key })
if (response) {
// we only expect string values
return response.json()
const value = await response.text()
const expiration = new Date(Number(response.headers.get(HEADER_KEY_EXPIRES))).toISOString()

return { value, expiration }
}
}

Expand All @@ -255,7 +274,7 @@ class AdobeState {
*
* @param {string} key state key identifier
* @param {string} value state value
* @param {AdobeStatePutOptions} [options={}] put options
* @param {AdobeStatePutOptions} [options] put options
* @returns {Promise<string>} key
* @memberof AdobeState
*/
Expand All @@ -278,7 +297,7 @@ class AdobeState {
const { valid, errors } = validate(schema, { key, value })
if (!valid) {
logAndThrow(new codes.ERROR_BAD_ARGUMENT({
messageValues: ['invalid key and/or value', JSON.stringify(errors, null, 2)],
messageValues: utils.formatAjvErrors(errors),
sdkDetails: { key, value, options, errors }
}))
}
Expand Down
28 changes: 23 additions & 5 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,47 @@ governing permissions and limitations under the License.
*/

const { PROD_ENV, STAGE_ENV } = require('@adobe/aio-lib-env')
const { isInternalToAdobeRuntime } = require('./utils')

// gets these values if the keys are set in the environment, if not it will use the defaults set
// omit the protocol (https)
const {
ADOBE_STATE_STORE_ENDPOINT_PROD = 'https://storage-state-amer.app-builder.adp.adobe.io',
ADOBE_STATE_STORE_ENDPOINT_STAGE = 'http://storage-state-amer.stg.app-builder.corp.adp.adobe.io'
ADOBE_STATE_STORE_ENDPOINT_PROD = 'storage-state-amer.app-builder.adp.adobe.io',
ADOBE_STATE_STORE_ENDPOINT_STAGE = 'storage-state-amer.stg.app-builder.corp.adp.adobe.io',
ADOBE_STATE_STORE_ENDPOINT_PROD_INTERNAL = 'storage-state-amer.app-builder.int.adp.adobe.io',
ADOBE_STATE_STORE_ENDPOINT_STAGE_INTERNAL = 'storage-state-amer.stg.app-builder.int.adp.adobe.io',
API_VERSION = 'v1beta1',
ADOBE_STATE_STORE_REGIONS = [ // first region is the default region
'amer',
'apac',
'emea'
]
} = process.env

const ADOBE_STATE_STORE_ENDPOINT = {
[PROD_ENV]: ADOBE_STATE_STORE_ENDPOINT_PROD,
[STAGE_ENV]: ADOBE_STATE_STORE_ENDPOINT_STAGE
[PROD_ENV]: isInternalToAdobeRuntime() ? ADOBE_STATE_STORE_ENDPOINT_PROD_INTERNAL : ADOBE_STATE_STORE_ENDPOINT_PROD,
[STAGE_ENV]: isInternalToAdobeRuntime() ? ADOBE_STATE_STORE_ENDPOINT_STAGE_INTERNAL : ADOBE_STATE_STORE_ENDPOINT_STAGE
shazron marked this conversation as resolved.
Show resolved Hide resolved
}

const MAX_KEY_SIZE = 1024 * 1 // 1KB
const MAX_TTL_SECONDS = 60 * 60 * 24 * 365 // 365 days
const HEADER_KEY_EXPIRES = 'x-key-expires-ms'

const REGEX_PATTERN_STORE_NAMESPACE = '^(development-)?([0-9]{3,10})-([a-z0-9]{1,20})(-([a-z0-9]{1,20}))?$'
// The regex for keys, allowed chars are alphanumerical with _ and -
const REGEX_PATTERN_STORE_KEY = `^[a-zA-Z0-9-_-]{1,${MAX_KEY_SIZE}}$`

module.exports = {
ADOBE_STATE_STORE_REGIONS,
ADOBE_STATE_STORE_ENDPOINT_PROD,
ADOBE_STATE_STORE_ENDPOINT_STAGE,
ADOBE_STATE_STORE_ENDPOINT_PROD_INTERNAL,
ADOBE_STATE_STORE_ENDPOINT_STAGE_INTERNAL,
API_VERSION,
MAX_KEY_SIZE,
MAX_TTL_SECONDS,
REGEX_PATTERN_STORE_NAMESPACE,
REGEX_PATTERN_STORE_KEY,
ADOBE_STATE_STORE_ENDPOINT
ADOBE_STATE_STORE_ENDPOINT,
HEADER_KEY_EXPIRES
}
4 changes: 2 additions & 2 deletions lib/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const { AdobeState } = require('./AdobeState')
*
* OpenWhisk credentials can also be read from environment variables `__OW_NAMESPACE` and `__OW_API_KEY`.
*
* @param {object} [config={}] used to init the sdk
* @param {object} [config] used to init the sdk
* @param {OpenWhiskCredentials} [config.ow]
* {@link OpenWhiskCredentials}. Set those if you want
* to use ootb credentials to access the state management service. OpenWhisk
Expand All @@ -50,7 +50,7 @@ async function init (config = {}) {
logger.debug(`init with config: ${JSON.stringify(logConfig, null, 2)}`)

const { auth: apikey, namespace } = (config.ow ?? {})
return AdobeState.init({ apikey, namespace })
return AdobeState.init({ apikey, namespace, region: config.region })
}

module.exports = { init }
Loading
Loading