Lesson 2: Explore the App Builder App

Within the newly created app, you have seen the .env file with your credentials for running the app.

package.json is the crucial part of almost every NodeJS project. It contains the list of dependencies, version, reproducible builds, and so on.

ext.config.yaml in the src/dx-excshell-1/ folder is the cockpit of your App Builder app back end. It lists the declaration of serverless actions including name, source files, runtime kind, default parameters, annotations, and so on. You can find the grammar of writing manifest here:

operations:
  workerProcess:
    - type: action
      impl: dx-asset-compute-worker-1/worker
hooks:
  post-app-run: adobe-asset-compute devtool
  test: adobe-asset-compute test-worker
actions: actions
runtimeManifest:
  packages:
    dx-asset-compute-worker-1:
      license: Apache-2.0
      actions:
        get-profiles:
          function: actions/get-profiles/index.js
          web: 'yes'
          runtime: 'nodejs:18'
          inputs:
              LOG_LEVEL: debug
              tenant: $CAMPAIGN_STANDARD_TENANT
              apiKey: $SERVICE_API_KEY
          annotations:
            require-adobe-auth: true
            final: true

Your app currently has only one action, get-profiles:

Now let's take a deeper look at the action's source code:

/**
 * This action gets a list of customer profiles the Adobe Campaign Standard API
 */

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

// main function that will be executed by Adobe I/O Runtime
async function main (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')

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

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

    // extract the user Bearer token from the input request parameters
    const token = getBearerToken(params)

    // initialize the sdk
    const campaignClient = await CampaignStandard.init(params.tenant, params.apiKey, token)

    // get profiles from Campaign Standard
    const profiles = await campaignClient.getAllProfiles()
    logger.debug('profiles = ' + JSON.stringify(profiles, null, 2))
    const response = {
      statusCode: 200,
      body: profiles
    }

    // 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

Here, the action exposes a main function, which accepts a list of parameters from the client. It checks that required parameters for using the Campaign Standard SDK are present in the list, including the Authorization header for authentication against Adobe IMS.

An access token is retrieved to initiate the SDK client instance, which is then used to retrieve the list of customer profiles using the getAllProfiles() function. Finally, the profiles are returned to the client. The entire execution is wrapped within a try-catch block, so errors are handled appropriately.

Next, let's see how the web UI communicates with the back end. All web assets are placed in the src/dx-excshell-1/web-src folder.

Beside a few auto-generated files useful for running your app on Adobe Experience Cloud (ExC) Shell, App.js is the extension point of your UI.

By default, it contains three pages: Home and About are just static pages listing reference docs; ActionsForm lists all available back-end actions, allows you to select which action to be invoke, and, when you click the "invoke" button, shows the invocation results in the browser console:

<View gridArea='content' padding='size-200'>
  <Switch>
    <Route exact path='/'>
      <Home></Home>
    </Route>
    <Route path='/actions'>
      <ActionsForm runtime={props.runtime} ims={props.ims} />
    </Route>
    <Route path='/about'>
      <About></About>
    </Route>
  </Switch>
</View>