From 011b6ee4f82ca77fa9277b9ff52fc4d0e9ee0da7 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 27 Jan 2026 05:01:57 +0000 Subject: [PATCH 1/3] feat: add resource parsing method to extract ID, type, and parent from resource string --- src/Audit/Adapter/ClickHouse.php | 10 ++++++++++ src/Audit/Adapter/SQL.php | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/Audit/Adapter/ClickHouse.php b/src/Audit/Adapter/ClickHouse.php index 8ef2a3b..9dcb529 100644 --- a/src/Audit/Adapter/ClickHouse.php +++ b/src/Audit/Adapter/ClickHouse.php @@ -760,6 +760,8 @@ public function create(array $log): Log // Separate data for the data column (non-schema attributes) $nonSchemaData = $logData; + $resource = $this->parseResource($log['resource'] ?? ''); + foreach ($schemaColumns as $columnName) { if ($columnName === 'time') { // Skip time - already handled above @@ -790,6 +792,10 @@ public function create(array $log): Log $hasAttributeValue = true; // Remove from non-schema data as it's now a dedicated column unset($nonSchemaData[$columnName]); + } elseif (isset($resource[$columnName])) { + // Value is in parsed resource (e.g., resourceType, resourceId, resourceParent) + $attributeValue = $resource[$columnName]; + $hasAttributeValue = true; } // Validate required attributes @@ -1140,6 +1146,7 @@ public function createBatch(array $logs): bool // Separate data for non-schema attributes $nonSchemaData = $logData; + $resource = $this->parseResource($log['resource'] ?? ''); $processedLog = $log; // Extract schema attributes: check main log first, then data array @@ -1152,6 +1159,9 @@ public function createBatch(array $logs): bool if (!isset($processedLog[$columnName]) && isset($logData[$columnName])) { $processedLog[$columnName] = $logData[$columnName]; unset($nonSchemaData[$columnName]); + } elseif (!isset($processedLog[$columnName]) && isset($resource[$columnName])) { + // Check parsed resource for resourceType, resourceId, resourceParent + $processedLog[$columnName] = $resource[$columnName]; } elseif (isset($processedLog[$columnName]) && isset($logData[$columnName])) { // If in both, main log takes precedence, remove from data unset($nonSchemaData[$columnName]); diff --git a/src/Audit/Adapter/SQL.php b/src/Audit/Adapter/SQL.php index 2c59ffa..3a71942 100644 --- a/src/Audit/Adapter/SQL.php +++ b/src/Audit/Adapter/SQL.php @@ -218,4 +218,37 @@ protected function getAllColumnDefinitions(): array return $definitions; } + + /** + * Parses the resource string from the payload and extracts its ID, type, and parent. + */ + protected function parseResource(string $resource): array + { + $parts = explode('/', $resource); + + $resourceType = ''; + $resourceParent = ''; + + // resource/resourceId/subResource/subResourceId + if (count($parts) === 4) { + $resourceId = $parts[3]; + $resourceType = $parts[2]; + + // resource/resourceId + $resourceParent = "{$parts[0]}/{$parts[1]}"; + } // resource/resourceId + elseif (count($parts) === 2) { + $resourceId = $parts[1]; + $resourceType = $parts[0]; + } else { + // default fallback + $resourceId = $resource; + } + + return [ + 'id' => $resourceId, + 'type' => $resourceType, + 'parent' => $resourceParent, + ]; + } } From 44d0d4257c61ce096ba0c33a7cd17e777e961822 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 27 Jan 2026 05:22:41 +0000 Subject: [PATCH 2/3] refactor: rename resource attributes for clarity in SQL adapter --- src/Audit/Adapter/SQL.php | 6 +-- tests/Audit/Adapter/ClickHouseTest.php | 51 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/Audit/Adapter/SQL.php b/src/Audit/Adapter/SQL.php index 3a71942..681a7ac 100644 --- a/src/Audit/Adapter/SQL.php +++ b/src/Audit/Adapter/SQL.php @@ -246,9 +246,9 @@ protected function parseResource(string $resource): array } return [ - 'id' => $resourceId, - 'type' => $resourceType, - 'parent' => $resourceParent, + 'resourceId' => $resourceId, + 'resourceType' => $resourceType, + 'resourceParent' => $resourceParent, ]; } } diff --git a/tests/Audit/Adapter/ClickHouseTest.php b/tests/Audit/Adapter/ClickHouseTest.php index 96c40d4..1b6bab9 100644 --- a/tests/Audit/Adapter/ClickHouseTest.php +++ b/tests/Audit/Adapter/ClickHouseTest.php @@ -413,4 +413,55 @@ public function testClickHouseAdapterIndexes(): void $this->assertContains($expected, $indexIds, "Parent index '{$expected}' not found in ClickHouse adapter"); } } + + /** + * Test parsing of complex resource paths into resourceType/resourceId/resourceParent + */ + public function testParseResourceComplexPath(): void + { + $userId = 'parseUser'; + $userAgent = 'UnitTestAgent/1.0'; + $ip = '127.0.0.1'; + $location = 'US'; + + $resource = 'database/6978484940ff05762e1a/table/697848498066e3d2ef64'; + + // Ensure we don't provide resourceType/resourceId in data so adapter must parse it + $data = ['example' => 'value']; + + $log = $this->audit->log($userId, 'create', $resource, $userAgent, $ip, $location, $data); + + $this->assertInstanceOf(\Utopia\Audit\Log::class, $log); + + $this->assertEquals('table', $log->getAttribute('resourceType')); + $this->assertEquals('697848498066e3d2ef64', $log->getAttribute('resourceId')); + $this->assertEquals('database/6978484940ff05762e1a', $log->getAttribute('resourceParent')); + } + + /** + * Directly test the protected parseResource method via reflection. + */ + public function testParseResourceMethod(): void + { + $adapter = new ClickHouse( + host: 'clickhouse', + username: 'default', + password: 'clickhouse' + ); + + $method = new \ReflectionMethod($adapter, 'parseResource'); + $method->setAccessible(true); + + $resource = 'database/6978484940ff05762e1a/table/697848498066e3d2ef64'; + $parsed = $method->invoke($adapter, $resource); + + $this->assertIsArray($parsed); + $this->assertArrayHasKey('id', $parsed); + $this->assertArrayHasKey('type', $parsed); + $this->assertArrayHasKey('parent', $parsed); + + $this->assertEquals('697848498066e3d2ef64', $parsed['resourceId']); + $this->assertEquals('table', $parsed['resourceType']); + $this->assertEquals('database/6978484940ff05762e1a', $parsed['resourceParent']); + } } From 02918ff3fa5495a2988b566bf6791a31a6df0c72 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 27 Jan 2026 05:36:19 +0000 Subject: [PATCH 3/3] fix: improve resource parsing and validation in ClickHouse adapter --- src/Audit/Adapter/ClickHouse.php | 12 ++++++++++-- src/Audit/Adapter/SQL.php | 3 +++ tests/Audit/Adapter/ClickHouseTest.php | 14 ++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Audit/Adapter/ClickHouse.php b/src/Audit/Adapter/ClickHouse.php index 9dcb529..8a36bab 100644 --- a/src/Audit/Adapter/ClickHouse.php +++ b/src/Audit/Adapter/ClickHouse.php @@ -760,7 +760,11 @@ public function create(array $log): Log // Separate data for the data column (non-schema attributes) $nonSchemaData = $logData; - $resource = $this->parseResource($log['resource'] ?? ''); + $resourceValue = $log['resource'] ?? null; + if (!\is_string($resourceValue)) { + $resourceValue = ''; + } + $resource = $this->parseResource($resourceValue); foreach ($schemaColumns as $columnName) { if ($columnName === 'time') { @@ -1146,7 +1150,11 @@ public function createBatch(array $logs): bool // Separate data for non-schema attributes $nonSchemaData = $logData; - $resource = $this->parseResource($log['resource'] ?? ''); + $resourceValue = $log['resource'] ?? null; + if (!\is_string($resourceValue)) { + $resourceValue = ''; + } + $resource = $this->parseResource($resourceValue); $processedLog = $log; // Extract schema attributes: check main log first, then data array diff --git a/src/Audit/Adapter/SQL.php b/src/Audit/Adapter/SQL.php index 681a7ac..f35625b 100644 --- a/src/Audit/Adapter/SQL.php +++ b/src/Audit/Adapter/SQL.php @@ -221,6 +221,9 @@ protected function getAllColumnDefinitions(): array /** * Parses the resource string from the payload and extracts its ID, type, and parent. + * + * @param string $resource + * @return array{ resourceId: string, resourceType: string, resourceParent: string } */ protected function parseResource(string $resource): array { diff --git a/tests/Audit/Adapter/ClickHouseTest.php b/tests/Audit/Adapter/ClickHouseTest.php index 1b6bab9..b6cf831 100644 --- a/tests/Audit/Adapter/ClickHouseTest.php +++ b/tests/Audit/Adapter/ClickHouseTest.php @@ -429,7 +429,13 @@ public function testParseResourceComplexPath(): void // Ensure we don't provide resourceType/resourceId in data so adapter must parse it $data = ['example' => 'value']; - $log = $this->audit->log($userId, 'create', $resource, $userAgent, $ip, $location, $data); + // Merge required adapter attributes so ClickHouse won't reject the log, + // but ensure we do NOT supply resourceType/resourceId/resourceParent so adapter parses them + $required = $this->getRequiredAttributes(); + unset($required['resourceType'], $required['resourceId'], $required['resourceParent']); + $dataWithAttributes = array_merge($data, $required); + + $log = $this->audit->log($userId, 'create', $resource, $userAgent, $ip, $location, $dataWithAttributes); $this->assertInstanceOf(\Utopia\Audit\Log::class, $log); @@ -456,9 +462,9 @@ public function testParseResourceMethod(): void $parsed = $method->invoke($adapter, $resource); $this->assertIsArray($parsed); - $this->assertArrayHasKey('id', $parsed); - $this->assertArrayHasKey('type', $parsed); - $this->assertArrayHasKey('parent', $parsed); + $this->assertArrayHasKey('resourceId', $parsed); + $this->assertArrayHasKey('resourceType', $parsed); + $this->assertArrayHasKey('resourceParent', $parsed); $this->assertEquals('697848498066e3d2ef64', $parsed['resourceId']); $this->assertEquals('table', $parsed['resourceType']);