Posted at

博士の愛したIsomorphicなSalesforceの数式

More than 1 year has passed since last update.


Salesforceの数式

Salesforceには数式項目というなかなか強力な仕組みがあります。オブジェクト中の他の項目の値や与えられた関数を元に計算ロジックを記述し、値を算出するものです。参照元の項目値が更新されれば自動的にプラットフォーム側で再計算され、プログラム側で再計算させる必要はありません。

例えば商品単価を元に販売価格を計算して算出したい場合、以下のような感じになります。

UnitPrice__c * Quantity__c

オブジェクトの参照関係もまたがって記述できるので、たとえば商品マスターを参照している明細のような場合、次のようになります。

Product__r.UnitPrice__c * Quantity__c

数式には関数を利用できます。たとえばIF関数によって条件分岐することが可能です。IF関数は第一引数に条件判定のための真偽値、第二引数には第一引数が真のときの値、第三引数には偽のときの値をとります。以下は商品が課税対象かどうかで金額に消費税率を掛けています。(税率ハードコーディングしてるのであまり良い例ではないですが)

IF(Product__r.IsTaxable__c, Product__r.UnitPrice__c * Quantity__c * 1.08, Product__r.UnitPrice__c * Quantity__c)

まあ、ここまではSalesforce入門レベルのおはなしですよね。


「Isomorphic」な開発

さて、いきなりですが、最近はSPA(Single Page Application)でアプリを作るのが流行りのようです。その流れの中でも特に、「Isomorphic」、あるいは「Universal」な開発がキーワードになっていたりします。

雑に説明すると、


  • Single Page Applicationのフロントエンド開発にはJavaScriptが事実上必須

  • しかしフロントエンドとバックエンド(サーバ側)で2種類の言語を使うのは面倒

  • じゃあフロントもバックも同じ(Isomorphic)JavaScriptを主体にしてしまおう

という話です。特にSEO対策などでサーバサイドレンダリングもしなければいけないとき、同じ描画ロジックを2種類も開発保守することは避けたいので、Isomorphicな開発への要求は顕著になります。

(ちなみに、サーバ側でJavaScript、というのは、一昔前はちょっと実用には厳しいところもあったのですが、最近はNode.jsが大手サービスでもがんがん活用されており、もはや全然特殊な話でもなくなりましたね)

しかしながらSalesforceの開発においては、残念なことにサーバサイドはApex言語で固定されてますので、Herokuなど外部サービスを使わないピュアな開発ではこのアプローチは使えません。そもそもSEOを含む要求はSalesforceの開発ではほとんどないため、Isomorphicな開発への要求自体があまり表面化しないところがあります。まああきらめてサーバ側はApex使いましょう、というのが仕方ないところのようです。


数式をIsomorphicに評価する

SPAの開発では、サーバ側に処理を全て任せずにフロントエンドで対処できるものは対処することによって、ユーザからの入力に即時に反応することが望まれます。たとえば最初に挙げた金額を計算する数式項目はSalesforce内部で計算されますので、一回サーバ側にデータを保存しないと何も出てきません。SPAのエクスペリエンス的には、たとえば数量を画面内で更新したらすぐに計算された金額を画面表示に反映してほしいものです。ただ、そのためにフロントエンド側で特別に計算ロジックを埋め込んでしまうのは避けたいところです。(例えば税率が変わったときのことを考えて下さい!)

これを実現するためには、数式表現をフロントエンド側でも解釈して評価する必要があるのですが、数式はJavaScriptではありませんので、そのまま実行することはできません。一旦Salesforceの数式表現を解析し、それをしかるべき手段でJavaScriptで実行してやる必要があります。

実はこれを解決するものとしてformulonというものがあります。これはずばりSalesforceの数式(Formula)をJavaScriptで解析・実行できるようにしようとするライブラリです。これによりSalesforceの数式をJavaScriptで実行することが可能になります。

ただ、残念なことにまだformulonは不完全です。サポートしている関数もすべてのSalesforce組み込みを網羅していないですし、何より参照項目を辿った評価に対応していません。

formulonはPEG.jsという構文解析器作成プログラム(Parser Generator)を使用して、文法を記述したファイルからSalesforceの数式を解析する構文解析器を生成しています。なので、この元になっている文法を少し手直ししてあげることで、不完全な対応を少しマシにすることができます。

今回、formulonをforkして、参照項目にも対応できるように書き直したものをこちらに上げてあります。

https://github.com/stomita/formulon

以下のコードで参照を含む数式も評価できます。ただしformulonの元々のインターフェースとはちょっと呼び出しインターフェースも変えているので注意してください(公式のREADMEとはやり方が違う)。

import { parse } from 'formulon/lib/ast';

const record = { Product__r: { UnitPrice__c: 100, IsTaxable__c: true }, Quantity__c: 4 };
const formula =
"IF(Product__r.IsTaxable__c, Product__r.UnitPrice__c * Quantity__c * 1.08, Product__r.UnitPrice__c * Quantity__c)";

const parsed = parse(formula);
parsed.evaluate(record);
// => 432

上記を見て、evaluateにコンテキストとして渡すレコードにProduct__rの内容を設定しなければいけないなんて、プログラムを書いた時点で数式の内容がわかっている場合でないと無理じゃん、という突っ込みがあるかもしれません。これについては、parseした結果から依存するフィールドを抽出する extract() というメソッドによって数式が依存しているフィールドをリストアップできますので、その結果から動的に依存する項目がどれかを判別することができるようになっています。

parsed.extract();

// => [ 'Product__r.IsTaxable__c',
// 'Product__r.UnitPrice__c',
// 'Quantity__c',
// 'Product__r.UnitPrice__c',
// 'Quantity__c' ]

あとはこれをつかってJSforceなどでクエリを組み立てればよいでしょう。あらかじめ数式に使われる可能性のある全項目を引っ張っておかないといけない、などということはありません。

以下のコードは、商品マスタを参照する請求明細のレコードを取得し、金額項目(Amount__c)に設定されている数式を再評価する関数を返すコードです。(非同期コードの見通しを良くするためにasync/await記法を使っています)

import { parse } from 'formulon/lib/ast';

import uniq from 'lodash.uniq';
import getLoggedInConnection from './getLoggedInConnection';

async function fetchInvoiceItemRecords(invoiceId) {
const conn = getLoggedInConnection(); // ログイン済みのJSforce Connectionオブジェクト
const InvoiceItem__c = conn.sobject('InvoiceItem__c'); // 請求明細
const targetFields = [
'Id', 'Name', 'Quantity__c', 'Amount__c'
]; // 請求明細内の画面表示対象のフィールド。Amount__c が数式項目

// JSforceで請求明細オブジェクトの項目定義を取得
const { fields } = await InvoiceItem__c.describe();

// 表示対象項目の定義リスト
const displayFields =
fields.filter((f) => targetFields.indexOf(f.name) >= 0);
// 数式項目の定義リスト。数式の解析結果もセットで持っておく
const calcFields =
displayFields
.filter((f) => f.calculatedFormula)
.map((f) => (
Object.assign({}, f, { parsedFormula: parse(f.calculatedFormula) })
));
// 表示対象の項目名のリスト
const displayFieldNames = displayFields.map((f) => f.name);
// 数式項目が依存している項目名のリスト
const dependingFieldNames = calcFields.reduce((depFields, field) => (
[ ...depFields, ...field.parsedFormula.extract() ]
), []);
// 最終的にクエリ対象となる項目名のリスト
const fieldNames = uniq([ ...displayFieldNames, ...dependingFieldNames ]);

// JSforceで該当する請求IDの明細一覧を取得
const records = await InvoiceItem__c.find({ Invoice__c: invoiceId }, fieldNames);

// レコード内容に変更があった時、数式値を再評価するための関数
const reevaluate = (recs) => (
recs.map((rec) => (
calcFields.reduce((r, calcField) => (
Object.assign({}, r, { [calcField.name]: calcField.parsedFormula.evaluate(r) })
), rec)
))
);
return { records, reevaluate };
}


まとめ

以上により、Salesforceの数式をフロントエンド環境でもIsomorphicに評価することができるようになりました。もちろんformulonがサポートしている関数はまだ限定的であり、一部の関数はおそらくSalesforceサーバ上でしかうまく動かない可能性もあります。ただ、構文解析を挟んでやることで、フロントエンドでもある程度リライアブルにSalesforce数式を使うことができる、というのが今回の記事の趣旨となります。うまく活用すれば設定変更に強くユーザエクスペリエンスにも秀でたSalesforceアプリケーションが開発できるでしょう。

なお、formulonによって構文解析実行後に生成されるASTはJavaScriptのAST表現と同じなので、escodegenなどを用いればそのままJSのコードに落とし込めたりもします。しかしながら、そういった動的生成したコードを組み込みのeval関数で実行するのはCSP環境下で制約もあるので、ツリーを辿って自前でevalulateしていくほうがよいかと思います。