├── 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 | You need to enable JavaScript to run this app.
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 | 
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 |
105 | )}
106 |
107 | {state.actionResponseError && (
108 |
109 | Failure! See the complete error in your browser console.
110 |
111 | )}
112 | {!state.actionResponseError && state.actionResponse && (
113 |
114 | Success! See the complete response in your browser console.
115 |
116 | )}
117 |
118 | {Object.keys(actions).length === 0 && You have no actions ! }
119 |
128 |
129 | )
130 |
131 | // Methods
132 |
133 | // parses a JSON input and adds it to the state
134 | async function setJSONInput (input, stateJSON, stateValid) {
135 | let content
136 | let validStr = null
137 | if (input) {
138 | try {
139 | content = JSON.parse(input)
140 | validStr = 'valid'
141 | } catch (e) {
142 | content = null
143 | validStr = 'invalid'
144 | }
145 | }
146 | setState({ ...state, [stateJSON]: content, [stateValid]: validStr })
147 | }
148 |
149 | // invokes a the selected backend actions with input headers and params
150 | async function invokeAction () {
151 | setState({ ...state, actionInvokeInProgress: true, actionResult: 'calling action ... ' })
152 | const actionName = state.actionSelected
153 | const headers = state.actionHeaders || {}
154 | const params = state.actionParams || {}
155 | const startTime = Date.now()
156 | // all headers to lowercase
157 | Object.keys(headers).forEach((h) => {
158 | const lowercase = h.toLowerCase()
159 | if (lowercase !== h) {
160 | headers[lowercase] = headers[h]
161 | headers[h] = undefined
162 | delete headers[h]
163 | }
164 | })
165 | // set the authorization header and org from the ims props object
166 | if (props.ims.token && !headers.authorization) {
167 | headers.authorization = `Bearer ${props.ims.token}`
168 | }
169 | if (props.ims.org && !headers['x-gw-ims-org-id']) {
170 | headers['x-gw-ims-org-id'] = props.ims.org
171 | }
172 | let formattedResult = ""
173 | try {
174 | // invoke backend action
175 | const actionResponse = await actionWebInvoke(actions[actionName], headers, params)
176 | formattedResult = `time: ${Date.now() - startTime} ms\n` + JSON.stringify(actionResponse,0,2)
177 | // store the response
178 | setState({
179 | ...state,
180 | actionResponse,
181 | actionResult:formattedResult,
182 | actionResponseError: null,
183 | actionInvokeInProgress: false
184 | })
185 | console.log(`Response from ${actionName}:`, actionResponse)
186 | } catch (e) {
187 | // log and store any error message
188 | formattedResult = `time: ${Date.now() - startTime} ms\n` + e.message
189 | console.error(e)
190 | setState({
191 | ...state,
192 | actionResponse: null,
193 | actionResult:formattedResult,
194 | actionResponseError: e.message,
195 | actionInvokeInProgress: false
196 | })
197 | }
198 | }
199 | }
200 |
201 | ActionsForm.propTypes = {
202 | runtime: PropTypes.any,
203 | ims: PropTypes.any
204 | }
205 |
206 | export default ActionsForm
207 |
--------------------------------------------------------------------------------