diff --git a/composer.json b/composer.json index 3ea3083..f3e2b67 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ }, "require": { "php": ">=8.5", - "utopia-php/database": "^7.0.0", + "utopia-php/database": "dev-feat-query-lib as 7.0.0", "utopia-php/fetch": "^1.1", "utopia-php/query": "0.6.*", "utopia-php/validators": "^1.0" @@ -37,5 +37,19 @@ "php-http/discovery": true, "tbachert/spi": true } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/utopia-php/database.git" + }, + { + "type": "vcs", + "url": "https://github.com/utopia-php/query.git" + }, + { + "type": "vcs", + "url": "https://github.com/utopia-php/async.git" + } + ] } diff --git a/src/Audit/Adapter/ClickHouse.php b/src/Audit/Adapter/ClickHouse.php index 7b3bdbe..188785a 100644 --- a/src/Audit/Adapter/ClickHouse.php +++ b/src/Audit/Adapter/ClickHouse.php @@ -5,7 +5,9 @@ use Exception; use Utopia\Audit\Log; use Utopia\Audit\Query; +use Utopia\Database\Attribute; use Utopia\Database\Database; +use Utopia\Database\Index; use Utopia\Fetch\Client; use Utopia\Query\Builder\ClickHouse as ClickHouseBuilder; use Utopia\Query\Builder\ClickHouse\Format; @@ -375,394 +377,65 @@ public function getRetention(): ?int * Override getAttributes to provide extended attributes for ClickHouse. * Includes existing attributes from parent and adds new missing ones. * - * @return array> + * @return array */ #[\Override] public function getAttributes(): array { $parentAttributes = parent::getAttributes(); - foreach ($parentAttributes as &$attribute) { - if (($attribute['$id'] ?? null) === 'userId') { - $attribute['$id'] = 'actorId'; + foreach ($parentAttributes as $index => $attribute) { + if ($attribute->key === 'userId') { + $parentAttributes[$index] = new Attribute( + key: 'actorId', + type: $attribute->type, + size: $attribute->size, + required: $attribute->required, + signed: $attribute->signed, + array: $attribute->array, + filters: $attribute->filters, + ); break; } } - unset($attribute); return [ ...$parentAttributes, - [ - '$id' => 'actorType', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => true, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'actorInternalId', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'resourceParent', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'resourceType', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => true, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'resourceId', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => true, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'resourceInternalId', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'country', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - // premium geo - [ - '$id' => 'city', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'continentCode', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'subdivisions', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'isp', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'autonomousSystemNumber', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'autonomousSystemOrganization', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'connectionType', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'connectionUsageType', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'connectionOrganization', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'default' => null, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'projectId', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'projectInternalId', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'teamId', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'teamInternalId', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'hostname', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => true, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'sdk', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'sdkVersion', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - // user-agent — parsed OS / client / device dimensions - [ - '$id' => 'osCode', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'osName', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'osVersion', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'clientType', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'clientCode', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'clientName', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'clientVersion', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'clientEngine', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'clientEngineVersion', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'deviceName', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'deviceBrand', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'deviceModel', - 'type' => Database::VAR_STRING, - 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], + Attribute::string(key: 'actorType', required: true), + Attribute::string(key: 'actorInternalId'), + Attribute::string(key: 'resourceParent'), + Attribute::string(key: 'resourceType', required: true), + Attribute::string(key: 'resourceId', required: true), + Attribute::string(key: 'resourceInternalId'), + Attribute::string(key: 'country'), + Attribute::string(key: 'city'), + Attribute::string(key: 'continentCode'), + Attribute::string(key: 'subdivisions'), + Attribute::string(key: 'isp'), + Attribute::string(key: 'autonomousSystemNumber'), + Attribute::string(key: 'autonomousSystemOrganization'), + Attribute::string(key: 'connectionType'), + Attribute::string(key: 'connectionUsageType'), + Attribute::string(key: 'connectionOrganization'), + Attribute::string(key: 'projectId', required: true), + Attribute::string(key: 'projectInternalId', required: true), + Attribute::string(key: 'teamId', required: true), + Attribute::string(key: 'teamInternalId', required: true), + Attribute::string(key: 'hostname', required: true), + Attribute::string(key: 'sdk'), + Attribute::string(key: 'sdkVersion'), + Attribute::string(key: 'osCode'), + Attribute::string(key: 'osName'), + Attribute::string(key: 'osVersion'), + Attribute::string(key: 'clientType'), + Attribute::string(key: 'clientCode'), + Attribute::string(key: 'clientName'), + Attribute::string(key: 'clientVersion'), + Attribute::string(key: 'clientEngine'), + Attribute::string(key: 'clientEngineVersion'), + Attribute::string(key: 'deviceName'), + Attribute::string(key: 'deviceBrand'), + Attribute::string(key: 'deviceModel'), ]; } @@ -770,80 +443,37 @@ public function getAttributes(): array * Override getIndexes to provide extended indexes for ClickHouse. * Includes existing indexes from parent and adds new missing ones. * - * @return array> + * @return array */ #[\Override] public function getIndexes(): array { $parentIndexes = parent::getIndexes(); - foreach ($parentIndexes as &$index) { - if (($index['$id'] ?? null) === 'idx_userId_event') { - $index['$id'] = 'idx_actorId_event'; - $index['attributes'] = ['actorId', 'event']; + foreach ($parentIndexes as $index => $definition) { + if ($definition->key === 'idx_userId_event') { + $parentIndexes[$index] = new Index( + key: 'idx_actorId_event', + type: $definition->type, + attributes: ['actorId', 'event'], + lengths: $definition->lengths, + orders: $definition->orders, + ttl: $definition->ttl, + ); break; } } - unset($index); return [ ...$parentIndexes, - [ - '$id' => '_key_actor_internal_and_event', - 'type' => Database::INDEX_KEY, - 'attributes' => ['actorInternalId', 'event'], - 'lengths' => [], - 'orders' => [], - ], - [ - '$id' => '_key_project_internal_id', - 'type' => Database::INDEX_KEY, - 'attributes' => ['projectInternalId'], - 'lengths' => [], - 'orders' => [], - ], - [ - '$id' => '_key_team_internal_id', - 'type' => Database::INDEX_KEY, - 'attributes' => ['teamInternalId'], - 'lengths' => [], - 'orders' => [], - ], - [ - '$id' => '_key_actor_internal_id', - 'type' => Database::INDEX_KEY, - 'attributes' => ['actorInternalId'], - 'lengths' => [], - 'orders' => [], - ], - [ - '$id' => '_key_actor_type', - 'type' => Database::INDEX_KEY, - 'attributes' => ['actorType'], - 'lengths' => [], - 'orders' => [], - ], - [ - '$id' => '_key_country', - 'type' => Database::INDEX_KEY, - 'attributes' => ['country'], - 'lengths' => [], - 'orders' => [], - ], - [ - '$id' => '_key_hostname', - 'type' => Database::INDEX_KEY, - 'attributes' => ['hostname'], - 'lengths' => [], - 'orders' => [], - ], - [ - '$id' => '_key_sdk', - 'type' => Database::INDEX_KEY, - 'attributes' => ['sdk'], - 'lengths' => [], - 'orders' => [], - ], + Index::key(key: '_key_actor_internal_and_event', attributes: ['actorInternalId', 'event']), + Index::key(key: '_key_project_internal_id', attributes: ['projectInternalId']), + Index::key(key: '_key_team_internal_id', attributes: ['teamInternalId']), + Index::key(key: '_key_actor_internal_id', attributes: ['actorInternalId']), + Index::key(key: '_key_actor_type', attributes: ['actorType']), + Index::key(key: '_key_country', attributes: ['country']), + Index::key(key: '_key_hostname', attributes: ['hostname']), + Index::key(key: '_key_sdk', attributes: ['sdk']), ]; } @@ -879,9 +509,7 @@ private function getColumnTypeMap(): array $map = ['id' => 'String']; foreach ($this->getAttributes() as $attribute) { - /** @var string $id */ - $id = $attribute['$id']; - $map[$id] = ($attribute['type'] ?? null) === Database::VAR_DATETIME + $map[$attribute->key] = $attribute->type === ColumnType::Datetime ? 'DateTime64(3)' : 'String'; } @@ -1058,7 +686,7 @@ public function setup(): void continue; } - $column = $table->addColumn($id, ($attribute['type'] ?? null) === Database::VAR_DATETIME + $column = $table->addColumn($id, $attribute->type === ColumnType::Datetime ? ColumnType::Datetime : ColumnType::String); if (\in_array($id, self::LOW_CARDINALITY_COLUMNS, true)) { @@ -2137,11 +1765,11 @@ protected function getColumnDefinition(string $id): string // Dynamically determine type based on attribute metadata // DateTime attributes use DateTime64(3), all others use String - $type = (isset($attribute['type']) && $attribute['type'] === Database::VAR_DATETIME) + $type = $attribute->type === ColumnType::Datetime ? 'DateTime64(3)' : 'String'; - $required = (bool) $attribute['required']; + $required = $attribute->required; if ($type === 'String' && \in_array($id, self::LOW_CARDINALITY_COLUMNS, true)) { $columnType = $required diff --git a/src/Audit/Adapter/Database.php b/src/Audit/Adapter/Database.php index 796f598..cc2a5de 100644 --- a/src/Audit/Adapter/Database.php +++ b/src/Audit/Adapter/Database.php @@ -4,6 +4,7 @@ use Exception; use Utopia\Audit\Log; +use Utopia\Database\Collection; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Exception\Authorization as AuthorizationException; @@ -48,7 +49,7 @@ public function ping(): bool /** * Setup database structure. * - * @throws \Exception + * @throws Exception */ public function setup(): void { @@ -60,11 +61,7 @@ public function setup(): void $indexes = $this->getIndexDocuments(); try { - $this->db->createCollection( - $this->getCollectionName(), - $attributes, - $indexes, - ); + $this->db->createCollection(new Collection(id: $this->getCollectionName(), attributes: $attributes, indexes: $indexes)); } catch (DuplicateException) { // Collection already exists } @@ -73,13 +70,14 @@ public function setup(): void /** * Create an audit log entry. * - * @param array $log - * @throws AuthorizationException|\Exception + * @param array $log + * + * @throws AuthorizationException|Exception */ public function create(array $log): Log { $log['time'] ??= DateTime::now(); - $document = $this->db->getAuthorization()->skip(fn(): \Utopia\Database\Document => $this->db->createDocument($this->getCollectionName(), new Document($log))); + $document = $this->db->getAuthorization()->skip(fn (): Document => $this->db->createDocument($this->getCollectionName(), new Document($log))); return new Log($document->getArrayCopy()); } @@ -87,19 +85,21 @@ public function create(array $log): Log /** * Create multiple audit log entries in batch. * - * @param array> $logs - * @throws AuthorizationException|\Exception + * @param array> $logs + * + * @throws AuthorizationException|Exception */ public function createBatch(array $logs): bool { $this->db->getAuthorization()->skip(function () use ($logs): void { - $documents = array_map(function (array $log): \Utopia\Database\Document { - $time = $log['time'] ?? new \DateTime(); + $documents = array_map(function (array $log): Document { + $time = $log['time'] ?? new \DateTime; if (\is_string($time)) { $time = new \DateTime($time); } \assert($time instanceof \DateTime); $log['time'] = DateTime::format($time); + return new Document($log); }, $logs); $this->db->createDocuments($this->getCollectionName(), $documents); @@ -112,11 +112,12 @@ public function createBatch(array $logs): bool * Get a single log by its ID. * * @return Log|null The log entry or null if not found - * @throws AuthorizationException|\Exception + * + * @throws AuthorizationException|Exception */ public function getById(string $id): ?Log { - $document = $this->db->getAuthorization()->skip(fn(): \Utopia\Database\Document => $this->db->getDocument($this->getCollectionName(), $id)); + $document = $this->db->getAuthorization()->skip(fn (): Document => $this->db->getDocument($this->getCollectionName(), $id)); if ($document->isEmpty()) { return null; @@ -139,6 +140,7 @@ private function buildTimeQueries(?\DateTime $after, ?\DateTime $before): array if ($afterStr !== null && $beforeStr !== null) { $queries[] = Query::between('time', $afterStr, $beforeStr); + return $queries; } @@ -157,7 +159,8 @@ private function buildTimeQueries(?\DateTime $after, ?\DateTime $before): array * Get audit logs by user ID. * * @return array - * @throws AuthorizationException|\Exception + * + * @throws AuthorizationException|Exception */ public function getByUser( string $userId, @@ -183,13 +186,13 @@ public function getByUser( ); }); - return array_map(fn(\Utopia\Database\Document $doc): \Utopia\Audit\Log => new Log($doc->getArrayCopy()), $documents); + return array_map(fn (Document $doc): Log => new Log($doc->getArrayCopy()), $documents); } /** * Count audit logs by user ID. * - * @throws AuthorizationException|\Exception + * @throws AuthorizationException|Exception */ public function countByUser( string $userId, @@ -198,7 +201,8 @@ public function countByUser( ?int $max = null, ): int { $timeQueries = $this->buildTimeQueries($after, $before); - return $this->db->getAuthorization()->skip(fn(): int => $this->db->count( + + return $this->db->getAuthorization()->skip(fn (): int => $this->db->count( collection: $this->getCollectionName(), queries: [ Query::equal('userId', [$userId]), @@ -212,6 +216,7 @@ public function countByUser( * Get logs by resource. * * @return array + * * @throws Timeout|\Utopia\Database\Exception|\Utopia\Database\Exception\Query */ public function getByResource( @@ -238,7 +243,7 @@ public function getByResource( ); }); - return array_map(fn(\Utopia\Database\Document $doc): \Utopia\Audit\Log => new Log($doc->getArrayCopy()), $documents); + return array_map(fn (Document $doc): Log => new Log($doc->getArrayCopy()), $documents); } /** @@ -253,7 +258,8 @@ public function countByResource( ?int $max = null, ): int { $timeQueries = $this->buildTimeQueries($after, $before); - return $this->db->getAuthorization()->skip(fn(): int => $this->db->count( + + return $this->db->getAuthorization()->skip(fn (): int => $this->db->count( collection: $this->getCollectionName(), queries: [ Query::equal('resource', [$resource]), @@ -266,8 +272,9 @@ public function countByResource( /** * Get logs by user and events. * - * @param array $events + * @param array $events * @return array + * * @throws Timeout|\Utopia\Database\Exception|\Utopia\Database\Exception\Query */ public function getByUserAndEvents( @@ -296,13 +303,14 @@ public function getByUserAndEvents( ); }); - return array_map(fn(\Utopia\Database\Document $doc): \Utopia\Audit\Log => new Log($doc->getArrayCopy()), $documents); + return array_map(fn (Document $doc): Log => new Log($doc->getArrayCopy()), $documents); } /** * Count logs by user and events. * - * @param array $events + * @param array $events + * * @throws \Utopia\Database\Exception */ public function countByUserAndEvents( @@ -313,7 +321,8 @@ public function countByUserAndEvents( ?int $max = null, ): int { $timeQueries = $this->buildTimeQueries($after, $before); - return $this->db->getAuthorization()->skip(fn(): int => $this->db->count( + + return $this->db->getAuthorization()->skip(fn (): int => $this->db->count( collection: $this->getCollectionName(), queries: [ Query::equal('userId', [$userId]), @@ -327,8 +336,9 @@ public function countByUserAndEvents( /** * Get logs by resource and events. * - * @param array $events + * @param array $events * @return array + * * @throws Timeout|\Utopia\Database\Exception|\Utopia\Database\Exception\Query */ public function getByResourceAndEvents( @@ -357,13 +367,14 @@ public function getByResourceAndEvents( ); }); - return array_map(fn(\Utopia\Database\Document $doc): \Utopia\Audit\Log => new Log($doc->getArrayCopy()), $documents); + return array_map(fn (Document $doc): Log => new Log($doc->getArrayCopy()), $documents); } /** * Count logs by resource and events. * - * @param array $events + * @param array $events + * * @throws \Utopia\Database\Exception */ public function countByResourceAndEvents( @@ -374,7 +385,8 @@ public function countByResourceAndEvents( ?int $max = null, ): int { $timeQueries = $this->buildTimeQueries($after, $before); - return $this->db->getAuthorization()->skip(fn(): int => $this->db->count( + + return $this->db->getAuthorization()->skip(fn (): int => $this->db->count( collection: $this->getCollectionName(), queries: [ Query::equal('resource', [$resource]), @@ -388,9 +400,7 @@ public function countByResourceAndEvents( /** * Delete logs older than the specified datetime. * - * @param \DateTime $datetime - /** - * @throws AuthorizationException|\Exception + * @throws AuthorizationException|Exception */ public function cleanup(\DateTime $datetime): bool { @@ -422,24 +432,21 @@ public function cleanup(\DateTime $datetime): bool * type mapping internally. However, this implementation is required to satisfy * the abstract method declaration in the base SQL adapter. * - * @param string $id Attribute identifier + * @param string $id Attribute identifier * @return string Database-agnostic column description + * * @throws Exception */ protected function getColumnDefinition(string $id): string { $attribute = $this->getAttribute($id); - if (!$attribute) { + if (! $attribute) { throw new Exception("Attribute {$id} not found"); } - // For the Database adapter, we use Utopia's VAR_* type constants internally - // This method provides a description for reference purposes - /** @var string $type */ - $type = $attribute['type']; - /** @var int $size */ - $size = $attribute['size'] ?? 0; + $type = $attribute->type->value; + $size = $attribute->size; if ($size > 0) { return "{$id}: {$type}({$size})"; @@ -453,17 +460,18 @@ protected function getColumnDefinition(string $id): string * * Translates Audit Query objects to Database Query objects. * - * @param array<\Utopia\Audit\Query> $queries - * @return array<\Utopia\Audit\Log> - * @throws AuthorizationException|\Exception + * @param array<\Utopia\Audit\Query> $queries + * @return array + * + * @throws AuthorizationException|Exception */ public function find(array $queries = []): array { $dbQueries = []; foreach ($queries as $query) { - if (!($query instanceof \Utopia\Audit\Query)) { - throw new \Exception('Invalid query type. Expected Utopia\\Audit\\Query'); + if (! ($query instanceof \Utopia\Audit\Query)) { + throw new Exception('Invalid query type. Expected Utopia\\Audit\\Query'); } // Convert Audit Query to Database Query @@ -471,12 +479,12 @@ public function find(array $queries = []): array $dbQueries[] = Query::parseQuery($query->toArray()); } - $documents = $this->db->getAuthorization()->skip(fn(): array => $this->db->find( + $documents = $this->db->getAuthorization()->skip(fn (): array => $this->db->find( collection: $this->getCollectionName(), queries: $dbQueries, )); - return array_map(fn(\Utopia\Database\Document $doc): \Utopia\Audit\Log => new Log($doc->getArrayCopy()), $documents); + return array_map(fn (Document $doc): Log => new Log($doc->getArrayCopy()), $documents); } /** @@ -485,17 +493,18 @@ public function find(array $queries = []): array * Translates Audit Query objects to Database Query objects. * Ignores limit, offset, and cursor queries as they don't apply to count. * - * @param array<\Utopia\Audit\Query> $queries - * @param int|null $max Optional upper bound (inclusive) for the count - * @throws AuthorizationException|\Exception + * @param array<\Utopia\Audit\Query> $queries + * @param int|null $max Optional upper bound (inclusive) for the count + * + * @throws AuthorizationException|Exception */ public function count(array $queries = [], ?int $max = null): int { $dbQueries = []; foreach ($queries as $query) { - if (!($query instanceof \Utopia\Audit\Query)) { - throw new \Exception('Invalid query type. Expected Utopia\\Audit\\Query'); + if (! ($query instanceof \Utopia\Audit\Query)) { + throw new Exception('Invalid query type. Expected Utopia\\Audit\\Query'); } // Skip limit, offset, and cursor queries — they don't apply to count @@ -519,7 +528,7 @@ public function count(array $queries = [], ?int $max = null): int $dbQueries[] = Query::parseQuery($queryArray); } - return $this->db->getAuthorization()->skip(fn(): int => $this->db->count( + return $this->db->getAuthorization()->skip(fn (): int => $this->db->count( collection: $this->getCollectionName(), queries: $dbQueries, max: $max, diff --git a/src/Audit/Adapter/SQL.php b/src/Audit/Adapter/SQL.php index f5208a3..10afbb8 100644 --- a/src/Audit/Adapter/SQL.php +++ b/src/Audit/Adapter/SQL.php @@ -3,8 +3,9 @@ namespace Utopia\Audit\Adapter; use Utopia\Audit\Adapter; +use Utopia\Database\Attribute; use Utopia\Database\Database; -use Utopia\Database\Document; +use Utopia\Database\Index; /** * Base SQL Adapter for Audit @@ -27,152 +28,63 @@ public function getCollectionName(): string /** * Get attribute definitions for audit logs. * - * Each attribute is an array with the following string keys: - * - $id: string (attribute identifier) - * - type: string - * - size: int - * - required: bool - * - signed: bool - * - array: bool - * - filters: array - * - * @return array> + * @return array */ public function getAttributes(): array { return [ - [ - '$id' => 'userId', - 'type' => Database::VAR_STRING, - 'size' => Database::LENGTH_KEY, - 'required' => false, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'event', - 'type' => Database::VAR_STRING, - 'size' => 255, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'resource', - 'type' => Database::VAR_STRING, - 'size' => 255, - 'required' => false, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'userAgent', - 'type' => Database::VAR_STRING, - 'size' => 65534, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'ip', - 'type' => Database::VAR_STRING, - 'size' => 45, - 'required' => true, - 'signed' => true, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'time', - 'type' => Database::VAR_DATETIME, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'array' => false, - 'filters' => ['datetime'], - ], - [ - '$id' => 'data', - 'type' => Database::VAR_STRING, - 'size' => 16777216, - 'required' => false, - 'signed' => true, - 'array' => false, - 'filters' => ['json'], - ], + Attribute::string(key: 'userId'), + Attribute::string(key: 'event', required: true), + Attribute::string(key: 'resource'), + Attribute::string(key: 'userAgent', size: 65534, required: true), + Attribute::string(key: 'ip', size: 45, required: true), + Attribute::datetime(key: 'time', filters: ['datetime']), + Attribute::string(key: 'data', size: 16777216, filters: ['json']), ]; } /** - * Get attribute documents for audit logs. + * Get attribute value objects for createCollection. * - * @return array + * @return array */ public function getAttributeDocuments(): array { - return array_map(static fn(array $attribute): \Utopia\Database\Document => new Document($attribute), $this->getAttributes()); + return $this->getAttributes(); } /** * Get index definitions for audit logs. * - * Each index is an array with the following string keys: - * - $id: string (index identifier) - * - type: string - * - attributes: array - * - * @return array> + * @return array */ public function getIndexes(): array { return [ - [ - '$id' => 'idx_event', - 'type' => 'key', - 'attributes' => ['event'], - ], - [ - '$id' => 'idx_userId_event', - 'type' => 'key', - 'attributes' => ['userId', 'event'], - ], - [ - '$id' => 'idx_resource_event', - 'type' => 'key', - 'attributes' => ['resource', 'event'], - ], - [ - '$id' => 'idx_time_desc', - 'type' => 'key', - 'attributes' => ['time'], - ], + Index::key(key: 'idx_event', attributes: ['event']), + Index::key(key: 'idx_userId_event', attributes: ['userId', 'event']), + Index::key(key: 'idx_resource_event', attributes: ['resource', 'event']), + Index::key(key: 'idx_time_desc', attributes: ['time']), ]; } /** - * Get index documents for audit logs. + * Get index value objects for createCollection. * - * @return array + * @return array */ public function getIndexDocuments(): array { - return array_map(static fn(array $index): \Utopia\Database\Document => new Document($index), $this->getIndexes()); + return $this->getIndexes(); } /** - * Get a single attribute by ID. - * - * @return array|null + * Get a single attribute by key. */ - protected function getAttribute(string $id) + protected function getAttribute(string $id): ?Attribute { foreach ($this->getAttributes() as $attribute) { - if ($attribute['$id'] === $id) { + if ($attribute->key === $id) { return $attribute; } } @@ -184,7 +96,7 @@ protected function getAttribute(string $id) * Get SQL column definition for a given attribute ID. * This method is database-specific and must be implemented by each concrete adapter. * - * @param string $id Attribute identifier + * @param string $id Attribute identifier * @return string Database-specific column definition */ abstract protected function getColumnDefinition(string $id): string; @@ -199,9 +111,7 @@ protected function getAllColumnDefinitions(): array { $definitions = []; foreach ($this->getAttributes() as $attribute) { - /** @var string $id */ - $id = $attribute['$id']; - $definitions[] = $this->getColumnDefinition($id); + $definitions[] = $this->getColumnDefinition($attribute->key); } return $definitions; diff --git a/tests/Audit/Adapter/ClickHouseTest.php b/tests/Audit/Adapter/ClickHouseTest.php index cc0b061..d2affae 100644 --- a/tests/Audit/Adapter/ClickHouseTest.php +++ b/tests/Audit/Adapter/ClickHouseTest.php @@ -9,6 +9,9 @@ use Utopia\Audit\Adapter\ClickHouse; use Utopia\Audit\Audit; use Utopia\Audit\Query; +use Utopia\Database\Attribute; +use Utopia\Database\Index; +use Utopia\Query\Schema\ColumnType; use Utopia\Tests\Audit\AuditBase; /** @@ -515,7 +518,7 @@ public function testClickHouseAdapterAttributes(): void ); $attributes = $adapter->getAttributes(); - $attributeIds = array_map(fn(array $attr): mixed => $attr['$id'], $attributes); + $attributeIds = array_map(fn (Attribute $attr): string => $attr->key, $attributes); // Verify all expected attributes exist $expectedAttributes = [ @@ -627,7 +630,7 @@ public function testPremiumGeoAttributesAreOptionalStrings(): void $attributes = $adapter->getAttributes(); $byId = []; foreach ($attributes as $attribute) { - $byId[$attribute['$id']] = $attribute; + $byId[$attribute->key] = $attribute; } $geoColumns = [ @@ -644,9 +647,9 @@ public function testPremiumGeoAttributesAreOptionalStrings(): void foreach ($geoColumns as $column) { $this->assertArrayHasKey($column, $byId, "Premium geo attribute '{$column}' not found"); - $this->assertEquals(\Utopia\Database\Database::VAR_STRING, $byId[$column]['type'], "'{$column}' should be a string"); - $this->assertFalse($byId[$column]['required'], "'{$column}' should be optional"); - $this->assertFalse($byId[$column]['array'], "'{$column}' should not be an array"); + $this->assertSame(ColumnType::String, $byId[$column]->type, "'{$column}' should be a string"); + $this->assertFalse($byId[$column]->required, "'{$column}' should be optional"); + $this->assertFalse($byId[$column]->array, "'{$column}' should not be an array"); } } @@ -805,7 +808,7 @@ public function testClickHouseAdapterIndexes(): void ); $indexes = $adapter->getIndexes(); - $indexIds = array_map(fn(array $idx): mixed => $idx['$id'], $indexes); + $indexIds = array_map(fn (Index $idx): string => $idx->key, $indexes); // Verify all ClickHouse-specific indexes exist $expectedClickHouseIndexes = [ diff --git a/tests/Audit/Adapter/DatabaseTest.php b/tests/Audit/Adapter/DatabaseTest.php index f1a818c..3a9ed86 100644 --- a/tests/Audit/Adapter/DatabaseTest.php +++ b/tests/Audit/Adapter/DatabaseTest.php @@ -28,7 +28,9 @@ protected function initializeAudit(): void $dbUser = 'root'; $dbPass = 'password'; - $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, MariaDB::getPdoAttributes()); + $attributes = MariaDB::getPdoAttributes(); + $attributes[PDO::ATTR_PERSISTENT] = false; + $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};charset=utf8mb4", $dbUser, $dbPass, $attributes); $cache = new Cache(new NoCache()); $database = new Database(new MariaDB($pdo), $cache); $database->setDatabase('utopiaTests'); diff --git a/tests/Audit/AuditBase.php b/tests/Audit/AuditBase.php index e0dcb10..1c937d0 100644 --- a/tests/Audit/AuditBase.php +++ b/tests/Audit/AuditBase.php @@ -677,13 +677,13 @@ public function testFind(): void // Test 6: Find with contains filter (substring match, like utopia-php/database) $logs = $this->audit->find([ - \Utopia\Audit\Query::contains('event', ['event_0', 'event_1']), + \Utopia\Audit\Query::containsString('event', ['event_0', 'event_1']), ]); $this->assertGreaterThanOrEqual(2, \count($logs)); // Substring needle matches without an exact value $logs = $this->audit->find([ - \Utopia\Audit\Query::contains('event', ['vent_0']), + \Utopia\Audit\Query::containsString('event', ['vent_0']), ]); $this->assertGreaterThanOrEqual(1, \count($logs)); foreach ($logs as $log) { @@ -761,7 +761,7 @@ public function testCount(): void // Test 3: Count with contains filter (substring match, like utopia-php/database) $count = $this->audit->count([ - \Utopia\Audit\Query::contains('event', ['event_0', 'event_1']), + \Utopia\Audit\Query::containsString('event', ['event_0', 'event_1']), ]); $this->assertGreaterThanOrEqual(2, $count);