Skip to content

Commit

Permalink
Added I/O events sample. (#11)
Browse files Browse the repository at this point in the history
* Added I/O events sample.

* Added EOF empty lines and license headers.

* Adding EOF empty line and README updated.

* Updated README with correct event payload.

* Creating parent folder to hold IO events apps.

---------

Co-authored-by: Gabriel Ranghetti <gabriel.ranghetti@toyota.com>
  • Loading branch information
GabrielRanghetti and Gabriel Ranghetti authored Jun 7, 2024
1 parent d241ce3 commit 1897a20
Show file tree
Hide file tree
Showing 11 changed files with 14,115 additions and 0 deletions.
20 changes: 20 additions & 0 deletions events/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Adobe I/O Events Applications

This section contains a collection of application examples that demonstrate the use of Adobe I/O Events.
Each application serves as a example demonstrating how to effectively use Adobe I/O Events to take action when a shopper performs an action on an Adobe product.

## Applications List

The applications covered in this section include:

- Adobe Commerce customer login.

Please refer to the README file of each individual application for more details.

## Getting Started

To get started with these applications, simply navigate to the directory of the application you are interested in and follow the instructions provided in its README file.

## Note

Please note that this section is still a **work in progress**. Application examples will be available soon.
10 changes: 10 additions & 0 deletions events/commerce-customer-login/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"env": {
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:jest/recommended"],
"parserOptions": {
"ecmaVersion": "latest"
}
}
42 changes: 42 additions & 0 deletions events/commerce-customer-login/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

# package directories
node_modules
jspm_packages

# build
build
dist
.manifest-dist.yml

# Config
config.json
.env*
.aio

# Adobe I/O console config
console.json

# Test output
junit.xml

# IDE & Temp
.cache
.idea
.nyc_output
.vscode
coverage
.aws.tmp.creds.json
.wskdebug.props.tmp

# Parcel
.parcel-cache

# OSX
.DS_Store

# yeoman
.yo-repository

# logs folder for aio-run-detached
logs

73 changes: 73 additions & 0 deletions events/commerce-customer-login/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Adobe I/O Events Sample

This is an example for I/O Events.

## Table of Contents

- [Introduction](#introduction)
- [Prerequisites](#prerequisites)
- [Explanation](#explanation)

## Introduction

With Adobe I/O Events, developers can create event-driven applications that take action when a shopper performs an action on an Adobe product. In this example the app will listen to the Adobe Commerce ```observer.customer_login``` event, get the payload and send to a Slack channel.

## Prerequisites

Before you begin, ensure you have the following:

- An Adobe Developer account
- Node.js and npm installed on your local machine (nvm 18.x.x (Mac/Linux) or nvm-windows (Windows))
- App Builder project created and configured to work with I/O Events.
- A Slack App configured to receive incoming webhooks. See link: [Slack API](https://api.slack.com/messaging/webhooks)

## Explanation

The runtime action ```events/customer-login``` will receive the Commerce event payload and send to the configured Slack channel.

### Commerce configured io_events.xml file:

```xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_AdobeCommerceEventsClient:etc/io_events.xsd">
<event name="observer.customer_login">
<fields>
<field name="customer.firstname" />
<field name="customer.lastname" />
</fields>
</event>
</config>
```

### Commerce event payload example:

```json
{
"data": {
"key": "4fbbb851-285f-4733-a3b4-77556e019691",
"value": {
"customer": {
"firstname": "John",
"lastname": "Doe"
}
},
"source": "evergreen.evergreen_staging",
"_metadata": {
"commerceEdition": "Adobe Commerce + B2B",
"commerceVersion": "2.4.6-p4",
"eventsClientVersion": "1.5.0",
"storeId": "1",
"websiteId": "1",
"storeGroupId": "1"
}
},
"id": "f7813962-c119-401c-aa8c-266421aa1053",
"source": "urn:uuid:420ba332-4e1c-4373-8e23-81175c9e79fd",
"specversion": "1.0",
"type": "com.adobe.commerce.observer.customer_login",
"datacontenttype": "application/json",
"time": "2024-06-04T16:05:04.542Z",
"event_id": "7c9ae597-f382-49bc-85b7-3a2b4da6e0bf",
"recipient_client_id": "531f2f763b2f4c34b03001d8bf2bd4df"
}
```
84 changes: 84 additions & 0 deletions events/commerce-customer-login/actions/customer-login/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

/**
* This is a sample action showcasing how to access an external API
*
* Note:
* You might want to disable authentication and authorization checks against Adobe Identity Management System for a generic action. In that case:
* - Remove the require-adobe-auth annotation for this action in the manifest.yml of your application
* - Remove the Authorization header from the array passed in checkMissingRequestInputs
* - The two steps above imply that every client knowing the URL to this deployed action will be able to invoke it without any authentication and authorization checks against Adobe Identity Management System
* - Make sure to validate these changes against your security requirements before deploying the action
*/


const fetch = require('node-fetch')
const { Core } = require('@adobe/aio-sdk')
const { errorResponse, stringParameters, checkMissingRequestInputs } = require('../utils')

// main function that will be executed by Adobe I/O Runtime
const main = async params => {
// create a Logger
const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' })

try {
// 'info' is the default level if not set
logger.info('Calling the main action of customer-login')

// log parameters, only if params.LOG_LEVEL === 'debug'
logger.debug(stringParameters(params))

// check for missing request input parameters and headers
const requiredHeaders = []
const errorMessage = checkMissingRequestInputs(params, requiredHeaders)
if (errorMessage) {
// return and log client errors
return errorResponse(400, errorMessage, logger)
}

// post the message to external api endpoint
var slackText = "Customer Login - " + JSON.stringify(params)

const payload = {
"text": slackText
}

const res = await fetch(params.SLACK_WEBHOOK, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
if (!res.ok) {
return errorResponse(res.status, 'Something is wrong with your Slack webhook URL.', logger)
}

const response = {
statusCode: 200,
body: {
message: "Commerce event information sent successfully."
}
}

// log the response status code
logger.info(`${response.statusCode}: successful request`)
return response
} catch (error) {
// log any server errors
logger.error(error)
// return with 500
return errorResponse(500, 'server error', logger)
}
};

exports.main = main
144 changes: 144 additions & 0 deletions events/commerce-customer-login/actions/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

/* This file exposes some common utilities for your actions */

/**
*
* Returns a log ready string of the action input parameters.
* The `Authorization` header content will be replaced by '<hidden>'.
*
* @param {object} params action input parameters.
*
* @returns {string}
*
*/
function stringParameters (params) {
// hide authorization token without overriding params
let headers = params.__ow_headers || {}
if (headers.authorization) {
headers = { ...headers, authorization: '<hidden>' }
}
return JSON.stringify({ ...params, __ow_headers: headers })
}

/**
*
* Returns the list of missing keys giving an object and its required keys.
* A parameter is missing if its value is undefined or ''.
* A value of 0 or null is not considered as missing.
*
* @param {object} obj object to check.
* @param {array} required list of required keys.
* Each element can be multi level deep using a '.' separator e.g. 'myRequiredObj.myRequiredKey'
*
* @returns {array}
* @private
*/
function getMissingKeys (obj, required) {
return required.filter(r => {
const splits = r.split('.')
const last = splits[splits.length - 1]
const traverse = splits.slice(0, -1).reduce((tObj, split) => { tObj = (tObj[split] || {}); return tObj }, obj)
return traverse[last] === undefined || traverse[last] === '' // missing default params are empty string
})
}

/**
*
* Returns the list of missing keys giving an object and its required keys.
* A parameter is missing if its value is undefined or ''.
* A value of 0 or null is not considered as missing.
*
* @param {object} params action input parameters.
* @param {array} requiredHeaders list of required input headers.
* @param {array} requiredParams list of required input parameters.
* Each element can be multi level deep using a '.' separator e.g. 'myRequiredObj.myRequiredKey'.
*
* @returns {string} if the return value is not null, then it holds an error message describing the missing inputs.
*
*/
function checkMissingRequestInputs (params, requiredParams = [], requiredHeaders = []) {
let errorMessage = null

// input headers are always lowercase
requiredHeaders = requiredHeaders.map(h => h.toLowerCase())
// check for missing headers
const missingHeaders = getMissingKeys(params.__ow_headers || {}, requiredHeaders)
if (missingHeaders.length > 0) {
errorMessage = `missing header(s) '${missingHeaders}'`
}

// check for missing parameters
const missingParams = getMissingKeys(params, requiredParams)
if (missingParams.length > 0) {
if (errorMessage) {
errorMessage += ' and '
} else {
errorMessage = ''
}
errorMessage += `missing parameter(s) '${missingParams}'`
}

return errorMessage
}

/**
*
* Extracts the bearer token string from the Authorization header in the request parameters.
*
* @param {object} params action input parameters.
*
* @returns {string|undefined} the token string or undefined if not set in request headers.
*
*/
function getBearerToken (params) {
if (params.__ow_headers &&
params.__ow_headers.authorization &&
params.__ow_headers.authorization.startsWith('Bearer ')) {
return params.__ow_headers.authorization.substring('Bearer '.length)
}
return undefined
}
/**
*
* Returns an error response object and attempts to log.info the status code and error message
*
* @param {number} statusCode the error status code.
* e.g. 400
* @param {string} message the error message.
* e.g. 'missing xyz parameter'
* @param {*} [logger] an optional logger instance object with an `info` method
* e.g. `new require('@adobe/aio-sdk').Core.Logger('name')`
*
* @returns {object} the error object, ready to be returned from the action main's function.
*
*/
function errorResponse (statusCode, message, logger) {
if (logger && typeof logger.info === 'function') {
logger.info(`${statusCode}: ${message}`)
}
return {
error: {
statusCode,
body: {
error: message
}
}
}
}

module.exports = {
errorResponse,
getBearerToken,
stringParameters,
checkMissingRequestInputs
}
Loading

0 comments on commit 1897a20

Please sign in to comment.