こんにちはジョージです。Claris FileMaker 2023 の目玉機能 Audit Logging について深掘りしてみましょう。
前提条件
カスタム App におけるレコード変更履歴取得とは、カスタム App に実装されたテーブルや機能、レイアウトを考慮するとさまざまなケースが考えられます。
今回は単一テーブルでのレコード変更履歴取得を目的として、関連レコードの変更やトランザクション制御下での複数レコード一括操作は考慮していません。
ただし、OnWindowTransaction イベントで生成される JSON オブジェクトの解析と展開は、ログ機能の流用性を考えてしっかりと実行していきます。
サンプルファイルの構成
ベースファイルは「連絡先.fmp12」をカスタマイズして作成しました。OnWindowTransaction イベントが発火した後は、Data API を経由して、ログテーブルにレコードを作成します。
OnWindowTransaction フィールドの内容
While (
[
~i = 1;
~fields = Get ( 変更されたフィールド );
~filter = List (
"作成情報タイムスタンプ";
"作成者";
"修正情報タイムスタンプ";
"修正者";
"主キー";
"写真";
"名";
);
~fields = FilterValues ( ~fields; ~filter );
~json = "[]"
];
~i ≤ ValueCount(~fields) ;
[
~json = Let ([
~field = GetValue ( ~fields; ~i );
~type = MiddleWords( FieldType ( Get( ファイル名 ) ; ~field ); 2; 1 )
];
JSONSetElement ( ~json;
ValueCount( JSONListKeys ( ~json; "" ));
JSONSetElement ( "{}";
~field;
If ( Exact ( ~type; "Container" ); Base64Encode ( GetField ( ~field ) ); GetField ( ~field ) );
Case (
Exact ( ~type; "Text" ); JSONString;
Exact ( ~type; "Number" ); JSONNumber;
Exact ( ~type; "Date" ); JSONNumber;
Exact ( ~type; "Time" ); JSONNumber;
Exact ( ~type; "Timestamp" ); JSONNumber;
Exact ( ~type; "Container" ); JSONString;
JSONString
)
);
JSONObject
)
);
~i = ~i + 1
];
~json
)
JSON オブジェクトフォーマット
今回のファイルで作成される JSON オブジェクトは以下の通りです。実際には修正されたフィールドの内容が全て含まれます。
Data API の準備
今回は Data API を使ってログテーブルにレコードを作成します。最低限、Data API が使えるユーザ、拡張アクセス権、レイアウトの準備をしておきます。
Data API でレコードを作成
ループ処理を使って受け取った JSON オブジェクト/配列から、ログテーブルに追加する JSON を生成し logArray 変数に代入します。
受け取った JSON オブジェクトをそのままログとして追加してもよいのですが、レコードIDや、操作(New、Modified、Deleted)や修正者、修正タイムスタンプを切り分けて、特定のフィールドに配置した方が、後からログレコードを確認する時に便利です。
受けとったカスタム JSON に登録されている修正されたフィールドの内容は「配列型」なので、以下のように「オブジェクト型」に変換しておく方が賢明です。
{
"主キー" : "49BFB9F9-2F6A-44C0-ACA6-83785C077D0C",
"作成情報タイムスタンプ" : 63819924204,
"作成者" : "admin",
"修正情報タイムスタンプ" : 63819924227,
"修正者" : "admin"
}
また、写真の登録や修正があった場合には、Data API の実行結果の戻り値に登録されている、ログテーブルのレコード ID を利用して、画像をログファイルにも書き込みます。
これらの制御は、カスタム App の構成や関連レコードの追加等を考慮して、独自の処理を実装する必要がある部分です。今回は最低限、単一テーブルの単一レコードの操作で想定される課題を処理してみました。
# #
#
変数を設定 [ $param ; 値: Get ( スクリプト引数 ) ]
#
# //--------------------------------------------------
# Data API 共通設定
変数を設定 [ $baseURL ; 値: "https://" & Get ( ホスト IP アドレス ) & "/fmi/data/v2" ]
変数を設定 [ $database ; 値: Get ( ファイル名 ) ]
変数を設定 [ $layout ; 値: GetAsURLEncoded ( "REST" ) ]
変数を設定 [ $authorization ; 値: Base64EncodeRFC ( 4648; "REST:PASSWORD" ) ]
#
# //--------------------------------------------------
# Data API ログイン
変数を設定 [ $url ; 値: "{{baseURL}}/databases/{{database}}/sessions" ]
変数を設定 [ $url ; 値: Substitute ( $url; [ "{{baseURL}}"; $baseURL ]; [ "{{database}}"; $database ] ) ]
変数を設定 [ $method ; 値: "POST" ]
変数を設定 [ $parameter ; 値: "-X " & $method & " -H \"Content-Type: application/json\"" & " -H \"Authorization: Basic " & $authorization & "\"" & " -d {}" ]
URL から挿入 [ 選択 ; ダイアログあり: オフ ; ターゲット: $result ; $url ; cURL オプション: $parameter ]
変数を設定 [ $token ; 値: JSONGetElement ( $result; "response.token" ) ]
#
# //--------------------------------------------------
# audit logging custom JSON sample
# {
"連絡先" : //ファイル名
{
"担当者" : //テーブル名
[
[ // 操作1
"Modified", //オペレーション
1, //レコードid
[
{
"修正者" : "admin"
},
{
"修正情報タイムスタンプ" : 63819839458
}
] // カスタムJSON
],
[ // 操作2
"Modified", //オペレーション
2, //レコードid
[
{
"修正者" : "admin"
},
{
"修正情報タイムスタンプ" : 63819839458
}
] // カスタムJSON
]
]
}
}
# // logArray
変数を設定 [ $logArray ; 値: "[]" ]
# // audit logging json 変換
変数を設定 [ $i ; 値: 0 ]
Loop
Exit Loop If [ $i ≥ ValueCount(JSONListKeys ( $param; "" )) ]
変数を設定 [ $fileName ; 値: GetValue ( JSONListKeys ( $param; "" ); $i + 1 ) ]
変数を設定 [ $fileElement ; 値: JSONGetElement ( $param; $fileName ) ]
# // // fileNames
変数を設定 [ $j ; 値: 0 ]
Loop
Exit Loop If [ $j ≥ ValueCount(JSONListKeys ( $fileElement; "" )) ]
変数を設定 [ $tableName ; 値: GetValue ( JSONListKeys ( $fileElement; "" ); $j + 1 ) ]
変数を設定 [ $tableElement ; 値: JSONGetElement ( $fileElement; $tableName ) ]
# // tableNames
変数を設定 [ $k ; 値: 0 ]
Loop
Exit Loop If [ $k ≥ ValueCount(JSONListKeys ( $tableElement; "" )) ]
変数を設定 [ $operationElement ; 値: JSONGetElement ( $tableElement; $k ) ]
# // operation
変数を設定 [ $logElement ; 値: "{}" ]
変数を設定 [ $logElement ; 値: JSONSetElement ( $logElement; "method"; JSONGetElement ( $operationElement; 0 ); JSONString ) ]
変数を設定 [ $logElement ; 値: JSONSetElement ( $logElement; "recid"; JSONGetElement ( $operationElement; 1 ); JSONNumber ) ]
変数を設定 [ $logElement ; 値: JSONSetElement ( $logElement; "contents"; "{}"; JSONObject ) ]
変数を設定 [ $fieldElement ; 値: JSONGetElement ( $operationElement; 2 ) ]
If [ not IsEmpty ( $fieldElement ) ]
# // fieldNames
変数を設定 [ $l ; 値: 0 ]
Loop
Exit Loop If [ $l ≥ ValueCount(JSONListKeys ( $fieldElement; "" )) ]
変数を設定 [ $element ; 値: JSONGetElement ( $fieldElement; $l ) ]
変数を設定 [ $logElement ; 値: Let ([ ~content = JSONGetElement ( $logElement; "contents" ); ~key = GetValue ( JSONListKeys ( $element; "" ); 1 ); ~value = JSONGetElement ( $element ; ~key ); ~type = JSONGetElementType ( $element ; ~key ) ]; JSONSetElement ( $logElement; "contents"; … ]
変数を設定 [ $l ; 値: $l + 1 ]
End Loop
End If
変数を設定 [ $logArray ; 値: JSONSetElement ( $logArray; $k; $logElement; JSONObject ) ]
変数を設定 [ $k ; 値: $k + 1 ]
End Loop
変数を設定 [ $j ; 値: $j + 1 ]
End Loop
変数を設定 [ $i ; 値: $i + 1 ]
End Loop
#
# //--------------------------------------------------
# レコード作成
変数を設定 [ $url ; 値: "{{baseURL}}/databases/{{database}}/layouts/{{layout}}/records" ]
変数を設定 [ $url ; 値: Substitute ( $url; [ "{{baseURL}}"; $baseURL ]; [ "{{database}}"; $database ]; [ "{{layout}}"; $layout ] ) ]
変数を設定 [ $method ; 値: "POST" ]
変数を設定 [ $fieldData ; 値: "{}" ]
# //複数操作の場合は、logArray の要素数だけ Data API を実行しなくてはいけない。とりあえず 1 操作だけログを取得
変数を設定 [ $logElement ; 値: JSONGetElement ( $logArray; 0 ) ]
変数を設定 [ $fieldData ; 値: JSONSetElement ( $fieldData; "recid"; JSONGetElement ( $logElement; "recid" ); JSONString ) ]
変数を設定 [ $fieldData ; 値: JSONSetElement ( $fieldData; "method"; JSONGetElement ( $logElement; "method" ); JSONString ) ]
変数を設定 [ $fieldData ; 値: JSONSetElement ( $fieldData; "contents"; JSONGetElement ( $logElement; "contents" ); JSONString ) ]
変数を設定 [ $fieldData ; 値: JSONSetElement ( $fieldData; "account"; JSONGetElement ( $logElement; "contents.修正者" ); JSONString ) ]
変数を設定 [ $fieldData ; 値: Let ([ ~format = "MM/dd/yyyy HH:mm:ss"; ~ymd = GetAsTimestamp ( JSONGetElement ( $logElement; "contents.修正情報タイムスタンプ" ) ) ]; JSONSetElement ( $fieldData; [ "timestamp"; Case ( //~空の場合 IsEmpty ( ~ymd ); ""; //ディフォルト Substitute ( ~format; [ "MM"; … ]
変数を設定 [ $payload ; 値: JSONSetElement ( "{}"; "fieldData"; $fieldData; JSONObject ) ]
変数を設定 [ $parameter ; 値: "-X " & $method & " -H \"Content-Type: application/json\"" & " -H \"Authorization: Bearer " & $token & "\"" & " -d @$payload" ]
URL から挿入 [ 選択 ; ダイアログあり: オフ ; ターゲット: $result ; $url ; cURL オプション: $parameter ]
変数を設定 [ $recordId ; 値: JSONGetElement ( $result; "response.recordId" ) ]
#
# //--------------------------------------------------
# ファイル送信(レコード編集)
If [ ValueCount ( FilterValues ( JSONListKeys ( $logElement; "contents" ); "写真" )) ]
変数を設定 [ $fieldName ; 値: GetAsURLEncoded ( "FILE" ) ]
変数を設定 [ $url ; 値: "{{baseURL}}/databases/{{database}}/layouts/{{layout}}/records/{{recordId}}/containers/{{fieldName}}" ]
変数を設定 [ $url ; 値: Substitute ( $url; [ "{{baseURL}}"; $baseURL ]; [ "{{database}}"; $database ]; [ "{{layout}}"; $layout ]; [ "{{recordId}}"; $recordId ]; [ "{{fieldName}}"; $fieldName ] ) ]
変数を設定 [ $method ; 値: "POST" ]
変数を設定 [ $container ; 値: 担当者::写真 ]
変数を設定 [ $fileName ; 値: Let ([ ~fileName = "#YMD.png" ]; Substitute ( ~fileName; [ "#YMD"; Filter ( Get ( ホストのタイムスタンプ ); "0123456789" ) ] ) ) ]
変数を設定 [ $parameter ; 値: "-X " & $method & " -H \"Content-Type: multipart/form-data\"" & " -H \"Authorization: Bearer " & $token & "\"" & " -F \"upload=@$container;filename=" & $fileName & "\"" ]
URL から挿入 [ 選択 ; ダイアログあり: オフ ; ターゲット: $result ; $url ; cURL オプション: $parameter ]
変数を設定 [ $recid ; 値: JSONGetElement ( $result; "response.recordid" ) ]
End If
#
# //--------------------------------------------------
# Data API ログアウト
変数を設定 [ $url ; 値: "{{baseURL}}/databases/{{database}}/sessions/{{token}}" ]
変数を設定 [ $url ; 値: Substitute ( $url; [ "{{baseURL}}"; $baseURL ]; [ "{{database}}"; $database ]; [ "{{token}}"; $token ] ) ]
変数を設定 [ $method ; 値: "DELETE" ]
変数を設定 [ $parameter ; 値: "-X " & $method ]
URL から挿入 [ 選択 ; ダイアログあり: オフ ; ターゲット: $result ; $url ; cURL オプション: $parameter ]
#
現在のスクリプト終了 [ テキスト結果: ]
#
#
#
#
#
動作確認
それでは動作確認をしていきます。まずは新規レコードを作成してみましょう。以下のような JSON とレコードが作成されました。
method フィールド:New
{
"主キー" : "49BFB9F9-2F6A-44C0-ACA6-83785C077D0C",
"作成情報タイムスタンプ" : 63819924204,
"作成者" : "admin",
"修正情報タイムスタンプ" : 63819924227,
"修正者" : "admin",
"写真" : "Base64データ",
"名" : "高岡"
}
レコードを修正してみましょう。修正されたフィールドのみが記録されています。
method フィールド:Modified
{
"修正情報タイムスタンプ" : 63819924239,
"名" : "高山"
"修正者" : "admin"
}
レコードを削除してみましょう。削除された内容が記録されています。Audit Logging を利用しない場合、削除操作自体を記録することはできなかったので、これは助かります。
{
"主キー" : "49BFB9F9-2F6A-44C0-ACA6-83785C077D0C",
"作成情報タイムスタンプ" : 63819924204,
"作成者" : "admin",
"修正情報タイムスタンプ" : 63819924239,
"修正者" : "admin",
"写真" : "Base64データ",
"名" : "高岡"
}
ちなみに、以下のような複数レコード操作をトランザクション制御下で実行した場合、生成される JSON オブジェクトは 2 レコード分の情報が生成されます。
ただし、今回実装したスクリプトでは 1 レコード分の処理しか想定していないので、スクリプトの調整が必要となります。
トランザクションを開く []
新規レコード/検索条件
新規レコード/検索条件
トランザクション確定
まとめ
今回は FileMaker 2023 から実装された Audit Logging 機能を深掘りしてみました。カスタム App 開発者にとってログ管理は是非とも実装したい機能です。是非チャレンジしてみてください。