diff --git a/src/Audit/Adapter/ClickHouse.php b/src/Audit/Adapter/ClickHouse.php index 8ef2a3b..8a36bab 100644 --- a/src/Audit/Adapter/ClickHouse.php +++ b/src/Audit/Adapter/ClickHouse.php @@ -760,6 +760,12 @@ public function create(array $log): Log // Separate data for the data column (non-schema attributes) $nonSchemaData = $logData; + $resourceValue = $log['resource'] ?? null; + if (!\is_string($resourceValue)) { + $resourceValue = ''; + } + $resource = $this->parseResource($resourceValue); + foreach ($schemaColumns as $columnName) { if ($columnName === 'time') { // Skip time - already handled above @@ -790,6 +796,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 +1150,11 @@ public function createBatch(array $logs): bool // Separate data for non-schema attributes $nonSchemaData = $logData; + $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 @@ -1152,6 +1167,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..f35625b 100644 --- a/src/Audit/Adapter/SQL.php +++ b/src/Audit/Adapter/SQL.php @@ -218,4 +218,40 @@ protected function getAllColumnDefinitions(): array return $definitions; } + + /** + * 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 + { + $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 [ + 'resourceId' => $resourceId, + 'resourceType' => $resourceType, + 'resourceParent' => $resourceParent, + ]; + } } diff --git a/tests/Audit/Adapter/ClickHouseTest.php b/tests/Audit/Adapter/ClickHouseTest.php index 96c40d4..b6cf831 100644 --- a/tests/Audit/Adapter/ClickHouseTest.php +++ b/tests/Audit/Adapter/ClickHouseTest.php @@ -413,4 +413,61 @@ 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']; + + // 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); + + $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('resourceId', $parsed); + $this->assertArrayHasKey('resourceType', $parsed); + $this->assertArrayHasKey('resourceParent', $parsed); + + $this->assertEquals('697848498066e3d2ef64', $parsed['resourceId']); + $this->assertEquals('table', $parsed['resourceType']); + $this->assertEquals('database/6978484940ff05762e1a', $parsed['resourceParent']); + } }