diff --git a/appinfo/routes.php b/appinfo/routes.php index 23fbf44..27548e5 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -19,6 +19,7 @@ ['name' => 'endpoints#logs', 'url' => '/api/endpoints-logs/{id}', 'verb' => 'GET'], ['name' => 'synchronizations#contracts', 'url' => '/api/synchronizations-contracts/{id}', 'verb' => 'GET'], ['name' => 'synchronizations#logs', 'url' => '/api/synchronizations-logs/{id}', 'verb' => 'GET'], + ['name' => 'synchronizations#test', 'url' => '/api/synchronizations-test/{id}', 'verb' => 'POST'], // Mapping endpoints ['name' => 'mappings#test', 'url' => '/api/mappings/test', 'verb' => 'POST'], // Running endpoints diff --git a/lib/Action/SynchronizationAction.php b/lib/Action/SynchronizationAction.php index f24f652..396e262 100644 --- a/lib/Action/SynchronizationAction.php +++ b/lib/Action/SynchronizationAction.php @@ -44,34 +44,17 @@ public function __construct( */ public function run(array $argument = []): array { + $response = []; // if we do not have a synchronization Id then everything is wrong - $response['stackTrace'][] = 'Check for a valid synchronization ID'; - if (!isset($argument['synchronizationId'])) { + $response['message'] = $response['stackTrace'][] = 'Check for a valid synchronization ID'; + if (isset($argument['synchronizationId']) === false) { // @todo: implement error handling $response['level'] = 'ERROR'; - $response['message'] = 'No synchronization ID provided'; - return $response; - } + $response['stackTrace'][] = $response['message'] = 'No synchronization ID provided'; - // We are going to allow for a single synchronization contract to be processed at a time - if (isset($argument['synchronizationContractId']) && is_int((int) $argument['synchronizationContractId'])) { - $response['level'] = 'INFO'; - $response['message'] = 'Synchronization single contract: '.$argument['synchronizationContractId']; - $synchronizationContract = $this->synchronizationContractMapper->find((int) $argument['synchronizationContractId']); - if($synchronizationContract === null){ - $response['level'] = 'ERROR'; - $response['message'] = 'Contract not found: '.$argument['synchronizationContractId']; - return $response; - } - try { - $this->callService->synchronizeContract($synchronization); - } catch (Exception $e) { - $response['level'] = 'ERROR'; - $response['message'] = 'Failed to synchronize contract: ' . $e->getMessage(); - return $response; - } + return $response; } // Let's find a synchronysation @@ -79,7 +62,7 @@ public function run(array $argument = []): array $synchronization = $this->synchronizationMapper->find((int) $argument['synchronizationId']); if ($synchronization === null){ $response['level'] = 'WARNING'; - $response['message'] = 'Synchronization not found: '.$argument['synchronizationId']; + $response['stackTrace'][] = $response['message'] = 'Synchronization not found: '.$argument['synchronizationId']; return $response; } @@ -89,11 +72,12 @@ public function run(array $argument = []): array $objects = $this->synchronizationService->synchronize($synchronization); } catch (Exception $e) { $response['level'] = 'ERROR'; - $response['message'] = 'Failed to synchronize: ' . $e->getMessage(); + $response['stackTrace'][] = $response['message'] = 'Failed to synchronize: ' . $e->getMessage(); return $response; } - $response['stackTrace'][] = 'Synchronized '.count($objects).' successfully'; + $response['level'] = 'INFO'; + $response['stackTrace'][] = $response['message'] = 'Synchronized '.count($objects).' successfully'; // Let's report back about what we have just done return $response; diff --git a/lib/Controller/JobsController.php b/lib/Controller/JobsController.php index 37c5d42..4ffe28b 100644 --- a/lib/Controller/JobsController.php +++ b/lib/Controller/JobsController.php @@ -2,6 +2,7 @@ namespace OCA\OpenConnector\Controller; +use Exception; use OCA\OpenConnector\Service\ObjectService; use OCA\OpenConnector\Service\SearchService; use OCA\OpenConnector\Db\Job; @@ -14,6 +15,10 @@ use OCP\BackgroundJob\IJobList; use OCA\OpenConnector\Db\JobLogMapper; use OCA\OpenConnector\Service\JobService; +use OCP\AppFramework\Db\DoesNotExistException; + +use OCA\OpenConnector\Service\SynchronizationService; +use OCA\OpenConnector\Db\SynchronizationMapper; class JobsController extends Controller { @@ -32,10 +37,12 @@ public function __construct( private JobLogMapper $jobLogMapper, private JobService $jobService, private IJobList $jobList, + private SynchronizationService $synchronizationService, + private SynchronizationMapper $synchronizationMapper ) { parent::__construct($appName, $request); - $this->IJobList = $jobList; + $this->jobList = $jobList; } /** @@ -219,13 +226,25 @@ public function run(int $id): JSONResponse { try { $job = $this->jobMapper->find(id: $id); - if (!$job->getJobListId()) { - return new JSONResponse(data: ['error' => 'Job not scheduled'], statusCode: 404); - } - $this->IJobList->getById($job->getJobListId())->start($this->IJobList); - return new JSONResponse($this->jobLogMapper->getLastCallLog()); } catch (DoesNotExistException $exception) { return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); } + + + if ($job->getJobListId() === false) { + return new JSONResponse(data: ['error' => 'Job not scheduled'], statusCode: 404); + } + + try { + $this->jobList->getById($job->getJobListId())->start($this->jobList); + $lastLog = $this->jobLogMapper->getLastCallLog(); + if ($lastLog !== null) { + return new JSONResponse(data: $lastLog, statusCode: 200); + } + + return new JSONResponse(data: ['error' => 'No job log could be found, job did not went succesfully or failed to log anything'], statusCode: 500); + } catch (Exception $exception) { + return new JSONResponse(data: ['error' => $exception->getMessage()], statusCode: 400); + } } } diff --git a/lib/Controller/SynchronizationsController.php b/lib/Controller/SynchronizationsController.php index 3dec88c..9ceb1e9 100644 --- a/lib/Controller/SynchronizationsController.php +++ b/lib/Controller/SynchronizationsController.php @@ -4,7 +4,7 @@ use OCA\OpenConnector\Service\ObjectService; use OCA\OpenConnector\Service\SearchService; -use OCA\OpenConnector\Db\Synchronization; +use OCA\OpenConnector\Service\SynchronizationService; use OCA\OpenConnector\Db\SynchronizationMapper; use OCA\OpenConnector\Db\SynchronizationContractMapper; use OCA\OpenConnector\Db\SynchronizationContractLogMapper; @@ -13,6 +13,8 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\IAppConfig; use OCP\IRequest; +use Exception; +use OCP\AppFramework\Db\DoesNotExistException; class SynchronizationsController extends Controller { @@ -29,16 +31,17 @@ public function __construct( private readonly IAppConfig $config, private readonly SynchronizationMapper $synchronizationMapper, private readonly SynchronizationContractMapper $synchronizationContractMapper, - private readonly SynchronizationContractLogMapper $synchronizationContractLogMapper + private readonly SynchronizationContractLogMapper $synchronizationContractLogMapper, + private readonly SynchronizationService $synchronizationService ) { - parent::__construct($appName, $request); + parent::__construct($appName, $request); } /** * Returns the template of the main app's page - * + * * This method renders the main page of the application, adding any necessary data to the template. * * @NoAdminRequired @@ -47,17 +50,17 @@ public function __construct( * @return TemplateResponse The rendered template response */ public function page(): TemplateResponse - { + { return new TemplateResponse( 'openconnector', 'index', [] ); } - + /** * Retrieves a list of all synchronizations - * + * * This method returns a JSON response containing an array of all synchronizations in the system. * * @NoAdminRequired @@ -79,7 +82,7 @@ public function index(ObjectService $objectService, SearchService $searchService /** * Retrieves a single synchronization by its ID - * + * * This method returns a JSON response containing the details of a specific synchronization. * * @NoAdminRequired @@ -99,7 +102,7 @@ public function show(string $id): JSONResponse /** * Creates a new synchronization - * + * * This method creates a new synchronization based on POST data. * * @NoAdminRequired @@ -116,17 +119,17 @@ public function create(): JSONResponse unset($data[$key]); } } - + if (isset($data['id'])) { unset($data['id']); } - + return new JSONResponse($this->synchronizationMapper->createFromArray(object: $data)); } /** * Updates an existing synchronization - * + * * This method updates an existing synchronization based on its ID. * * @NoAdminRequired @@ -152,7 +155,7 @@ public function update(int $id): JSONResponse /** * Deletes a synchronization - * + * * This method deletes a synchronization based on its ID. * * @NoAdminRequired @@ -182,7 +185,7 @@ public function destroy(int $id): JSONResponse public function contracts(int $id): JSONResponse { try { - $contracts = $this->synchronizationContractMapper->findAll($null, null, ['synchronization_id' => $id]); + $contracts = $this->synchronizationContractMapper->findAll(null, null, ['synchronization_id' => $id]); return new JSONResponse($contracts); } catch (DoesNotExistException $e) { return new JSONResponse(['error' => 'Contracts not found'], 404); @@ -199,7 +202,7 @@ public function contracts(int $id): JSONResponse * * @param int $id The ID of the source to retrieve logs for * @return JSONResponse A JSON response containing the call logs - */ + */ public function logs(int $id): JSONResponse { try { @@ -209,4 +212,55 @@ public function logs(int $id): JSONResponse return new JSONResponse(['error' => 'Logs not found'], 404); } } + + /** + * Tests a synchronization + * + * This method tests a synchronization without persisting anything to the database. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $id The ID of the synchronization + * + * @return JSONResponse A JSON response containing the test results + * + * @example + * Request: + * empty POST + * + * Response: + * { + * "resultObject": { + * "fullName": "John Doe", + * "userAge": 30, + * "contactEmail": "john@example.com" + * }, + * "isValid": true, + * "validationErrors": [] + * } + */ + public function test(int $id): JSONResponse + { + try { + $synchronization = $this->synchronizationMapper->find(id: $id); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); + } + + // Try to synchronize + try { + $logAndContractArray = $this->synchronizationService->synchronize(synchronization: $synchronization, isTest: true); + // Return the result as a JSON response + return new JSONResponse(data: $logAndContractArray, statusCode: 200); + } catch (Exception $e) { + // If synchronizaiton fails, return an error response + return new JSONResponse([ + 'error' => 'Synchronization error', + 'message' => $e->getMessage() + ], 400); + } + + return new JSONResponse($resultFromTest, 200); + } } \ No newline at end of file diff --git a/lib/Cron/ActionTask.php b/lib/Cron/ActionTask.php index a8e4a2a..a5b3a54 100644 --- a/lib/Cron/ActionTask.php +++ b/lib/Cron/ActionTask.php @@ -59,7 +59,7 @@ public function __construct( public function run($argument) { // if we do not have a job id then everything is wrong - if (isset($arguments['jobId']) && is_int($argument['jobId'])) { + if (isset($arguments['jobId']) === true && is_int($argument['jobId']) === true) { return; } @@ -71,12 +71,12 @@ public function run($argument) } // If the job is not enabled, we don't need to do anything - if (!$job->getIsEnabled()) { + if ($job->getIsEnabled() === false) { return; } // if the next run is in the the future, we don't need to do anything - if ($job->getNextRun() && $job->getNextRun() > new DateTime()) { + if ($job->getNextRun() !== null && $job->getNextRun() > new DateTime()) { return; } @@ -84,7 +84,7 @@ public function run($argument) $action = $this->containerInterface->get($job->getJobClass()); $arguments = $job->getArguments(); - if(!is_array($arguments)){ + if (is_array($arguments) === false) { $arguments = []; } $result = $action->run($arguments); @@ -93,7 +93,7 @@ public function run($argument) $executionTime = ( $time_end - $time_start ) * 1000; // deal with single run - if ($job->isSingleRun()) { + if ($job->isSingleRun() === true) { $job->setIsEnabled(false); } @@ -104,31 +104,29 @@ public function run($argument) $this->jobMapper->update($job); // Log the job - $jobLog = new JobLog(); - $jobLog->setUuid(Uuid::v4()); - $jobLog->setJobId($job->getId()); - $jobLog->setJobClass($job->getJobClass()); - $jobLog->setJobListId($job->getJobListId()); - $jobLog->setArguments($job->getArguments()); - $jobLog->setLastRun($job->getLastRun()); - $jobLog->setNextRun($job->getNextRun()); - $jobLog->setExecutionTime($executionTime); + $jobLog = $this->jobLogMapper->createFromArray([ + 'jobId' => $job->getId(), + 'jobClass' => $job->getJobClass(), + 'jobListId' => $job->getJobListId(), + 'arguments' => $job->getArguments(), + 'lastRun' => $job->getLastRun(), + 'nextRun' => $job->getNextRun(), + 'executionTime' => $executionTime + ]); // Get the result and set it to the job log - if (is_array($result)) { - if (isset($result['level'])) { + if (is_array($result) === true) { + if (isset($result['level']) === true) { $jobLog->setLevel($result['level']); } - if (isset($result['message'])) { + if (isset($result['message']) === true) { $jobLog->setMessage($result['message']); } - if (isset($result['stackTrace'])) { + if (isset($result['stackTrace']) === true) { $jobLog->setStackTrace($result['stackTrace']); } } - $this->jobLogMapper->insert($jobLog); - // Let's report back about what we have just done return $jobLog; } diff --git a/lib/Db/MappingMapper.php b/lib/Db/MappingMapper.php index 0e0e7c7..9b5871c 100644 --- a/lib/Db/MappingMapper.php +++ b/lib/Db/MappingMapper.php @@ -75,9 +75,13 @@ public function updateFromArray(int $id, array $object): Mapping $obj->hydrate($object); // Set or update the version - $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; - $obj->setVersion(implode('.', $version)); + if ($obj->getVersion() !== null) { + $version = explode('.', $obj->getVersion()); + if (isset($version[2]) === true) { + $version[2] = (int) $version[2] + 1; + $obj->setVersion(implode('.', $version)); + } + } return $this->update($obj); diff --git a/lib/Db/Synchronization.php b/lib/Db/Synchronization.php index 3b6a8aa..0460859 100644 --- a/lib/Db/Synchronization.php +++ b/lib/Db/Synchronization.php @@ -103,19 +103,19 @@ public function jsonSerialize(): array 'sourceHash' => $this->sourceHash, 'sourceTargetMapping' => $this->sourceTargetMapping, 'sourceConfig' => $this->sourceConfig, - 'sourceLastChanged' => $this->sourceLastChanged, - 'sourceLastChecked' => $this->sourceLastChecked, - 'sourceLastSynced' => $this->sourceLastSynced, + 'sourceLastChanged' => isset($this->sourceLastChanged) === true ? $this->sourceLastChanged->format('c') : null, + 'sourceLastChecked' => isset($this->sourceLastChecked) === true ? $this->sourceLastChecked->format('c') : null, + 'sourceLastSynced' => isset($this->sourceLastSynced) === true ? $this->sourceLastSynced->format('c') : null, 'targetId' => $this->targetId, 'targetType' => $this->targetType, 'targetHash' => $this->targetHash, 'targetSourceMapping' => $this->targetSourceMapping, 'targetConfig' => $this->targetConfig, - 'targetLastChanged' => $this->targetLastChanged, - 'targetLastChecked' => $this->targetLastChecked, - 'targetLastSynced' => $this->targetLastSynced, - 'created' => isset($this->created) ? $this->created->format('c') : null, - 'updated' => isset($this->updated) ? $this->updated->format('c') : null + 'targetLastChanged' => isset($this->targetLastChanged) === true ? $this->targetLastChanged->format('c') : null, + 'targetLastChecked' => isset($this->targetLastChecked) === true ? $this->targetLastChecked->format('c') : null, + 'targetLastSynced' => isset($this->targetLastSynced) === true ? $this->targetLastSynced->format('c') : null, + 'created' => isset($this->created) === true ? $this->created->format('c') : null, + 'updated' => isset($this->updated) === true ? $this->updated->format('c') : null ]; } } diff --git a/lib/Db/SynchronizationContract.php b/lib/Db/SynchronizationContract.php index b851298..168f818 100644 --- a/lib/Db/SynchronizationContract.php +++ b/lib/Db/SynchronizationContract.php @@ -13,12 +13,16 @@ */ class SynchronizationContract extends Entity implements JsonSerializable { + // @todo can be removed when migrations are merged + protected ?string $sourceId = null; // OLD The id of the object in the source + protected ?string $sourceHash = null; // OLD The hash of the object in the source + protected ?string $uuid = null; protected ?string $version = null; protected ?string $synchronizationId = null; // The synchronization that this contract belongs to // Source - protected ?string $sourceId = null; // The id of the object in the source - protected ?string $sourceHash = null; // The hash of the object in the source + protected ?string $originId = null; // The id of the object in the source + protected ?string $originHash = null; // The hash of the object in the source protected ?DateTime $sourceLastChanged = null; // The last changed date of the object in the source protected ?DateTime $sourceLastChecked = null; // The last checked date of the object in the source protected ?DateTime $sourceLastSynced = null; // The last synced date of the object in the source @@ -37,8 +41,8 @@ public function __construct() { $this->addType('uuid', 'string'); $this->addType('version', 'string'); $this->addType('synchronizationId', 'string'); - $this->addType('sourceId', 'string'); - $this->addType('sourceHash', 'string'); + $this->addType('originId', 'string'); + $this->addType('originHash', 'string'); $this->addType('sourceLastChanged', 'datetime'); $this->addType('sourceLastChecked', 'datetime'); $this->addType('sourceLastSynced', 'datetime'); @@ -49,6 +53,10 @@ public function __construct() { $this->addType('targetLastSynced', 'datetime'); $this->addType('created', 'datetime'); $this->addType('updated', 'datetime'); + + // @todo can be removed when migrations are merged + $this->addType('sourceId', 'string'); + $this->addType('sourceHash', 'string'); } public function getJsonFields(): array @@ -88,18 +96,21 @@ public function jsonSerialize(): array 'uuid' => $this->uuid, 'version' => $this->version, 'synchronizationId' => $this->synchronizationId, - 'sourceId' => $this->sourceId, - 'sourceHash' => $this->sourceHash, - 'sourceLastChanged' => $this->sourceLastChanged, - 'sourceLastChecked' => $this->sourceLastChecked, - 'sourceLastSynced' => $this->sourceLastSynced, + 'originId' => $this->originId, + 'originHash' => $this->originHash, + 'sourceLastChanged' => isset($this->sourceLastChanged) ? $this->sourceLastChanged->format('c') : null, + 'sourceLastChecked' => isset($this->sourceLastChecked) ? $this->sourceLastChecked->format('c') : null, + 'sourceLastSynced' => isset($this->sourceLastSynced) ? $this->sourceLastSynced->format('c') : null, 'targetId' => $this->targetId, 'targetHash' => $this->targetHash, - 'targetLastChanged' => $this->targetLastChanged, - 'targetLastChecked' => $this->targetLastChecked, - 'targetLastSynced' => $this->targetLastSynced, - 'created' => $this->created, - 'updated' => $this->updated + 'targetLastChanged' => isset($this->targetLastChanged) ? $this->targetLastChanged->format('c') : null, + 'targetLastChecked' => isset($this->targetLastChecked) ? $this->targetLastChecked->format('c') : null, + 'targetLastSynced' => isset($this->targetLastSynced) ? $this->targetLastSynced->format('c') : null, + 'created' => isset($this->created) ? $this->created->format('c') : null, + 'updated' => isset($this->updated) ? $this->updated->format('c') : null, + // @todo these 2 can be removed when migrations are merged + 'sourceId' => $this->sourceId, + 'sourceHash' => $this->sourceHash ]; } } diff --git a/lib/Db/SynchronizationContractLog.php b/lib/Db/SynchronizationContractLog.php index 6024eba..39df916 100644 --- a/lib/Db/SynchronizationContractLog.php +++ b/lib/Db/SynchronizationContractLog.php @@ -71,8 +71,8 @@ public function jsonSerialize(): array 'target' => $this->target, 'userId' => $this->userId, 'sessionId' => $this->sessionId, - 'expires' => $this->expires, - 'created' => $this->created, + 'expires' => isset($this->expires) ? $this->expires->format('c') : null, + 'created' => isset($this->created) ? $this->created->format('c') : null, ]; } } diff --git a/lib/Db/SynchronizationContractMapper.php b/lib/Db/SynchronizationContractMapper.php index 61ba47a..4549d7f 100644 --- a/lib/Db/SynchronizationContractMapper.php +++ b/lib/Db/SynchronizationContractMapper.php @@ -4,92 +4,152 @@ use OCA\OpenConnector\Db\SynchronizationContract; use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Db\QBMapper; +use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use Symfony\Component\Uid\Uuid; - +/** + * Mapper class for SynchronizationContract entities + * + * This class handles database operations for synchronization contracts including + * CRUD operations and specialized queries. + * + * @package OCA\OpenConnector\Db + * @extends QBMapper + * + * @psalm-suppress PropertyNotSetInConstructor + * @phpstan-extends QBMapper + */ class SynchronizationContractMapper extends QBMapper { - public function __construct(IDBConnection $db) - { - parent::__construct($db, 'openconnector_synchronization_contracts'); - } - - public function find(int $id): SynchronizationContract - { - $qb = $this->db->getQueryBuilder(); - - $qb->select('*') - ->from('openconnector_synchronization_contracts') - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) - ); - - return $this->findEntity(query: $qb); - } - - public function findOnSynchronizationIdSourceId(string $synchronizationId, string $sourceId): ?SynchronizationContract - { - $qb = $this->db->getQueryBuilder(); - - $qb->select('*') - ->from('openconnector_synchronization_contracts') - ->where( - $qb->expr()->eq('synchronization_id', $qb->createNamedParameter($synchronizationId)) - ) - ->andWhere( - $qb->expr()->eq('source_id', $qb->createNamedParameter($sourceId)) - ); - - try { - return $this->findEntity($qb); - } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { - return null; - } - } - - - public function findOnTarget(string $synchronization, string $targetId): SynchronizationContract|bool|null - { - $qb = $this->db->getQueryBuilder(); - - $qb->select('*') - ->from('openconnector_synchronization_contracts') - ->where( - $qb->expr()->eq('synchronization_id', $qb->createNamedParameter($synchronization)) - ) - ->andWhere( - $qb->expr()->eq('target_id', $qb->createNamedParameter($targetId)) - ); - - - try { - return $this->findEntity($qb); - } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { - return null; - } - } - - public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array - { - $qb = $this->db->getQueryBuilder(); - - $qb->select('*') - ->from('openconnector_synchronization_contracts') - ->setMaxResults($limit) - ->setFirstResult($offset); + /** + * Constructor for SynchronizationContractMapper + * + * @param IDBConnection $db Database connection instance + */ + public function __construct(IDBConnection $db) + { + parent::__construct($db, 'openconnector_synchronization_contracts'); + } + + /** + * Find a synchronization contract by ID + * + * @param int $id The ID of the contract to find + * @return SynchronizationContract The found contract entity + * @throws \OCP\AppFramework\Db\DoesNotExistException If contract not found + */ + public function find(int $id): SynchronizationContract + { + // Create query builder + $qb = $this->db->getQueryBuilder(); + + // Build select query with ID filter + $qb->select('*') + ->from('openconnector_synchronization_contracts') + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ); + + return $this->findEntity(query: $qb); + } + + /** + * Find a synchronization contract by synchronization ID and origin ID + * + * @param string $synchronizationId The synchronization ID + * @param string $originId The origin ID + * + * @return SynchronizationContract|null The found contract or null if not found + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function findSyncContractByOriginId(string $synchronizationId, string $originId): ?SynchronizationContract + { + // Create query builder + $qb = $this->db->getQueryBuilder(); + + // Build select query with synchronization and origin ID filters + $qb->select('*') + ->from('openconnector_synchronization_contracts') + ->where( + $qb->expr()->eq('synchronization_id', $qb->createNamedParameter($synchronizationId)) + ) + ->andWhere( + $qb->expr()->eq('origin_id', $qb->createNamedParameter($originId)) + ); + + try { + return $this->findEntity($qb); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return null; + } + } + + /** + * Find a synchronization contract by synchronization ID and target ID + * + * @param string $synchronization The synchronization ID + * @param string $targetId The target ID + * @return SynchronizationContract|bool|null The found contract, false, or null if not found + */ + public function findOnTarget(string $synchronization, string $targetId): SynchronizationContract|bool|null + { + // Create query builder + $qb = $this->db->getQueryBuilder(); + // Build select query with synchronization and target ID filters + $qb->select('*') + ->from('openconnector_synchronization_contracts') + ->where( + $qb->expr()->eq('synchronization_id', $qb->createNamedParameter($synchronization)) + ) + ->andWhere( + $qb->expr()->eq('target_id', $qb->createNamedParameter($targetId)) + ); + + try { + return $this->findEntity($qb); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return null; + } + } + + /** + * Find all synchronization contracts with optional filtering and pagination + * + * @param int|null $limit Maximum number of results to return + * @param int|null $offset Number of results to skip + * @param array|null $filters Associative array of field => value filters + * @param array|null $searchConditions Array of search conditions + * @param array|null $searchParams Array of search parameters + * @return array Array of found contracts + */ + public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array + { + // Create query builder + $qb = $this->db->getQueryBuilder(); + + // Build base select query with pagination + $qb->select('*') + ->from('openconnector_synchronization_contracts') + ->setMaxResults($limit) + ->setFirstResult($offset); + + // Add filters if provided foreach ($filters as $filter => $value) { - if ($value === 'IS NOT NULL') { - $qb->andWhere($qb->expr()->isNotNull($filter)); - } elseif ($value === 'IS NULL') { - $qb->andWhere($qb->expr()->isNull($filter)); - } else { - $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); - } + if ($value === 'IS NOT NULL') { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL') { + $qb->andWhere($qb->expr()->isNull($filter)); + } else { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } } + // Add search conditions if provided if (empty($searchConditions) === false) { $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); foreach ($searchParams as $param => $value) { @@ -97,86 +157,98 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters } } - return $this->findEntities(query: $qb); - } - - public function createFromArray(array $object): SynchronizationContract - { - $obj = new SynchronizationContract(); - $obj->hydrate(object: $object); - // Set uuid - if ($obj->getUuid() === null) { - $obj->setUuid(Uuid::v4()); - } - return $this->insert(entity: $synchronizationContract); - } - - public function updateFromArray(int $id, array $object): SynchronizationContract - { - $obj = $this->find($id); - $obj->hydrate($object); - - // Set or update the version - $version = explode('.', $obj->getVersion()); - $version[2] = (int)$version[2] + 1; - $obj->setVersion(implode('.', $version)); - - return $this->update($obj); - } + return $this->findEntities(query: $qb); + } - /** - * Find synchronization contracts by type and ID - * - * This method searches for synchronization contracts where either the source or target - * matches the given type and ID. - * - * @param string $type The type to search for (e.g., 'user', 'group', etc.) - * @param string $id The ID to search for within the given type - * @return array An array of SynchronizationContract entities matching the criteria - */ - public function findByTypeAndId(string $type, string $id): array - { - $qb = $this->db->getQueryBuilder(); - - // Build a query to select all columns from the synchronization contracts table - $qb->select('*') - ->from('openconnector_synchronization_contracts') - ->where( - $qb->expr()->orX( - // Check if the contract matches as a source - $qb->expr()->andX( - $qb->expr()->eq('source_type', $qb->createNamedParameter($type)), - $qb->expr()->eq('source_id', $qb->createNamedParameter($id)) - ), - // Check if the contract matches as a target - $qb->expr()->andX( - $qb->expr()->eq('target_type', $qb->createNamedParameter($type)), - $qb->expr()->eq('target_id', $qb->createNamedParameter($id)) - ) - ) - ); - - // Execute the query and return the resulting entities - return $this->findEntities($qb); - } + /** + * Create a new synchronization contract from array data + * + * @param array $object Array of contract data + * @return SynchronizationContract The created contract entity + */ + public function createFromArray(array $object): SynchronizationContract + { + // Create and hydrate new contract object + $obj = new SynchronizationContract(); + $obj->hydrate(object: $object); + + // Generate UUID if not provided + if ($obj->getUuid() === null) { + $obj->setUuid(Uuid::v4()); + } + + return $this->insert(entity: $obj); + } + + /** + * Update an existing synchronization contract from array data + * + * @param int $id ID of contract to update + * @param array $object Array of updated contract data + * @return SynchronizationContract The updated contract entity + */ + public function updateFromArray(int $id, array $object): SynchronizationContract + { + // Find and hydrate existing contract + $obj = $this->find($id); + $obj->hydrate($object); + + // Increment version number + $version = explode('.', $obj->getVersion()); + $version[2] = (int)$version[2] + 1; + $obj->setVersion(implode('.', $version)); + + return $this->update($obj); + } + + /** + * Find synchronization contracts by type and ID + * + * @param string $type The type to search for (e.g., 'user', 'group') + * @param string $id The ID to search for + * @return array Array of matching contracts + */ + public function findByTypeAndId(string $type, string $id): array + { + // Create query builder + $qb = $this->db->getQueryBuilder(); + + // Build query to find contracts matching type/id as either source or target + $qb->select('*') + ->from('openconnector_synchronization_contracts') + ->where( + $qb->expr()->orX( + $qb->expr()->andX( + $qb->expr()->eq('source_type', $qb->createNamedParameter($type)), + $qb->expr()->eq('origin_id', $qb->createNamedParameter($id)) + ), + $qb->expr()->andX( + $qb->expr()->eq('target_type', $qb->createNamedParameter($type)), + $qb->expr()->eq('target_id', $qb->createNamedParameter($id)) + ) + ) + ); + + return $this->findEntities($qb); + } /** - * Get the total count of all call logs. + * Get total count of synchronization contracts * - * @return int The total number of call logs in the database. + * @return int Total number of contracts */ public function getTotalCallCount(): int { + // Create query builder $qb = $this->db->getQueryBuilder(); - // Select count of all logs + // Build count query $qb->select($qb->createFunction('COUNT(*) as count')) ->from('openconnector_synchronization_contracts'); $result = $qb->execute(); $row = $result->fetch(); - // Return the total count return (int)$row['count']; } } diff --git a/lib/Migration/Version0Date20240826193657.php b/lib/Migration/Version0Date20240826193657.php index e9b4b8f..1306d7f 100644 --- a/lib/Migration/Version0Date20240826193657.php +++ b/lib/Migration/Version0Date20240826193657.php @@ -214,7 +214,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addIndex(['synchronization_id'], 'openconnector_sync_contracts_sync_index'); $table->addIndex(['source_id'], 'openconnector_sync_contracts_source_id_index'); $table->addIndex(['target_id'], 'openconnector_sync_contracts_target_id_index'); - $table->addIndex(['synchronization_id', 'source_id'], 'openconnector_sync_contracts_sync_source_index'); + $table->addIndex(['synchronization_id', 'source_id'], 'openconnector_sync_contracts_sync_origin_index'); $table->addIndex(['synchronization_id', 'target_id'], 'openconnector_sync_contracts_sync_target_index'); } diff --git a/lib/Migration/Version1Date20241111144800.php b/lib/Migration/Version1Date20241111144800.php new file mode 100644 index 0000000..9eb4651 --- /dev/null +++ b/lib/Migration/Version1Date20241111144800.php @@ -0,0 +1,117 @@ +connection = $connection; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** + * @var ISchemaWrapper $schema + */ + $schema = $schemaClosure(); + $table = $schema->getTable('openconnector_synchronization_contracts'); + + // Step 1: Add new columns for 'origin_id' and 'origin_hash' + if ($table->hasColumn('origin_id') === false) { + $table->addColumn('origin_id', Types::STRING, [ + 'length' => 255, + 'notnull' => true, + ]); + } + if ($table->hasColumn('origin_hash') === false) { + $table->addColumn('origin_hash', Types::STRING, [ + 'length' => 255, + 'notnull' => false, + ]); + } + + // Step 4: Adjust indexes in preparation for data migration + if ($table->hasIndex('openconnector_sync_contracts_source_id_index') === true) { + $table->dropIndex('openconnector_sync_contracts_source_id_index'); + } + if ($table->hasIndex('openconnector_sync_contracts_origin_id_index') === false) { + $table->addIndex(['origin_id'], 'openconnector_sync_contracts_origin_id_index'); + } + + if ($table->hasIndex('openconnector_sync_contracts_sync_source_index') === true) { + $table->dropIndex('openconnector_sync_contracts_sync_source_index'); + } + if ($table->hasIndex('openconnector_sync_contracts_sync_origin_index') === false) { + $table->addIndex(['synchronization_id', 'origin_id'], 'openconnector_sync_contracts_sync_origin_index'); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + /** + * @var ISchemaWrapper $schema + */ + $schema = $schemaClosure(); + $table = $schema->getTable('openconnector_synchronization_contracts'); + + // Step 2: Copy data from old columns to new columns + if ($table->hasColumn('origin_id') === true && $table->hasColumn('origin_hash') === true + && $table->hasColumn('source_id') === true && $table->hasColumn('source_hash') === true + ) { + $this->connection->executeQuery(" + UPDATE openconnector_synchronization_contracts + SET origin_id = source_id, origin_hash = source_hash + WHERE source_id IS NOT NULL + "); + } + + if ($table->hasColumn('source_id') === true) { + $table->dropColumn('source_id'); + } + if ($table->hasColumn('source_hash') === true) { + $table->dropColumn('source_hash'); + } + } +} diff --git a/lib/Service/CallService.php b/lib/Service/CallService.php index 025b8c2..4e7ca38 100644 --- a/lib/Service/CallService.php +++ b/lib/Service/CallService.php @@ -181,6 +181,11 @@ public function call( $config['headers'] = []; } + if (isset($config['pagination']) === true) { + $config['query'][$config['pagination']['paginationQuery']] = $config['pagination']['page']; + unset($config['pagination']); + } + // We want to surpress guzzle exceptions and return the response instead $config['http_errors'] = false; diff --git a/lib/Service/MappingService.php b/lib/Service/MappingService.php index 9e22256..8e61346 100644 --- a/lib/Service/MappingService.php +++ b/lib/Service/MappingService.php @@ -36,11 +36,11 @@ class MappingService */ public function __construct( ArrayLoader $loader, - MappingMapper $mappingMapper + private readonly MappingMapper $mappingMapper ) { $this->twig = new Environment($loader); $this->twig->addExtension(new MappingExtension()); - $this->twig->addRuntimeLoader(new MappingRuntimeLoader(mappingService: $this, mappingMapper: $mappingMapper)); + $this->twig->addRuntimeLoader(new MappingRuntimeLoader(mappingService: $this, mappingMapper: $this->mappingMapper)); }//end __construct() @@ -110,6 +110,7 @@ public function executeMapping(Mapping $mapping, array $input, bool $list = fals return $list; }//end if + $originalInput = $input; $input = $this->encodeArrayKeys($input, '.', '.'); // @todo: error logging @@ -135,7 +136,7 @@ public function executeMapping(Mapping $mapping, array $input, bool $list = fals } // Render the value from twig. - $dotArray->set($key, $this->twig->createTemplate($value)->render($input)); + $dotArray->set($key, $this->twig->createTemplate($value)->render($originalInput)); } // Unset unwanted key's. @@ -415,4 +416,41 @@ public function coordinateStringToArray(string $coordinates): array }//end coordinateStringToArray() + + /** + * Retrieves a single mapping by its ID. + * + * This is a wrapper function that provides controlled access to the mapping mapper. + * We use this wrapper pattern to ensure other Nextcloud apps can only interact with + * mappings through this service layer, rather than accessing the mapper directly. + * This maintains proper encapsulation and separation of concerns. + * + * @param string $mappingId The unique identifier of the mapping to retrieve + * @return Mapping The requested mapping entity + * @throws \OCP\AppFramework\Db\DoesNotExistException If mapping is not found + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException If multiple mappings found + */ + public function getMapping(string $mappingId): Mapping + { + // Forward the find request to the mapper while maintaining encapsulation + return $this->mappingMapper->find($mappingId); + } + + /** + * Retrieves all available mappings. + * + * This is a wrapper function that provides controlled access to the mapping mapper. + * We use this wrapper pattern to ensure other Nextcloud apps can only interact with + * mappings through this service layer, rather than accessing the mapper directly. + * This maintains proper encapsulation and separation of concerns. + * + * @return array An array containing all mapping entities + */ + public function getMappings(): array + { + // Forward the findAll request to the mapper while maintaining encapsulation + // @todo: add filtering options + return $this->mappingMapper->findAll(); + } + } diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php index b0b7d61..3acc7d7 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -3,9 +3,12 @@ namespace OCA\OpenConnector\Service; use Exception; +use GuzzleHttp\Exception\GuzzleException; use OCA\OpenConnector\Db\CallLog; +use OCA\OpenConnector\Db\Mapping; use OCA\OpenConnector\Db\Source; use OCA\OpenConnector\Db\SourceMapper; +use OCA\OpenConnector\Db\MappignMapper; use OCA\OpenConnector\Db\Synchronization; use OCA\OpenConnector\Db\SynchronizationMapper; use OCA\OpenConnector\Db\SynchronizationContract; @@ -14,12 +17,20 @@ use OCA\OpenConnector\Db\SynchronizationContractMapper; use OCA\OpenConnector\Service\CallService; use OCA\OpenConnector\Service\MappingService; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use Symfony\Component\Uid\Uuid; +use OCP\AppFramework\Db\DoesNotExistException; +use Adbar\Dot; use Psr\Container\ContainerInterface; use DateInterval; use DateTime; - +use OCA\OpenConnector\Db\MappingMapper; +use OCP\AppFramework\Http\NotFoundResponse; +use Twig\Error\LoaderError; +use Twig\Error\SyntaxError; class SynchronizationService { @@ -28,6 +39,7 @@ class SynchronizationService private ContainerInterface $containerInterface; private SynchronizationMapper $synchronizationMapper; private SourceMapper $sourceMapper; + private MappingMapper $mappingMapper; private SynchronizationContractMapper $synchronizationContractMapper; private SynchronizationContractLogMapper $synchronizationContractLogMapper; private ObjectService $objectService; @@ -39,6 +51,7 @@ public function __construct( MappingService $mappingService, ContainerInterface $containerInterface, SourceMapper $sourceMapper, + MappingMapper $mappingMapper, SynchronizationMapper $synchronizationMapper, SynchronizationContractMapper $synchronizationContractMapper, SynchronizationContractLogMapper $synchronizationContractLogMapper @@ -47,6 +60,7 @@ public function __construct( $this->mappingService = $mappingService; $this->containerInterface = $containerInterface; $this->synchronizationMapper = $synchronizationMapper; + $this->mappingMapper = $mappingMapper; $this->synchronizationContractMapper = $synchronizationContractMapper; $this->synchronizationContractLogMapper = $synchronizationContractLogMapper; $this->sourceMapper = $sourceMapper; @@ -56,70 +70,154 @@ public function __construct( * Synchronizes a given synchronization (or a complete source). * * @param Synchronization $synchronization + * @param bool|null $isTest False by default, currently added for synchronziation-test endpoint + * * @return array - * @throws Exception + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws GuzzleException + * @throws LoaderError + * @throws SyntaxError + * @throws MultipleObjectsReturnedException + * @throws \OCP\DB\Exception */ - public function synchronize(Synchronization $synchronization): array + public function synchronize(Synchronization $synchronization, ?bool $isTest = false): array { - $objectList = $this->getAllObjectsFromSource($synchronization); + + $objectList = $this->getAllObjectsFromSource(synchronization: $synchronization, isTest: $isTest); foreach ($objectList as $key => $object) { + // If the source configuration contains a dot notation for the id position, we need to extract the id from the source object + $originId = $this->getOriginId($synchronization, $object); + // Get the synchronization contract for this object - $synchronizationContract = $this->synchronizationContractMapper->findOnSynchronizationIdSourceId($synchronization->id, $object['id']); + $synchronizationContract = $this->synchronizationContractMapper->findSyncContractByOriginId(synchronizationId: $synchronization->id, originId: $originId); + + if ($synchronizationContract instanceof SynchronizationContract === false) { + // Only persist if not test + if ($isTest === false) { + $synchronizationContract = $this->synchronizationContractMapper->createFromArray([ + 'synchronizationId' => $synchronization->getId(), + 'originId' => $originId, + 'originHash' => md5(serialize($object)) + ]); + } else { + $synchronizationContract = new SynchronizationContract(); + $synchronizationContract->setSynchronizationId($synchronization->getId()); + $synchronizationContract->setOriginId($originId); + $synchronizationContract->setOriginHash(md5(serialize($object))); + } - if (!($synchronizationContract instanceof SynchronizationContract)) { - $synchronizationContract = new SynchronizationContract(); - $synchronizationContract->setUuid(Uuid::v4()); - $synchronizationContract->setSynchronizationId($synchronization->id); - $synchronizationContract->setSourceId($object['id']); - $synchronizationContract->setSourceHash(md5(serialize($object))); + $synchronizationContract = $this->synchronizeContract(synchronizationContract: $synchronizationContract, synchronization: $synchronization, object: $object, isTest: $isTest); - $synchronizationContract = $this->synchronizeContract($synchronizationContract, $synchronization, $object); - $objectList[$key] = $this->synchronizationContractMapper->insert($synchronizationContract); + if ($isTest === true && is_array($synchronizationContract) === true) { + // If this is a log and contract array return for the test endpoint. + $logAndContractArray = $synchronizationContract; + return $logAndContractArray; + } + } else { + // @todo this is wierd + $synchronizationContract = $this->synchronizeContract(synchronizationContract: $synchronizationContract, synchronization: $synchronization, object: $object, isTest: $isTest); + if ($isTest === false && $synchronizationContract instanceof SynchronizationContract === true) { + // If this is a regular synchronizationContract update it to the database. + $objectList[$key] = $this->synchronizationContractMapper->update(entity: $synchronizationContract); + } elseif ($isTest === true && is_array($synchronizationContract) === true) { + // If this is a log and contract array return for the test endpoint. + $logAndContractArray = $synchronizationContract; + return $logAndContractArray; + } } - else{ - // @todo this is wierd - $synchronizationContract = $this->synchronizeContract($synchronizationContract, $synchronization, $object); - $objectList[$key] = $this->synchronizationContractMapper->update($synchronizationContract); - } + + $this->synchronizationContractMapper->update($synchronizationContract); } return $objectList; } + /** + * Gets id from object as is in the origin + * + * @param Synchronization $synchronization + * @param array $object + * + * @return string|int id + * @throws Exception + */ + private function getOriginId(Synchronization $synchronization, array $object): int|string + { + // Default ID position is 'id' if not specified in source config + $originIdPosition = 'id'; + $sourceConfig = $synchronization->getSourceConfig(); + + // Check if a custom ID position is defined in the source configuration + if (isset($sourceConfig['idPosition']) === true) { + // Override default with custom ID position from config + $originIdPosition = $sourceConfig['idPosition']; + } + + // Create Dot object for easy access to nested array values + $objectDot = new Dot($object); + + // Try to get the ID value from the specified position in the object + $originId = $objectDot->get($originIdPosition); + + // If no ID was found at the specified position, throw an error + if ($originId === null) { + throw new Exception('Could not find origin id in object for key: ' . $originIdPosition); + } + + // Return the found ID value + return $originId; + } + /** * Synchronize a contract + * * @param SynchronizationContract $synchronizationContract * @param Synchronization|null $synchronization * @param array $object + * @param bool|null $isTest False by default, currently added for synchronization-test endpoint * - * @return SynchronizationContract - * @throws Exception + * @return SynchronizationContract|Exception|array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws LoaderError + * @throws SyntaxError */ - public function synchronizeContract(SynchronizationContract $synchronizationContract, Synchronization $synchronization = null, array $object = []) - { + public function synchronizeContract(SynchronizationContract $synchronizationContract, Synchronization $synchronization = null, array $object = [], ?bool $isTest = false): SynchronizationContract|Exception|array + { // Let create a source hash for the object - $sourceHash = md5(serialize($object)); + $originHash = md5(serialize($object)); $synchronizationContract->setSourceLastChecked(new DateTime()); // Let's prevent pointless updates @todo account for omnidirectional sync, unless the config has been updated since last check then we do want to rebuild and check if the tagert object has changed - if ($sourceHash === $synchronizationContract->getSourceHash() && $synchronization->getUpdated() < $synchronizationContract->getSourceLastChecked()) { + if ($originHash === $synchronizationContract->getOriginHash() && $synchronization->getUpdated() < $synchronizationContract->getSourceLastChecked()) { // The object has not changed and the config has not been updated since last check // return $synchronizationContract; // @todo: somehow this always returns true, so we never do the updateTarget } // The object has changed, oke let do mappig and bla die bla - $synchronizationContract->setSourceHash($sourceHash); + $synchronizationContract->setOriginHash($originHash); $synchronizationContract->setSourceLastChanged(new DateTime()); - // let do the mapping if provided - if ($synchronization->getSourceTargetMapping()){ - $targetObject = $this->mappingService->executeMapping($synchronization->getSourceTargetMapping(), $object); - } - else{ + // If no source target mapping is defined, use original object + if (empty($synchronization->getSourceTargetMapping()) === true) { $targetObject = $object; + } else { + try { + $sourceTargetMapping = $this->mappingMapper->find(id: $synchronization->getSourceTargetMapping()); + } catch (DoesNotExistException $exception) { + return new Exception($exception->getMessage()); + } + + // Execute mapping if found + if ($sourceTargetMapping) { + $targetObject = $this->mappingService->executeMapping(mapping: $sourceTargetMapping, input: $object); + } else { + $targetObject = $object; + } } @@ -130,37 +228,51 @@ public function synchronizeContract(SynchronizationContract $synchronizationCont $synchronizationContract->setTargetLastSynced(new DateTime()); $synchronizationContract->setSourceLastSynced(new DateTime()); - // Do the magic!! - - $this->updateTarget($synchronizationContract, $targetObject); - - // Log it - $log = new SynchronizationContractLog(); - $log->setUuid(Uuid::v4()); - $log->setSynchronizationId($synchronizationContract->getSynchronizationId()); - $log->setSynchronizationContractId($synchronizationContract->getId()); - $log->setSource($object); - $log->setTarget($targetObject); - $log->setExpires(new DateTime('+1 day')); // @todo make this configurable - $this->synchronizationContractLogMapper->insert($log); + // prepare log + $log = [ + 'synchronizationId' => $synchronizationContract->getSynchronizationId(), + 'synchronizationContractId' => $synchronizationContract->getId(), + 'source' => $object, + 'target' => $targetObject, + 'expires' => new DateTime('+1 day') + ]; + + // Handle synchronization based on test mode + if ($isTest === true) { + // Return test data without updating target + return [ + 'log' => $log, + 'contract' => $synchronizationContract->jsonSerialize() + ]; + } + + // Update target and create log when not in test mode + $synchronizationContract = $this->updateTarget( + synchronizationContract: $synchronizationContract, + targetObject: $targetObject + ); + + // Create log entry for the synchronization + $this->synchronizationContractLogMapper->createFromArray($log); return $synchronizationContract; } /** - * Write the data to the target + * Write the data to the target * * @param SynchronizationContract $synchronizationContract * @param array $targetObject * - * @return void - * @throws Exception + * @return SynchronizationContract + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function updateTarget(SynchronizationContract $synchronizationContract, array $targetObject): void + public function updateTarget(SynchronizationContract $synchronizationContract, array $targetObject): SynchronizationContract { // The function can be called solo set let's make sure we have the full synchronization object - if (!$synchronization){ + if (isset($synchronization) === false) { $synchronization = $this->synchronizationMapper->find($synchronizationContract->getSynchronizationId()); } @@ -172,19 +284,24 @@ public function updateTarget(SynchronizationContract $synchronizationContract, a $type = $synchronization->getTargetType(); - switch($type){ + switch ($type) { case 'register/schema': // Setup the object service $objectService = $this->containerInterface->get('OCA\OpenRegister\Service\ObjectService'); - // if we alreadey have an id, we need to get the object and update it - if ($synchronizationContract->getTargetId()){ + + // if we already have an id, we need to get the object and update it + if ($synchronizationContract->getTargetId() !== null) { $targetObject['id'] = $synchronizationContract->getTargetId(); } + // Extract register and schema from the targetId + // The targetId needs to be filled in as: {registerId} + / + {schemaId} for example: 1/1 $targetId = $synchronization->getTargetId(); list($register, $schema) = explode('/', $targetId); + // Save the object to the target $target = $objectService->saveObject($register, $schema, $targetObject); + // Get the id form the target object $synchronizationContract->setTargetId($target->getUuid()); break; @@ -198,105 +315,198 @@ public function updateTarget(SynchronizationContract $synchronizationContract, a default: throw new Exception("Unsupported target type: $type"); } + + return $synchronizationContract; } - /** - * Get all the object from a source - * - * @param SynchronizationContract $synchronizationContract - * @return void - */ - public function getAllObjectsFromSource(Synchronization $synchronization) - { + /** + * Get all the object from a source + * + * @param Synchronization $synchronization + * @param bool|null $isTest False by default, currently added for synchronziation-test endpoint + * + * @return array + * @throws ContainerExceptionInterface + * @throws GuzzleException + * @throws NotFoundExceptionInterface + * @throws \OCP\DB\Exception + */ + public function getAllObjectsFromSource(Synchronization $synchronization, ?bool $isTest = false): array + { $objects = []; $type = $synchronization->getSourceType(); - switch($type){ + switch ($type) { case 'register/schema': // Setup the object service $this->objectService = $this->containerInterface->get('OCA\OpenRegister\Service\ObjectService'); break; case 'api': - $objects = $this->getAllObjectsFromApi($synchronization); + $objects = $this->getAllObjectsFromApi(synchronization: $synchronization, isTest: $isTest); break; case 'database': //@todo: implement break; } + return $objects; } - /** - * Retrieves all objects from an API source for a given synchronization. - * - * @param Synchronization $synchronization The synchronization object containing source information. - * @return array An array of all objects retrieved from the API. - */ - public function getAllObjectsFromApi(Synchronization $synchronization) - { + + /** + * Retrieves all objects from an API source for a given synchronization. + * + * @param Synchronization $synchronization The synchronization object containing source information. + * @param bool $isTest If we only want to return a single object (for example a test) + * + * @return array An array of all objects retrieved from the API. + * @throws GuzzleException + * @throws \OCP\DB\Exception + */ + public function getAllObjectsFromApi(Synchronization $synchronization, ?bool $isTest = false): array + { $objects = []; - // Retrieve the source object based on the synchronization's source ID - $source = $this->sourceMapper->find($synchronization->getSourceId()); + $source = $this->sourceMapper->find(id: $synchronization->getSourceId()); + + // Lets get the source config + $sourceConfig = $synchronization->getSourceConfig(); + $endpoint = $sourceConfig['endpoint'] ?? ''; + $headers = $sourceConfig['headers'] ?? []; + $query = $sourceConfig['query'] ?? []; + $config = [ + 'headers' => $headers, + 'query' => $query, + ]; // Make the initial API call - $response = $this->callService->call($source)->getResponse(); + $response = $this->callService->call(source: $source, endpoint: $endpoint, method: 'GET', config: $config)->getResponse(); $body = json_decode($response['body'], true); - $objects = array_merge($objects, $this->getAllObjectsFromArray($body, $synchronization)); - $nextLink = $this->getNextlinkFromCall($body, $synchronization); - - // Continue making API calls if there are more pages of results - while ($nextLink !== null && $nextLink !== '' && $nextLink !== false) { - $endpoint = str_replace($source->getLocation(), '', $nextLink); - $response = $this->callService->call($source, $endpoint)->getResponse(); - $body = json_decode($response['body'], true); - $objects = array_merge($objects, $this->getAllObjectsFromArray($body, $synchronization)); - $nextLink = $this->getNextlinkFromCall($body, $synchronization); + $objects = array_merge($objects, $this->getAllObjectsFromArray(array: $body, synchronization: $synchronization)); + + // Return a single object or empty array if in test mode + if ($isTest === true) { + return [$objects[0]] ?? []; } + // Current page is 2 because the first call made above is page 1. + $currentPage = 2; + $usedNextEndpoint = false; + + + // Continue making API calls if there are more pages from 'next' the response body or if paginationQuery is set + while($nextEndpoint = $this->getNextEndpoint(body: $body, url: $source->getLocation(), sourceConfig: $sourceConfig, currentPage: $currentPage)) { + $usedNextEndpoint = true; + // Do not pass $config here becuase it overwrites the query attached to nextEndpoint + $response = $this->callService->call(source: $source, endpoint: $nextEndpoint)->getResponse(); + $body = json_decode($response['body'], true); + $objects = array_merge($objects, $this->getAllObjectsFromArray($body, $synchronization)); + } + + if ($usedNextEndpoint === false) { + do { + $config = $this->getNextPage(config: $config, sourceConfig: $sourceConfig, currentPage: $currentPage); + $response = $this->callService->call(source: $source, endpoint: $endpoint, method: 'GET', config: $config)->getResponse(); + $body = json_decode($response['body'], true); + + if (empty($body) === true) { + break; + } + + $newObjects = $this->getAllObjectsFromArray(array: $body, synchronization: $synchronization); + $objects = array_merge($objects, $newObjects); + $currentPage++; + } while (empty($newObjects) === false); + } + return $objects; } + /** + * Determines the next API endpoint based on a provided next. + * + * @param array $body + * @param string $url + * @param array $sourceConfig + * @param int $currentPage + * + * @return string|null The next endpoint URL if a next link or pagination query is available, or null if neither exists. + */ + private function getNextEndpoint(array $body, string $url, array $sourceConfig, int $currentPage): ?string + { + $nextLink = $this->getNextlinkFromCall($body); + + if ($nextLink !== null) { + return str_replace($url, '', $nextLink); + } + + return null; + } + + /** + * Updatesc config with pagination from pagination config. + * + * @param array $config + * @param array $sourceConfig + * @param int $currentPage The current page number for pagination, used if no next link is available. + * + * @return array $config + */ + private function getNextPage(array $config, array $sourceConfig, int $currentPage): array + { + // If paginationQuery exists, replace any placeholder with the current page number + $config['pagination'] = [ + 'paginationQuery' => $sourceConfig['paginationQuery'] ?? 'page', + 'page' => $currentPage + ]; + + return $config; + } + /** * Extracts all objects from the API response body. * * @param array $body The decoded JSON body of the API response. * @param Synchronization $synchronization The synchronization object containing source configuration. - * @return array An array of items extracted from the response body. + * * @throws Exception If the position of objects in the return body cannot be determined. + * + * @return array An array of items extracted from the response body. */ - public function getAllObjectsFromArray(array $array, Synchronization $synchronization) - { + public function getAllObjectsFromArray(array $array, Synchronization $synchronization): array + { // Get the source configuration from the synchronization object $sourceConfig = $synchronization->getSourceConfig(); // Check if a specific objects position is defined in the source configuration - if (isset($sourceConfig['objectsPosition'])) { + if (empty($sourceConfig['objectsPosition']) === false) { $position = $sourceConfig['objectsPosition']; // Use Dot notation to access nested array elements $dot = new Dot($array); - if ($dot->has($position)) { + if ($dot->has($position) === true) { // Return the objects at the specified position return $dot->get($position); } else { // Throw an exception if the specified position doesn't exist - throw new Exception("Cannot find the specified position of objects in the return body."); + + return []; + // @todo log error + // throw new Exception("Cannot find the specified position of objects in the return body."); } } - // Check for common keys where objects might be stored - // If 'items' key exists, return its value - if (isset($array['items'])) { - return $array['items']; - } + // Define common keys to check for objects + $commonKeys = ['items', 'result', 'results']; - // If 'result' key exists, return its value - if (isset($array['result'])) { - return $array['result']; + // Loop through common keys and return first match found + foreach ($commonKeys as $key) { + if (isset($array[$key]) === true) { + return $array[$key]; + } } // If 'results' key exists, return its value - if (isset($array['results'])) { + if (isset($array['results']) === true) { return $array['results']; } @@ -304,16 +514,16 @@ public function getAllObjectsFromArray(array $array, Synchronization $synchroniz throw new Exception("Cannot determine the position of objects in the return body."); } - /** - * Retrieves the next link for pagination from the API response body. - * - * @param array $body The decoded JSON body of the API response. - * @param Synchronization $synchronization The synchronization object (unused in this method, but kept for consistency). - * @return string|bool The URL for the next page of results, or false if there is no next page. - */ - public function getNextlinkFromCall(array $body, Synchronization $synchronization): string | bool | null + /** + * Retrieves the next link for pagination from the API response body. + * + * @param array $body The decoded JSON body of the API response. + * + * @return string|null The URL for the next page of results, or null if there is no next page. + */ + public function getNextlinkFromCall(array $body): ?string { // Check if the 'next' key exists in the response body - return $body['next']; + return $body['next'] ?? null; } } diff --git a/package-lock.json b/package-lock.json index 342ddf1..32efce0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@mdi/js": "^7.4.47", "@nextcloud/axios": "^2.5.0", "@nextcloud/dialogs": "^3.2.0", "@nextcloud/initial-state": "^2.2.0", @@ -3098,6 +3099,11 @@ "unist-util-is": "^3.0.0" } }, + "node_modules/@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==" + }, "node_modules/@nextcloud/auth": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.4.0.tgz", @@ -24332,6 +24338,11 @@ } } }, + "@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==" + }, "@nextcloud/auth": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.4.0.tgz", diff --git a/package.json b/package.json index 0dd1039..8c00479 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@mdi/js": "^7.4.47", "@nextcloud/axios": "^2.5.0", "@nextcloud/dialogs": "^3.2.0", "@nextcloud/initial-state": "^2.2.0", diff --git a/src/modals/Consumer/DeleteConsumer.vue b/src/modals/Consumer/DeleteConsumer.vue index 5523df1..ae7ca01 100644 --- a/src/modals/Consumer/DeleteConsumer.vue +++ b/src/modals/Consumer/DeleteConsumer.vue @@ -8,7 +8,7 @@ import { consumerStore, navigationStore } from '../../store/store.js' size="normal" :can-close="false">

- Do you want to delete {{ consumerStore.consumerItem.name }}? This action cannot be undone. + Do you want to delete {{ consumerStore.consumerItem?.name }}? This action cannot be undone.

diff --git a/src/modals/Log/ViewSynchronizationContract.vue b/src/modals/Log/ViewSynchronizationContract.vue new file mode 100644 index 0000000..7af566f --- /dev/null +++ b/src/modals/Log/ViewSynchronizationContract.vue @@ -0,0 +1,109 @@ + + + + + + diff --git a/src/modals/Log/ViewSynchronizationLog.vue b/src/modals/Log/ViewSynchronizationLog.vue new file mode 100644 index 0000000..48baa55 --- /dev/null +++ b/src/modals/Log/ViewSynchronizationLog.vue @@ -0,0 +1,133 @@ + + + + + + diff --git a/src/modals/MappingTest/components/TestMappingMappingSelect.vue b/src/modals/MappingTest/components/TestMappingMappingSelect.vue index 7f12fc0..9de68ca 100644 --- a/src/modals/MappingTest/components/TestMappingMappingSelect.vue +++ b/src/modals/MappingTest/components/TestMappingMappingSelect.vue @@ -140,8 +140,8 @@ import { mappingStore } from '../../../store/store.js' + :error="!validJson(mappingItem.cast, true)" + :helper-text="!validJson(mappingItem.cast, true) ? 'Invalid JSON' : ''" />
- @@ -418,7 +418,7 @@ export default { name: this.mappingItem.name, description: this.mappingItem.description, mapping: JSON.parse(this.mappingItem.mapping), - cast: JSON.parse(this.mappingItem.cast), + cast: this.mappingItem.cast ? JSON.parse(this.mappingItem.cast) : null, }, inputObject: JSON.parse(this.inputObject.value), schema: this.schemas.value?.id, @@ -491,7 +491,11 @@ export default { this.fetchSchemas() } }, - validJson(object) { + validJson(object, optional = false) { + if (optional && !object) { + return true + } + try { JSON.parse(object) return true diff --git a/src/modals/MappingTest/components/TestMappingResult.vue b/src/modals/MappingTest/components/TestMappingResult.vue index 1aaa698..be06719 100644 --- a/src/modals/MappingTest/components/TestMappingResult.vue +++ b/src/modals/MappingTest/components/TestMappingResult.vue @@ -15,9 +15,41 @@
{{ JSON.stringify(mappingTest.result, null, 2) }}
+					-->{{ JSON.stringify(mappingTest.result?.resultObject, null, 2) }}
 				
+ +
+

+ input object is valid +

+

+ input object is invalid +

+
+ +
+ + + + + + + + + + + + + +
FieldErrors
{{ field }} +
    +
  • + {{ error }} +
  • +
+
+
@@ -25,12 +57,16 @@ + + + + + diff --git a/src/store/modules/mapping.js b/src/store/modules/mapping.js index cd8ca30..18c11a4 100644 --- a/src/store/modules/mapping.js +++ b/src/store/modules/mapping.js @@ -147,6 +147,7 @@ export const useMappingStore = defineStore('mapping', { inputObject: mappingTestObject.inputObject, mapping: mappingTestObject.mapping, schema: mappingTestObject?.schema || null, + validation: !!mappingTestObject?.schema, } // assert that the data is an object diff --git a/src/store/modules/synchronization.js b/src/store/modules/synchronization.js index edbec97..75c966e 100644 --- a/src/store/modules/synchronization.js +++ b/src/store/modules/synchronization.js @@ -2,184 +2,206 @@ import { defineStore } from 'pinia' import { Synchronization } from '../../entities/index.js' -export const useSynchronizationStore = defineStore( - 'synchronization', { - state: () => ({ - synchronizationItem: false, - synchronizationList: [], - synchronizationContracts: [], - synchronizationLogs: [], - }), - actions: { - setSynchronizationItem(synchronizationItem) { - this.synchronizationItem = synchronizationItem && new Synchronization(synchronizationItem) - console.log('Active synchronization item set to ' + synchronizationItem) - }, - setSynchronizationList(synchronizationList) { - this.synchronizationList = synchronizationList.map( - (synchronizationItem) => new Synchronization(synchronizationItem), +export const useSynchronizationStore = defineStore('synchronization', { + state: () => ({ + synchronizationItem: false, + synchronizationList: [], + synchronizationContracts: [], + synchronizationTest: null, + synchronizationLogs: [], + }), + actions: { + setSynchronizationItem(synchronizationItem) { + this.synchronizationItem = synchronizationItem && new Synchronization(synchronizationItem) + console.log('Active synchronization item set to ' + synchronizationItem) + }, + setSynchronizationList(synchronizationList) { + this.synchronizationList = synchronizationList.map( + (synchronizationItem) => new Synchronization(synchronizationItem), + ) + console.log('Synchronization list set to ' + synchronizationList.length + ' items') + }, + setSynchronizationContracts(synchronizationContracts) { + this.synchronizationContracts = synchronizationContracts + console.log('Synchronization contracts set to ' + synchronizationContracts?.length + ' items') + }, + setSynchronizationLogs(synchronizationLogs) { + this.synchronizationLogs = synchronizationLogs + + console.log('Synchronization logs set to ' + synchronizationLogs?.length + ' items') + }, + + /* istanbul ignore next */ // ignore this for Jest until moved into a service + async refreshSynchronizationList(search = null) { + // @todo this might belong in a service? + let endpoint = '/index.php/apps/openconnector/api/synchronizations' + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } + return fetch(endpoint, { + method: 'GET', + }) + .then( + (response) => { + response.json().then( + (data) => { + this.setSynchronizationList(data.results) + }, + ) + }, + ) + .catch( + (err) => { + console.error(err) + }, + ) + }, + /* istanbul ignore next */ // ignore this for Jest until moved into a service + async refreshSynchronizationContracts(search = null) { + // @todo this might belong in a service? + let endpoint = `/index.php/apps/openconnector/api/synchronizations-contracts/${this.synchronizationItem.id}` + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } + return fetch(endpoint, { + method: 'GET', + }) + .then( + (response) => { + response.json().then( + (data) => { + this.setSynchronizationContracts(data) + }, + ) + }, ) - console.log('Synchronization list set to ' + synchronizationList.length + ' items') - }, - setSynchronizationContracts(synchronizationContracts) { - this.synchronizationContracts = synchronizationContracts.map( - (synchronizationContract) => new SynchronizationContract(synchronizationContract), + .catch( + (err) => { + console.error(err) + }, + ) + }, + /* istanbul ignore next */ // ignore this for Jest until moved into a service + async refreshSynchronizationLogs(search = null) { + // @todo this might belong in a service? + let endpoint = `/index.php/apps/openconnector/api/synchronizations-logs/${this.synchronizationItem.id}` + if (search !== null && search !== '') { + endpoint = endpoint + '?_search=' + search + } + return fetch(endpoint, { + method: 'GET', + }) + .then( + (response) => { + response.json().then( + (data) => { + this.setSynchronizationLogs(data) + }, + ) + }, ) - console.log('Synchronization contracts set to ' + synchronizationContracts.length + ' items') - }, - setSynchronizationLogs(synchronizationLogs) { - this.synchronizationLogs = synchronizationLogs.map( - (synchronizationLog) => new SynchronizationLog(synchronizationLog), + .catch( + (err) => { + console.error(err) + }, ) - console.log('Synchronization logs set to ' + synchronizationLogs.length + ' items') - }, - /* istanbul ignore next */ // ignore this for Jest until moved into a service - async refreshSynchronizationList(search = null) { - // @todo this might belong in a service? - let endpoint = '/index.php/apps/openconnector/api/synchronizations' - if (search !== null && search !== '') { - endpoint = endpoint + '?_search=' + search - } - return fetch(endpoint, { + }, + // New function to get a single synchronization + async getSynchronization(id) { + const endpoint = `/index.php/apps/openconnector/api/synchronizations/${id}` + try { + const response = await fetch(endpoint, { method: 'GET', }) - .then( - (response) => { - response.json().then( - (data) => { - this.setSynchronizationList(data.results) - }, - ) - }, - ) - .catch( - (err) => { - console.error(err) - }, - ) - }, - /* istanbul ignore next */ // ignore this for Jest until moved into a service - async refreshSynchronizationContracts(search = null) { - // @todo this might belong in a service? - let endpoint = `/index.php/apps/openconnector/api/synchronizations-contracts/${this.synchronizationItem.id}` - if (search !== null && search !== '') { - endpoint = endpoint + '?_search=' + search - } - return fetch(endpoint, { - method: 'GET', + const data = await response.json() + this.setSynchronizationItem(data) + return data + } catch (err) { + console.error(err) + throw err + } + }, + // Delete a synchronization + deleteSynchronization() { + if (!this.synchronizationItem || !this.synchronizationItem.id) { + throw new Error('No synchronization item to delete') + } + + console.log('Deleting synchronization...') + + const endpoint = `/index.php/apps/openconnector/api/synchronizations/${this.synchronizationItem.id}` + + return fetch(endpoint, { + method: 'DELETE', + }) + .then((response) => { + this.refreshSynchronizationList() }) - .then( - (response) => { - response.json().then( - (data) => { - this.setSynchronizationContracts(data.results) - }, - ) - }, - ) - .catch( - (err) => { - console.error(err) - }, - ) - }, - /* istanbul ignore next */ // ignore this for Jest until moved into a service - async refreshSynchronizationLogs(search = null) { - // @todo this might belong in a service? - let endpoint = `/index.php/apps/openconnector/api/synchronizations-logs/${this.synchronizationItem.id}` - if (search !== null && search !== '') { - endpoint = endpoint + '?_search=' + search - } - return fetch(endpoint, { - method: 'GET', + .catch((err) => { + console.error('Error deleting synchronization:', err) + throw err }) - .then( - (response) => { - response.json().then( - (data) => { - this.setSynchronizationLogs(data.results) - }, - ) - }, - ) - .catch( - (err) => { - console.error(err) - }, - ) - }, - // New function to get a single synchronization - async getSynchronization(id) { - const endpoint = `/index.php/apps/openconnector/api/synchronizations/${id}` - try { - const response = await fetch(endpoint, { - method: 'GET', - }) - const data = await response.json() + }, + // Create or save a synchronization from store + saveSynchronization(synchronizationItem) { + if (!synchronizationItem) { + throw new Error('No synchronization item to save') + } + + console.log('Saving synchronization...') + + const isNewSynchronization = !synchronizationItem?.id + const endpoint = isNewSynchronization + ? '/index.php/apps/openconnector/api/synchronizations' + : `/index.php/apps/openconnector/api/synchronizations/${synchronizationItem.id}` + const method = isNewSynchronization ? 'POST' : 'PUT' + + return fetch( + endpoint, + { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(synchronizationItem), + }, + ) + .then((response) => response.json()) + .then((data) => { this.setSynchronizationItem(data) - return data - } catch (err) { - console.error(err) + console.log('Synchronization saved') + + this.refreshSynchronizationList() + }) + .catch((err) => { + console.error('Error saving synchronization:', err) throw err - } - }, - // Delete a synchronization - deleteSynchronization() { - if (!this.synchronizationItem || !this.synchronizationItem.id) { - throw new Error('No synchronization item to delete') - } + }) + }, + // Test a synchronization + async testSynchronization() { + if (!this.synchronizationItem) { + throw new Error('No synchronization item to test') + } - console.log('Deleting synchronization...') + console.log('Testing synchronization...') - const endpoint = `/index.php/apps/openconnector/api/synchronizations/${this.synchronizationItem.id}` + const endpoint = `/index.php/apps/openconnector/api/synchronizations-test/${this.synchronizationItem.id}` - return fetch(endpoint, { - method: 'DELETE', - }) - .then((response) => { - this.refreshSynchronizationList() - }) - .catch((err) => { - console.error('Error deleting synchronization:', err) - throw err - }) - }, - // Create or save a synchronization from store - saveSynchronization(synchronizationItem) { - if (!synchronizationItem) { - throw new Error('No synchronization item to save') - } - - console.log('Saving synchronization...') - - const isNewSynchronization = !synchronizationItem?.id - const endpoint = isNewSynchronization - ? '/index.php/apps/openconnector/api/synchronizations' - : `/index.php/apps/openconnector/api/synchronizations/${synchronizationItem.id}` - const method = isNewSynchronization ? 'POST' : 'PUT' - - return fetch( - endpoint, - { - method, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(synchronizationItem), - }, - ) - .then((response) => response.json()) - .then((data) => { - this.setSynchronizationItem(data) - console.log('Synchronization saved') - - this.refreshSynchronizationList() - }) - .catch((err) => { - console.error('Error saving synchronization:', err) - throw err - }) - }, + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }) + + const data = await response.json() + this.synchronizationTest = data + + console.log('Synchronization tested') + this.refreshSynchronizationLogs() + + return { response, data } }, }, -) +}) diff --git a/src/views/Synchronization/SynchronizationDetails.vue b/src/views/Synchronization/SynchronizationDetails.vue index 477d35c..c2920f2 100644 --- a/src/views/Synchronization/SynchronizationDetails.vue +++ b/src/views/Synchronization/SynchronizationDetails.vue @@ -1,5 +1,5 @@ Edit + + + Test + @@ -163,7 +163,7 @@ import { synchronizationStore, navigationStore } from '../../store/store.js'
+
@@ -194,6 +202,8 @@ import Pencil from 'vue-material-design-icons/Pencil.vue' import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue' import FileCertificateOutline from 'vue-material-design-icons/FileCertificateOutline.vue' import TimelineQuestionOutline from 'vue-material-design-icons/TimelineQuestionOutline.vue' +import Sync from 'vue-material-design-icons/Sync.vue' +import EyeOutline from 'vue-material-design-icons/EyeOutline.vue' export default { name: 'SynchronizationDetails', @@ -204,15 +214,25 @@ export default { Pencil, TrashCanOutline, }, - mounted() { - synchronizationStore.refreshSynchronizationLogs() - synchronizationStore.refreshSynchronizationContracts() - }, data() { return { contracts: [], } }, + mounted() { + synchronizationStore.refreshSynchronizationLogs() + synchronizationStore.refreshSynchronizationContracts() + }, + methods: { + viewLog(log) { + logStore.setViewLogItem(log) + navigationStore.setModal('viewSynchronizationLog') + }, + viewContract(contract) { + logStore.setViewLogItem(contract) + navigationStore.setModal('viewSynchronizationContract') + }, + }, }