💹

GA後の Salesforce Functions を試してみよう

36 min read

Salesforce に関わるみなさん( ノ゚Д゚)こんにちわ、Salesforce Advent Calendar 2021 の 23日目、みんなご存じ、かしゆかの誕生日イェ━━━━━ヽ( ゚Д゚)人(゚Д゚ )ノ━━━━━━イ!!

ゆかちゃんの誕生日を祝うべく、本年も Salesforce 小ネタ行きます。今日は、アレです。Salesforce Functions です。


すでに齋藤さんがSalesforce Functionsについてという内容で19日目に記載いただいていて、ありがたいことに、わいのWebinarも紹介下さっています。

https://www.youtube.com/watch?v=JjMK47u9YZM

ほぼ現時点の最強バイブル

とか書かれてしまうと、わいも Functions ちゃんと書かなきゃと本日に至ります。

今日は何するんですか?と問われたら、そのWebinar内で、わかりやすいだろと思ってイメージ図にしたら「実際の画面が良い」って方がいらっしゃったので、実際のソースコードと実行結果満載でお送りすることにしました。

齋藤さんと同じ会社の白石さんがSalesforce Functions (Pilot版) 環境構築をやってみたという小ネタを準備下さっているのもあります。こちらもご参考に。コマンドなどずいぶん変わってしまったので、雰囲気を感じていただければ幸いです。

基本原則として、現時点での唯一にして必要な情報が詰まっている公式ガイドGet Started with Salesforce Functions をなぞっていきます。

前提条件

Set Up Your Development Environmentに詳しいですが、ここにも載せておきます。

  • git コマンド
  • sfdx コマンド
  • sf コマンド
  • JavaScript/TypeScript を使う場合: Node.js 14以降, npm 6.13.4以降
  • Java を使う場合: OpenJDK 8以降, Apaceh Maven 3.6.3以降
  • Functions を Local で実行する場合: Docker Desktop
  • GitHub あると便利
  • RFC5424 に対応した Logging as a Services あるとログ保管できて便利

Salesforce 環境の準備

こちらは白石さんのサイトやGet Started with Salesforce Functions をご覧ください。

ごめんね、わいの力ではセットアップするための権限がないのだよ(ノД`)シクシク

ということで、この後の処理に関しては、次の点が前提条件となります。

  • DevHub が Enable されていること
  • Functions が Enable されていること
  • 適切な権限セットが開発者のプロファイルに適用されていること

詳細はConfigure Your Salesforce Orgをご覧ください。以後 Quick Start: Salesforce Functions にならって進めていきますね。

実行環境の準備

まずは、ソースコードや、Scratch Org、Functions を実行する環境などを準備していきましょう。

Salesforce DX プロジェクトの準備

まずは、手元のローカル環境からです。ソースコードを準備しましょう。適当な Working Directory へいって、次のコマンドです。今回作成するプロジェクトは YukaFunctionProject にしましょう。ゆかちゃんの誕生日だし。

sf generate project -n YukaFunctionProject

わいの偉いところは、実行結果もそのまま載せてしまうことです。どうだ

$ sf generate project -n YukaFunctionProject
target dir = /Users/sho/working/sfdx/work
   create YukaFunctionProject/config/project-scratch-def.json
   create YukaFunctionProject/README.md
   create YukaFunctionProject/sfdx-project.json
   create YukaFunctionProject/.husky/pre-commit
   create YukaFunctionProject/.vscode/extensions.json
   create YukaFunctionProject/.vscode/launch.json
   create YukaFunctionProject/.vscode/settings.json
   create YukaFunctionProject/force-app/main/default/lwc/.eslintrc.json
   create YukaFunctionProject/force-app/main/default/aura/.eslintrc.json
   create YukaFunctionProject/scripts/soql/account.soql
   create YukaFunctionProject/scripts/apex/hello.apex
   create YukaFunctionProject/.eslintignore
   create YukaFunctionProject/.forceignore
   create YukaFunctionProject/.gitignore
   create YukaFunctionProject/.prettierignore
   create YukaFunctionProject/.prettierrc
   create YukaFunctionProject/jest.config.js
   create YukaFunctionProject/package.json

ということで、わいの Local Directory の構成を惜しみなく出していくパターン。恥じらいなどない。

参考: Create a Salesforce DX Project

DevHub への Login

まずは今回利用する Salesforce 環境を認証しておきましょう。sf login します。ご丁寧に Org かい? Functions かい?と聞いてきてくれるので、まずは Org です。

$ sf login
? What would you like to log into? (Use arrow keys)
❯ Salesforce Org
  Salesforce Functions

したらば、いつもの Salesforce Login 画面が出てくるので、精一杯ログインしてください。

次にログイン先のOrgのAliasを決められますので、YukaDevHub と名付けましょう。sfdxとは違い、sfコマンドはインタラクティブ要素高め。ちなみにsfdxコマンド慣れしてるならそれでもいいですが、sfにしとくといい気がしますよ。

? Set an alias for the org (leave blank for no alias) YukaDevHub

次は、Default Org にするかどうかです。DevHub側かOrg側かを選択できます。今回は(私に権限がないので)ストーリー通りに DevHub(Scratch Org)に環境を準備していきます。スペースを押して選択してね。忘れずに。私はいつも忘れます。忘れたら、コマンドやり直しましょう。

? Set the org as your default org? (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
❯◉ target-dev-hub
 ◯ target-org

ということで、結果、こうなりました。

$ sf login
? What would you like to log into? Salesforce Org
? Set an alias for the org (leave blank for no alias) YukaDevHub
? Set the org as your default org? target-dev-hub
You're now logged in to tabe@functions-xxxx.xxx.

sf env list コマンドで状況も確認できますよ。

sf env list
Salesforce Orgs
===============================================================================================================================================================
| Aliases           Username                          Org ID             Instance Url                                                Auth Method Config
| ───────────────── ───────────────────────────────── ────────────────── ─────────────────────────────────────────────────────────── ─────────── ──────────────
(中略)
| YukaDevHub        tabe@xxxxxxxxx-xxxx-xxx.xxx       00xx0000000xxxxXXX https://bdc40732-20210913-xxxxxxxxx-xxxxx.my.salesforce.com web         target-dev-hub

今度は Functions 環境へのログインです。今度は functionsもつけてログインしましょう。一気にいけます。

$ sf login functions
Opening browser to https://cli-auth.heroku.com/sfdx/auth/cli/browser/(中略)?requestor=(中略)

Waiting for login... done
Saving credentials... done

このような Salesforce Functions へようこそ感のある画面が出てくるので、素直にlog In to Salesforceしてください。そこで、先ほどの DevHubと同じSalesforce環境へログインして紐付けます。

ログインがうまくいけばこのように、やったぜ(๑•̀ㅂ•́)و✧みたいな画面になります。

画面まで貼り付けて、わい偉い。

参考: Connect to Your Dev Hub

Scratch Org と Function Compute 環境の作成

次は実際に動かすための環境を準備します。Salesforce側は Scratch Org。Functions の呼び出し側ですね。それと、Functions が実際に動く Platform 環境を作成します。これは Compute Environment と呼ばれています。

まずは忘れないように Change Directory して、Working directory をプロジェクトルートディレクトリにしておきましょう。その後、みなさん Visual Studio Code 使っている気がするので、そういった方は code . とかで起動しましょう。

$ cd YukaFunctionProject/
 ~/working/sfdx/work/YukaFunctionProject >

まず、このプロジェクトではFunctionsを使いますよ、と明言します。config/project-scratch-def.json ファイル内のfeatures"Functions" を追加します。ファイル全体だとこんな感じ。default にそのまま追記してます。にしても、sho compnay ってなんやねん。

config/project-scratch-def.json
{
  "orgName": "sho company",
  "edition": "Developer",
  "features": ["Functions", "EnableSetPasswordInApi"],
  "settings": {
    "lightningExperienceSettings": {
      "enableS1DesktopEnabled": true
    },
    "mobileSettings": {
      "enableS1EncryptedStoragePref2": false
    }
  }
}

次に、この Projectファイルをベースに Scratch org を作成します。

Scratch org なので、どうせ環境なくなるぜとユーザ名も公開していくスタイル。この Scratch org の名前は YukaScratchOrg にしてます。Default の Org を設定しているので、default の DevHub内に Scratch org作ります。defaultしてしいていない場合は、-v で該当する DevHub のユーザ名を指定しましょう。

$ sfdx force:org:create -s -f config/project-scratch-def.json -a YukaScratchOrg
(node:56403) [DEP0147] DeprecationWarning: In future versions of Node.js, fs.rmdir(path, { recursive: true }) will be removed. Use fs.rm(path, { recursive: true }) instead
(Use `node --trace-deprecation ...` to show where the warning was created)
Successfully created scratch org: 00D9A0000005HVMUA2, username: test-rc2ac7uzxq9b@example.com

今度は Functions の Compute環境を作りましょう。今作成した YukaScratchOrg に関連する Compute環境を作成します。Compute 名をYukaComputeEnv にしましたよ。

$ sf env create compute -o YukaScratchOrg -a YukaComputeEnv
Creating compute environment for org ID 00D9A0000005HVMUA2... done
New compute environment created with ID yukafun-00d9a0000005hvmua2-527
Connecting environments... done
Your compute environment with local alias YukaComputeEnv is ready.

参考: Create a Scratch Org and Compute Environment

ソースコードの準備とデプロイ

さぁ、実行するための環境を準備しました。ここまでの内容をまとめますね。

Salesforce 組織(DevHub): YukaDevHub (Default DevHub)
Scratch org: YukaScratchOrg (Default Org)
Functions Compute 環境: YukaComputeEnv

ちな、環境はこのように。

sf env list
Salesforce Orgs
===============================================================================================================================================================
| Aliases           Username                          Org ID             Instance Url                                                Auth Method Config
| ───────────────── ───────────────────────────────── ────────────────── ─────────────────────────────────────────────────────────── ─────────── ──────────────
| YukaDevHub        tabe@xxxxxxxxx-xxxx-xxx.xxx       00xx0000000xxxxXXX https://bdc40732-20210913-xxxxxxxxx-xxxxx.my.salesforce.com web         target-dev-hub

Scratch Orgs
==================================================================================================================================================
| Aliases        Username                      Org ID             Instance Url                                              Auth Method Config
| ────────────── ───────────────────────────── ────────────────── ───────────────────────────────────────────────────────── ─────────── ──────────
| YukaScratchOrg test-rc2ac7uzxq9b@example.com 00D9A0000005HVMUA2 https://site-platform-2388-dev-ed.cs46.my.salesforce.com  web         target-org

では、サンプルコードを準備していきましょう。

Function ソースコードの開発

まず、Functions 用のテンプレートを準備します。sf generate function ですね。今回実行する関数の Function名をyukafunction、利用する言語を javascript とします。言語は、現時点でjavascript,typescript,javaが利用できます。

$ sf generate function -n yukafunction -l javascript
Created javascript function yukafunction in /Users/sho/working/sfdx/work/YukaFunctionProject/functions/yukafunction.

Before creating Scratch Orgs for development, please ensure that:
1. Enable Functions in your DevHub org
2. Add Functions to the "features" list in your scratch org definition JSON file, e.g. "features": ["Functions"]

プロジェクトルートの下に functions/yukafunction ができたことがわかります。ここに Functions で実行するべきファイル(index.js)を実装します。作成されたファイル構成はこんな感じ。npm用の package.json や、mochaのテスト用ファイルなども準備されてます。.eslintrc も準備があって気が利いてます。

functions/yukafunction/
├── .eslintrc
├── .mocharc.json
├── README.md
├── index.js
├── package.json
├── project.toml
└── test
    └── index.test.js

なお、ここにあるproject.tomlファイルは、Functions 用のメタデータを定義するファイルです。詳細は Manage FUnction Metadata TOML Filesをどうぞ。

今回準備するソースコードは、こちら。外部ライブラリを試していただきたいので、yahoo-stock-pricesパッケージを使って株価を取得するという、ただそれだけのコードです。とは言え、外部のシステムとの通信が発生するので、同期や非同期の考慮が必要なパターンです。

import { getCurrentData } from "yahoo-stock-prices";

export default async function (event, context, logger) {
    logger.info(`${JSON.stringify(event.data || {})}`);
    const result = await getCurrentData(event.data.id);
    logger.info(JSON.stringify(result));
    return result;
};

ペイロードに id を指定して、特定の株価情報を取得できます。また logger.info で logdrain 経由でログが出力されます。うまく活用しましょう。

それから今回は、外部のパッケージを利用するので、package.json も更新しておきましょう。
次の行を追加しておきます。

  "dependencies": {
    "yahoo-stock-prices": "^1.1.0"
  },

参考: Create a Salesforce Function

権限セットの更新とデプロイ

実は今回のケースでは権限セットをいじる必要がありません。なぜなら、Functions から Salesforce 内部へアクセスが発生しないからです!(ババン)

面倒事は後回しという気持ちが少なからずあったことは認めましょう。ともかく、functions のcontextを利用して Salesforceへアクセスする場合には、該当する権限セットの情報を準備して、それをデプロイする必要がある。ことを覚えましょう。

参考: Update and Deploy Permissions

ローカルでテストしてみよう

ローカルでテストを実行するには、次の前提条件が必要です。

  • Docker Desktop が稼働していること
  • Docker Desktop が Docker Service にログインしていること
  • Function のルートディレクトリで実行すること

ローカルでは、Docker のイメージを動かして、その Listen port へ特定のペイロード情報を送って実行結果の確認ができます。いずれも sf コマンドで行います。

Docker イメージの作成と起動

まず、ディレクトリをプロジェクトルートから、Function のルートへ移ります。今回の場合は、yukafunction という名前を付けていますから...

$ cd functions/yukafunction/

そのあとは、そのディレクトリで sf run function start するだけです。

長いけど、ログがあると嬉しい人がいると思うので、そのまま載せておきます。試せない人もいると思うので。

$ sf run function start
Building yukafunction
20: Pulling from heroku/buildpacks
0012d49a4463: Pull complete
b21ff80e29f9: Pull complete
22f8656dcc5d: Pull complete
17c352941ff7: Pull complete
54fb12506777: Pull complete
357fefdf9bc9: Pull complete
d9d5783cd0f5: Pull complete
ae860f4e3e08: Pull complete
6018af265953: Pull complete
5035dafe4b03: Pull complete
c10fd1b9aa75: Pull complete
03fb7dba7baa: Pull complete
8f7ab136a2c9: Pull complete
08d54b036422: Pull complete
0494ca24cd11: Pull complete
a0a5647c84a6: Pull complete
6a19c4a9cc6d: Pull complete
bcbb9964778f: Pull complete
96466d5c0dc7: Pull complete
f18d74887f79: Pull complete
d2a2659301d3: Pull complete
04ee91025a13: Pull complete
088b851c25af: Pull complete
a24d744b6321: Pull complete
bcd31301c840: Pull complete
4bfdb67d04f5: Pull complete
918e3d4177c3: Pull complete
9215aa0839f1: Pull complete
4f4fb700ef54: Pull complete
Digest: sha256:35b866c6ce0c5bdc55f0e888bad420f537cfedb4544b80b049ad60ca021e4501
Status: Downloaded newer image for heroku/buildpacks:20
20: Pulling from heroku/pack
7b1a6ab2e44d: Already exists
525e7e2b2cee: Already exists
0012d49a4463: Already exists
a4a4b0767176: Pull complete
5f2534680b11: Pull complete
Digest: sha256:41e80b174e8b4fe932f8c2b752198af2523b752783db7e27be941fe88387c1dd
Status: Downloaded newer image for heroku/pack:20
===> DETECTING
3 of 4 buildpacks participating
heroku/nodejs-engine           0.7.4
heroku/nodejs-npm              0.4.4
heroku/nodejs-function-invoker 0.2.7
===> ANALYZING
Previous image with name "yukafunction" not found
===> RESTORING
===> BUILDING
[INFO] Node.js Buildpack
[INFO] Setting NODE_ENV to production
[INFO] Installing toolbox
[INFO] - yj
[Installing Node]
[INFO] Getting Node version
[INFO] Resolving Node version
[INFO] Downloading and extracting Node v16.13.1
[Parsing package.json]
[INFO] Parsing package.json
[INFO] Using npm v8.1.2 from Node
[INFO] Installing node modules
added 224 packages, and audited 225 packages in 12s
26 packages are looking for funding
  run `npm fund` for details
5 moderate severity vulnerabilities
To address all issues (including breaking changes), run:
  npm audit fix --force
Run `npm audit` for details.
npm notice
npm notice
 New minor version of npm available! 8.1.2 -> 8.3.0
npm
 notice Changelog: <https://github.com/npm/cli/releases/tag/v8.3.0>
npm notice Run `npm install -g npm@8.3.0` to update!
npm
 notice
[Warning: Skip pruning because NODE_ENV is not 'production'.]
[Installing Node.js function runtime]
[INFO] Installing @heroku/sf-fx-runtime-nodejs@0.9.1...
npm WARN deprecated har-validator@5.1.5: this library is no longer supported
npm WARN deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
added 245 packages, and audited 246 packages in 16s
14 packages are looking for funding
  run `npm fund` for details
3 moderate severity vulnerabilities
To address all issues, run:
  npm audit fix
Run `npm audit` for details.
[INFO] @heroku/sf-fx-runtime-nodejs@0.9.1 installation successful
===> EXPORTING
Adding layer 'heroku/nodejs-engine:nodejs'
Adding layer 'heroku/nodejs-function-invoker:opt'
Adding layer 'heroku/nodejs-function-invoker:sf-fx-runtime-nodejs'
Adding 1/1 app layer(s)
Adding layer 'launcher'
Adding layer 'config'
Adding layer 'process-types'
Adding label 'io.buildpacks.lifecycle.metadata'
Adding label 'io.buildpacks.build.metadata'
Adding label 'io.buildpacks.project.metadata'
Setting default process type 'web'
Saving yukafunction...
*** Images (a52d9e8b9519):
      yukafunction
Adding cache layer 'heroku/nodejs-engine:nodejs'
Adding cache layer 'heroku/nodejs-engine:toolbox'
Starting yukafunction
Running on port :8080
Debugger running on port :9229
Debugger listening on ws://0.0.0.0:9229/8a40ce5f-1df1-401e-958e-26045307974b
For help, see: https://nodejs.org/en/docs/inspector

Running on port などがでてくればローカルでうまく動いてます。停止するには Ctrl+cですね。なんかそこら中に heroku って書かれていることが気になるかもしれませんが、気にしないことが正義です。

実際に function を実行してみよう

今度は別のコマンドコンソールから実行します。sf run function コマンドを使います。-p オプションで、渡したいペイロードの内容を指定します。今回は "CRM" ですね。Salesforce です。日本時間の2021/12/23の株価を取得できました。-o で該当する Salesforce組織も指定できます。今回のように指定しないと、default の組織が利用されます。

$ sf run function -l http://localhost:8080 -p '{"id": "CRM"}'
 ›   Warning: No -o connected org or target-org found, context will be partially initialized
Using target-org undefined login credential to initialize context
POST http://localhost:8080... 200
{
  "currency": "USD",
  "price": 252.8
}

ちゃんと取得できているようですね。嬉しい。

なお、我らがゆかちゃんの所属するAmuseも取得できます。2021/12/23 の終値です。

sf run function -l http://localhost:8080 -p '{"id": "4301.T"}'
 ›   Warning: No -o connected org or target-org found, context will be partially initialized
Using target-org undefined login credential to initialize context
POST http://localhost:8080... 200
{
  "currency": "JPY",
  "price": 2035
}

え、ちゃんと動いてるじゃん、スゴイ、を体験できます。Functions 環境を持てない方でもこのようにローカルでお試しできます。

Function をデプローイ

さあ、ついにデプロイの時間です。まずは Function からデプロイしていきます。ローカルのテストをされた方は Working directory が変わっているので、DXプロジェクトのルートディレクトリへ戻ってください。たいてい cd - で戻れそうですけれども。ともかく、ディレクトリは間違わないように。

さて、Functions はデプロイにあたり、git で commit されている必要があります。まだ git 準備していないので、git の準備からです。次の三種の神器で git初期化です。

git init
git add .
git commit

実際に試してみると、こう。

$ git init
Initialized empty Git repository in /Users/sho/working/sfdx/work/YukaFunctionProject/.git/
$ git add .
$ git commit -m 'first commit'
[main (root-commit) fa3ccfb] first commit
 25 files changed, 395 insertions(+)
 create mode 100644 .eslintignore
 create mode 100755 .forceignore
 create mode 100644 .gitignore
 create mode 100755 .husky/pre-commit
 create mode 100755 .prettierignore
 create mode 100755 .prettierrc
 create mode 100644 .vscode/extensions.json
 create mode 100644 .vscode/launch.json
 create mode 100644 .vscode/settings.json
 create mode 100644 README.md
 create mode 100644 config/project-scratch-def.json
 create mode 100644 force-app/main/default/aura/.eslintrc.json
 create mode 100644 force-app/main/default/lwc/.eslintrc.json
 create mode 100644 functions/yukafunction/.eslintrc
 create mode 100644 functions/yukafunction/.mocharc.json
 create mode 100644 functions/yukafunction/README.md
 create mode 100644 functions/yukafunction/index.js
 create mode 100644 functions/yukafunction/package.json
 create mode 100644 functions/yukafunction/project.toml
 create mode 100644 functions/yukafunction/test/index.test.js
 create mode 100644 jest.config.js
 create mode 100644 package.json
 create mode 100644 scripts/apex/hello.apex
 create mode 100644 scripts/soql/account.soql
 create mode 100644 sfdx-project.json

よし、git commit できたら sf deploy functions コマンドでデプロイ行います。-o でデプロイ先の組織を指定できます。今回は Scratch orgなので、YukaScratchOrgですね。default なので指定しなくても良さそうだけど。

sf deploy functions -o YukaScratchOrg
remote: Compressing source files... done.
remote: Building source:
remote: [yukafunction] 3 of 4 buildpacks participating
remote: [yukafunction] heroku/nodejs-engine           0.7.4
remote: [yukafunction] heroku/nodejs-npm              0.4.4
remote: [yukafunction] heroku/nodejs-function-invoker 0.2.7
remote: [yukafunction] Previous image with name "*****/7e4dd32c-88b5-4b30-b196-d4fd77ebfdea/yukafunction" not found
remote: [yukafunction] Layer cache not found
remote: [yukafunction] Layer cache not found
remote: [yukafunction] [INFO] Node.js Buildpack
remote: [yukafunction] [INFO] Setting NODE_ENV to production
remote: [yukafunction] [INFO] Installing toolbox
remote: [yukafunction] [INFO] - yj
remote: [yukafunction]
remote: [yukafunction] [Installing Node]
remote: [yukafunction] [INFO] Getting Node version
remote: [yukafunction] [INFO] Resolving Node version
remote: [yukafunction] [INFO] Downloading and extracting Node v16.13.1
remote: [yukafunction]
remote: [yukafunction] [Parsing package.json]
remote: [yukafunction] [INFO] Parsing package.json
remote: [yukafunction] [INFO] Using npm v8.1.2 from Node
remote: [yukafunction] [INFO] Installing node modules
remote: [yukafunction] npm WARN deprecated har-validator@5.1.5: this library is no longer supported
remote: [yukafunction] npm WARN deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
remote: [yukafunction] npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
remote: [yukafunction]
remote: [yukafunction] added 264 packages, and audited 265 packages in 7s
remote: [yukafunction]
remote: [yukafunction] 26 packages are looking for funding
remote: [yukafunction]   run `npm fund` for details
remote: [yukafunction]
remote: [yukafunction] 5 moderate severity vulnerabilities
remote: [yukafunction]
remote: [yukafunction] To address all issues (including breaking changes), run:
remote: [yukafunction]   npm audit fix --force
remote: [yukafunction]
remote: [yukafunction] Run `npm audit` for details.
remote: [yukafunction] npm notice
remote: [yukafunction] npm notice New minor version of npm available! 8.1.2 -> 8.3.0
remote: [yukafunction] npm notice Changelog: <https://github.com/npm/cli/releases/tag/v8.3.0>
remote: [yukafunction] npm notice Run `npm install -g npm@8.3.0` to update!
remote: [yukafunction] npm notice
remote: [yukafunction]
remote: [yukafunction] [Warning: Skip pruning because NODE_ENV is not 'production'.]
remote: [yukafunction]
remote: [yukafunction]
remote: [yukafunction] [Installing Node.js function runtime]
remote: [yukafunction] [INFO] Installing @heroku/sf-fx-runtime-nodejs@0.9.1...
remote: [yukafunction] npm WARN deprecated har-validator@5.1.5: this library is no longer supported
remote: [yukafunction] npm WARN deprecated uuid@3.4.0: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
remote: [yukafunction] npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142
remote: [yukafunction]
remote: [yukafunction] added 245 packages, and audited 246 packages in 6s
remote: [yukafunction]
remote: [yukafunction] 14 packages are looking for funding
remote: [yukafunction]   run `npm fund` for details
remote: [yukafunction]
remote: [yukafunction] 3 moderate severity vulnerabilities
remote: [yukafunction]
remote: [yukafunction] To address all issues, run:
remote: [yukafunction]   npm audit fix
remote: [yukafunction]
remote: [yukafunction] Run `npm audit` for details.
remote: [yukafunction] [INFO] @heroku/sf-fx-runtime-nodejs@0.9.1 installation successful
remote: [yukafunction] Adding layer 'heroku/nodejs-engine:nodejs'
remote: [yukafunction] Adding layer 'heroku/nodejs-function-invoker:opt'
remote: [yukafunction] Adding layer 'heroku/nodejs-function-invoker:sf-fx-runtime-nodejs'
remote: [yukafunction] Adding 1/1 app layer(s)
remote: [yukafunction] Adding layer 'launcher'
remote: [yukafunction] Adding layer 'config'
remote: [yukafunction] Adding layer 'process-types'
remote: [yukafunction] Adding label 'io.buildpacks.lifecycle.metadata'
remote: [yukafunction] Adding label 'io.buildpacks.build.metadata'
remote: [yukafunction] Adding label 'io.buildpacks.project.metadata'
remote: [yukafunction] Setting default process type 'web'
remote: [yukafunction] Saving *****/7e4dd32c-88b5-4b30-b196-d4fd77ebfdea/yukafunction...
remote: [yukafunction] *** Images (sha256:22de57e28ea2252be9ba6807b71d447bd22889caf8f77b1bb0339a27a5b1c813):
remote: [yukafunction]       *****/7e4dd32c-88b5-4b30-b196-d4fd77ebfdea/yukafunction:071851ff-9659-4c29-abbc-867eec6381e4
remote: [yukafunction] Layer cache not found
remote: [yukafunction] Adding cache layer 'heroku/nodejs-engine:nodejs'
remote: [yukafunction] Adding cache layer 'heroku/nodejs-engine:toolbox'
remote: Verifying deploy... done.
To https://git.heroku.com/yukafun-00d9a0000005hvmua2-527.git
 * [new branch]      main -> master
Reference for YukaFunctionProject-yukafunction created
Pushing changes to functions... done

はい、ということでなんか見覚えのあるようなないような感じですね。git 使ってるのは、そうかデプロイ先が git で受け付ける仕組みでソースコードが pushされてるんだな。ははーんみたいな気持ちになりますね。なんですかね。見覚えのある単語もたくさんあります。へぇ。

参考: Deploy a Function

Apex をデプローイ

ついにやってきました。最後です。Webinar でも紹介しましたが、現時点では Salesforce Functions は Apex からコールして実行する方法をとります。従って、Apex からの実行が必要です。では、Scratch org 側に function をコールする Apex を準備しましょう。

久方ぶりに登場、まずは sfdx force:apex:class:create を使って、Apex Class のひな形を準備しましょう。せっかくなので、Apex の名前も YukaFunctionApex としてみます。

$ sfdx force:apex:class:create -n YukaFunctionApex -d force-app/main/default/classes
target dir = /Users/sho/working/sfdx/work/YukaFunctionProject/force-app/main/default/classes
   create force-app/main/default/classes/YukaFunctionApex.cls
   create force-app/main/default/classes/YukaFunctionApex.cls-meta.xml

あと、ちょっと Callback(非同期) のコードもサンプルお見せしたいので、Callback 用のClassも準備します。こっちは急に日和って testCallback とかにしてます。

$ sfdx force:apex:class:create -n testCallback -d force-app/main/default/classes
target dir = /Users/sho/working/sfdx/work/YukaFunctionProject/force-app/main/default/classes
   create force-app/main/default/classes/testCallback.cls
   create force-app/main/default/classes/testCallback.cls-meta.xml

ということでソースコードです。

functions.Function.get() で、呼び出したい function を指定します。指定方法は プロジェクト名.function名 ですから、今回は YukaFunctionProject.yukafunction となります。

コールする時は .invoke します。ペイロードのJSONを、そのまま引数として入れます。id のみを今回は受け付けるので、{"id": "CRM"} のようにしています。

コールした時のオブジェクトに .getResponse() すると function の実行結果を取得できます。

なお、このソースコードは testSync() メソッドが同期呼び出しで、testAsync()が非同期呼び出しとなります。

非同期の場合には、invoke する時に Callback の Class が必要です。Callback Class を引数に入れるだけで、自動的に非同期コールになります。また、testAsync() には @InvocableMethod を付与していますので、フローなどからの呼び出しにも対応してます。

force-app/main/default/classes/YukaFunctionApex.cls
public with sharing class YukaFunctionApex {
    public static void testSync() {
        System.debug('Invoking myfunction');

        functions.Function myFunction = functions.Function.get('YukaFunctionProject.yukafunction');
        functions.FunctionInvocation invocation = myFunction.invoke('{"id": "CRM"}');
        String jsonResponse = invocation.getResponse();

        System.debug('Response from myfunction ' + jsonResponse);
    }

    @InvocableMethod(label='Invoke yukafunctionFunctions(asyncnronous)')
    public static void testAsync() {
        System.debug('Invoking asyncronous function');

        functions.Function myFunction = functions.Function.get('YukaFunctionProject.yukafunction');
        functions.FunctionInvocation invocation = myFunction.invoke('{"id": "4301.T"}', new testCallback());
        String jsonResponse = invocation.getResponse();

        System.debug('Response from myfunction ' + jsonResponse);
    }
}

そしてもう一つ。Callback 側のソースコードです。

force-app/main/default/classes/testCallback.cls
public class testCallback implements functions.FunctionCallback {
    public void handleResponse(functions.FunctionInvocation result) {
        System.debug('Response from callback ' + result);
    }
}

functions.FunctionCallbackimplements した Class 内に handleResponse メソッドを上書きすることで、コールバック関数として対処されます。ここでは、結果を debug log に吐き出しているだけですね。

あ、それと、今だけだと思いますが、テンプレートのメタデータでは APIバージョンが低いので変更の必要があります。YukaFunctionApex.cls-meta.xmltestCallback.cls-meta.xml<apiVersion>52.0</apiVersion><apiVersion>53.0</apiVersion> にします。Functions は Api53.0以降での提供になるので、ここでエラーになってしまうという罠があります。気をつけましょう。

*.xml
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>53.0</apiVersion>
    <status>Active</status>
</ApexClass>

さて、それでは、Scratch org にこれらのApex Classをpushして実装しましょう。sfdx force:sorce:push コマンドで -u で対象の Scratch org を指定します。YukaScratchOrg ですね。では、実行。

$ sfdx force:source:push -u YukaScratchOrg
*** Deploying with SOAP ***
Job ID | 0Af9A00000XP8ArSAL
SOURCE PROGRESS | ████████████████████████████████████████ | 2/2 Components
=== Pushed Source
STATE  FULL NAME         TYPE       PROJECT PATH
─────  ────────────────  ─────────  ────────────────────────────────────────────────────────────
Add    YukaFunctionApex  ApexClass  force-app/main/default/classes/YukaFunctionApex.cls
Add    YukaFunctionApex  ApexClass  force-app/main/default/classes/YukaFunctionApex.cls-meta.xml
Add    testCallback      ApexClass  force-app/main/default/classes/testCallback.cls
Add    testCallback      ApexClass  force-app/main/default/classes/testCallback.cls-meta.xml

デプロイうまく行きましたね!!! では、ちょっと試してみましょう。sfdx force:apex:execute を使ってたたくのが楽です。echo で Class 名を指定して実行です。USER_DEBUG に今日の株価情報のJSONが帰ってきていることが分かります。やったぜ(๑•̀ㅂ•́)و✧

$ echo "YukaFunctionApex.testSync();" | sfdx force:apex:execute -f /dev/stdin
Compiled successfully.
Executed successfully.

54.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO
Execute Anonymous: YukaFunctionApex.testSync();
00:35:27.385 (385355369)|USER_INFO|[EXTERNAL]|0059A000004swgT|test-rc2ac7uzxq9b@example.com|(GMT-08:00) Pacific Standard Time (America/Los_Angeles)|GMT-08:00
00:35:27.385 (385425026)|EXECUTION_STARTED
00:35:27.385 (385449089)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
00:35:27.385 (409544662)|USER_DEBUG|[3]|DEBUG|Invoking myfunction
00:35:30.478 (3478610625)|USER_DEBUG|[9]|DEBUG|Response from myfunction {"currency":"USD","price":252.8}
00:35:30.481 (3481485985)|CUMULATIVE_LIMIT_USAGE
00:35:30.481 (3481485985)|LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 0 out of 100
  Number of query rows: 0 out of 50000
  Number of SOSL queries: 0 out of 20
  Number of DML statements: 0 out of 150
  Number of Publish Immediate DML: 0 out of 150
  Number of DML rows: 0 out of 10000
  Maximum CPU time: 0 out of 10000
  Maximum heap size: 0 out of 6000000
  Number of callouts: 0 out of 100
  Number of Email Invocations: 0 out of 10
  Number of future calls: 0 out of 50
  Number of queueable jobs added to the queue: 0 out of 50
  Number of Mobile Apex push calls: 0 out of 10

00:35:30.481 (3481485985)|CUMULATIVE_LIMIT_USAGE_END

00:35:30.478 (3481636943)|CODE_UNIT_FINISHED|execute_anonymous_apex
00:35:30.478 (3481659914)|EXECUTION_FINISHED

なお、非同期で実行すると、こう。

echo "YukaFunctionApex.testAsync();" | sfdx force:apex:execute -f /dev/stdin
Compiled successfully.
Executed successfully.

54.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO
Execute Anonymous: YukaFunctionApex.testAsync();
00:40:40.190 (190047301)|USER_INFO|[EXTERNAL]|0059A000004swgT|test-rc2ac7uzxq9b@example.com|(GMT-08:00) Pacific Standard Time (America/Los_Angeles)|GMT-08:00
00:40:40.190 (190094631)|EXECUTION_STARTED
00:40:40.190 (190106483)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
00:40:40.190 (192176364)|USER_DEBUG|[14]|DEBUG|Invoking asyncronous function
00:40:42.252 (2252991248)|USER_DEBUG|[20]|DEBUG|Response from myfunction null
00:40:42.409 (2409363885)|CUMULATIVE_LIMIT_USAGE
00:40:42.409 (2409363885)|LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 0 out of 100
  Number of query rows: 0 out of 50000
  Number of SOSL queries: 0 out of 20
  Number of DML statements: 0 out of 150
  Number of Publish Immediate DML: 0 out of 150
  Number of DML rows: 0 out of 10000
  Maximum CPU time: 0 out of 10000
  Maximum heap size: 0 out of 6000000
  Number of callouts: 0 out of 100
  Number of Email Invocations: 0 out of 10
  Number of future calls: 0 out of 50
  Number of queueable jobs added to the queue: 0 out of 50
  Number of Mobile Apex push calls: 0 out of 10

00:40:42.409 (2409363885)|CUMULATIVE_LIMIT_USAGE_END

00:40:42.252 (2409514393)|CODE_UNIT_FINISHED|execute_anonymous_apex
00:40:42.252 (2409539244)|EXECUTION_FINISHED

こちらは結果が帰ってきていませんね、当然です。非同期コールの後、testCallback が実行され、Scratch org側の Debug logに出力されます。こんな感じで、なんかJPYっぽいアミューズらしいデータが返ってる気配ありますね。

参考: Invoke a Function From Apex


ということで、GA された環境での実行方法について流してみました。デバッグログの見方や、Loggin as a Service との連携なども行うことで、本番としての利用方法が見えてくるでしょう。次回以降は、パッケージやロギングについてもひとつずつ紹介します。今回みたいに長いのは、私も苦痛だし、呼んでるみなさまも苦痛でしょう。申し訳ない。

ところで「いいな、あたいも試したいずらよ!」という方、現在、トライアルはサインナップが必要でかつ制限があります。お金があるよと言う場合は購入も検討ください。購入に当たっては、私(と仲間たち)で精一杯ご支援します。まずは、みなさまのご担当の営業にご相談ください。

Discussion

ログインするとコメントできます