この記事は、Qiita ServiceNow アドベントカレンダーの2日目の記事です。
車輪の再発明をしよう
これを使って何か作れないかを考えた結果、毒にも薬にもならない車輪の再発明を思いついたので作っていきます
せっかくなのでAIさんの力を借りて極力コードを書かないようにしてみます
やること
GlideScopedEvaluatorを使って任意のスクリプトを実行できるので、添付されたCSVファイルを読み込んで指定したテーブルにデータを入れるようなものをスコープアプリで作ってみます。
ちょっとしたメリット
Import Setを利用するにはそれなりの権限が必要ですが、この機能をこっそり実装することで誰でもデータを投入することができるはずです
やってみよう
作っていきます
GlideScopedEvaluatorで任意のスクリプトを実行するテーブル&UIアクションを作成
スクリプトの名前を管理するカラム、スクリプト自体を保存するカラム、実行結果をとかを入力するログ用のカラムを追加しました

Can createにチェックを入れないとレコードにファイル添付ができないはずなので設定します
双子さんに相談して添付されたCSVを読み込むコードを書いてもらいます
自分のレコードに添付されたCSVファイルを読み込んでパースするコード書いて
/*
* スコープアプリケーションでの使用を推奨: sn_impex.CSVParser
* グローバルスコープでのみ使用可能: GlideCSVParser
*/
(function processCSVAttachment(current) {
var recordSysId = current.getUniqueValue(); // 現在のレコードのsys_id
var tableName = current.getTableName(); // 現在のレコードのテーブル名
var attachmentSysId = null;
// 1. 添付ファイルの検索 (最新のCSVファイルを見つける)
var grAttachment = new GlideRecord('sys_attachment');
grAttachment.addQuery('table_name', tableName);
grAttachment.addQuery('table_sys_id', recordSysId);
grAttachment.addQuery('file_name', 'ENDSWITH', '.csv'); // ファイル名の末尾が.csvのものに限定
grAttachment.setLimit(1); // 最新のものを1つ取得
grAttachment.orderByDesc('sys_created_on');
grAttachment.query();
if (grAttachment.next()) {
attachmentSysId = grAttachment.getValue('sys_id');
gs.info('CSV Attachment found: ' + grAttachment.file_name);
} else {
gs.warn('No CSV attachment found for record: ' + tableName + ' : ' + recordSysId);
return; // 添付ファイルがない場合は終了
}
// 2. 添付ファイルの内容をストリームとして取得
var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysId);
// 3. CSVパーサーの初期化と解析
try {
// スコープアプリケーションの場合は sn_impex.CSVParser() を使用
// グローバルスコープの場合は GlideCSVParser() を使用
var csvParser = new sn_impex.CSVParser();
csvParser.parse(attachmentStream);
// 4. 行ごとにデータを処理
var rowCount = 0;
gs.info('--- Starting CSV Parsing ---');
while (csvParser.next()) {
rowCount++;
// getValue(headerName): CSVのヘッダー名を使って値を取得します
var columnAValue = csvParser.getValue('column_a_header'); // CSVのヘッダー名に合わせて変更してください
var columnBValue = csvParser.getValue('column_b_header'); // CSVのヘッダー名に合わせて変更してください
// 例: 取得した値をログに出力
gs.info('Row ' + rowCount + ': Column A = ' + columnAValue + ', Column B = ' + columnBValue);
// TODO: ここに実際の処理(別のテーブルへのデータ挿入、既存レコードの更新など)を記述します
/*
var grTarget = new GlideRecord('target_table');
grTarget.initialize();
grTarget.setValue('field_on_target_table_a', columnAValue);
grTarget.insert();
*/
}
gs.info('--- CSV Parsing Finished. Total rows processed: ' + rowCount + ' ---');
} catch (ex) {
gs.error('CSV Parsing Error: ' + ex.getMessage());
}
})(current);
ちょっと手直しをして
/*
* スコープアプリケーションでの使用を推奨: sn_impex.CSVParser
* グローバルスコープでのみ使用可能: GlideCSVParser
*/
var recordSysId = current.getUniqueValue(); // 現在のレコードのsys_id
var tableName = current.getTableName(); // 現在のレコードのテーブル名
var attachmentSysId = null;
var delimiter = ',';
var quoteCharacter = '"';
var columnNames = ["aru", "nashi"];
// 1. 添付ファイルの検索 (最新のCSVファイルを見つける)
var grAttachment = new GlideRecord('sys_attachment');
grAttachment.addQuery('table_name', tableName);
grAttachment.addQuery('table_sys_id', recordSysId);
grAttachment.addQuery('file_name', 'ENDSWITH', '.csv'); // ファイル名の末尾が.csvのものに限定
grAttachment.setLimit(1); // 最新のものを1つ取得
grAttachment.orderByDesc('sys_created_on');
grAttachment.query();
if (grAttachment.next()) {
attachmentSysId = grAttachment.getValue('sys_id');
gs.addInfoMessage('CSV Attachment found: ' + grAttachment.file_name);
} else {
gs.addInfoMessage('No CSV attachment found for record: ' + tableName + ' : ' + recordSysId);
}
// 2. 添付ファイルの内容をストリームとして取得
var attachment = new GlideSysAttachment();
var attachmentStream = attachment.getContentStream(attachmentSysId);
// 3. CSVパーサーの初期化と解析
var reader = new GlideTextReader(attachmentStream);
var csvLine = ' ';
var isHeader = true;
while ((csvLine = reader.readLine()) != null) {
if (isHeader) { // first line is header so skip
isHeader = false;
continue;
}
try {
var csvObj = new sn_impex.CSVParser().parseLineToObject(csvLine, columnNames, delimiter, quoteCharacter);
// gs.addInfoMessage("【CSV Check】csvObj:" + JSON.stringify(csvObj));
// 4. 行ごとにデータを処理
var grTarget = new GlideRecord('x_〇〇〇〇〇〇_test_arunashiquiz');
grTarget.initialize();
grTarget.setValue('aru', csvObj.aru);
grTarget.setValue('nashi', csvObj.nashi);
grTarget.insert();
} catch (ex) {
gs.addInfoMessage('CSV Parsing Error: ' + ex.getMessage());
}
}
Functionである必要はなかったので変更しました。
あとヘッダーを変更して
CSVParser#parseがスコープアプリでは使えなさそうでしたので読み込み方を変えました。
そして別テーブルへInsertします
結果
実行すると

添付したCSVファイルを解析してImprotを行うImport Setのようなものができました
作成したExecボタン
var ge = new GlideScopedEvaluator();
ge.evaluateScript(current,"script");
さいごに
クイズの答えはわかりましたでしょうか?
仕事ではセキュリティ、コンプラの関係などで、利用できないとは思いますがちょっと面白いものが作れました!
ちなみにこのコードを双子さんにレビューしてもらいました
ご提示いただいたServiceNowのスクリプトは、添付されたCSVファイルを読み込み、その内容をターゲットテーブル (`x_〇〇〇〇〇〇_test_arunashiquiz`) にインポートするという目的において、**全体としてよく構成されています**。特に、ファイル検索、ストリーム処理、CSVパース、およびレコード挿入のロジックが明確に分かれています。
しかし、このコードには**Scoped Application**におけるAPI利用の観点から、いくつかの**重要な問題点と改善点**があります。
-----
## 🧐 コードレビューと改善点
### 1\. 🚨 重大な問題点: スコープとAPIの混在
このスクリプトは、Scoped Applicationで実行することを意図しているようですが、グローバルスコープでのみ利用可能なAPIを直接呼び出しています。
| 行数周辺 | API | スコープ | 問題点 | 解決策 (推奨) |
| :--- | :--- | :--- | :--- | :--- |
| `var attachment = new GlideSysAttachment();` | **`GlideSysAttachment`** | Global | Scoped Appで直接利用不可。 | **`sn_attachment.Attachment`** または **`GlideSysAttachmentScoped`** を使用。 |
| `var attachmentStream = attachment.getContentStream(attachmentSysId);` | **`getContentStream`** | Global | Scoped Appで直接利用不可。 | Base64でコンテンツを取得し、文字列として処理する。 |
| `var reader = new GlideTextReader(attachmentStream);` | **`GlideTextReader`** | Global | Scoped Appで直接利用不可。 | Base64デコード後の文字列を直接処理する。 |
**推奨される解決策:**
`GlideSysAttachment`、`getContentStream`、および `GlideTextReader` の代わりに、**`GlideSysAttachmentScoped`** または **`sn_attachment.Attachment`** を使用して添付ファイルの内容を**文字列**として取得し、その文字列を処理するようにコードを修正すべきです。
### 2\. ✅ CSVパーサーの選択
| 行数周辺 | API | スコープ | 評価 | 備考 |
| :--- | :--- | :--- | :--- | :--- |
| `var csvObj = new sn_impex.CSVParser().parseLineToObject(...)` | **`sn_impex.CSVParser`** | Scoped | **適切** | Scoped Appで利用可能な公式なパーサーであり、目的(オブジェクトへのパース)に合致しています。 |
### 3\. 改善点: 添付ファイル処理の効率化
現在のコードは、行ごとにファイルを読み込んでいますが、これはテキストファイルには適していますが、CSVパースには冗長になる場合があります。
* **改善案:** `GlideTextReader`を使わず、ファイル全体を一度に文字列として読み込み(例:`getContent().toString()`や`readBase64()`からのデコード)、その文字列全体を\*\*`sn_impex.CSVParser().parse()`\*\* メソッドで一度にパースする方が、コードがシンプルになり、効率も良くなります。
-----
## 📝 修正後のコード例 (GlideSysAttachmentScoped利用)
Global APIへの依存を排除し、Scoped Appで公式に利用可能なAPIとパターンに修正したコード例を以下に示します。
```javascript
// Scoped App向けのAPIとパターンを使用
var recordSysId = current.getUniqueValue();
var tableName = current.getTableName();
var attachmentSysId = null;
var csvContent = null; // CSVファイルの内容全体を格納する変数
// --- CSV設定 ---
var delimiter = ',';
var quoteCharacter = '"';
var columnNames = ["aru", "nashi"];
// --- CSV設定 ---
// 1. 添付ファイルの検索 (最新のCSVファイルを見つける)
var grAttachment = new GlideRecord('sys_attachment');
grAttachment.addQuery('table_name', tableName);
grAttachment.addQuery('table_sys_id', recordSysId);
grAttachment.addQuery('file_name', 'ENDSWITH', '.csv');
grAttachment.setLimit(1);
grAttachment.orderByDesc('sys_created_on');
grAttachment.query();
if (grAttachment.next()) {
attachmentSysId = grAttachment.getValue('sys_id');
gs.addInfoMessage('CSV Attachment found: ' + grAttachment.file_name);
try {
// 2. 添付ファイルの内容を文字列として取得
// Scoped App公式API: GlideSysAttachmentScoped または sn_attachment.Attachment を使用
// GlideSysAttachmentScoped の利用
var saScoped = new GlideSysAttachmentScoped();
var contentBytes = saScoped.getContent(attachmentSysId);
csvContent = contentBytes.toString();
} catch (e) {
gs.error('Attachment reading failed: ' + e.getMessage());
gs.addInfoMessage('Error: Attachment reading failed.');
return; // 処理を終了
}
} else {
gs.addInfoMessage('No CSV attachment found for record.');
return; // 処理を終了
}
// 3. CSVパーサーの初期化と解析 (ファイル全体を一度に処理)
try {
// sn_impex.CSVParserを使用し、ファイル全体を一度にパース
var parser = new sn_impex.CSVParser();
// parse()メソッドは、ヘッダーをスキップせず、全ての行を二次元配列として返します。
var results = parser.parse(csvContent, delimiter, quoteCharacter);
// ヘッダー行 (1行目) をスキップするためのロジック
var headerRow = results[0];
gs.addInfoMessage('CSV Header: ' + headerRow.join(', '));
for (var i = 1; i < results.length; i++) { // 2行目 (インデックス1) から開始
var row = results[i];
// 4. 行ごとにデータを処理
var grTarget = new GlideRecord('x_〇〇〇〇〇〇_test_arunashiquiz');
grTarget.initialize();
// ヘッダーに基づいて列を割り当てる
for (var j = 0; j < columnNames.length; j++) {
// columnNames (aru, nashi) の順序とCSVファイルの列の順序が一致していることを前提
grTarget.setValue(columnNames[j], row[j]);
}
grTarget.insert();
}
gs.addInfoMessage('CSV data successfully processed and imported: ' + (results.length - 1) + ' rows.');
} catch (ex) {
gs.error('CSV Processing Error: ' + ex.getMessage());
gs.addInfoMessage('CSV Processing Error: ' + ex.getMessage());
}
GlideSysAttachmentScopedってありますか・・?






