ServiceNowの入力チェック実装について考える

## はじめに
フォームをユーザーに対して表示して、ユーザーが入力内容を送信する際にその内容の妥当性を検証し、問題がなければ後続処理をするものの、問題がある場合は後続処理に入らずに突き返す、という実装は何もServiceNowに限らなくても、世のなかの多くのWebアプリケーションで極めて広範に行われている実装です。

もちろんServiceNowでもこういう機能は実装できますが、実装のやり方にいろいろな選択肢があり、実装は簡単だけれど制限がある、あるいは使い勝手に難があるやり方がある一方で、完成度は高いものの実装は手間であるやり方もあります。

今回はそういう入力チェックの実装について考えてみたいと思います。

## 実装方法の検討

### 一番簡単な必須チェック

入力チェックの中でもいちばん実装の機会が多いのは必須項目のチェックでしょう。これはプラットフォームレベルで非常に簡単な機能が用意されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/c3963eaf-482e-5e16-82c2-38e7a6998744.png)

辞書テーブル（`sys_dictionary`）に定義されたフィールドの属性のうち、「必須（Mandatory）」をチェックすると、この項目は必須になります。そのため、入力をせずに保存しようとするとこんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/328f0363-8f1d-5e1b-9bdd-d39753957722.png)

このやり方はわかりやすくて実装容易ですが、以下の点には注意しておくべきでしょう。

* 常に必須になってしまうので、他のフィールドの値（例えばステータス）に依存して必須になったりならなかったりする実装はできない。
* 上の状態の一種とも言えますが、「複数項目のうち1つが必須」というような制御はできません。
* スクリプトで必須フィールドが空のレコードを作ることはできてしまう。

逆に言うと、この制限のために業務要件が満たせない場合は別の方法を考える必要があります。

### ビジネスルールによるチェック

最小の手間で複雑な入力チェックを実装するのであれば、ビジネスルールによるのが一番簡単だと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/22f62834-42f5-6051-5e07-570bf3fac86d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/bc359312-1257-6db8-7e63-9386aebe4298.png)

チェックだけであればノーコードでも結構ややこしい条件を実装できます。

これでこのような実行結果になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/9232194a-74fb-c88d-72a4-a6f535b6812f.png)

すでに実は難点の片鱗が見えつつあるんですが、それは次のセクションでお話ししたいと思います。

### UIアクションでステータスを変えて、ビジネスルールでチェックする

上の方法の応用として、フォーム上のボタンを押したときに、入力チェックを行って、ステータス変更をするまでを一括で行うという、ワークフローにありがちな実装にしてみましょう。

今回は、タスクテーブル（`task`）を拡張したこのようなテーブルを考えてみます。[^1]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/4d3a037f-518c-e4e3-bdc1-dd0116e1ecb1.png)

素のタスクテーブルは、ステータス遷移を行うためのボタンを持っておらず、ステータスフィールドも手で修正するようになっていますが、ここでは「ステータスを『対応中（Work in Progress）』に遷移させるときには、アサイン先（`assigned_to`）を必須にする」という仕様にして、それをボタンひとつで確実に実施したいと思います。そのためには、以下の2つを実装することになります。

* ステータス遷移のためのボタン（UIアクション）
* ステータスがWork in progressになる際にアサイン先が入力されていることを確認するビジネスルール
* （ここではやりませんが、ステータスフィールドを読み取り専用にすべきでしょう）

UIアクションはこうなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/e37eb194-dcfa-908c-f890-945dc9db61f2.png)

ビジネスルールはこんな感じ。（アクションタブは省略します。先ほどの例とほぼ同じです。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/31113a78-9e43-d8c7-cd36-6ee43d397fe0.png)

このように実装して、サンプルのタスクのアサイン先（`assigned-to`）を空白にしたまま新しく作った「Start Working」ボタンを押すとこうなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/91dfed64-b6cc-6463-b547-4599e0895f87.png)

### 問題発生

これは一見すると、うまくいっていますし、これで別に構わないというケースもあると思います。しかし実は困ったことが一つ起きていて、それは「**画面上は少なくともこのレコードがWork in Progress」に進んでいるように見える**」ということです。これは、先ほどOpenステータスでしか表示されないように実装したWork in Progressボタンが表示されていないことからもわかります。さらにわかりやすくするために、Work in Progressステータスのときだけ表示されるようなCompleteボタンというのも実装してみました。これが表示されているのはおかしい。

ちなみにこのCompleteボタンは機能しません。クリックするとこのような形で、「押すことが許されていない」というエラーで失敗します。また、この遷移先画面からもわかるように、実際のプラットフォーム上のレコードはWork in Progressには進んでおらず、依然としてOpenにとどまっているのも重要なポイントです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/e62c54bd-9952-7062-b706-25d618232456.png)

このチェックロジックは極めて簡単な実装ですし、何はともあれ機能はしていますが、いかにも格好が悪いです。（最小工数を追求するときはこれで済ませるという判断もなくはないと思います。）

なぜこのような問題が起きるかというと、このステータス遷移と処理が完全にサーバーサイドのみで行われている一方、ステータス遷移後の画面を表示する処理自体はサーバーサイドの処理を待たずにクライアントで行われるためです。ボタンが押せないのはプラットフォームのセキュリティ機能なのでしょう。

## 理想的にはこうするという実装

結局、こういう問題を避けるには、サーバーサイドに処理をゆだねる前にクライアントサイドでチェックを行うべきなのです。そのため、先ほどのUIアクションに以下のような変更を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/cd84523e-22ce-03d1-191f-84f488404771.png)

この意味は後ほど記載しますが、このコードで同じようにアサイン先空欄でStart workingボタンを押すとこういう動作になります。これはやりたかった機能になったはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491737/6688c1df-b0d0-1a1d-1ade-f5507d765985.png)

### このUIアクションスクリプトの意味

改めてコードを掲示します。

``` JavaScript: StartWorking (UI Action)
// クライアント側で実行されるスクリプト
function startWorking() {
    if (g_form.getValue('assigned_to') === '') {
        g_form.addErrorMessage('Assigned-to is mandatory when advanced to Work in Progress.');
		return;
    }
    gsftSubmit(null, g_form.getFormElement(), 'x_start');
}

if (typeof window === 'undefined') {
    serverAction();
}

// サーバー側で実行されるスクリプト
function serverAction() {
    current.state = '2'; // Work in progress
    current.update();
    action.setRedirectURL(current);
}
```

新しいバージョンにおける最も重要な変更は、「Client」にチェックを入れたことです。これによって、**このUIアクションはサーバーサイドではなく、クライアントサイドで実行される**ようになります。Clientにチェックを入れるとUIアクションフォームに「Onclick」というフィールドが表示されますが、ここに、クライアントサイドで実行する際に呼び出す関数名を記入します。これによって、**クライアントサイドで実行されるこのUIスクリプトはまず`startWorking()`関数から実行されます**。（コードの先頭から実行されるわけではないことに注意してください）

クライアントサイドではまず入力チェックが行われ、アサイン先が空欄であるときにはエラーメッセージを表示してリターンします。問題がなかった場合は、`gsftSubmit()`という見慣れない関数が呼ばれます。この関数の仕様はこちらのドキュメントを見ていただくとして、この関数がやることは、**引数に渡した名称のUIアクションを呼び出した体で、フォームをサーバーに送信する**という動作です。

https://docs.servicenow.com/csh?topicname=using-client-server-code-ui-action.html&version=latest

第1引数と第2引数はほとんどの利用ケースでおまじないになると思いますが、第3引数が重要で、ここで指定した名称を「アクション名（Action name）」に指定されているUIアクションが呼び出されます。ここでは`x_start`が指定されていますが、この名前のアクション名を持つUIアクションとは、すなわちこの「Start working」UIアクションそのものです。上の定義画面のスクリーンショットを改めて確認してください。

ここがわかりにくいポイントですが、`gsftSubmit()`を呼び出すことによって、結局は**このスクリプト自体が今度はサーバーサイドスクリプトとして実行される**ことになります。

クライアントサイドと異なり、**サーバーサイドのUIアクションは1行目から実行される**ので、`startWorking()`関数は定義だけ読んでスルーすることになり、if文の判定に入ります。このif文が判定しているのは、それがクライアント側で実行されているのか、サーバー側で実行されているかです。`window`グローバル変数はサーバーサイドでは利用できないためです。if文が`true`に評価されると、`serverAction()`関数が呼ばれ、ステータス遷移の処理が行われます。ここからの処理は最初に実装したサーバーサイドだけの実装と同じです。

なお、最初に作ったビジネスルールは残しておくべきです。クライアントサイドのチェックロジックは悪意あるユーザーであれば回避可能だからです。サーバーサイドで同じチェックを行っておけば、そのような不正使用を防ぐことが可能です。

## まとめ

必須チェックから話を始めて、最終的にはかなり長い記事になってしまいましたが、このような、`gsftSubmit()`関数を利用して、1本のスクリプトをサーバーサイドでもクライアントサイドでも実行するというテクニックは、サーバー側とクライアント側で同じ開発言語を使うServiceNowならではの実装で、ワークフローのステータス遷移ボタンを実装するときには定番のイディオムだと思います。

なお、先に示した素朴な実装はこれはこれで必要なものであり、重要です。併せて理解し、利用できるようになればよいと思います。

## 参考

少し前の記事ですが、@NIWATOさんの[こちらの記事](https://qiita.com/NIWATO/items/055259f390478ed15cf7)は`gsftSubmit()`を使ったクライアント・サーバーの両サイドで動くスクリプトに関する技術的な解説です。内容は現在でも100%有効ですので、純粋に実装技術を知りたい方はこちらの記事をご覧になる方が話が早いです。当記事をご覧いただく価値は前段の解説が色々ついていることだけで、今回はそれが必要な事情があったので被っていることは承知しつつも公開させていただく次第です。

[^1]: 本来、タスクテーブルを拡張して利用するときには十分な考慮が必要です。今回はステータスを持つテーブルを簡単な例として取り上げたいのでこのようなテーブルを想定しています。ただ、タスクテーブルとその拡張テーブルは1つの物理テーブルを共用しているため、野放図な拡張を行うと他のタスク系テーブルにまで影響が及ぶ可能性があります。そのあたりの議論はこちらのKBに詳しいです。（英語）
https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0723580