├── demo.png ├── app.config.yaml ├── .babelrc ├── dot-env ├── .gitignore ├── src └── dx-excshell-1 │ ├── web-src │ ├── index.html │ └── src │ │ ├── components │ │ ├── SideBar.js │ │ ├── About.js │ │ ├── App.js │ │ ├── Home.js │ │ └── ActionsForm.js │ │ ├── exc-runtime.js │ │ ├── utils.js │ │ ├── index.css │ │ └── index.js │ ├── e2e │ ├── send-promo.e2e.test.js │ ├── generate-code.e2e.test.js │ └── get-profiles.e2e.test.js │ ├── ext.config.yaml │ ├── actions │ ├── generate-code │ │ └── index.js │ ├── get-profiles │ │ └── index.js │ ├── send-promo │ │ └── index.js │ └── utils.js │ └── test │ ├── generate-code.test.js │ ├── send-promo.test.js │ ├── get-profiles.test.js │ └── utils.test.js ├── jest.setup.js ├── package.json └── README.md /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdobeDocs/adobeio-samples-customers-dashboard/master/demo.png -------------------------------------------------------------------------------- /app.config.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | dx/excshell/1: 3 | $include: src/dx-excshell-1/ext.config.yaml 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-transform-react-jsx" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /dot-env: -------------------------------------------------------------------------------- 1 | # Specify your secrets here 2 | # This file should not be committed to source control 3 | 4 | ## please provide your Adobe I/O Runtime credentials 5 | AIO_runtime_auth= 6 | AIO_runtime_namespace= 7 | 8 | ## please provide your Adobe I/O Campaign Standard integration tenant and api key 9 | SERVICE_API_KEY= 10 | CAMPAIGN_STANDARD_TENANT= 11 | CAMPAIGN_STANDARD_WORKFLOW_ID= 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # package directories 3 | node_modules 4 | jspm_packages 5 | 6 | # build 7 | build 8 | dist 9 | .manifest-dist.yml 10 | 11 | # Config 12 | config.json 13 | .env* 14 | .aio 15 | 16 | # Adobe I/O console config 17 | console.json 18 | 19 | # Test output 20 | junit.xml 21 | 22 | # IDE & Temp 23 | .cache 24 | .idea 25 | .nyc_output 26 | .vscode 27 | coverage 28 | .aws.tmp.creds.json 29 | .wskdebug.props.tmp 30 | 31 | # Parcel 32 | .parcel-cache 33 | 34 | # OSX 35 | .DS_Store 36 | 37 | # yeoman 38 | .yo-repository 39 | 40 | # logs folder for aio-run-detached 41 | logs 42 | 43 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Customer Dashboard 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | jest.setTimeout(10000) 14 | 15 | beforeEach(() => { }) 16 | afterEach(() => { }) 17 | -------------------------------------------------------------------------------- /src/dx-excshell-1/e2e/send-promo.e2e.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | const { Config } = require('@adobe/aio-sdk').Core 6 | const fs = require('fs') 7 | const fetch = require('node-fetch') 8 | 9 | // get action url 10 | const namespace = Config.get('runtime.namespace') 11 | const hostname = Config.get('cna.hostname') || 'adobeioruntime.net' 12 | const packagejson = JSON.parse(fs.readFileSync('package.json').toString()) 13 | const runtimePackage = `${packagejson.name}-${packagejson.version}` 14 | const actionUrl = `https://${namespace}.${hostname}/api/v1/web/${runtimePackage}/send-promo` 15 | 16 | // The deployed actions are secured with the `require-adobe-auth` annotation. 17 | // If the authorization header is missing, Adobe I/O Runtime returns with a 401 before the action is executed. 18 | test('returns a 401 when missing Authorization header', async () => { 19 | const res = await fetch(actionUrl) 20 | expect(res).toEqual(expect.objectContaining({ 21 | status: 401 22 | })) 23 | }) 24 | -------------------------------------------------------------------------------- /src/dx-excshell-1/e2e/generate-code.e2e.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | const { Config } = require('@adobe/aio-sdk').Core 6 | const fs = require('fs') 7 | const fetch = require('node-fetch') 8 | 9 | // get action url 10 | const namespace = Config.get('runtime.namespace') 11 | const hostname = Config.get('cna.hostname') || 'adobeioruntime.net' 12 | const packagejson = JSON.parse(fs.readFileSync('package.json').toString()) 13 | const runtimePackage = `${packagejson.name}-${packagejson.version}` 14 | const actionUrl = `https://${namespace}.${hostname}/api/v1/web/${runtimePackage}/generate-code` 15 | 16 | // The deployed actions are secured with the `require-adobe-auth` annotation. 17 | // If the authorization header is missing, Adobe I/O Runtime returns with a 401 before the action is executed. 18 | test('returns a 401 when missing Authorization header', async () => { 19 | const res = await fetch(actionUrl) 20 | expect(res).toEqual(expect.objectContaining({ 21 | status: 401 22 | })) 23 | }) 24 | -------------------------------------------------------------------------------- /src/dx-excshell-1/e2e/get-profiles.e2e.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | const { Config } = require('@adobe/aio-sdk').Core 6 | const fs = require('fs') 7 | const fetch = require('node-fetch') 8 | 9 | // get action url 10 | const namespace = Config.get('runtime.namespace') 11 | const hostname = Config.get('cna.hostname') || 'adobeioruntime.net' 12 | const packagejson = JSON.parse(fs.readFileSync('package.json').toString()) 13 | const runtimePackage = `${packagejson.name}-${packagejson.version}` 14 | const actionUrl = `https://${namespace}.${hostname}/api/v1/web/${runtimePackage}/get-profiles` 15 | 16 | // The deployed actions are secured with the `require-adobe-auth` annotation. 17 | // If the authorization header is missing, Adobe I/O Runtime returns with a 401 before the action is executed. 18 | test('returns a 401 when missing Authorization header', async () => { 19 | const res = await fetch(actionUrl) 20 | expect(res).toEqual(expect.objectContaining({ 21 | status: 401 22 | })) 23 | }) 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customers-dashboard", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "@adobe/aio-sdk": "^3.0.0", 6 | "@adobe/exc-app": "^0.2.21", 7 | "@adobe/react-spectrum": "^3.4.0", 8 | "@spectrum-icons/workflow": "^3.2.0", 9 | "bwip-js": "^3.0.2", 10 | "core-js": "^3.6.4", 11 | "node-fetch": "^2.6.0", 12 | "react": "^16.13.1", 13 | "react-dom": "^16.13.1", 14 | "react-error-boundary": "^1.2.5", 15 | "react-router-dom": "^5.2.0", 16 | "regenerator-runtime": "^0.13.5", 17 | "uuid": "^8.3.2" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.8.7", 21 | "@babel/plugin-transform-react-jsx": "^7.8.3", 22 | "@babel/polyfill": "^7.8.7", 23 | "@babel/preset-env": "^7.8.7", 24 | "@openwhisk/wskdebug": "^1.3.0", 25 | "jest": "^26.6.3" 26 | }, 27 | "scripts": { 28 | "test": "jest --passWithNoTests ./test", 29 | "e2e": "jest --collectCoverage=false --testRegex ./e2e" 30 | }, 31 | "engines": { 32 | "node": "^10 || ^12 || ^14" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/dx-excshell-1/ext.config.yaml: -------------------------------------------------------------------------------- 1 | operations: 2 | view: 3 | - type: web 4 | impl: index.html 5 | actions: actions 6 | web: web-src 7 | runtimeManifest: 8 | packages: 9 | dx-excshell-1: 10 | license: Apache-2.0 11 | actions: 12 | get-profiles: 13 | function: actions/get-profiles/index.js 14 | web: 'yes' 15 | runtime: 'nodejs:14' 16 | inputs: 17 | LOG_LEVEL: debug 18 | tenant: $CAMPAIGN_STANDARD_TENANT 19 | apiKey: $SERVICE_API_KEY 20 | annotations: 21 | require-adobe-auth: true 22 | final: true 23 | generate-code: 24 | function: actions/generate-code/index.js 25 | web: 'yes' 26 | runtime: 'nodejs:14' 27 | inputs: 28 | LOG_LEVEL: debug 29 | annotations: 30 | final: true 31 | send-promo: 32 | function: actions/send-promo/index.js 33 | web: 'yes' 34 | runtime: 'nodejs:14' 35 | inputs: 36 | LOG_LEVEL: debug 37 | tenant: $CAMPAIGN_STANDARD_TENANT 38 | apiKey: $SERVICE_API_KEY 39 | workflowId: $CAMPAIGN_STANDARD_WORKFLOW_ID 40 | annotations: 41 | require-adobe-auth: true 42 | final: true 43 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/src/components/SideBar.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import React from 'react' 14 | import { NavLink } from 'react-router-dom' 15 | 16 | function SideBar () { 17 | return ( 18 |
    19 |
  • 20 | Home 21 |
  • 22 |
  • 23 | Your App Actions 24 |
  • 25 |
  • 26 | About Project Firefly Apps 27 |
  • 28 |
29 | ) 30 | } 31 | 32 | export default SideBar 33 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/src/components/About.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import React from 'react' 14 | import { Heading, View, Content, Link } from '@adobe/react-spectrum' 15 | export const About = () => ( 16 | 17 | Useful documentation for your app 18 | 19 | 49 | 50 | 51 | ) 52 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/src/exc-runtime.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * 15 | * Script to load the Adobe Experience Cloud Runtime. 16 | * 17 | * @throws {Error} error in case of failure, most likely when the app is not running in 18 | * the Experience Cloud Shell. 19 | * 20 | */ 21 | /* eslint-disable-next-line */ 22 | (function(e,t){if(t.location===t.parent.location)throw new Error("Module Runtime: Needs to be within an iframe!");var o=function(e){var t=new URL(e.location.href).searchParams.get("_mr");return t||!e.EXC_US_HMR?t:e.sessionStorage.getItem("unifiedShellMRScript")}(t);if(!o)throw new Error("Module Runtime: Missing script!");if("https:"!==(o=new URL(decodeURIComponent(o))).protocol)throw new Error("Module Runtime: Must be HTTPS!");if(!/^(exc-unifiedcontent\.)?experience(-qa|-stage|-cdn|-cdn-stage)?\.adobe\.(com|net)$/.test(o.hostname)&&!/localhost\.corp\.adobe\.com$/.test(o.hostname))throw new Error("Module Runtime: Invalid domain!");if(!/\.js$/.test(o.pathname))throw new Error("Module Runtime: Must be a JavaScript file!");t.EXC_US_HMR&&t.sessionStorage.setItem("unifiedShellMRScript",o.toString());var n=e.createElement("script");n.async=1,n.src=o.toString(),n.onload=n.onreadystatechange=function(){n.readyState&&!/loaded|complete/.test(n.readyState)||(n.onload=n.onreadystatechange=null,n=void 0,"EXC_MR_READY"in t&&t.EXC_MR_READY())},e.head.appendChild(n)})(document,window); 23 | -------------------------------------------------------------------------------- /src/dx-excshell-1/actions/generate-code/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * This action generates a barcode of a random UUID as personalized promo code 15 | */ 16 | 17 | const { Core } = require('@adobe/aio-sdk') 18 | const { v4: uuid4 } = require('uuid') 19 | const bwipjs = require('bwip-js') 20 | const { errorResponse } = require('../utils') 21 | 22 | // main function that will be executed by Adobe I/O Runtime 23 | async function main (params) { 24 | // create a Logger 25 | const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }) 26 | 27 | try { 28 | // 'info' is the default level if not set 29 | logger.info('Calling the main action') 30 | 31 | // generate UUID code 32 | const promoCode = uuid4() 33 | const buffer = await bwipjs.toBuffer({ 34 | bcid: 'code128', 35 | text: promoCode, 36 | scale: 2, 37 | includetext: true, 38 | backgroundcolor: 'ffffff' 39 | }) 40 | const response = { 41 | headers: { 'Content-Type': 'image/png' }, 42 | statusCode: 200, 43 | body: buffer.toString('base64') 44 | } 45 | 46 | // log the response status code 47 | logger.info(`${response.statusCode}: successful request`) 48 | return response 49 | } catch (error) { 50 | // log any server errors 51 | logger.error(error) 52 | // return with 500 53 | return errorResponse(500, 'server error', logger) 54 | } 55 | } 56 | 57 | exports.main = main 58 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /* global fetch */ 14 | 15 | /** 16 | * 17 | * Invokes a web action 18 | * 19 | * @param {string} actionUrl 20 | * @param {object} headers 21 | * @param {object} params 22 | * 23 | * @returns {Promise} the response 24 | * 25 | */ 26 | 27 | async function actionWebInvoke (actionUrl, headers = {}, params = {}, options = { method: 'POST' }) { 28 | const actionHeaders = { 29 | 'Content-Type': 'application/json', 30 | ...headers 31 | } 32 | 33 | const fetchConfig = { 34 | headers: actionHeaders 35 | } 36 | 37 | if (window.location.hostname === 'localhost') { 38 | actionHeaders['x-ow-extra-logging'] = 'on' 39 | } 40 | 41 | fetchConfig.method = options.method.toUpperCase() 42 | 43 | if (fetchConfig.method === 'GET') { 44 | actionUrl = new URL(actionUrl) 45 | Object.keys(params).forEach(key => actionUrl.searchParams.append(key, params[key])) 46 | } else if (fetchConfig.method === 'POST') { 47 | fetchConfig.body = JSON.stringify(params) 48 | } 49 | 50 | const response = await fetch(actionUrl, fetchConfig) 51 | 52 | let content = await response.text() 53 | 54 | if (!response.ok) { 55 | throw new Error(`failed request to '${actionUrl}' with status: ${response.status} and message: ${content}`) 56 | } 57 | try { 58 | content = JSON.parse(content) 59 | } catch (e) { 60 | // response is not json 61 | } 62 | return content 63 | } 64 | 65 | export default actionWebInvoke 66 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | 14 | html, 15 | body { 16 | margin: 0; 17 | } 18 | 19 | /** Custom Sidebar Styling **/ 20 | .SideNav { 21 | list-style-type: none; 22 | margin: 0; 23 | margin-bottom: 0px; 24 | padding: 0; 25 | outline: none; 26 | height: 100%; 27 | } 28 | 29 | .SideNav-item { 30 | list-style-type: none; 31 | margin: var(--spectrum-global-dimension-size-50) 0; 32 | } 33 | 34 | .SideNav-itemLink { 35 | position: relative; 36 | display: inline-flex; 37 | align-items: center; 38 | justify-content: start; 39 | box-sizing: border-box; 40 | width: 100%; 41 | min-height: var(--spectrum-alias-single-line-height, var(--spectrum-global-dimension-size-400)); 42 | padding: var(--spectrum-global-dimension-size-65) var(--spectrum-global-dimension-size-150); 43 | border-radius: var(--spectrum-alias-border-radius-regular, var(--spectrum-global-dimension-size-50)); 44 | font-size: var(--spectrum-alias-font-size-default, var(--spectrum-global-dimension-font-size-100)); 45 | font-weight: var(--spectrum-global-font-weight-regular, 400); 46 | font-style: normal; 47 | text-decoration: none; 48 | word-break: break-word; 49 | cursor: pointer; 50 | background-color: var(--spectrum-alias-background-color-transparent,transparent); 51 | color: var(--spectrum-alias-text-color,var(--spectrum-global-color-gray-800)); 52 | } 53 | 54 | .SideNav-itemLink.is-selected { 55 | color: var(--spectrum-alias-text-color-hover,var(--spectrum-global-color-gray-900)); 56 | background-color: var(--spectrum-alias-highlight-hover); 57 | } 58 | -------------------------------------------------------------------------------- /src/dx-excshell-1/actions/get-profiles/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * This action retrieves a list of customer profiles from the Adobe Campaign Standard API 15 | */ 16 | 17 | const { Core } = require('@adobe/aio-sdk') 18 | const { CampaignStandard } = require('@adobe/aio-sdk') 19 | const { errorResponse, getBearerToken, stringParameters, checkMissingRequestInputs } = require('../utils') 20 | 21 | // main function that will be executed by Adobe I/O Runtime 22 | async function main (params) { 23 | // create a Logger 24 | const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }) 25 | 26 | try { 27 | // 'info' is the default level if not set 28 | logger.info('Calling the main action') 29 | 30 | // log parameters, only if params.LOG_LEVEL === 'debug' 31 | logger.debug(stringParameters(params)) 32 | 33 | // check for missing request input parameters and headers 34 | const requiredParams = ['apiKey', 'tenant'] 35 | const errorMessage = checkMissingRequestInputs(params, requiredParams, ['Authorization']) 36 | if (errorMessage) { 37 | // return and log client errors 38 | return errorResponse(400, errorMessage, logger) 39 | } 40 | 41 | // extract the user Bearer token from the input request parameters 42 | const token = getBearerToken(params) 43 | 44 | // initialize the sdk 45 | const campaignClient = await CampaignStandard.init(params.tenant, params.apiKey, token) 46 | 47 | // get profiles from Campaign Standard 48 | const profiles = await campaignClient.getAllProfiles() 49 | logger.debug('profiles = ' + JSON.stringify(profiles, null, 2)) 50 | const response = { 51 | statusCode: 200, 52 | body: profiles 53 | } 54 | 55 | // log the response status code 56 | logger.info(`${response.statusCode}: successful request`) 57 | return response 58 | } catch (error) { 59 | // log any server errors 60 | logger.error(error) 61 | // return with 500 62 | return errorResponse(500, 'server error', logger) 63 | } 64 | } 65 | 66 | exports.main = main 67 | -------------------------------------------------------------------------------- /src/dx-excshell-1/test/generate-code.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | jest.mock('@adobe/aio-sdk', () => ({ 6 | Core: { 7 | Logger: jest.fn() 8 | } 9 | })) 10 | 11 | const { Core } = require('@adobe/aio-sdk') 12 | const mockLoggerInstance = { info: jest.fn(), debug: jest.fn(), error: jest.fn() } 13 | Core.Logger.mockReturnValue(mockLoggerInstance) 14 | 15 | jest.mock('node-fetch') 16 | const fetch = require('node-fetch') 17 | const action = require('./../../actions/generate-code/index.js') 18 | 19 | beforeEach(() => { 20 | Core.Logger.mockClear() 21 | mockLoggerInstance.info.mockReset() 22 | mockLoggerInstance.debug.mockReset() 23 | mockLoggerInstance.error.mockReset() 24 | }) 25 | 26 | const fakeParams = { __ow_headers: { authorization: 'Bearer fake' } } 27 | describe('generate-code', () => { 28 | test('main should be defined', () => { 29 | expect(action.main).toBeInstanceOf(Function) 30 | }) 31 | test('should set logger to use LOG_LEVEL param', async () => { 32 | await action.main({ ...fakeParams, LOG_LEVEL: 'fakeLevel' }) 33 | expect(Core.Logger).toHaveBeenCalledWith(expect.any(String), { level: 'fakeLevel' }) 34 | }) 35 | test('should return an http reponse with the fetched content', async () => { 36 | const mockFetchResponse = { 37 | ok: true, 38 | json: () => Promise.resolve({ content: 'fake' }) 39 | } 40 | fetch.mockResolvedValue(mockFetchResponse) 41 | const response = await action.main(fakeParams) 42 | expect(response).toEqual({ 43 | statusCode: 200, 44 | body: { content: 'fake' } 45 | }) 46 | }) 47 | test('if there is an error should return a 500 and log the error', async () => { 48 | const fakeError = new Error('fake') 49 | fetch.mockRejectedValue(fakeError) 50 | const response = await action.main(fakeParams) 51 | expect(response).toEqual({ 52 | error : { 53 | statusCode: 500, 54 | body: { error: 'server error' } 55 | } 56 | }) 57 | expect(mockLoggerInstance.error).toHaveBeenCalledWith(fakeError) 58 | }) 59 | test('if returned service status code is not ok should return a 500 and log the status', async () => { 60 | const mockFetchResponse = { 61 | ok: false, 62 | status: 404 63 | } 64 | fetch.mockResolvedValue(mockFetchResponse) 65 | const response = await action.main(fakeParams) 66 | expect(response).toEqual({ 67 | error: { 68 | statusCode: 500, 69 | body: { error: 'server error' } 70 | } 71 | }) 72 | // error message should contain 404 73 | expect(mockLoggerInstance.error).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining('404') })) 74 | }) 75 | test('missing input request parameters, should return 400', async () => { 76 | const response = await action.main({}) 77 | expect(response).toEqual({ 78 | error: { 79 | statusCode: 400, 80 | body: { error: 'missing header(s) \'authorization\'' } 81 | } 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/dx-excshell-1/test/send-promo.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | jest.mock('@adobe/aio-sdk', () => ({ 6 | CampaignStandard: { 7 | init: jest.fn() 8 | }, 9 | Core: { 10 | Logger: jest.fn() 11 | } 12 | })) 13 | 14 | const { Core, CampaignStandard } = require('@adobe/aio-sdk') 15 | const mockCampaignStandardInstance = { getAllProfiles: jest.fn() } 16 | const mockLoggerInstance = { info: jest.fn(), debug: jest.fn(), error: jest.fn() } 17 | Core.Logger.mockReturnValue(mockLoggerInstance) 18 | CampaignStandard.init.mockResolvedValue(mockCampaignStandardInstance) 19 | 20 | const action = require('./../../actions/send-promo/index.js') 21 | 22 | beforeEach(() => { 23 | CampaignStandard.init.mockClear() // only clears calls stats 24 | mockCampaignStandardInstance.getAllProfiles.mockReset() // clears calls + mock implementation 25 | 26 | Core.Logger.mockClear() 27 | mockLoggerInstance.info.mockReset() 28 | mockLoggerInstance.debug.mockReset() 29 | mockLoggerInstance.error.mockReset() 30 | }) 31 | 32 | const fakeRequestParams = { tenant: 'fakeId', apiKey: 'fakeKey', __ow_headers: { authorization: 'Bearer fakeToken' } } 33 | describe('send-promo', () => { 34 | test('main should be defined', () => { 35 | expect(action.main).toBeInstanceOf(Function) 36 | }) 37 | test('should set logger to use LOG_LEVEL param', async () => { 38 | await action.main({ ...fakeRequestParams, LOG_LEVEL: 'fakeLevel' }) 39 | expect(Core.Logger).toHaveBeenCalledWith(expect.any(String), { level: 'fakeLevel' }) 40 | }) 41 | test('CampaignStandard sdk should be initialized with input credentials', async () => { 42 | await action.main({ ...fakeRequestParams, otherParam: 'fake4' }) 43 | expect(CampaignStandard.init).toHaveBeenCalledWith('fakeId', 'fakeKey', 'fakeToken') 44 | }) 45 | test('should return an http response with CampaignStandard profiles', async () => { 46 | const fakeResponse = { profiles: 'fake' } 47 | mockCampaignStandardInstance.getAllProfiles.mockResolvedValue(fakeResponse) 48 | const response = await action.main(fakeRequestParams) 49 | expect(response).toEqual({ 50 | statusCode: 200, 51 | body: fakeResponse 52 | }) 53 | }) 54 | test('if there is an error should return a 500 and log the error', async () => { 55 | const fakeError = new Error('fake') 56 | mockCampaignStandardInstance.getAllProfiles.mockRejectedValue(fakeError) 57 | const response = await action.main(fakeRequestParams) 58 | expect(response).toEqual({ 59 | error: { 60 | statusCode: 500, 61 | body: { error: 'server error' } 62 | } 63 | }) 64 | expect(mockLoggerInstance.error).toHaveBeenCalledWith(fakeError) 65 | }) 66 | test('missing input request parameters, should return 400', async () => { 67 | const response = await action.main({}) 68 | expect(response).toEqual({ 69 | error: { 70 | statusCode: 400, 71 | body: { error: 'missing header(s) \'authorization\' and missing parameter(s) \'apiKey,tenant\'' } 72 | } 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/dx-excshell-1/test/get-profiles.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | jest.mock('@adobe/aio-sdk', () => ({ 6 | CampaignStandard: { 7 | init: jest.fn() 8 | }, 9 | Core: { 10 | Logger: jest.fn() 11 | } 12 | })) 13 | 14 | const { Core, CampaignStandard } = require('@adobe/aio-sdk') 15 | const mockCampaignStandardInstance = { getAllProfiles: jest.fn() } 16 | const mockLoggerInstance = { info: jest.fn(), debug: jest.fn(), error: jest.fn() } 17 | Core.Logger.mockReturnValue(mockLoggerInstance) 18 | CampaignStandard.init.mockResolvedValue(mockCampaignStandardInstance) 19 | 20 | const action = require('./../../actions/get-profiles/index.js') 21 | 22 | beforeEach(() => { 23 | CampaignStandard.init.mockClear() // only clears calls stats 24 | mockCampaignStandardInstance.getAllProfiles.mockReset() // clears calls + mock implementation 25 | 26 | Core.Logger.mockClear() 27 | mockLoggerInstance.info.mockReset() 28 | mockLoggerInstance.debug.mockReset() 29 | mockLoggerInstance.error.mockReset() 30 | }) 31 | 32 | const fakeRequestParams = { tenant: 'fakeId', apiKey: 'fakeKey', __ow_headers: { authorization: 'Bearer fakeToken' } } 33 | describe('get-profiles', () => { 34 | test('main should be defined', () => { 35 | expect(action.main).toBeInstanceOf(Function) 36 | }) 37 | test('should set logger to use LOG_LEVEL param', async () => { 38 | await action.main({ ...fakeRequestParams, LOG_LEVEL: 'fakeLevel' }) 39 | expect(Core.Logger).toHaveBeenCalledWith(expect.any(String), { level: 'fakeLevel' }) 40 | }) 41 | test('CampaignStandard sdk should be initialized with input credentials', async () => { 42 | await action.main({ ...fakeRequestParams, otherParam: 'fake4' }) 43 | expect(CampaignStandard.init).toHaveBeenCalledWith('fakeId', 'fakeKey', 'fakeToken') 44 | }) 45 | test('should return an http response with CampaignStandard profiles', async () => { 46 | const fakeResponse = { profiles: 'fake' } 47 | mockCampaignStandardInstance.getAllProfiles.mockResolvedValue(fakeResponse) 48 | const response = await action.main(fakeRequestParams) 49 | expect(response).toEqual({ 50 | statusCode: 200, 51 | body: fakeResponse 52 | }) 53 | }) 54 | test('if there is an error should return a 500 and log the error', async () => { 55 | const fakeError = new Error('fake') 56 | mockCampaignStandardInstance.getAllProfiles.mockRejectedValue(fakeError) 57 | const response = await action.main(fakeRequestParams) 58 | expect(response).toEqual({ 59 | error: { 60 | statusCode: 500, 61 | body: { error: 'server error' } 62 | } 63 | }) 64 | expect(mockLoggerInstance.error).toHaveBeenCalledWith(fakeError) 65 | }) 66 | test('missing input request parameters, should return 400', async () => { 67 | const response = await action.main({}) 68 | expect(response).toEqual({ 69 | error: { 70 | statusCode: 400, 71 | body: { error: 'missing header(s) \'authorization\' and missing parameter(s) \'apiKey,tenant\'' } 72 | } 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/dx-excshell-1/actions/send-promo/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | /** 14 | * This action triggers Adobe Campaign Standard workflow to send promotion email to a specific email address 15 | */ 16 | 17 | const { Core } = require('@adobe/aio-sdk') 18 | const { CampaignStandard } = require('@adobe/aio-sdk') 19 | const { errorResponse, getBearerToken, stringParameters, checkMissingRequestInputs } = require('../utils') 20 | 21 | // main function that will be executed by Adobe I/O Runtime 22 | async function main (params) { 23 | // create a Logger 24 | const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' }) 25 | 26 | try { 27 | // 'info' is the default level if not set 28 | logger.info('Calling the main action') 29 | 30 | // log parameters, only if params.LOG_LEVEL === 'debug' 31 | logger.debug(stringParameters(params)) 32 | 33 | // check for missing request input parameters and headers 34 | const requiredParams = ['apiKey', 'tenant', 'workflowId', 'email'] 35 | const errorMessage = checkMissingRequestInputs(params, requiredParams, ['Authorization']) 36 | if (errorMessage) { 37 | // return and log client errors 38 | return errorResponse(400, errorMessage, logger) 39 | } 40 | 41 | // extract the user Bearer token from the input request parameters 42 | const token = getBearerToken(params) 43 | 44 | // initialize the sdk 45 | const campaignClient = await CampaignStandard.init(params.tenant, params.apiKey, token) 46 | 47 | // get workflow from Campaign Standard 48 | const workflow = await campaignClient.getWorkflow(params.workflowId) 49 | const wkfHref = workflow.body.activities.activity.signal1.trigger.href 50 | 51 | // trigger the signal activity API 52 | const triggerResult = await campaignClient.triggerSignalActivity(wkfHref, { source: 'API', parameters: { email: params.email } }) 53 | 54 | // log the trigger result 55 | logger.debug(JSON.stringify(triggerResult, null, 2)) 56 | 57 | const response = { 58 | statusCode: 200, 59 | body: { success: 'ok' } 60 | } 61 | 62 | // log the response status code 63 | logger.info(`${response.statusCode}: successful request`) 64 | return response 65 | } catch (error) { 66 | // log any server errors 67 | logger.error(error) 68 | // return with 500 69 | return errorResponse(500, 'server error', logger) 70 | } 71 | } 72 | 73 | exports.main = main 74 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import 'core-js/stable' 14 | import 'regenerator-runtime/runtime' 15 | 16 | window.React = require('react') 17 | import ReactDOM from 'react-dom' 18 | 19 | import Runtime, { init } from '@adobe/exc-app' 20 | 21 | import App from './components/App' 22 | import './index.css' 23 | /* Here you can bootstrap your application and configure the integration with the Adobe Experience Cloud Shell */ 24 | try { 25 | // attempt to load the Experience Cloud Runtime 26 | require('./exc-runtime') 27 | // if there are no errors, bootstrap the app in the Experience Cloud Shell 28 | init(bootstrapInExcShell) 29 | } catch (e) { 30 | console.log('application not running in Adobe Experience Cloud Shell') 31 | // fallback mode, run the application without the Experience Cloud Runtime 32 | bootstrapRaw() 33 | } 34 | 35 | function bootstrapRaw () { 36 | /* **here you can mock the exc runtime and ims objects** */ 37 | const mockRuntime = { on: () => {} } 38 | const mockIms = {} 39 | 40 | // render the actual react application and pass along the runtime object to make it available to the App 41 | ReactDOM.render( 42 | , 43 | document.getElementById('root') 44 | ) 45 | } 46 | 47 | function bootstrapInExcShell () { 48 | // get the Experience Cloud Runtime object 49 | const runtime = Runtime() 50 | 51 | // use this to set a favicon 52 | // runtime.favicon = 'url-to-favicon' 53 | 54 | // use this to respond to clicks on the app-bar title 55 | // runtime.heroClick = () => window.alert('Did I ever tell you you\'re my hero?') 56 | 57 | // ready event brings in authentication/user info 58 | runtime.on('ready', ({ imsOrg, imsToken, imsProfile, locale }) => { 59 | // tell the exc-runtime object we are done 60 | runtime.done() 61 | console.log('Ready! received imsProfile:', imsProfile) 62 | const ims = { 63 | profile: imsProfile, 64 | org: imsOrg, 65 | token: imsToken 66 | } 67 | // render the actual react application and pass along the runtime and ims objects to make it available to the App 68 | ReactDOM.render( 69 | , 70 | document.getElementById('root') 71 | ) 72 | }) 73 | 74 | // set solution info, shortTitle is used when window is too small to display full title 75 | runtime.solution = { 76 | icon: 'AdobeExperienceCloud', 77 | title: 'Customer Dashboard', 78 | shortTitle: 'JGR' 79 | } 80 | runtime.title = 'Customer Dashboard' 81 | } 82 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/src/components/App.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import React from 'react' 14 | import { Provider, defaultTheme, Grid, View } from '@adobe/react-spectrum' 15 | import ErrorBoundary from 'react-error-boundary' 16 | import { HashRouter as Router, Switch, Route } from 'react-router-dom' 17 | import SideBar from './SideBar' 18 | import ActionsForm from './ActionsForm' 19 | import Home from './Home' 20 | import { About } from './About' 21 | 22 | function App (props) { 23 | console.log('runtime object:', props.runtime) 24 | console.log('ims object:', props.ims) 25 | 26 | // use exc runtime event handlers 27 | // respond to configuration change events (e.g. user switches org) 28 | props.runtime.on('configuration', ({ imsOrg, imsToken, locale }) => { 29 | console.log('configuration change', { imsOrg, imsToken, locale }) 30 | }) 31 | // respond to history change events 32 | props.runtime.on('history', ({ type, path }) => { 33 | console.log('history change', { type, path }) 34 | }) 35 | 36 | return ( 37 | 38 | 39 | 40 | 47 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | ) 72 | 73 | // Methods 74 | 75 | // error handler on UI rendering failure 76 | function onError (e, componentStack) { } 77 | 78 | // component to show if UI fails rendering 79 | function fallbackComponent ({ componentStack, error }) { 80 | return ( 81 | 82 |

83 | Something went wrong :( 84 |

85 |
{componentStack + '\n' + error.message}
86 |
87 | ) 88 | } 89 | } 90 | 91 | export default App 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firefly Sample App: Customer Profiles Dashboard 2 | 3 | This Firefly app is a complete solution of the codelab [Build a Firefly App for Customer Profiles using Adobe Campaign Standard API](https://github.com/AdobeDocs/adobeio-codelabs-campaign-standard). It lists customer profiles from Adobe Campaign Standard and allows sending marketing campaign emails with personalized promo code. 4 | 5 | ![demo](demo.png) 6 | 7 | ## Setup 8 | 9 | - Populate the `.env` file in the project root and fill it as shown [below](#env). You can also use [dot-env](dot-env) as a template. 10 | 11 | ## Local Dev 12 | 13 | - `aio app run` to start your local Dev server 14 | - App will run on `localhost:9080` by default 15 | 16 | By default the UI will be served locally but actions will be deployed and served from Adobe I/O Runtime. To start a 17 | local serverless stack and also run your actions locally use the `aio app run --local` option. 18 | 19 | ## Test & Coverage 20 | 21 | - Run `aio app test` to run unit tests for ui and actions 22 | - Run `aio app test -e` to run e2e tests 23 | 24 | ## Deploy & Cleanup 25 | 26 | - `aio app deploy` to build and deploy all actions on Runtime and static files to CDN 27 | - `aio app undeploy` to undeploy the app 28 | 29 | ## Config 30 | 31 | ### `.env` 32 | 33 | ```bash 34 | # This file should not be committed to source control 35 | 36 | ## please provide your Adobe I/O Runtime credentials 37 | AIO_RUNTIME_AUTH= 38 | AIO_RUNTIME_NAMESPACE= 39 | 40 | ## please provide your Adobe I/O Campaign Standard integration parameters 41 | CAMPAIGN_STANDARD_TENANT= 42 | CAMPAIGN_STANDARD_API_KEY= 43 | CAMPAIGN_STANDARD_WORKFLOW_ID= 44 | ``` 45 | 46 | ### `package.json` 47 | 48 | - We use the `name` and `version` fields for the deployment. Make sure to fill 49 | those out. Do not use illegal characters as this might break the deployment 50 | (e.g. `/`, `@`, `#`, ..). 51 | 52 | ### `manifest.yml` 53 | 54 | - List your backend actions under the `actions` field within the `__APP_PACKAGE__` 55 | package placeholder. We will take care of replacing the package name placeholder 56 | by your project name and version. 57 | - For each action, use the `function` field to indicate the path to the action 58 | code. 59 | - More documentation for supported action fields can be found 60 | [here](https://github.com/apache/incubator-openwhisk-wskdeploy/blob/master/specification/html/spec_actions.md#actions). 61 | 62 | #### Action Dependencies 63 | 64 | - You have two options to resolve your actions' dependencies: 65 | 66 | 1. **Packaged action file**: Add your action's dependencies to the root 67 | `package.json` and install them using `npm install`. Then set the `function` 68 | field in `manifest.yml` to point to the **entry file** of your action 69 | folder. We will use `parcelJS` to package your code and dependencies into a 70 | single minified js file. The action will then be deployed as a single file. 71 | Use this method if you want to reduce the size of your actions. 72 | 73 | 2. **Zipped action folder**: In the folder containing the action code add a 74 | `package.json` with the action's dependencies. Then set the `function` 75 | field in `manifest.yml` to point to the **folder** of that action. We will 76 | install the required dependencies within that directory and zip the folder 77 | before deploying it as a zipped action. Use this method if you want to keep 78 | your action's dependencies separated. 79 | 80 | ## Debugging in VS Code 81 | 82 | While running your local server (`aio app run`), both UI and actions can be debugged, to do so open the vscode debugger 83 | and select the debugging configuration called `WebAndActions`. 84 | Alternatively, there are also debug configs for only UI and each separate action. 85 | -------------------------------------------------------------------------------- /src/dx-excshell-1/actions/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | /* This file exposes some common utilities for your actions */ 6 | 7 | /** 8 | * 9 | * Returns a log ready string of the action input parameters. 10 | * The `Authorization` header content will be replaced by ''. 11 | * 12 | * @param {object} params action input parameters. 13 | * 14 | * @returns {string} 15 | * 16 | */ 17 | function stringParameters (params) { 18 | // hide authorization token without overriding params 19 | let headers = params.__ow_headers || {} 20 | if (headers.authorization) { 21 | headers = { ...headers, authorization: '' } 22 | } 23 | return JSON.stringify({ ...params, __ow_headers: headers }) 24 | } 25 | 26 | /** 27 | * 28 | * Returns the list of missing keys giving an object and its required keys. 29 | * A parameter is missing if its value is undefined or ''. 30 | * A value of 0 or null is not considered as missing. 31 | * 32 | * @param {object} obj object to check. 33 | * @param {array} required list of required keys. 34 | * Each element can be multi level deep using a '.' separator e.g. 'myRequiredObj.myRequiredKey' 35 | * 36 | * @returns {array} 37 | * @private 38 | */ 39 | function getMissingKeys (obj, required) { 40 | return required.filter(r => { 41 | const splits = r.split('.') 42 | const last = splits[splits.length - 1] 43 | const traverse = splits.slice(0, -1).reduce((tObj, split) => { tObj = (tObj[split] || {}); return tObj }, obj) 44 | return traverse[last] === undefined || traverse[last] === '' // missing default params are empty string 45 | }) 46 | } 47 | 48 | /** 49 | * 50 | * Returns the list of missing keys giving an object and its required keys. 51 | * A parameter is missing if its value is undefined or ''. 52 | * A value of 0 or null is not considered as missing. 53 | * 54 | * @param {object} params action input parameters. 55 | * @param {array} requiredHeaders list of required input headers. 56 | * @param {array} requiredParams list of required input parameters. 57 | * Each element can be multi level deep using a '.' separator e.g. 'myRequiredObj.myRequiredKey'. 58 | * 59 | * @returns {string} if the return value is not null, then it holds an error message describing the missing inputs. 60 | * 61 | */ 62 | function checkMissingRequestInputs (params, requiredParams = [], requiredHeaders = []) { 63 | let errorMessage = null 64 | 65 | // input headers are always lowercase 66 | requiredHeaders = requiredHeaders.map(h => h.toLowerCase()) 67 | // check for missing headers 68 | const missingHeaders = getMissingKeys(params.__ow_headers || {}, requiredHeaders) 69 | if (missingHeaders.length > 0) { 70 | errorMessage = `missing header(s) '${missingHeaders}'` 71 | } 72 | 73 | // check for missing parameters 74 | const missingParams = getMissingKeys(params, requiredParams) 75 | if (missingParams.length > 0) { 76 | if (errorMessage) { 77 | errorMessage += ' and ' 78 | } else { 79 | errorMessage = '' 80 | } 81 | errorMessage += `missing parameter(s) '${missingParams}'` 82 | } 83 | 84 | return errorMessage 85 | } 86 | 87 | /** 88 | * 89 | * Extracts the bearer token string from the Authorization header in the request parameters. 90 | * 91 | * @param {object} params action input parameters. 92 | * 93 | * @returns {string|undefined} the token string or undefined if not set in request headers. 94 | * 95 | */ 96 | function getBearerToken (params) { 97 | if (params.__ow_headers && 98 | params.__ow_headers.authorization && 99 | params.__ow_headers.authorization.startsWith('Bearer ')) { 100 | return params.__ow_headers.authorization.substring('Bearer '.length) 101 | } 102 | return undefined 103 | } 104 | /** 105 | * 106 | * Returns an error response object and attempts to log.info the status code and error message 107 | * 108 | * @param {number} statusCode the error status code. 109 | * e.g. 400 110 | * @param {string} message the error message. 111 | * e.g. 'missing xyz parameter' 112 | * @param {*} [logger] an optional logger instance object with an `info` method 113 | * e.g. `new require('@adobe/aio-sdk').Core.Logger('name')` 114 | * 115 | * @returns {object} the error object, ready to be returned from the action main's function. 116 | * 117 | */ 118 | function errorResponse (statusCode, message, logger) { 119 | if (logger && typeof logger.info === 'function') { 120 | logger.info(`${statusCode}: ${message}`) 121 | } 122 | return { 123 | error: { 124 | statusCode, 125 | body: { 126 | error: message 127 | } 128 | } 129 | } 130 | } 131 | 132 | module.exports = { 133 | errorResponse, 134 | getBearerToken, 135 | stringParameters, 136 | checkMissingRequestInputs 137 | } 138 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/src/components/Home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import React from 'react' 14 | import PropTypes from 'prop-types' 15 | import { ActionButton, AlertDialog, DialogTrigger, Flex, Grid, ProgressCircle, Heading, Text, View } from '@adobe/react-spectrum' 16 | import actions from '../config.json' 17 | import actionWebInvoke from '../utils' 18 | 19 | class Home extends React.Component { 20 | constructor (props) { 21 | super(props) 22 | 23 | this.state = { 24 | actionResponseError: null, 25 | actionInvokeInProgress: false, 26 | profiles: null 27 | } 28 | } 29 | 30 | async componentWillMount () { 31 | this.setState({ actionInvokeInProgress: true }) 32 | 33 | const headers = {} 34 | const params = {} 35 | 36 | // set the authorization header and org from the ims props object 37 | if (this.props.ims.token && !headers.authorization) { 38 | headers.authorization = 'Bearer ' + this.props.ims.token 39 | } 40 | if (this.props.ims.org && !headers['x-gw-ims-org-id']) { 41 | headers['x-gw-ims-org-id'] = this.props.ims.org 42 | } 43 | try { 44 | const actionResponse = await actionWebInvoke(actions['get-profiles'], headers, params) 45 | this.setState({ profiles: actionResponse.body.content, actionResponseError: null, actionInvokeInProgress: false }) 46 | console.log(`action response:`, actionResponse) 47 | } catch (e) { 48 | console.error(e) 49 | this.setState({ profiles: null, actionResponseError: e.message, actionInvokeInProgress: false }) 50 | } 51 | } 52 | 53 | // invoke send-promo action by user email 54 | async sendPromo (email) { 55 | try { 56 | const headers = {} 57 | 58 | // set the authorization header and org from the ims props object 59 | if (this.props.ims.token && !headers.authorization) { 60 | headers.authorization = 'Bearer ' + this.props.ims.token 61 | } 62 | if (this.props.ims.org && !headers['x-gw-ims-org-id']) { 63 | headers['x-gw-ims-org-id'] = this.props.ims.org 64 | } 65 | const actionResponse = await actionWebInvoke(actions['send-promo'], headers, { email }) 66 | console.log(`Response from send-promo:`, actionResponse) 67 | } catch (e) { 68 | // log and store any error message 69 | console.error(e) 70 | } 71 | } 72 | 73 | render () { 74 | const profiles = this.state.profiles 75 | console.log(`profiles object:`, profiles) 76 | return ( 77 | 78 | Customer Profiles 79 | 80 | 85 | { !!profiles && 86 | 87 | {profiles.map((profile, i) => { 88 | return 89 | 90 | 92 | Send promo code 93 | 94 | this.sendPromo(profile['email']) }> 100 | Do you want to send promo to { profile['email'] }? 101 | 102 | 103 | Name: { profile['firstName'] } { profile['lastName'] } - Email: { profile['email'] } - Date of birth: { profile['birthDate'] } 104 | 105 | })} 106 | 107 | } 108 | { !profiles && 109 | No profiles! 110 | } 111 | 112 | 113 | ) 114 | } 115 | } 116 | 117 | Home.propTypes = { 118 | ims: PropTypes.any 119 | } 120 | 121 | export default Home -------------------------------------------------------------------------------- /src/dx-excshell-1/test/utils.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | const utils = require('./../actions/utils.js') 6 | 7 | test('interface', () => { 8 | expect(typeof utils.errorResponse).toBe('function') 9 | expect(typeof utils.stringParameters).toBe('function') 10 | expect(typeof utils.checkMissingRequestInputs).toBe('function') 11 | expect(typeof utils.getBearerToken).toBe('function') 12 | }) 13 | 14 | describe('errorResponse', () => { 15 | test('(400, errorMessage)', () => { 16 | const res = utils.errorResponse(400, 'errorMessage') 17 | expect(res).toEqual({ 18 | error: { 19 | statusCode: 400, 20 | body: { error: 'errorMessage' } 21 | } 22 | }) 23 | }) 24 | 25 | test('(400, errorMessage, logger)', () => { 26 | const logger = { 27 | info: jest.fn() 28 | } 29 | const res = utils.errorResponse(400, 'errorMessage', logger) 30 | expect(logger.info).toHaveBeenCalledWith('400: errorMessage') 31 | expect(res).toEqual({ 32 | error: { 33 | statusCode: 400, 34 | body: { error: 'errorMessage' } 35 | } 36 | }) 37 | }) 38 | }) 39 | 40 | describe('stringParameters', () => { 41 | test('no auth header', () => { 42 | const params = { 43 | a: 1, b: 2, __ow_headers: { 'x-api-key': 'fake-api-key' } 44 | } 45 | expect(utils.stringParameters(params)).toEqual(JSON.stringify(params)) 46 | }) 47 | test('with auth header', () => { 48 | const params = { 49 | a: 1, b: 2, __ow_headers: { 'x-api-key': 'fake-api-key', authorization: 'secret' } 50 | } 51 | expect(utils.stringParameters(params)).toEqual(expect.stringContaining('"authorization":""')) 52 | expect(utils.stringParameters(params)).not.toEqual(expect.stringContaining('secret')) 53 | }) 54 | }) 55 | 56 | describe('checkMissingRequestInputs', () => { 57 | test('({ a: 1, b: 2 }, [a])', () => { 58 | expect(utils.checkMissingRequestInputs({ a: 1, b: 2 }, ['a'])).toEqual(null) 59 | }) 60 | test('({ a: 1 }, [a, b])', () => { 61 | expect(utils.checkMissingRequestInputs({ a: 1 }, ['a', 'b'])).toEqual('missing parameter(s) \'b\'') 62 | }) 63 | test('({ a: { b: { c: 1 } }, f: { g: 2 } }, [a.b.c, f.g.h.i])', () => { 64 | expect(utils.checkMissingRequestInputs({ a: { b: { c: 1 } }, f: { g: 2 } }, ['a.b.c', 'f.g.h.i'])).toEqual('missing parameter(s) \'f.g.h.i\'') 65 | }) 66 | test('({ a: { b: { c: 1 } }, f: { g: 2 } }, [a.b.c, f.g.h])', () => { 67 | expect(utils.checkMissingRequestInputs({ a: { b: { c: 1 } }, f: { g: 2 } }, ['a.b.c', 'f'])).toEqual(null) 68 | }) 69 | test('({ a: 1, __ow_headers: { h: 1, i: 2 } }, undefined, [h])', () => { 70 | expect(utils.checkMissingRequestInputs({ a: 1, __ow_headers: { h: 1, i: 2 } }, undefined, ['h'])).toEqual(null) 71 | }) 72 | test('({ a: 1, __ow_headers: { f: 2 } }, [a], [h, i])', () => { 73 | expect(utils.checkMissingRequestInputs({ a: 1, __ow_headers: { f: 2 } }, ['a'], ['h', 'i'])).toEqual('missing header(s) \'h,i\'') 74 | }) 75 | test('({ c: 1, __ow_headers: { f: 2 } }, [a, b], [h, i])', () => { 76 | expect(utils.checkMissingRequestInputs({ c: 1 }, ['a', 'b'], ['h', 'i'])).toEqual('missing header(s) \'h,i\' and missing parameter(s) \'a,b\'') 77 | }) 78 | test('({ a: 0 }, [a])', () => { 79 | expect(utils.checkMissingRequestInputs({ a: 0 }, ['a'])).toEqual(null) 80 | }) 81 | test('({ a: null }, [a])', () => { 82 | expect(utils.checkMissingRequestInputs({ a: null }, ['a'])).toEqual(null) 83 | }) 84 | test('({ a: \'\' }, [a])', () => { 85 | expect(utils.checkMissingRequestInputs({ a: '' }, ['a'])).toEqual('missing parameter(s) \'a\'') 86 | }) 87 | test('({ a: undefined }, [a])', () => { 88 | expect(utils.checkMissingRequestInputs({ a: undefined }, ['a'])).toEqual('missing parameter(s) \'a\'') 89 | }) 90 | }) 91 | 92 | describe('getBearerToken', () => { 93 | test('({})', () => { 94 | expect(utils.getBearerToken({})).toEqual(undefined) 95 | }) 96 | test('({ authorization: Bearer fake, __ow_headers: {} })', () => { 97 | expect(utils.getBearerToken({ authorization: 'Bearer fake', __ow_headers: {} })).toEqual(undefined) 98 | }) 99 | test('({ authorization: Bearer fake, __ow_headers: { authorization: fake } })', () => { 100 | expect(utils.getBearerToken({ authorization: 'Bearer fake', __ow_headers: { authorization: 'fake' } })).toEqual(undefined) 101 | }) 102 | test('({ __ow_headers: { authorization: Bearerfake} })', () => { 103 | expect(utils.getBearerToken({ __ow_headers: { authorization: 'Bearerfake' } })).toEqual(undefined) 104 | }) 105 | test('({ __ow_headers: { authorization: Bearer fake} })', () => { 106 | expect(utils.getBearerToken({ __ow_headers: { authorization: 'Bearer fake' } })).toEqual('fake') 107 | }) 108 | test('({ __ow_headers: { authorization: Bearer fake Bearer fake} })', () => { 109 | expect(utils.getBearerToken({ __ow_headers: { authorization: 'Bearer fake Bearer fake' } })).toEqual('fake Bearer fake') 110 | }) 111 | }) 112 | -------------------------------------------------------------------------------- /src/dx-excshell-1/web-src/src/components/ActionsForm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Adobe Inc. All rights reserved. 3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. You may obtain a copy 5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * 7 | * Unless required by applicable law or agreed to in writing, software distributed under 8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | * OF ANY KIND, either express or implied. See the License for the specific language 10 | * governing permissions and limitations under the License. 11 | */ 12 | 13 | import React, { useState } from 'react' 14 | import PropTypes from 'prop-types' 15 | import ErrorBoundary from 'react-error-boundary' 16 | import { 17 | Flex, 18 | Heading, 19 | Form, 20 | Picker, 21 | TextArea, 22 | Button, 23 | ActionButton, 24 | StatusLight, 25 | ProgressCircle, 26 | Item, 27 | Text, 28 | View 29 | } from '@adobe/react-spectrum' 30 | import Function from '@spectrum-icons/workflow/Function' 31 | 32 | import actions from '../config.json' 33 | import actionWebInvoke from '../utils' 34 | 35 | const ActionsForm = (props) => { 36 | const [state, setState] = useState({ 37 | actionSelected: null, 38 | actionResponse: null, 39 | actionResponseError: null, 40 | actionHeaders: null, 41 | actionHeadersValid: null, 42 | actionParams: null, 43 | actionParamsValid: null, 44 | actionInvokeInProgress: false, 45 | actionResult: '' 46 | }) 47 | 48 | return ( 49 | 50 | Run your application backend actions 51 | {Object.keys(actions).length > 0 && ( 52 |
53 | ({ name: k }))} 59 | itemKey="name" 60 | onSelectionChange={(name) => 61 | setState({ 62 | ...state, 63 | actionSelected: name, 64 | actionResponseError: null, 65 | actionResponse: null 66 | }) 67 | } 68 | > 69 | {(item) => {item.name}} 70 | 71 | 72 |