* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderJmapc\Stores; use KTXC\Db\DataStore; use KTXF\Security\Crypto; use KTXF\Utile\UUID; use KTXM\ProviderJmapc\Providers\Mail\Service; /** * JMAP Service Store * * Shared by Mail, Calendar, and Contacts providers. */ class ServiceStore { protected const COLLECTION_NAME = 'provider_jmapc_services'; public function __construct( protected readonly DataStore $dataStore, protected readonly Crypto $crypto, ) {} /** * List services for a tenant and user, optionally filtered by service IDs */ public function list(string $tenantId, string $userId, ?array $filter = null): array { $filterCondition = [ 'tid' => $tenantId, 'uid' => $userId, ]; if ($filter !== null && !empty($filter)) { $filterCondition['sid'] = ['$in' => $filter]; } $cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find($filterCondition); $list = []; foreach ($cursor as $entry) { if (isset($entry['identity']['secret'])) { $entry['identity']['secret'] = $this->crypto->decrypt($entry['identity']['secret']); } $list[$entry['sid']] = $entry; } return $list; } /** * Check existence of services by IDs for a tenant and user */ public function extant(string $tenantId, string $userId, array $identifiers): array { if (empty($identifiers)) { return []; } $cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find( [ 'tid' => $tenantId, 'uid' => $userId, 'sid' => ['$in' => array_map('strval', $identifiers)] ], ['projection' => ['sid' => 1]] ); $existingIds = []; foreach ($cursor as $document) { $existingIds[] = $document['sid']; } // Build result map: all identifiers default to false, existing ones set to true $result = []; foreach ($identifiers as $id) { $result[(string) $id] = in_array((string) $id, $existingIds, true); } return $result; } /** * Retrieve a single service by ID */ public function fetch(string $tenantId, string $userId, string|int $serviceId): ?Service { $document = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne([ 'tid' => $tenantId, 'uid' => $userId, 'sid' => (string)$serviceId, ]); if (!$document) { return null; } if (isset($document['identity']['secret'])) { $document['identity']['secret'] = $this->crypto->decrypt($document['identity']['secret']); } return (new Service())->fromStore($document); } /** * Create a new service */ public function create(string $tenantId, string $userId, Service $service): Service { $document = $service->toStore(); // prepare document for insertion $document['tid'] = $tenantId; $document['uid'] = $userId; $document['sid'] = UUID::v4(); $document['createdOn'] = new \MongoDB\BSON\UTCDateTime(); $document['modifiedOn'] = new \MongoDB\BSON\UTCDateTime(); if (isset($document['identity']['secret'])) { $document['identity']['secret'] = $this->crypto->encrypt($document['identity']['secret']); } $result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->insertOne($document); return (new Service())->fromStore($document); } /** * Modify an existing service */ public function modify(string $tenantId, string $userId, Service $service): Service { $serviceId = $service->id(); if (empty($serviceId)) { throw new \InvalidArgumentException('Service ID is required for update'); } // prepare document for modification $document = $service->toStore(); $document['modifiedOn'] = new \MongoDB\BSON\UTCDateTime(); if (isset($document['identity']['secret'])) { $document['identity']['secret'] = $this->crypto->encrypt($document['identity']['secret']); } unset($document['sid'], $document['tid'], $document['uid'], $document['createdOn']); $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne( [ 'tid' => $tenantId, 'uid' => $userId, 'sid' => (string)$serviceId, ], ['$set' => $document] ); return (new Service())->fromStore($document); } /** * Delete a service */ public function delete(string $tenantId, string $userId, string|int $serviceId): bool { $result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteOne([ 'tid' => $tenantId, 'uid' => $userId, 'sid' => (string)$serviceId, ]); return $result->getDeletedCount() > 0; } }