FileMaker 2023 Audit Logging

  • URLをコピーしました!

こんにちはジョージです。Claris FileMaker 2023 の目玉機能 Audit Logging について深掘りしてみましょう。

2023051701.gif (1.1 MB)
目次

前提条件

カスタム App におけるレコード変更履歴取得とは、カスタム App に実装されたテーブルや機能、レイアウトを考慮するとさまざまなケースが考えられます。
今回は単一テーブルでのレコード変更履歴取得を目的として、関連レコードの変更やトランザクション制御下での複数レコード一括操作は考慮していません。

ただし、OnWindowTransaction イベントで生成される JSON オブジェクトの解析と展開は、ログ機能の流用性を考えてしっかりと実行していきます。

サンプルファイルの構成

ベースファイルは「連絡先.fmp12」をカスタマイズして作成しました。OnWindowTransaction イベントが発火した後は、Data API を経由して、ログテーブルにレコードを作成します。

2023051701.002.png (271.2 kB)
2023051701.002.png (166.7 kB)
2023051701.003.png (200.6 kB)
2023051701.004.png (190.7 kB)

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
)
2023051701.006.png (185.8 kB)
2023051701.005.png (207.9 kB)

JSON オブジェクトフォーマット

今回のファイルで作成される JSON オブジェクトは以下の通りです。実際には修正されたフィールドの内容が全て含まれます。

2023051701.001.png (68.7 kB)

Data API の準備

今回は Data API を使ってログテーブルにレコードを作成します。最低限、Data API が使えるユーザ、拡張アクセス権、レイアウトの準備をしておきます。

2023051701.008.png (157.0 kB)
2023051701.009.png (240.3 kB)
2023051701.010.png (102.5 kB)

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 開発者にとってログ管理は是非とも実装したい機能です。是非チャレンジしてみてください。

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
目次