Skip to content

Latest commit

 

History

History
364 lines (284 loc) · 19 KB

File metadata and controls

364 lines (284 loc) · 19 KB

Node Functions

The following instructions describe how to apply and configure the Reference Functions Framework for Node (Javascript and Typescript) functions.

Node Proxy Overview

The Node Proxy is a Fastify based app, deployed alongside Node functions, that has the following APIs:

  • /sync for synchronous function requests that proxies to the function server using @fastify/http-proxy.
  • /async for asynchronous function requests that uses the onResponse hook to return 200 to the client. It then handles proxying to request the function.
  • /healthcheck to monitor the function server and restart, if needed.

To learn Fastify, check out the Fastify documentation.

Example Function Framework Artifacts

Copy the following proxy/ and bin/ directories to the root of your Node function directories.

proxy/ contents are the proxy source and artifacts.

bin/ contents are an inline buildpack invoked on deployment to build the proxy.

# Inline buildpack
functions/typescriptfunction/bin/
├── compile  // Compiles Node proxy
├── detect
└── release  // App startup command

# Proxy app
functions/typescriptfunction/proxy/
├── bin
├── config
├── index.js
├── lib
├── node_modules
├── package.json
├── package-lock.json
└── test

Function Project Changes

If not already present, install @heroku/sf-fx-runtime-nodejs as a production dependency. In each Node function’s root directory, run:

$ npm install @heroku/sf-fx-runtime-nodejs --save-prod 

package.json:

"dependencies": {
    "@heroku/sf-fx-runtime-nodejs": "^0.14.0"
}

Function App Creation and Build Configuration

For each function, create a Heroku app where you deploy the function source.

Creating an app and configuring buildpacks require the Heroku CLI.

Buildpacks transform deployed code into a deployment unit. The proxy and function use the following buildpacks:

These buildpacks are required to build the Reference Functions Framework. You can apply additional buildpacks to fulfill your function's requirements such as libraries for PDF generation, custom application metrics, or apt-based dependencies. For more information, see officially supported buildpacks and third-party buildpacks at Heroku Buildpacks.

For more information on how buildpacks are part of Heroku application development and deployment, see How Heroku Works.

# Create app for function
$ heroku create typescriptfunction

# Apply remote to repo
heroku git:remote -a typescriptfunction

# Apply buildpacks
$ heroku buildpacks:add -a typescriptfunction \
    https://github.com/lstoll/heroku-buildpack-monorepo
$ heroku buildpacks:add -a typescriptfunction heroku/nodejs
$ heroku buildpacks:add -a typescriptfunction heroku-community/inline

# List buildpacks
$ heroku buildpacks -a typescriptfunction
=== typescriptfunction Buildpack URLs

1. https://github.com/lstoll/heroku-buildpack-monorepo
2. heroku/nodejs
3. heroku-community/inline

Function App Configuration

Config var configuration requires the Heroku CLI.

Each function must have the following config vars.

The config vars, CONSUMER_KEY and ENCODED_PRIVATE_KEY, are values from the authorization connected app. Complete the following steps in MIGRATION.md before proceeding:

Ensure to target commands to your function via -a <function app name>.

# For repos with multiple functions, the heroku-buildpack-monorepo needs APP_BASE to point to the base dir of the function
$ heroku config:set -a typescriptfunction APP_BASE=functions/typescriptfunction

# In your Org under Company Settings -> Company Information find your Org's OrgId and convert to 18-char
$ heroku config:set -a typescriptfunction ORG_ID_18=00DDH0000005zbM2AQ

# Set the function's authorization Connected App's Consumer Key (from step 5 in MIGRATION.md)
$ heroku config:set -a typescriptfunction CONSUMER_KEY=3MVG9bm...

# Set the function's authorization Connected App's digital certificate/key, base64 encoded
# For macOs, use -b instead of -w to wrap or insert line breaks
$ heroku config:set -a typescriptfunction ENCODED_PRIVATE_KEY=`cat server.key | base64 -w 0`

# List config vars
$ heroku config -a typescriptfunction
=== typescriptfunction Config Vars

APP_BASE:            functions/typescriptfunction
CONSUMER_KEY:        3MVG9bm...
ENCODED_PRIVATE_KEY: LS0tLSx...
ORG_ID_18:           00DB0000000EjT0MAK

Function App Deployment

After development and testing are complete, deploy your function to Heroku.

$ heroku git:remote -a typescriptfunction
set git remote heroku to https://git.heroku.com/typescriptfunction.git

$ git push heroku main
Enumerating objects: 466, done.
Counting objects: 100% (466/466), done.
Delta compression using up to 36 threads
Compressing objects: 100% (361/361), done.
Writing objects: 100% (442/442), 318.44 KiB | 5.22 MiB/s, done.
Total 442 (delta 194), reused 16 (delta 5)
remote: Resolving deltas: 100% (194/194), completed with 15 local objects.
remote: Updated 194 paths from ca2964f
remote: Compressing source files... done.
remote: Building source:
remote: 
...
<log output deleted>
...
remote: 
remote: -----> Compressing...
remote:        Done: 125M
remote: -----> Launching...
remote:        Released v21
remote:        https://typescriptfunction.herokuapp.com/ deployed to Heroku
remote: 
remote: Verifying deploy... done.
To https://git.heroku.com/typescriptfunction.git
   079575a..a13f0b2  main -> main

Function App Startup

Ensure the build completed successfully by checking the app’s logs.

$ $ heroku logs -a typescriptfunction -t
...
2023-04-06T21:20:55.141367+00:00 heroku[web.1]: Starting process with command `cd proxy && npm start`
2023-04-06T21:20:57.143644+00:00 app[web.1]: 
2023-04-06T21:20:57.143662+00:00 app[web.1]: > [email protected] start
2023-04-06T21:20:57.143663+00:00 app[web.1]: > node index.js
2023-04-06T21:20:57.143663+00:00 app[web.1]: 
2023-04-06T21:20:57.540789+00:00 app[web.1]: {"level":30,"time":1680816057540,"pid":20,"hostname":"c3a624ba-d308-4253-ae50-634ff7c76442","msg":"Starting function w/ args: /app/proxy/../node_modules/@heroku/sf-fx-runtime-nodejs/bin/cli.js serve /app/proxy/.. -p 8080"}
2023-04-06T21:20:57.564002+00:00 app[web.1]: {"level":30,"time":1680816057545,"pid":20,"hostname":"c3a624ba-d308-4253-ae50-634ff7c76442","msg":"Started function started on port 8080, process pid 31"}
2023-04-06T21:20:57.564004+00:00 app[web.1]: {"level":30,"time":1680816057563,"pid":20,"hostname":"c3a624ba-d308-4253-ae50-634ff7c76442","msg":"Server listening at http://0.0.0.0:32780"}
2023-04-06T21:20:57.917174+00:00 heroku[web.1]: State changed from starting to up
2023-04-06T21:20:59.251062+00:00 app[web.1]: {"level":30,"time":1680816059250,"pid":20,"hostname":"c3a624ba-d308-4253-ae50-634ff7c76442","msg":"[fn] name=functionLogger hostname=c3a624ba-d308-4253-ae50-634ff7c76442 pid=42 worker=1 level=30 msg=\"started function worker 1\" time=2023-04-06T21:20:59.250Z v=0\n"}

Function Invocation

Sync

Example logs of a synchronous function invocation:

2023-04-10T15:02:31.956768+00:00 app[web.1]: {"level":30,"time":1681138951956,"pid":20,"hostname":"7c0e8423-9bd4-4091-8fe0-fc411c137df5","reqId":"00DB0000000gJmXMAU-4pHPy-10T0VBNUIdp8TgJ--4HrUCSDR72O1CitPdjgC2ES3czY=-sffxtest1.sfhxhello_typescriptfunction-2023-04-10T08:02:31.591-0700","req":{"method":"POST","url":"/sync","hostname":"typescriptfunction.herokuapp.com","remoteAddress":"10.1.90.50","remotePort":25862},"msg":"incoming request"}
2023-04-10T15:02:32.336330+00:00 app[web.1]: {"level":30,"time":1681138952336,"pid":20,"hostname":"7c0e8423-9bd4-4091-8fe0-fc411c137df5","reqId":"00DB0000000gJmXMAU-4pHPy-10T0VBNUIdp8TgJ--4HrUCSDR72O1CitPdjgC2ES3czY=-sffxtest1.sfhxhello_typescriptfunction-2023-04-10T08:02:31.591-0700","msg":"[cdf3e6a5-93ad-4f8c-ba87-9d7e9eed5ce3] Handling com.salesforce.function.invoke.sync request to function 'sfhxhello_typescriptfunction'..."}
2023-04-10T15:02:32.340266+00:00 app[web.1]: {"level":30,"time":1681138952338,"pid":20,"hostname":"7c0e8423-9bd4-4091-8fe0-fc411c137df5","reqId":"00DB0000000gJmXMAU-4pHPy-10T0VBNUIdp8TgJ--4HrUCSDR72O1CitPdjgC2ES3czY=-sffxtest1.sfhxhello_typescriptfunction-2023-04-10T08:02:31.591-0700","msg":"[cdf3e6a5-93ad-4f8c-ba87-9d7e9eed5ce3] Minting function  token for user [email protected], audience https://login.salesforce.com, url https://mycompany.my.salesforce.com/services/oauth2/token, issuer 3MVG9..."}
...
<log output deleted>
...
2023-04-10T15:02:33.185142+00:00 app[web.1]: {"level":30,"time":1681138953184,"pid":20,"hostname":"7c0e8423-9bd4-4091-8fe0-fc411c137df5","reqId":"00DB0000000gJmXMAU-4pHPy-10T0VBNUIdp8TgJ--4HrUCSDR72O1CitPdjgC2ES3czY=-sffxtest1.sfhxhello_typescriptfunction-2023-04-10T08:02:31.591-0700","res":{"statusCode":200},"responseTime":1228.1788830161095,"msg":"request completed"}
2023-04-10T15:02:33.185682+00:00 heroku[router]: at=info method=POST path="/sync" host=typescriptfunction.herokuapp.com request_id=cdf3e6a5-93ad-4f8c-ba87-9d7e9eed5ce3 fwd="136.147.46.8" dyno=web.1 connect=0ms service=1229ms status=200 bytes=2113 protocol=https

Async

Example logs of an asynchronous function invocation:

2023-04-10T23:07:12.334645+00:00 app[web.1]: {"level":30,"time":1681168032334,"pid":20,"hostname":"97e7e956-a995-4dcc-9619-0635dd01c996","reqId":"00DB0000000gJmXMAU-4pHpPcjoiD_NZpmt-SUG---a00B000000OtzVjIAJ-sffxtest1.sfhxhello_typescriptfunction-2023-04-10T16:07:11.768-0700","req":{"method":"POST","url":"/async","hostname":"typescriptfunction.herokuapp.com","remoteAddress":"10.1.93.65","remotePort":12329},"msg":"incoming request"}
2023-04-10T23:07:12.345209+00:00 app[web.1]: {"level":30,"time":1681168032336,"pid":20,"hostname":"97e7e956-a995-4dcc-9619-0635dd01c996","reqId":"00DB0000000gJmXMAU-4pHpPcjoiD_NZpmt-SUG---a00B000000OtzVjIAJ-sffxtest1.sfhxhello_typescriptfunction-2023-04-10T16:07:11.768-0700","msg":"[c836fe3f-2a99-4fe4-8ef3-0665132bcc3e] Validated context headers - well done"}
...
<log output deleted>
...
2023-04-10T23:07:14.460591+00:00 app[web.1]: {"level":30,"time":1681168034460,"pid":20,"hostname":"97e7e956-a995-4dcc-9619-0635dd01c996","reqId":"00DB0000000gJmXMAU-4pHpPcjoiD_NZpmt-SUG---a00B000000OtzVjIAJ-sffxtest1.sfhxhello_typescriptfunction-2023-04-10T16:07:11.768-0700","msg":"[c836fe3f-2a99-4fe4-8ef3-0665132bcc3e] Updated function response [200] to sffxtest1__AsyncFunctionInvocationRequest__c [a00B000000OtzVjIAJ]"}
2023-04-10T23:07:14.460934+00:00 app[web.1]: {"level":30,"time":1681168034460,"pid":20,"hostname":"97e7e956-a995-4dcc-9619-0635dd01c996","reqId":"00DB0000000gJmXMAU-4pHpPcjoiD_NZpmt-SUG---a00B000000OtzVjIAJ-sffxtest1.sfhxhello_typescriptfunction-2023-04-10T16:07:11.768-0700","res":{"statusCode":201},"responseTime":1058.924006998539,"msg":"request completed"}

Asynchronous function invocations are tracked and the AsyncFunctionInvocationRequest__c Custom Object handles the responses.

sfdx data query --query "SELECT Id, Response__c, Status__c, StatusCode__c, ExtraInfo__c, LastModifiedDate FROM AsyncFunctionInvocationRequest__c ORDER BY LastModifiedDate DESC LIMIT 1" --json 
{
  "status": 0,
  "result": {
    "records": [
      {
        "attributes": {
          "type": "AsyncFunctionInvocationRequest__c",
          "url": "/services/data/v58.0/sobjects/AsyncFunctionInvocationRequest__c/a00B000000OuIquIAF"
        },
        "Id": "a00B000000OuIquIAF",
        "Response__c": "{\"accounts\":[{\"id\":\"001B000001PH6aOIAT\",\"name\":\"Sample Account for Entitlements\"},{\"id\":\"001B000001PH6ZwIAL\",\"name\":\"GenePoint\"},{\"id\":\"001B000001PH6ZxIAL\",\"name\":\"United Oil \\u0026 Gas, UK\"},{\"id\":\"001B000001PH6ZyIAL\",\"name\":\"United Oil \\u0026 Gas, Singapore\"},{\"id\":\"001B000001PH6ZzIAL\",\"name\":\"Edge Communications\"},{\"id\":\"001B000001PH6a0IAD\",\"name\":\"Burlington Textiles Corp of America\"},{\"id\":\"001B000001PH6a1IAD\",\"name\":\"Pyramid Construction Inc.\"},{\"id\":\"001B000001PH6a2IAD\",\"name\":\"Dickenson plc\"},{\"id\":\"001B000001PH6a3IAD\",\"name\":\"Grand Hotels \\u0026 Resorts Ltd\"},{\"id\":\"001B000001PH6a4IAD\",\"name\":\"Express Logistics and Transport\"},{\"id\":\"001B000001PH6a5IAD\",\"name\":\"University of Arizona\"},{\"id\":\"001B000001PH6a6IAD\",\"name\":\"United Oil \\u0026 Gas Corp.\"}]}",
        "Status__c": "SUCCESS",
        "StatusCode__c": 200,
        "ExtraInfo__c": "%7B%22requestId%22%3A%2200DB0000000gJmXMAU-4pa5Qlzen5qguUIdp8TgJ--a00B000000OuIquIAF-sffxtest1.sfhxhello_typescriptfunction-2023-04-26T08%3A42%3A28.344-0700%22%2C%22source%22%3A%22urn%3Aevent%3Afrom%3Asalesforce%2FGS0%2F00DB0000000gJmXMAU%2Fapex%22%2C%22execTimeMs%22%3A246%2C%22statusCode%22%3A200%2C%22isFunctionError%22%3Afalse%2C%22stack%22%3A%5B%5D%7D",
        "LastModifiedDate": "2023-04-26T15:42:33.000+0000"
      }
    ],
    "totalSize": 1,
    "done": true
  },
  "warnings": []
}

Local Development

Start Proxy and Function

Starting the proxy also starts the function.

Ensure that you installed the target function, typescriptfunction, and the proxy, typescriptfunction/proxy, locally and built with npm install && npm run build.

Set the following environment variables:

  • CONSUMER_KEY: consumer key of the function’s authorization connected app
  • ENCODED_PRIVATE_KEY: base64 encoded private key of the function’s authorization connected app
  • HOME: full path to the function directory
  • ORG_ID_18: ID of the function authenticated Salesforce org

Starting the Proxy with the Command Line

$ pwd
/home/functions/git/function-migration/functions/typescriptfunction/proxy

$ npm start
...

In addition, the following npm scripts are available:

  • npm run dev: to start the proxy app in debug mode.
  • npm run test: run tests.

Starting the Proxy with the IDE

Set environment variables noted in Start Proxy and Function.

Run proxy/index.js or npm start via IDE configuration.

Invocation Bash Scripts

The following scripts invoke local functions synchronously and asynchronously.

/home/functions/git/function-migration/functions/typescriptfunction/proxy/bin
├── invokeAsync.sh
├── invokeSync.sh
├── queryAsync.sh
├── sfContext.json
├── sfContextStdUser.json
├── sfFnAsyncContext.json
└── sfFnSyncContext.json

Edit the .json files that reflect your Salesforce org and function payload.

Supply an access token provided by sfdx force org display.

$ sfdx org display
=== Org Description

 KEY              VALUE                                                                                                            
 ──────────────── ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── 
 Access Token     00DB0000000gJmX!AQEA... 
...

# Sync invoke function
$ ./invokeSync.sh '00DB0000000gJmX!AQEA...'
...

# Async invoke function
$ ./invokeAsync.sh '00DB0000000gJmX!AQEA...'
...

For async requests, use ./queryAsync.sh to query for the last AsyncFunctionInvocationRequest__c record. Ensure that the querying user has Read access to AsyncFunctionInvocationRequest__c and fields.

If your Salesforce org has a namespace, edit ./queryAsync.sh prepending the namespace to AsyncFunctionInvocationRequest__c and to each field.

$ ./queryAsync.sh
{
  "status": 0,
  "result": {
    "records": [
      {
        "attributes": {
          "type": "AsyncFunctionInvocationRequest__c",
          "url": "/services/data/v57.0/sobjects/AsyncFunctionInvocationRequest__c/a00xx000000bz3tAAA"
        },
        "Id": "a00xx000000bz3tAAA",
        "Response__c": "[{\"type\":\"Account\",\"fields\":{...}]",
        "Status__c": "SUCCESS",
        "StatusCode__c": 200,
        "ExtraInfo__c": "%7B%22requestId%22%3A%2200DB0000000gJmXMAU-4pU5ZP6e4Yo6spmt-SLE3--a00B000000OuAvYIAV-sffxtest1.sfhxhello_javafunction-2023-04-20T15%3A55%3A15.519-0700%22%2C%22source%22%3A%22urn%3Aevent%3Afrom%3Asalesforce%2FGS0%2F00DB0000000gJmXMAU%2Fapex%22%2C%22execTimeMs%22%3A763%2C%22statusCode%22%3A200%2C%22isFunctionError%22%3Afalse%2C%22stack%22%3A%5B%5D%7D",
        "Callback__c": "{\"functionName\":\"TypescriptFunction\"}",
        "CallbackType__c": "InvokeTypescriptFunction.Callback",
        "LastModifiedDate": "2023-04-10T22:48:12.000+0000"
      }
    ],
    "totalSize": 1,
    "done": true
  }
}

AsyncFunctionInvocationRequest__c.ExtraInfo__c is the URL encode. To decode and view, use the jq utility:

$ (IFS="+"; read _z; echo -e ${_z//%/\\x}"") <<< `queryAsync.sh | jq -r '.result.records[0].sffxtest1__ExtraInfo__c'` | jq
{
  "requestId": "00DB0000000gJmXMAU-4pU5ZP6e4Yo6spmt-SLE3--a00B000000OuAvYIAV-sfhxhello_typescriptfunction-2023-04-20T15:55:15.519-0700",
  "source": "urn:event:from:salesforce/GS0/00DB0000000gJmXMAU/apex",
  "execTimeMs": 763,
  "statusCode": 200,
  "isFunctionError": false,
  "stack": []
}