diff --git a/appinfo/routes.php b/appinfo/routes.php index c735c636d..a07197c6d 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -9,5 +9,9 @@ ], 'routes' => [ ['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'], + ['name' => 'sources#test', 'url' => '/api/source-test/{id}', 'verb' => 'POST'], + ['name' => 'sources#logs', 'url' => '/api/sources-logs/{id}', 'verb' => 'GET'], + ['name' => 'jobs#run', 'url' => '/api/jobs-test/{id}', 'verb' => 'POST'], + ['name' => 'jobs#logs', 'url' => '/api/jobs-logs/{id}', 'verb' => 'GET'], ], ]; diff --git a/composer.json b/composer.json index 0190ed046..94c7cf279 100644 --- a/composer.json +++ b/composer.json @@ -30,12 +30,12 @@ }, "require": { "php": "^8.1", - "adbario/php-dot-notation": "^3.3.0", + "adbario/php-dot-notation": "^3.3", "bamarni/composer-bin-plugin": "^1.8", "elasticsearch/elasticsearch": "^v8.14.0", - "adbario/php-dot-notation": "^3.3.0", "guzzlehttp/guzzle": "^7.0", - "symfony/uid": "^6.4" + "symfony/uid": "^6.4", + "twig/twig": "^3.14" }, "require-dev": { "nextcloud/ocp": "dev-stable29", diff --git a/composer.lock b/composer.lock index 89a3ceaec..6ad031621 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1bc131410d3a7e5c8dd57cfb5ba73af6", + "content-hash": "ec854a22d6d44bfbdb801251ce3fb5f8", "packages": [ { "name": "adbario/php-dot-notation", @@ -1195,6 +1195,165 @@ ], "time": "2024-04-18T09:32:20+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, { "name": "symfony/polyfill-php80", "version": "v1.30.0", @@ -1579,6 +1738,85 @@ } ], "time": "2024-05-31T14:49:08+00:00" + }, + { + "name": "twig/twig", + "version": "v3.14.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72", + "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72", + "shasum": "" + }, + "require": { + "php": ">=8.0.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php81": "^1.29" + }, + "require-dev": { + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.14.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-09-09T17:55:12+00:00" } ], "packages-dev": [ diff --git a/docker-compose.yml b/docker-compose.yml index c9781ff93..bf406b9dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,9 +18,17 @@ services: - MYSQL_DATABASE=nextcloud - MYSQL_USER=nextcloud + # init-ubuntu: + # image: ubuntu + # command: sh /home/ubuntu/docker/init-ubuntu.sh + # volumes: + # - ./docker:/home/ubuntu/docker + # - .:/home/ubuntu/app + nextcloud: user: root container_name: nextcloud +# entrypoint: occ app:enable openconnector image: nextcloud restart: always ports: @@ -29,14 +37,13 @@ services: - db volumes: - nextcloud:/var/www/html:rw - - ./custom-apps:/var/www/html/custom_apps + - ./custom_apps:/var/www/html/custom_apps - .:/var/www/html/custom_apps/openconnector - environment: - MYSQL_PASSWORD='!ChangeMe!' - MYSQL_DATABASE=nextcloud - MYSQL_USER=nextcloud - MYSQL_HOST=db + - TZ=Europe/Amsterdam - NEXTCLOUD_ADMIN_USER=admin - NEXTCLOUD_ADMIN_PASSWORD=admin - - TZ=Europe/Amsterdam diff --git a/lib/Action/EventAction.php b/lib/Action/EventAction.php new file mode 100644 index 000000000..f5fa4a4ca --- /dev/null +++ b/lib/Action/EventAction.php @@ -0,0 +1,33 @@ +callService = $callService; + } + + //@todo: make this a bit more generic :') + public function run($argument) + { + // @todo: implement this + + // Lets report back about what we have just done + return; + } + +} diff --git a/lib/Action/PingAction.php b/lib/Action/PingAction.php new file mode 100644 index 000000000..e18e175f5 --- /dev/null +++ b/lib/Action/PingAction.php @@ -0,0 +1,41 @@ +callService = $callService; + } + + //@todo: make this a bit more generic :') + public function run($argument) + { + // For now we only have one action, so this is a bit overkill, but it's a good starting point + if (isset($arguments['sourceId']) && is_int($argument['sourceId'])) { + $source = $this->sourceMapper->find($argument['sourceId']); + $this->callService->call($source); + } + else { + $source = $this->sourceMapper->find(1); + $this->callService->call($source); + } + + // Lets report back about what we have just done + return; + } + +} diff --git a/lib/Action/SynchronizationAction.php b/lib/Action/SynchronizationAction.php new file mode 100644 index 000000000..6932ff00c --- /dev/null +++ b/lib/Action/SynchronizationAction.php @@ -0,0 +1,56 @@ +callService = $callService; + $this->synchronizationMapper = $synchronizationMapper; + $this->synchronizationContractMapper = $synchronizationContractMapper; + } + + //@todo: make this a bit more generic :') + public function run($argument) + { + + + // if we do not have a synchronization Id then everything is wrong + if (isset($arguments['synchronizationId']) && is_int($argument['synchronizationId'])) { + // @todo: implement error handling + return; + } + + // We are going to allow for a single synchronization contract to be processed at a time + if (isset($arguments['synchronizationContractId']) && is_int($argument['synchronizationContractId'])) { + $synchronizationContract = $this->synchronizationContractMapper->find($argument['synchronizationContractId']); + + return; + } + + // oke lets synchronyse a source, why not + $synchronization = $this->synchronizationMapper->find($argument['synchronizationId']); + + // @todo: implement this + + // Lets report back about what we have just done + return; + } + +} diff --git a/lib/Controller/JobsController.php b/lib/Controller/JobsController.php index 4b4e2ef27..0e814c542 100644 --- a/lib/Controller/JobsController.php +++ b/lib/Controller/JobsController.php @@ -11,6 +11,9 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\IAppConfig; use OCP\IRequest; +use OCP\BackgroundJob\IJobList; +use OCA\OpenConnector\Db\JobLogMapper; +use OCA\OpenConnector\Service\JobService; class JobsController extends Controller { @@ -25,7 +28,10 @@ public function __construct( $appName, IRequest $request, private readonly IAppConfig $config, - private readonly JobMapper $jobMapper + private readonly JobMapper $jobMapper, + private readonly JobLogMapper $jobLogMapper, + private readonly JobService $jobService, + private readonly IJobList $jobList ) { parent::__construct($appName, $request); @@ -115,8 +121,13 @@ public function create(): JSONResponse if (isset($data['id'])) { unset($data['id']); } + + // Create the job + $job = $this->jobMapper->createFromArray(object: $data); + // Lets schedule the job + $job = $this->jobService->scheduleJob($job); - return new JSONResponse($this->jobMapper->createFromArray(object: $data)); + return new JSONResponse($job); } /** @@ -142,7 +153,13 @@ public function update(int $id): JSONResponse if (isset($data['id'])) { unset($data['id']); } - return new JSONResponse($this->jobMapper->updateFromArray(id: (int) $id, object: $data)); + + // Create the job + $job = $this->jobMapper->updateFromArray(id: (int) $id, object: $data); + // Lets schedule the job + $job = $this->jobService->scheduleJob($job); + + return new JSONResponse($job); } /** @@ -162,4 +179,52 @@ public function destroy(int $id): JSONResponse return new JSONResponse([]); } -} \ No newline at end of file + + /** + * Retrieves call logs for a source + * + * This method returns all the call logs associated with a source based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @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 { + $job = $this->jobMapper->find($id); + $jobLogs = $this->jobLogMapper->findAll(null, null, ['job_id' => $job->getId()]); + return new JSONResponse($jobLogs); + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Job not found'], 404); + } + } + /** + * Test a source + * + * This method fires a test call to the source and returns the response. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * Endpoint: /api/job-run/{id} + * + * @param int $id The ID of the job to test + * @return JSONResponse A JSON response containing the test results + */ + public function run(int $id): JSONResponse + { + try { + $job = $this->jobMapper->find(id: (int) $id); + if (!$job->getJobListId()) { + return new JSONResponse(data: ['error' => 'Job not scheduled'], statusCode: 404); + } + $log = $this->jobService->getById($job->getJobListId())->start(); + return new JSONResponse($log); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); + } + } +} diff --git a/lib/Controller/SourcesController.php b/lib/Controller/SourcesController.php index b464f6e72..ba757392d 100644 --- a/lib/Controller/SourcesController.php +++ b/lib/Controller/SourcesController.php @@ -4,8 +4,10 @@ use OCA\OpenConnector\Service\ObjectService; use OCA\OpenConnector\Service\SearchService; +use OCA\OpenConnector\Service\CallService; use OCA\OpenConnector\Db\Source; use OCA\OpenConnector\Db\SourceMapper; +use OCA\OpenConnector\Db\CallLogMapper; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; @@ -25,7 +27,8 @@ public function __construct( $appName, IRequest $request, private readonly IAppConfig $config, - private readonly SourceMapper $sourceMapper + private readonly SourceMapper $sourceMapper, + private readonly CallLogMapper $callLogMapper ) { parent::__construct($appName, $request); @@ -162,4 +165,106 @@ public function destroy(int $id): JSONResponse return new JSONResponse([]); } + + /** + * Retrieves call logs for a source + * + * This method returns all the call logs associated with a source based on its ID. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @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 { + $source = $this->sourceMapper->find($id); + $callLogs = $this->callLogMapper->findAll(null, null, ['source_id' => $source->getId()]); + return new JSONResponse($callLogs); + } catch (DoesNotExistException $e) { + return new JSONResponse(['error' => 'Source not found'], 404); + } + } + + /** + * Test a source + * + * This method fires a test call to the source and returns the response. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * Endpoint: /api/source-test/{id} + * Properties: + * query: (expected key-value array) + * headers: (expected key-value array) + * method: (string, one of POST, GET, PUT, DELETE) -> defaults to POST + * endpoint: (string) can be empty + * type: (string, one of: json, xml, yaml) + * body: (string) + * + * @param int $id The ID of the source to test + * @return JSONResponse A JSON response containing the test results + */ + public function test(CallService $callService,int $id): JSONResponse + { + // get the source + try { + $source = $this->sourceMapper->find(id: (int) $id); + } catch (DoesNotExistException $exception) { + return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); + } + + // Get the request data + $requestData = $this->request->getParams(); + + // Build Guzzle call configuration array + $config = []; + + // Add headers if present + if (isset($requestData['headers']) && is_array($requestData['headers'])) { + $config['headers'] = $requestData['headers']; + } + + // Add query parameters if present + if (isset($requestData['query']) && is_array($requestData['query'])) { + $config['query'] = $requestData['query']; + } + + // Set method, default to POST if not provided + $method = $requestData['method'] ?? 'GET'; + + // Set endpoint + $endpoint = $requestData['endpoint'] ?? ''; + + // Set body if present + if (isset($requestData['body'])) { + $config['body'] = $requestData['body']; + } + + // Set content type based on the type parameter + if (isset($requestData['type'])) { + switch ($requestData['type']) { + case 'json': + $config['headers']['Content-Type'] = 'application/json'; + break; + case 'xml': + $config['headers']['Content-Type'] = 'application/xml'; + break; + case 'yaml': + $config['headers']['Content-Type'] = 'application/x-yaml'; + break; + } + } + + // fire the call + + $time_start = microtime(true); + $callLog = $callService->call($source, $endpoint, $method, $config); + $time_end = microtime(true); + + return new JSONResponse($callLog->jsonSerialize()); + } } \ No newline at end of file diff --git a/lib/Cron/ActionTask.php b/lib/Cron/ActionTask.php new file mode 100644 index 000000000..96fdba98b --- /dev/null +++ b/lib/Cron/ActionTask.php @@ -0,0 +1,107 @@ +jobMapper = $jobMapper; + $this->jobLogMapper = $jobLogMapper; + $this->jobList = $jobList; + $this->containerInterface = $containerInterface; + // Run every 5 minutes + //$this->setInterval(300); + + // Delay until low-load time + //$this->setTimeSensitivity(\OCP\BackgroundJob\IJob::TIME_SENSITIVE); + // Or $this->setTimeSensitivity(\OCP\BackgroundJob\IJob::TIME_INSENSITIVE); + + // Only run one instance of this job at a time + //$this->setAllowParallelRuns(false); + } + + //@todo: make this a bit more generic :') + public function run($argument) + { + // if we do not have a job id then everything is wrong + if (isset($arguments['jobId']) && is_int($argument['jobId'])) { + return; + } + + // lets get the job, the user might have deleted it in the mean time + try { + $job = $this->jobMapper->find($argument['jobId']); + } catch (Exception $e) { + return; + } + + // If the job is not enabled, we don't need to do anything + if (!$job->getIsEnabled()) { + return; + } + + // if the next run is in the the future, we don't need to do anything + if ($job->getNextRun() && $job->getNextRun() > $this->time->getTime()) { + return; + } + + $time_start = microtime(true); + + $action = $this->containerInterface->get($job->getClass()); + $action->run($job->getArguments()); + + $time_end = microtime(true); + $executionTime = ( $time_end - $time_start ) * 1000; + + // deal with single run + if ($job->isSingleRun()) { + $job->setIsEnabled(false); + } + + + // Update the job + //$job->setLastRun($this->time->getTime()); + //$job->setNextRun($this->time->getTime() + $job->getInterval()); + //$this->jobMapper->update($job); + + // Log the job + $jobLog = new JobLog(); + $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); + $this->jobLogMapper->insert($jobLog); + + // Lets report back about what we have just done + return; + } + +} diff --git a/lib/Cron/LogCleanUpTask.php b/lib/Cron/LogCleanUpTask.php new file mode 100644 index 000000000..0af048ad8 --- /dev/null +++ b/lib/Cron/LogCleanUpTask.php @@ -0,0 +1,15 @@ +addType('statusCode', 'integer'); + $this->addType('statusMessage', 'string'); + $this->addType('request', 'json'); + $this->addType('response', 'json'); + $this->addType('sourceId', 'integer'); + $this->addType('actionId', 'integer'); + $this->addType('synchronizationId', 'integer'); + $this->addType('createdAt', 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); + } + + public function hydrate(array $object): self + { + $jsonFields = $this->getJsonFields(); + + foreach($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = []; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + // Handle or log the exception if needed + } + } + + return $this; + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'statusCode' => $this->statusCode, + 'statusMessage' => $this->statusMessage, + 'request' => $this->request, + 'response' => $this->response, + 'sourceId' => $this->sourceId, + 'actionId' => $this->actionId, + 'synchronizationId' => $this->synchronizationId, + 'createdAt' => $this->createdAt, + ]; + } +} \ No newline at end of file diff --git a/lib/Db/CallLogMapper.php b/lib/Db/CallLogMapper.php new file mode 100644 index 000000000..2fdd9656b --- /dev/null +++ b/lib/Db/CallLogMapper.php @@ -0,0 +1,79 @@ +db->getQueryBuilder(); + + $qb->select('*') + ->from('openconnector_call_logs') + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ); + + return $this->findEntity($qb); + } + + public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openconnector_call_logs') + ->setMaxResults($limit) + ->setFirstResult($offset); + + 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 (!empty($searchConditions)) { + $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); + foreach ($searchParams as $param => $value) { + $qb->setParameter($param, $value); + } + } + + return $this->findEntities($qb); + } + + public function createFromArray(array $object): CallLog + { + $callLog = new CallLog(); + $callLog->hydrate($object); + return $this->insert($callLog); + } + + public function updateFromArray(int $id, array $object): CallLog + { + $callLog = $this->find($id); + $callLog->hydrate($object); + + return $this->update($callLog); + } + + public function clearLogs(): Bool + { + // @todo: find expired logs and delete them + + return true; + } +} \ No newline at end of file diff --git a/lib/Db/Job.php b/lib/Db/Job.php index c75a7ba8d..3b7fdbba7 100644 --- a/lib/Db/Job.php +++ b/lib/Db/Job.php @@ -10,54 +10,44 @@ class Job extends Entity implements JsonSerializable { protected ?string $name = null; protected ?string $description = null; - protected ?string $reference = null; - protected ?string $version = null; - protected ?string $crontab = null; - protected ?string $userId = null; - protected ?string $throws = null; - protected ?array $data = null; - protected ?DateTime $lastRun = null; - protected ?DateTime $nextRun = null; - protected ?bool $isEnabled = null; - protected ?DateTime $dateCreated = null; - protected ?DateTime $dateModified = null; - protected ?array $listens = null; - protected ?array $conditions = null; - protected ?string $class = null; - protected ?int $priority = null; - protected ?bool $async = null; - protected ?array $configuration = null; - protected ?bool $isLockable = null; - protected ?bool $locked = null; - protected ?int $lastRunTime = null; - protected ?bool $status = null; - protected ?array $actionHandlerConfiguration = null; + protected ?string $jobClass = 'OCA\OpenConnector\Action\PingAction'; + protected ?array $arguments = null; + protected ?int $interval = 3600; // seconds in an hour + protected ?int $executionTime = 3600; // maximum execution time in seconds + protected ?bool $timeSensitive = true; // if the job is time sensitive and should be executed even if the server is under heavy load + protected ?bool $allowParallelRuns = false; // if the job can be executed in parallel + protected ?bool $isEnabled = true; // if the job is enabled + protected ?bool $singleRun = false; // if set, the job will only run once and then disable itself + protected ?DateTime $scheduleAfter = null; // if the job should be executed after a certain date and time + protected ?string $userId = null; // the user which the job is running for security reasons + protected ?string $jobListId = null; // the id of the job in the job list + protected ?int $logRetention = 3600; // seconds to save all logs + protected ?int $errorRetention = 86400; // seconds to save error logs + protected ?DateTime $lastRun = null; // the last time the job was run + protected ?DateTime $nextRun = null; // the next time the job will be run + protected ?DateTime $created = null; // the date and time the job was created + protected ?DateTime $updated = null; // the date and time the job was updated public function __construct() { $this->addType('name', 'string'); $this->addType('description', 'string'); - $this->addType('reference', 'string'); - $this->addType('version', 'string'); - $this->addType('crontab', 'string'); + $this->addType('jobClass', 'string'); + $this->addType('arguments', 'json'); + $this->addType('interval', 'integer'); + $this->addType('executionTime', 'integer'); + $this->addType('timeSensitive', 'boolean'); + $this->addType('allowParallelRuns', 'boolean'); + $this->addType('isEnabled', 'boolean'); + $this->addType('singleRun', 'boolean'); + $this->addType('scheduleAfter', 'datetime'); $this->addType('userId', 'string'); - $this->addType('throws', 'string'); - $this->addType('data', 'json'); + $this->addType('jobListId', 'string'); + $this->addType('logRetention', 'integer'); + $this->addType('errorRetention', 'integer'); $this->addType('lastRun', 'datetime'); $this->addType('nextRun', 'datetime'); - $this->addType('isEnabled', 'boolean'); - $this->addType('dateCreated', 'datetime'); - $this->addType('dateModified', 'datetime'); - $this->addType('listens', 'json'); - $this->addType('conditions', 'json'); - $this->addType('class', 'string'); - $this->addType('priority', 'integer'); - $this->addType('async', 'boolean'); - $this->addType('configuration', 'json'); - $this->addType('isLockable', 'boolean'); - $this->addType('locked', 'boolean'); - $this->addType('lastRunTime', 'integer'); - $this->addType('status', 'boolean'); - $this->addType('actionHandlerConfiguration', 'json'); + $this->addType('created', 'datetime'); + $this->addType('updated', 'datetime'); } public function getJsonFields(): array @@ -96,28 +86,23 @@ public function jsonSerialize(): array 'id' => $this->id, 'name' => $this->name, 'description' => $this->description, - 'reference' => $this->reference, - 'version' => $this->version, - 'crontab' => $this->crontab, + 'jobClass' => $this->jobClass, + 'arguments' => $this->arguments, + 'interval' => $this->interval, + 'executionTime' => $this->executionTime, + 'timeSensitive' => $this->timeSensitive, + 'allowParallelRuns' => $this->allowParallelRuns, + 'isEnabled' => $this->isEnabled, + 'singleRun' => $this->singleRun, + 'scheduleAfter' => $this->scheduleAfter, 'userId' => $this->userId, - 'throws' => $this->throws, - 'data' => $this->data, + 'jobListId' => $this->jobListId, + 'logRetention' => $this->logRetention, + 'errorRetention' => $this->errorRetention, 'lastRun' => $this->lastRun, 'nextRun' => $this->nextRun, - 'isEnabled' => $this->isEnabled, - 'dateCreated' => $this->dateCreated, - 'dateModified' => $this->dateModified, - 'listens' => $this->listens, - 'conditions' => $this->conditions, - 'class' => $this->class, - 'priority' => $this->priority, - 'async' => $this->async, - 'configuration' => $this->configuration, - 'isLockable' => $this->isLockable, - 'locked' => $this->locked, - 'lastRunTime' => $this->lastRunTime, - 'status' => $this->status, - 'actionHandlerConfiguration' => $this->actionHandlerConfiguration + 'created' => $this->created, + 'updated' => $this->updated, ]; } } \ No newline at end of file diff --git a/lib/Db/JobLog.php b/lib/Db/JobLog.php new file mode 100644 index 000000000..7730c5bc9 --- /dev/null +++ b/lib/Db/JobLog.php @@ -0,0 +1,78 @@ +addType('jobId', 'string'); + $this->addType('jobListId', 'string'); + $this->addType('jobClass', 'string'); + $this->addType('arguments', 'json'); + $this->addType('executionTime', 'integer'); + $this->addType('userId', 'string'); + $this->addType('lastRun', 'datetime'); + $this->addType('nextRun', 'datetime'); + $this->addType('created', 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); + } + + public function hydrate(array $object): self + { + $jsonFields = $this->getJsonFields(); + + foreach($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = []; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + // Handle or log the exception if needed + } + } + + return $this; + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'jobId' => $this->jobId, + 'jobListId' => $this->jobListId, + 'jobClass' => $this->jobClass, + 'arguments' => $this->arguments, + 'executionTime' => $this->executionTime, + 'userId' => $this->userId, + 'lastRun' => $this->lastRun, + 'nextRun' => $this->nextRun, + 'created' => $this->created, + ]; + } +} \ No newline at end of file diff --git a/lib/Db/JobLogMapper.php b/lib/Db/JobLogMapper.php new file mode 100644 index 000000000..29a99b64e --- /dev/null +++ b/lib/Db/JobLogMapper.php @@ -0,0 +1,72 @@ +db->getQueryBuilder(); + + $qb->select('*') + ->from('openconnector_job_logs') + ->where( + $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) + ); + + return $this->findEntity($qb); + } + + public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openconnector_job_logs') + ->setMaxResults($limit) + ->setFirstResult($offset); + + 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 (!empty($searchConditions)) { + $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); + foreach ($searchParams as $param => $value) { + $qb->setParameter($param, $value); + } + } + + return $this->findEntities($qb); + } + + public function createFromArray(array $object): JobLog + { + $jobLog = new JobLog(); + $jobLog->hydrate($object); + return $this->insert($jobLog); + } + + public function updateFromArray(int $id, array $object): JobLog + { + $jobLog = $this->find($id); + $jobLog->hydrate($object); + + return $this->update($jobLog); + } +} \ No newline at end of file diff --git a/lib/Db/Log.php b/lib/Db/Log.php deleted file mode 100644 index ada5b8779..000000000 --- a/lib/Db/Log.php +++ /dev/null @@ -1,129 +0,0 @@ -addType('type', 'string'); - $this->addType('callId', 'string'); - $this->addType('requestMethod', 'string'); - $this->addType('requestHeaders', 'json'); - $this->addType('requestQuery', 'json'); - $this->addType('requestPathInfo', 'string'); - $this->addType('requestLanguages', 'json'); - $this->addType('requestServer', 'json'); - $this->addType('requestContent', 'string'); - $this->addType('responseStatus', 'string'); - $this->addType('responseStatusCode', 'integer'); - $this->addType('responseHeaders', 'json'); - $this->addType('responseContent', 'string'); - $this->addType('userId', 'string'); - $this->addType('session', 'string'); - $this->addType('sessionValues', 'json'); - $this->addType('responseTime', 'integer'); - $this->addType('routeName', 'string'); - $this->addType('routeParameters', 'json'); - $this->addType('entity', 'string'); - $this->addType('endpoint', 'string'); - $this->addType('gateway', 'string'); - $this->addType('handler', 'string'); - $this->addType('objectId', 'string'); - $this->addType('dateCreated', 'datetime'); - $this->addType('dateModified', 'datetime'); - } - - public function getJsonFields(): array - { - return array_keys( - array_filter($this->getFieldTypes(), function ($field) { - return $field === 'json'; - }) - ); - } - - public function hydrate(array $object): self - { - $jsonFields = $this->getJsonFields(); - - foreach($object as $key => $value) { - if (in_array($key, $jsonFields) === true && $value === []) { - $value = []; - } - - $method = 'set'.ucfirst($key); - - try { - $this->$method($value); - } catch (\Exception $exception) { -// ("Error writing $key"); - } - } - - return $this; - } - - public function jsonSerialize(): array - { - return [ - 'id' => $this->id, - 'type' => $this->type, - 'callId' => $this->callId, - 'requestMethod' => $this->requestMethod, - 'requestHeaders' => $this->requestHeaders, - 'requestQuery' => $this->requestQuery, - 'requestPathInfo' => $this->requestPathInfo, - 'requestLanguages' => $this->requestLanguages, - 'requestServer' => $this->requestServer, - 'requestContent' => $this->requestContent, - 'responseStatus' => $this->responseStatus, - 'responseStatusCode' => $this->responseStatusCode, - 'responseHeaders' => $this->responseHeaders, - 'responseContent' => $this->responseContent, - 'userId' => $this->userId, - 'session' => $this->session, - 'sessionValues' => $this->sessionValues, - 'responseTime' => $this->responseTime, - 'routeName' => $this->routeName, - 'routeParameters' => $this->routeParameters, - 'entity' => $this->entity, - 'endpoint' => $this->endpoint, - 'gateway' => $this->gateway, - 'handler' => $this->handler, - 'objectId' => $this->objectId, - 'dateCreated' => $this->dateCreated, - 'dateModified' => $this->dateModified - ]; - } -} \ No newline at end of file diff --git a/lib/Db/LogMapper.php b/lib/Db/LogMapper.php deleted file mode 100644 index 4e3143d64..000000000 --- a/lib/Db/LogMapper.php +++ /dev/null @@ -1,74 +0,0 @@ -db->getQueryBuilder(); - - $qb->select('*') - ->from('openconnector_logs') - ->where( - $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) - ); - - return $this->findEntity(query: $qb); - } - - public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array - { - $qb = $this->db->getQueryBuilder(); - - $qb->select('*') - ->from('openconnector_logs') - ->setMaxResults($limit) - ->setFirstResult($offset); - - 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 (!empty($searchConditions)) { - $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); - foreach ($searchParams as $param => $value) { - $qb->setParameter($param, $value); - } - } - - return $this->findEntities(query: $qb); - } - - public function createFromArray(array $object): Log - { - $log = new Log(); - $log->hydrate(object: $object); - return $this->insert(entity: $log); - } - - public function updateFromArray(int $id, array $object): Log - { - $log = $this->find($id); - $log->hydrate($object); - - return $this->update($log); - } -} diff --git a/lib/Db/Source.php b/lib/Db/Source.php index 22182d082..77bc7f98b 100644 --- a/lib/Db/Source.php +++ b/lib/Db/Source.php @@ -36,6 +36,8 @@ class Source extends Entity implements JsonSerializable protected ?array $configuration = null; protected ?array $endpointsConfig = null; protected ?string $status = null; + protected ?int $logRetention = 3600; // seconds to save all logs + protected ?int $errorRetention = 86400; // seconds to save error logs protected ?DateTime $lastCall = null; protected ?DateTime $lastSync = null; protected ?int $objectCount = null; @@ -72,6 +74,8 @@ public function __construct() { $this->addType('configuration', 'json'); $this->addType('endpointsConfig', 'json'); $this->addType('status', 'string'); + $this->addType('logRetention', 'integer'); + $this->addType('errorRetention', 'integer'); $this->addType('lastCall', 'datetime'); $this->addType('lastSync', 'datetime'); $this->addType('objectCount', 'integer'); @@ -142,6 +146,8 @@ public function jsonSerialize(): array 'configuration' => $this->configuration, 'endpointsConfig' => $this->endpointsConfig, 'status' => $this->status, + 'logRetention' => $this->logRetention, + 'errorRetention' => $this->errorRetention, 'lastCall' => $this->lastCall, 'lastSync' => $this->lastSync, 'objectCount' => $this->objectCount, diff --git a/lib/Db/Synchronization.php b/lib/Db/Synchronization.php index 5fa46d465..e2751a531 100644 --- a/lib/Db/Synchronization.php +++ b/lib/Db/Synchronization.php @@ -8,44 +8,52 @@ class Synchronization extends Entity implements JsonSerializable { - protected ?string $entity = null; - protected ?string $object = null; - protected ?string $action = null; - protected ?string $gateway = null; - protected ?string $sourceObject = null; - protected ?string $endpoint = null; - protected ?string $sourceId = null; - protected ?string $hash = null; - protected ?string $sha = null; - protected ?bool $blocked = null; - protected ?DateTime $sourceLastChanged = null; - protected ?DateTime $lastChecked = null; - protected ?DateTime $lastSynced = null; - protected ?DateTime $dateCreated = null; - protected ?DateTime $dateModified = null; - protected ?int $tryCounter = null; - protected ?DateTime $dontSyncBefore = null; - protected ?array $mapping = null; + protected ?string $name = null; // The name of the synchronization + protected ?string $description = null; // The description of the synchronization + // Source + protected ?string $sourceId = null; // The id of the source object + protected ?string $sourceType = null; // The type of the source object (e.g. api, database, register/schema.) + protected ?string $sourceHash = null; // The hash of the source object when it was last synced. + protected ?string $sourceTargetMapping = null; // The mapping of the source object to the target object + protected ?array $sourceConfig = null; // The configuration of the object in the source + protected ?DateTime $sourceLastChanged = null; // The last changed date of the source object + protected ?DateTime $sourceLastChecked = null; // The last checked date of the source object + protected ?DateTime $sourceLastSynced = null; // The last synced date of the source object + // Target + protected ?string $targetId = null; // The id of the target object + protected ?string $targetType = null; // The type of the target object (e.g. api, database, register/schema.) + protected ?string $targetHash = null; // The hash of the target object + protected ?string $targetSourceMapping = null; // The mapping of the target object to the source object + protected ?array $targetConfig = null; // The configuration of the object in the target + protected ?DateTime $targetLastChanged = null; // The last changed date of the target object + protected ?DateTime $targetLastChecked = null; // The last checked date of the target object + protected ?DateTime $targetLastSynced = null; // The last synced date of the target object + // General + protected ?DateTime $created = null; // The date and time the synchronization was created + protected ?DateTime $updated = null; // The date and time the synchronization was updated + public function __construct() { - $this->addType('entity', 'string'); - $this->addType('object', 'string'); - $this->addType('action', 'string'); - $this->addType('gateway', 'string'); - $this->addType('sourceObject', 'string'); - $this->addType('endpoint', 'string'); + $this->addType('name', 'string'); + $this->addType('description', 'string'); $this->addType('sourceId', 'string'); - $this->addType('hash', 'string'); - $this->addType('sha', 'string'); - $this->addType('blocked', 'boolean'); + $this->addType('sourceType', 'string'); + $this->addType('sourceHash', 'string'); + $this->addType('sourceTargetMapping', 'string'); + $this->addType('sourceConfig', 'json'); $this->addType('sourceLastChanged', 'datetime'); - $this->addType('lastChecked', 'datetime'); - $this->addType('lastSynced', 'datetime'); - $this->addType('dateCreated', 'datetime'); - $this->addType('dateModified', 'datetime'); - $this->addType('tryCounter', 'integer'); - $this->addType('dontSyncBefore', 'datetime'); - $this->addType('mapping', 'json'); + $this->addType('sourceLastChecked', 'datetime'); + $this->addType('sourceLastSynced', 'datetime'); + $this->addType('targetId', 'string'); + $this->addType('targetType', 'string'); + $this->addType('targetHash', 'string'); + $this->addType('targetSourceMapping', 'string'); + $this->addType('targetConfig', 'json'); + $this->addType('targetLastChanged', 'datetime'); + $this->addType('targetLastChecked', 'datetime'); + $this->addType('targetLastSynced', 'datetime'); + $this->addType('created', 'datetime'); + $this->addType('updated', 'datetime'); } public function getJsonFields(): array @@ -71,7 +79,7 @@ public function hydrate(array $object): self try { $this->$method($value); } catch (\Exception $exception) { -// ("Error writing $key"); + // Error handling could be improved here } } @@ -82,24 +90,26 @@ public function jsonSerialize(): array { return [ 'id' => $this->id, - 'entity' => $this->entity, - 'object' => $this->object, - 'action' => $this->action, - 'gateway' => $this->gateway, - 'sourceObject' => $this->sourceObject, - 'endpoint' => $this->endpoint, + 'name' => $this->name, + 'description' => $this->description, 'sourceId' => $this->sourceId, - 'hash' => $this->hash, - 'sha' => $this->sha, - 'blocked' => $this->blocked, + 'sourceType' => $this->sourceType, + 'sourceHash' => $this->sourceHash, + 'sourceTargetMapping' => $this->sourceTargetMapping, + 'sourceConfig' => $this->sourceConfig, 'sourceLastChanged' => $this->sourceLastChanged, - 'lastChecked' => $this->lastChecked, - 'lastSynced' => $this->lastSynced, - 'dateCreated' => $this->dateCreated, - 'dateModified' => $this->dateModified, - 'tryCounter' => $this->tryCounter, - 'dontSyncBefore' => $this->dontSyncBefore, - 'mapping' => $this->mapping + 'sourceLastChecked' => $this->sourceLastChecked, + 'sourceLastSynced' => $this->sourceLastSynced, + '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' => $this->created, + 'updated' => $this->updated ]; } } \ No newline at end of file diff --git a/lib/Db/SynchronizationContract.php b/lib/Db/SynchronizationContract.php new file mode 100644 index 000000000..ddb56ab2f --- /dev/null +++ b/lib/Db/SynchronizationContract.php @@ -0,0 +1,105 @@ +addType('name', 'string'); + $this->addType('description', 'string'); + $this->addType('synchronization', 'string'); + $this->addType('sourceId', 'string'); + $this->addType('sourceHash', 'string'); + $this->addType('sourceLastChanged', 'datetime'); + $this->addType('sourceLastChecked', 'datetime'); + $this->addType('sourceLastSynced', 'datetime'); + $this->addType('targetId', 'string'); + $this->addType('targetHash', 'string'); + $this->addType('targetLastChanged', 'datetime'); + $this->addType('targetLastChecked', 'datetime'); + $this->addType('targetLastSynced', 'datetime'); + $this->addType('created', 'datetime'); + $this->addType('updated', 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); + } + + public function hydrate(array $object): self + { + $jsonFields = $this->getJsonFields(); + + foreach($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = []; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + // Error handling could be improved here + } + } + + return $this; + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'description' => $this->description, + 'synchronization' => $this->synchronization, + 'sourceId' => $this->sourceId, + 'sourceHash' => $this->sourceHash, + 'sourceLastChanged' => $this->sourceLastChanged, + 'sourceLastChecked' => $this->sourceLastChecked, + 'sourceLastSynced' => $this->sourceLastSynced, + 'targetId' => $this->targetId, + 'targetHash' => $this->targetHash, + 'targetLastChanged' => $this->targetLastChanged, + 'targetLastChecked' => $this->targetLastChecked, + 'targetLastSynced' => $this->targetLastSynced, + 'created' => $this->created, + 'updated' => $this->updated + ]; + } +} \ No newline at end of file diff --git a/lib/Db/SynchronizationContractLog.php b/lib/Db/SynchronizationContractLog.php new file mode 100644 index 000000000..9d8b8aac1 --- /dev/null +++ b/lib/Db/SynchronizationContractLog.php @@ -0,0 +1,78 @@ +addType('jobId', 'string'); + $this->addType('jobListId', 'string'); + $this->addType('jobClass', 'string'); + $this->addType('arguments', 'json'); + $this->addType('executionTime', 'integer'); + $this->addType('userId', 'string'); + $this->addType('lastRun', 'datetime'); + $this->addType('nextRun', 'datetime'); + $this->addType('created', 'datetime'); + } + + public function getJsonFields(): array + { + return array_keys( + array_filter($this->getFieldTypes(), function ($field) { + return $field === 'json'; + }) + ); + } + + public function hydrate(array $object): self + { + $jsonFields = $this->getJsonFields(); + + foreach($object as $key => $value) { + if (in_array($key, $jsonFields) === true && $value === []) { + $value = []; + } + + $method = 'set'.ucfirst($key); + + try { + $this->$method($value); + } catch (\Exception $exception) { + // Handle or log the exception if needed + } + } + + return $this; + } + + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'jobId' => $this->jobId, + 'jobListId' => $this->jobListId, + 'jobClass' => $this->jobClass, + 'arguments' => $this->arguments, + 'executionTime' => $this->executionTime, + 'userId' => $this->userId, + 'lastRun' => $this->lastRun, + 'nextRun' => $this->nextRun, + 'created' => $this->created, + ]; + } +} \ No newline at end of file diff --git a/lib/Db/SynchronizationContractMapper.php b/lib/Db/SynchronizationContractMapper.php new file mode 100644 index 000000000..879954343 --- /dev/null +++ b/lib/Db/SynchronizationContractMapper.php @@ -0,0 +1,108 @@ +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 findOnSource(string $target, string $sourceId): SynchronizationContract|bool + { + $qb = $this->db->getQueryBuilder(); + + $qb->select('*') + ->from('openconnector_synchronization_contracts') + ->where( + $qb->expr()->eq('synchronization_id', $qb->createNamedParameter($synchronization)) + ) + ->andWhere( + $qb->expr()->eq('source_id', $qb->createNamedParameter($sourceId)) + ); + + return $this->findEntity(query: $qb); + } + + + public function findOnTarget(string $synchronization, string $targetId): SynchronizationContract|bool + { + $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)) + ); + + return $this->findEntity(query: $qb); + } + + + 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); + + 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 (!empty($searchConditions)) { + $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); + foreach ($searchParams as $param => $value) { + $qb->setParameter($param, $value); + } + } + + return $this->findEntities(query: $qb); + } + + public function createFromArray(array $object): SynchronizationContract + { + $synchronizationContract = new SynchronizationContract(); + $synchronizationContract->hydrate(object: $object); + return $this->insert(entity: $synchronizationContract); + } + + public function updateFromArray(int $id, array $object): SynchronizationContract + { + $synchronizationContract = $this->find($id); + $synchronizationContract->hydrate($object); + + return $this->update($synchronizationContract); + } +} diff --git a/lib/Migration/Version0Date20240826193657.php b/lib/Migration/Version0Date20240826193657.php index cf6e160af..e7570f1f4 100644 --- a/lib/Migration/Version0Date20240826193657.php +++ b/lib/Migration/Version0Date20240826193657.php @@ -46,60 +46,23 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'length' => 20]); $table->addColumn('name', Types::STRING, ['notnull' => true, 'length' => 255]); $table->addColumn('description', Types::TEXT, ['notnull' => false]); - $table->addColumn('reference', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('version', Types::STRING, ['notnull' => false, 'length' => 50]); - $table->addColumn('crontab', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('job_class', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('arguments', Types::TEXT, ['notnull' => false]); + $table->addColumn('interval', Types::INTEGER, ['notnull' => true, 'default' => 3600]); + $table->addColumn('execution_time', Types::INTEGER, ['notnull' => true, 'default' => 3600]); + $table->addColumn('time_sensitive', Types::BOOLEAN, ['notnull' => true, 'default' => true]); + $table->addColumn('allow_parallel_runs', Types::BOOLEAN, ['notnull' => true, 'default' => false]); + $table->addColumn('is_enabled', Types::BOOLEAN, ['notnull' => true, 'default' => true]); + $table->addColumn('single_run', Types::BOOLEAN, ['notnull' => true, 'default' => false]); + $table->addColumn('schedule_after', Types::DATETIME, ['notnull' => false]); $table->addColumn('user_id', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('throws', Types::TEXT, ['notnull' => false]); - $table->addColumn('data', Types::TEXT, ['notnull' => false]); + $table->addColumn('job_list_id', Types::STRING, ['notnull' => false, 'length' => 255]); $table->addColumn('last_run', Types::DATETIME, ['notnull' => false]); $table->addColumn('next_run', Types::DATETIME, ['notnull' => false]); - $table->addColumn('is_enabled', Types::BOOLEAN, ['notnull' => true, 'default' => true]); - $table->addColumn('listens', Types::TEXT, ['notnull' => false]); - $table->addColumn('conditions', Types::TEXT, ['notnull' => false]); - $table->addColumn('class', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('priority', Types::INTEGER, ['notnull' => false]); - $table->addColumn('async', Types::BOOLEAN, ['notnull' => true, 'default' => false]); - $table->addColumn('configuration', Types::TEXT, ['notnull' => false]); - $table->addColumn('is_lockable', Types::BOOLEAN, ['notnull' => true, 'default' => false]); - $table->addColumn('locked', Types::BOOLEAN, ['notnull' => true, 'default' => false]); - $table->addColumn('last_run_time', Types::INTEGER, ['notnull' => false]); - $table->addColumn('status', Types::BOOLEAN, ['notnull' => true, 'default' => true]); - $table->addColumn('action_handler_configuration', Types::TEXT, ['notnull' => false]); - $table->addColumn('date_created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); - $table->addColumn('date_modified', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); - $table->setPrimaryKey(['id']); - } - - if (!$schema->hasTable('openconnector_logs')) { - $table = $schema->createTable('openconnector_logs'); - $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'length' => 20]); - $table->addColumn('type', Types::STRING, ['notnull' => true, 'length' => 255]); - $table->addColumn('call_id', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('request_method', Types::STRING, ['notnull' => false, 'length' => 10]); - $table->addColumn('request_headers', Types::TEXT, ['notnull' => false]); - $table->addColumn('request_query', Types::TEXT, ['notnull' => false]); - $table->addColumn('request_path_info', Types::TEXT, ['notnull' => false]); - $table->addColumn('request_languages', Types::TEXT, ['notnull' => false]); - $table->addColumn('request_server', Types::TEXT, ['notnull' => false]); - $table->addColumn('request_content', Types::TEXT, ['notnull' => false]); - $table->addColumn('response_status', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('response_status_code', Types::INTEGER, ['notnull' => false]); - $table->addColumn('response_headers', Types::TEXT, ['notnull' => false]); - $table->addColumn('response_content', Types::TEXT, ['notnull' => false]); - $table->addColumn('user_id', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('session', Types::TEXT, ['notnull' => false]); - $table->addColumn('session_values', Types::TEXT, ['notnull' => false]); - $table->addColumn('response_time', Types::INTEGER, ['notnull' => false]); - $table->addColumn('route_name', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('route_parameters', Types::TEXT, ['notnull' => false]); - $table->addColumn('entity', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('endpoint', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('gateway', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('handler', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('object_id', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('date_created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); - $table->addColumn('date_modified', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('logRetention', Types::INTEGER, ['notnull' => true, 'default' => 3600]); + $table->addColumn('errorRetention', Types::INTEGER, ['notnull' => true, 'default' => 86400]); + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); } @@ -154,6 +117,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $table->addColumn('last_sync', Types::DATETIME, ['notnull' => false]); $table->addColumn('object_count', Types::INTEGER, ['notnull' => false]); $table->addColumn('test', Types::BOOLEAN, ['notnull' => false]); + $table->addColumn('logRetention', Types::INTEGER, ['notnull' => true, 'default' => 3600]); + $table->addColumn('errorRetention', Types::INTEGER, ['notnull' => true, 'default' => 86400]); $table->addColumn('date_created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->addColumn('date_modified', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); @@ -162,27 +127,125 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt if (!$schema->hasTable('openconnector_synchronizations')) { $table = $schema->createTable('openconnector_synchronizations'); $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'length' => 20]); - $table->addColumn('entity', Types::STRING, ['notnull' => true, 'length' => 255]); - $table->addColumn('object', Types::STRING, ['notnull' => true, 'length' => 255]); - $table->addColumn('action', Types::STRING, ['notnull' => true, 'length' => 255]); - $table->addColumn('gateway', Types::STRING, ['notnull' => true, 'length' => 255]); - $table->addColumn('sourceObject', Types::STRING, ['notnull' => true, 'length' => 255]); - $table->addColumn('endpoint', Types::STRING, ['notnull' => true, 'length' => 255]); - $table->addColumn('sourceId', Types::STRING, ['notnull' => true, 'length' => 255]); - $table->addColumn('hash', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('sha', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('blocked', Types::BOOLEAN, ['notnull' => false]); - $table->addColumn('sourceLastChanged', Types::DATETIME, ['notnull' => false]); - $table->addColumn('lastChecked', Types::DATETIME, ['notnull' => false]); - $table->addColumn('lastSynced', Types::DATETIME, ['notnull' => false]); - $table->addColumn('tryCounter', Types::INTEGER, ['notnull' => false]); - $table->addColumn('dontSyncBefore', Types::DATETIME, ['notnull' => false]); - $table->addColumn('mapping', Types::TEXT, ['notnull' => false]); - $table->addColumn('date_created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); - $table->addColumn('date_modified', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('name', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('description', Types::TEXT, ['notnull' => false]); + // Source + $table->addColumn('source_id', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('source_type', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('source_hash', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('source_target_mapping', Types::TEXT, ['notnull' => false]); + $table->addColumn('source_config', Types::JSON, ['notnull' => false]); + $table->addColumn('source_last_changed', Types::DATETIME, ['notnull' => false]); + $table->addColumn('source_last_checked', Types::DATETIME, ['notnull' => false]); + $table->addColumn('source_last_synced', Types::DATETIME, ['notnull' => false]); + // Target + $table->addColumn('target_id', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('target_type', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('target_hash', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('target_source_mapping', Types::TEXT, ['notnull' => false]); + $table->addColumn('target_config', Types::JSON, ['notnull' => false]); + $table->addColumn('target_last_changed', Types::DATETIME, ['notnull' => false]); + $table->addColumn('target_last_checked', Types::DATETIME, ['notnull' => false]); + $table->addColumn('target_last_synced', Types::DATETIME, ['notnull' => false]); + // General + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); + $table->addIndex(['source_id'], 'openconnector_synchronizations_source_id_index'); + $table->addIndex(['target_id'], 'openconnector_synchronizations_target_id_index'); } + if (!$schema->hasTable('openconnector_call_logs')) { + $table = $schema->createTable('openconnector_call_logs'); + $table->addColumn('id', 'integer', [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('status_code', 'integer', [ + 'notnull' => false, + 'length' => 3 + ]); + $table->addColumn('status_message', 'string', [ + 'notnull' => false, + 'length' => 256 + ]); + $table->addColumn('request', 'json', [ + 'notnull' => false, + ]); + $table->addColumn('response', 'json', [ + 'notnull' => false, + ]); + $table->addColumn('source_id', 'integer', [ + 'notnull' => true, + ]); + $table->addColumn('action_id', 'integer', [ + 'notnull' => false, + ]); + $table->addColumn('synchronization_id', 'integer', [ + 'notnull' => false, + ]); + $table->addColumn('created_at', 'datetime', [ + 'notnull' => true, + 'default' => 'CURRENT_TIMESTAMP' + ]); + $table->addColumn('expires', Types::DATETIME, ['notnull' => false]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['source_id'], 'openconnector_call_logs_source_id_index'); + $table->addIndex(['action_id'], 'openconnector_call_logs_action_id_index'); + $table->addIndex(['synchronization_id'], 'openconnector_call_logs_sync_id_index'); + $table->addIndex(['status_code'], 'openconnector_call_logs_status_code_index'); + } + + if (!$schema->hasTable('openconnector_job_logs')) { + $table = $schema->createTable('openconnector_job_logs'); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'length' => 20]); + $table->addColumn('job_id', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('job_list_id', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('job_class', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('arguments', Types::JSON, ['notnull' => false]); + $table->addColumn('execution_time', Types::INTEGER, ['notnull' => true, 'default' => 0]); + $table->addColumn('user_id', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('last_run', Types::DATETIME, ['notnull' => false]); + $table->addColumn('next_run', Types::DATETIME, ['notnull' => false]); + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('expires', Types::DATETIME, ['notnull' => false]); + $table->setPrimaryKey(['id']); + $table->addIndex(['job_id'], 'openconnector_job_logs_job_id_index'); + $table->addIndex(['job_list_id'], 'openconnector_job_logs_job_list_id_index'); + $table->addIndex(['user_id'], 'openconnector_job_logs_user_id_index'); + } + + if (!$schema->hasTable('openconnector_synchronization_contracts')) { + $table = $schema->createTable('openconnector_synchronization_contracts'); + $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'length' => 20]); + $table->addColumn('name', Types::STRING, ['notnull' => true, 'length' => 255]); + $table->addColumn('description', Types::TEXT, ['notnull' => false]); + $table->addColumn('synchronization_id', Types::STRING, ['notnull' => true, 'length' => 255]); + // Source + $table->addColumn('source_id', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('source_hash', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('source_last_changed', Types::DATETIME, ['notnull' => false]); + $table->addColumn('source_last_checked', Types::DATETIME, ['notnull' => false]); + $table->addColumn('source_last_synced', Types::DATETIME, ['notnull' => false]); + // Target + $table->addColumn('target_id', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('target_hash', Types::STRING, ['notnull' => false, 'length' => 255]); + $table->addColumn('target_last_changed', Types::DATETIME, ['notnull' => false]); + $table->addColumn('target_last_checked', Types::DATETIME, ['notnull' => false]); + $table->addColumn('target_last_synced', Types::DATETIME, ['notnull' => false]); + // General + $table->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + + $table->setPrimaryKey(['id']); + $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', 'target_id'], 'openconnector_sync_contracts_sync_target_index'); + } + return $schema; } diff --git a/lib/Service/AuthenticationService.php b/lib/Service/AuthenticationService.php new file mode 100644 index 000000000..ab4b08c30 --- /dev/null +++ b/lib/Service/AuthenticationService.php @@ -0,0 +1,11 @@ +client = new Client([]); + $this->callLogMapper = $callLogMapper; + } + + /** + * Calls a source according to given configuration. + * + * @param Source $source The source to call. + * @param string $endpoint The endpoint on the source to call. + * @param string $method The method on which to call the source. + * @param array $config The additional configuration to call the source. + * @param bool $asynchronous Whether or not to call the source asynchronously. + * @param bool $createCertificates Whether or not to create certificates for this source. + * + * @throws Exception + * + * @return Response + */ + public function call( + Source $source, + string $endpoint = '', + string $method = 'GET', + array $config = [], + bool $asynchronous = false, + bool $createCertificates = true, + bool $overruleAuth = false + ): CallLog + { + $this->source = $source; + + if ($this->source->getIsEnabled() === null || $this->source->getIsEnabled() === false) { + // Create and save the CallLog + $callLog = new CallLog(); + $callLog->setSourceId($this->source->getId()); + $callLog->setStatusCode(409); + $callLog->setStatusMessage("This source is not enabled"); + $callLog->setCreatedAt(new \DateTime()); + $callLog->setUpdatedAt(new \DateTime()); + + $this->callLogMapper->insert($callLog); + + return $callLog; + } + + if (empty($this->source->getLocation()) === true) { + // Create and save the CallLog + $callLog = new CallLog(); + $callLog->setSourceId($this->source->getId()); + $callLog->setStatusCode(409); + $callLog->setStatusMessage("This source has no location"); + $callLog->setCreatedAt(new \DateTime()); + $callLog->setUpdatedAt(new \DateTime()); + + $this->callLogMapper->insert($callLog); + + return $callLog; + } + + // Check if the source has a configuration and merge it with the given config + if (empty($this->source->getConfiguration()) === false) { + $config = array_merge_recursive($config, $this->source->getConfiguration()); + } + + // Check if the config has a Content-Type header and overwrite it if it does + if (isset($config['headers']['Content-Type']) === true) { + $overwriteContentType = $config['headers']['Content-Type']; + } + + // decapiitilized fall back for content-type + if (isset($config['headers']['content-type']) === true) { + $overwriteContentType = $config['headers']['content-type']; + } + + // Make sure we do not have an array of accept headers but just one value + if (isset($config['headers']['accept']) === true && is_array($config['headers']['accept']) === true) { + $config['headers']['accept'] = $config['headers']['accept'][0]; + } + + + // Check if the config has a headers array and create it if it doesn't + if (isset($config['headers']) === false) { + $config['headers'] = []; + } + + // We want to suprres guzzle exceptions and return the response instead + $config['http_errors'] = false; + + // Set the URL to call and add an endpoint if needed + $url = $this->source->getLocation().$endpoint; + + // Set authentication if needed. @todo: create the authentication service + //$createCertificates && $this->getCertificate($config); + + // Let's log the call. + $this->source->setLastCall(new \DateTime()); + // @todo: save the source + + // Let's make the call. + $time_start = microtime(true); + try { + if ($asynchronous === false) { + $response = $this->client->request($method, $url, $config); + } else { + return $this->client->requestAsync($method, $url, $config); + } + } catch (GuzzleHttp\Exception\BadResponseException $e) { + $response = $e->getResponse(); + } + + $time_end = microtime(true); + + // Let create the data array + $data = [ + 'request' => [ + 'url' => $url, + 'method' => $method, + ...$config + ], + 'response' => [ + 'statusCode' => $response->getStatusCode(), + 'statusMessage' => $response->getReasonPhrase(), + 'responseTime' => ( $time_end - $time_start ) * 1000, + 'size' => $response->getBody()->getSize(), + 'remoteIp' => $response->getHeaderLine('X-Real-IP') ?: $response->getHeaderLine('X-Forwarded-For') ?: null, + 'headers' => $response->getHeaders(), + 'body' => $response->getBody()->getContents(), + ] + ]; + + // Create and save the CallLog + $callLog = new CallLog(); + $callLog->setSourceId($this->source->getId()); + $callLog->setStatusCode($data['response']['statusCode']); + $callLog->setStatusMessage($data['response']['statusMessage']); + $callLog->setRequest($data['request']); + $callLog->setResponse($data['response']); + $callLog->setCreatedAt(new \DateTime()); + + $this->callLogMapper->insert($callLog); + + return $callLog; + } +} diff --git a/lib/Service/JobService.php b/lib/Service/JobService.php new file mode 100644 index 000000000..d4e148f49 --- /dev/null +++ b/lib/Service/JobService.php @@ -0,0 +1,84 @@ +jobList = $jobList; + $this->jobMapper = $jobMapper; + $this->actionTask = $actionTask; + $this->connection = $connection; + } + + public function scheduleJob(Job $job): Job + { + // Lets first check if the job should be disabled + if (!$job->getIsEnabled() || $job->getJobListId()) { + + $this->jobList->removeById($job->getId()); + $job->setJobListId(null); + return $this->jobMapper->save(job); + } + + // lets not update the job if it's already scheduled @todo we should + if($job->getJobListId()) { + return $job; + } + + // Oke this is a new job lets schedule it + $arguments = $job->getArguments(); + $arguments['jobId'] = $job->getId(); + + if(!$job->getScheduleAfter()) { + $iJob = $this->jobList->add($this->actionTask::class, $arguments); + } else { + $runAfter = $job->getScheduleAfter()->getTimestamp(); + $iJob = $this->jobList->scheduleAfter($this->actionTask::class, $runAfter, $arguments); + } + + // Set the job list id + $job->setJobListId($this->getJobListId($this->actionTask::class)); + // Save the job to the database + return $this->jobMapper->update($job); + } + + /** + * This function will get the job list id of the last job in the list + * + * Why the NC job list dosn't support a better way to get the last job in the list is beyond me :') + * https://github.com/nextcloud/server/blob/master/lib/private/BackgroundJob/JobList.php#L134 + * + * @param IJob|class-string $job + * @param mixed $argument + */ + public function getJobListId($job): int|null { + $class = ($job instanceof IJob) ? get_class($job) : $job; + + $query = $this->connection->getQueryBuilder(); + $query->select('id') + ->from('jobs') + ->where($query->expr()->eq('class', $query->createNamedParameter($class))) + ->orderBy('id', 'DESC') + ->setMaxResults(1); + + $result = $query->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + return $row['id'] ?? null; + } + +} diff --git a/lib/Service/MappingService.php b/lib/Service/MappingService.php new file mode 100644 index 000000000..726ceaf40 --- /dev/null +++ b/lib/Service/MappingService.php @@ -0,0 +1,414 @@ +twig = $twig; + + }//end __construct() + + /** + * Replaces strings in array keys, helpful for characters like . in array keys. + * + * @param array $array The array to encode the array keys for. + * @param string $toReplace The character to encode. + * @param string $replacement The encoded character. + * + * @return array The array with encoded array keys + */ + public function encodeArrayKeys(array $array, string $toReplace, string $replacement): array + { + $result = []; + foreach ($array as $key => $value) { + $newKey = str_replace($toReplace, $replacement, $key); + + if (\is_array($value) === true && $value !== []) { + $result[$newKey] = $this->encodeArrayKeys($value, $toReplace, $replacement); + continue; + } + + $result[$newKey] = $value; + } + + return $result; + + }//end encodeArrayKeys() + + /** + * Maps (transforms) an array (input) to a different array (output). + * + * @param Mapping $mappingObject The mapping object that forms the recipe for the mapping + * @param array $input The array that need to be mapped (transformed) otherwise known as input + * @param bool $list Wheter we want a list instead of a sngle item + * + * @throws LoaderError|SyntaxError Twig Exceptions + * + * @return array The result (output) of the mapping process + */ + public function mapping(Mapping $mappingObject, array $input, bool $list = false): array + { + // Make sure we don't have BSONDocument (MongoDB) in our input. + $input = $this->bsonDocumentToArray($input); + + // Check for list + if ($list === true) { + $list = []; + $extraValues = []; + + // Allow extra(input)values to be passed down for mapping while dealing with a list. + if (array_key_exists('listInput', $input) === true) { + $extraValues = $input; + $input = $input['listInput']; + unset($extraValues['listInput'], $extraValues['value']); + } + + foreach ($input as $key => $value) { + // Mapping function expects an array for $input, make sure we always pass an array to this function. + if (is_array($value) === false || empty($extraValues) === false) { + // todo: we want to remove ['value' => $value] from this at some point, for now required for DOWR to work + $value = array_merge((array) $value, ['value' => $value], $extraValues); + } + + $list[$key] = $this->mapping($mappingObject, $value); + } + + return $list; + }//end if + + $input = $this->encodeArrayKeys($input, '.', '.'); + + // @todo: error loging + // isset($this->style) === true && $this->style->info('Mapping array based on mapping object '.$mappingObject->getName().' (id:'.$mappingObject->getId()->toString().' / ref:'.$mappingObject->getReference().') v:'.$mappingObject->getversion()); + + // Determine pass trough. + // Let's get the dot array based on https://github.com/adbario/php-dot-notation. + if ($mappingObject->getPassTrough()) { + $dotArray = new Dot($input); + // @todo: error loging + // isset($this->style) === true && $this->style->info('Mapping *with* pass trough'); + } else { + $dotArray = new Dot(); + // @todo: error loging + // isset($this->style) === true && $this->style->info('Mapping *without* pass trough'); + } + + $dotInput = new Dot($input); + + // Let's do the actual mapping. + foreach ($mappingObject->getMapping() as $key => $value) { + // If the value exists in the input dot take it from there. + if ($dotInput->has($value)) { + $dotArray->set($key, $dotInput->get($value)); + continue; + } + + // Render the value from twig. + $dotArray->set($key, $this->twig->createTemplate($value)->render($input)); + } + + // Unset unwanted key's. + $unsets = ($mappingObject->getUnset() ?? []); + foreach ($unsets as $unset) { + if ($dotArray->has($unset) === false) { + // @todo: error loging + // isset($this->style) === true && $this->style->info("Trying to unset an property that doesn't exist during mapping"); + continue; + } + + $dotArray->delete($unset); + } + + // Cast values to a specific type. + $casts = ($mappingObject->getCast() ?? []); + + foreach ($casts as $key => $cast) { + if ($dotArray->has($key) === false) { + // @todo: error loging + //isset($this->style) === true && $this->style->info("Trying to cast an property that doesn't exist during mapping"); + continue; + } + + if (is_array($cast) === false) { + $cast = explode(',', $cast); + } + + if ($cast === false) { + // @todo: error loging + //isset($this->style) === true && $this->style->info("Cast for property $key is an empty string"); + continue; + } + + foreach ($cast as $singleCast) { + $this->handleCast($dotArray, $key, $singleCast); + } + } + + // Back to array. + $output = $dotArray->all(); + + $output = $this->encodeArrayKeys($output, '.', '.'); + + // If something has been defined to work on root level (i.e. the object lives on root level), we can use # to define writing the root object. + $keys = array_keys($output); + if (count($keys) === 1 && $keys[0] === '#') { + $output = $output['#']; + } + + // Log the result. + // @todo: error handling + /* + isset($this->style) === true && $this->style->info( + 'Mapped object', + [ + 'input' => $input, + 'output' => $output, + 'passTrough' => $mappingObject->getPassTrough(), + 'mapping' => $mappingObject->getMapping(), + ] + ); + */ + + return $output; + + }//end mapping() + + /** + * Handles a single cast. + * + * @param Dot $dotArray The dotArray of the array we are mapping. + * @param string $key The key of the field we want to cast. + * @param string $cast The type of cast we want to do. + * + * @return void + */ + private function handleCast(Dot $dotArray, string $key, string $cast) + { + $value = $dotArray->get($key); + + if (str_starts_with($cast, 'unsetIfValue==') === true) { + $unsetIfValue = substr($cast, 14); + $cast = 'unsetIfValue'; + } else if (str_starts_with($cast, 'setNullIfValue==') === true) { + $setNullIfValue = substr($cast, 16); + $cast = 'setNullIfValue'; + } else if (str_starts_with($cast, 'countValue:') === true) { + $countValue = substr($cast, 11); + $cast = 'countValue'; + } + + // Todo: Add more casts. + switch ($cast) { + case 'string': + $value = (string) $value; + break; + case 'bool': + case 'boolean': + if ((int) $value === 1 || strtolower($value) === 'true' || strtolower($value) === 'yes') { + $value = true; + break; + } + + $value = false; + break; + case 'int': + case 'integer': + $value = (int) $value; + break; + case 'float': + $value = (float) $value; + break; + case 'array': + $value = (array) $value; + break; + case 'date': + $value = date($value); + break; + case 'url': + $value = urlencode($value); + break; + case 'urlDecode': + $value = urldecode($value); + break; + case 'rawurl': + $value = rawurlencode($value); + break; + case 'rawurlDecode': + $value = rawurldecode($value); + break; + case 'html': + $value = htmlentities($value); + break; + case 'htmlDecode': + $value = html_entity_decode($value); + break; + case 'base64': + $value = base64_encode($value); + break; + case 'base64Decode': + $value = \Safe\base64_decode($value); + break; + case 'json': + $value = json_encode($value); + break; + case 'jsonToArray': + $value = html_entity_decode($value); + $value = json_decode($value, true); + break; + case 'utf8': + // https://www.php.net/manual/en/function.iconv.php + setlocale(LC_CTYPE, 'cs_CZ'); + $value = iconv('UTF-8', 'ASCII//TRANSLIT', $value); + break; + case 'nullStringToNull': + if ($value === 'null') { + $value = null; + } + break; + case 'coordinateStringToArray': + $value = $this->coordinateStringToArray($value); + break; + case 'keyCantBeValue': + if ($key == $value) { + $dotArray->delete($key); + } + break; + case 'unsetIfValue': + if (isset($unsetIfValue) === true + && $value == $unsetIfValue + || ($unsetIfValue === '' && empty($value)) + || ($unsetIfValue === '' && $value === null) + ) { + $dotArray->delete($key); + } + + if ($unsetIfValue === '' && is_array($value) === true && $this->areAllArrayKeysNull($value) === true) { + $dotArray->delete($key); + } + break; + case 'setNullIfValue': + if (isset($setNullIfValue) === true + && $value == $setNullIfValue + || ($setNullIfValue === '' && empty($value)) + || ($setNullIfValue === '' && $value === null) + ) { + $value = null; + } + + if ($setNullIfValue === '' && is_array($value) === true && $this->areAllArrayKeysNull($value) === true) { + $value = null; + } + break; + case 'countValue': + if (isset($countValue) === true + && empty($countValue) === false + && $dotArray->has($countValue) === true + && is_countable($dotArray->get($countValue)) === true + ) { + $value = count($dotArray->get($countValue)); + } + break; + case 'moneyStringToInt': + $value = str_replace('.', '', $value); + $value = (int) str_replace(',', '', $value); + break; + case 'intToMoneyString': + $value = ($value / 100); + $value = number_format($value, 2, ',', '.'); + break; + default: + // @todo: error handling + //isset($this->style) === true && $this->style->info('Trying to cast to an unsupported cast type: '.$cast); + break; + }//end switch + + // Don't reset key that was deleted on purpose. + if ($dotArray->has($key)) { + $dotArray->set($key, $value); + } + + }//end handleCast() + + /** + * Checks if all keys in multi-dimensional array are null. + * + * @param array $array Array to check. + * + * @return bool True if array keys are null else false. + */ + private function areAllArrayKeysNull(array $array): bool + { + if (empty($array) === true) { + return true; + } + + foreach ($array as $value) { + if (is_array($value) === true) { + if ($this->areAllArrayKeysNull($value) === false) { + return false; + } + } else if (empty($value) === false) { + return false; + } + } + + return true; + + }//end areAllArrayKeysNull() + + /** + * Converts a coordinate string to an array of coordinates. + * + * @param string $coordinates A string containing coordinates. + * + * @return array An array of coordinates. + */ + public function coordinateStringToArray(string $coordinates): array + { + $halfs = explode(' ', $coordinates); + $point = []; + $coordinateArray = []; + foreach ($halfs as $half) { + if (count($point) > 1) { + $coordinateArray[] = $point; + $point = []; + } + + $point[] = $half; + }//end foreach + + $coordinateArray[] = $point; + + if (count($coordinateArray) === 1) { + $coordinateArray = $coordinateArray[0]; + } + + return $coordinateArray; + + }//end coordinateStringToArray() + +} diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php new file mode 100644 index 000000000..88507b195 --- /dev/null +++ b/lib/Service/SynchronizationService.php @@ -0,0 +1,202 @@ +callService = $callService; + $this->mappingService = $mappingService; + $this->containerInterface = $containerInterface; + $this->synchronizationMapper = $synchronizationMapper; + $this->synchronizationContractMapper = $synchronizationContractMapper; + } + + /** + * Synchronizes a given synchronization (or a complete source). + * + * @param Synchronization $synchronization + * @return void + */ + public function synchronize(Synchronization $synchronization) + { + $this->synchronization = $synchronization; + $objectList = []; + + foreach($objectList as $object) { + // Get the synchronization contract for this object + $synchronizationContract = $this->synchronizationContractMapper->findOnSource($synchronization->id, $object['id']); + if(!$synchronizationContract) { + $synchronizationContract = new SynchronizationContract(); + $synchronizationContract->setSynchronizationId($synchronization->id); + $synchronizationContract->setSourceId($object['id']); + $synchronizationContract->setSourceHash(md5(serialize($object))); + // @todo: should we do this here + $this->synchronizationContractMapper->insert($synchronizationContract); + } + + $this->synchronizeContract($synchronizationContract); + } + + } + + /** + * @param SynchronizationContract $synchronizationContract + * @return void + */ + public function synchronizeContract(SynchronizationContract $synchronizationContract, $object = null) + { + // The function can be called solo set let's make sure we have the full synchronization object + if(!$this->synchronization){ + $this->synchronization = $this->synchronizationMapper->findById($synchronizationContract->getSynchronizationId()); + } + + // We should have an object but lets make sure we have the full object + if(!$object){ + $object = $this->getAllObjectsFromSource($synchronizationContract); + } + + // Let create a source hash for the object + $sourceHash = md5(serialize($object)); + $synchronizationContract->sourceLastChecked(new DateTime()); + + // Lets prevent pointless updates @todo acount for omnidirectional sync + if($sourceHash === $synchronizationContract->getSourceHash()){ + // The object has not changed + return $this->synchronizationContractMapper->update($synchronizationContract); + } + + // The object has changed, oke let do mappig and bla die bla + $synchronizationContract->setSourceHash($sourceHash); + $synchronizationContract->sourceLastChanged(new DateTime()); + + // let do the mapping if provided + if($synchronizationContract->getSourceTargetMapping()){ + $targetObject = $this->mappingService->mapping($synchronizationContract->getSourceTargetMapping(), $object); + } + else{ + $targetObject = $object; + } + + // set the target hash + $targetHash = md5(serialize($targetObject)); + $synchronizationContract->setTargetHash($targetHash); + $synchronizationContract->targetLastChanged(new DateTime()); + $synchronizationContract->targetLastSynced(new DateTime()); + $synchronizationContract->sourceLastSynced(new DateTime()); + + // Do the magic!! + + $this->updateTarget($synchronizationContract, $targetObject); + + // Save results + $this->synchronizationContractMapper->update($synchronizationContract); + + return $synchronizationContract; + + } + + /** + * Write the data to the target + * + * @param SynchronizationContract $synchronizationContract + * @return void + */ + public function updateTarget(SynchronizationContract $synchronizationContract, array $targetObject) + { + // The function can be called solo set let's make sure we have the full synchronization object + if(!$this->synchronization){ + $this->synchronization = $this->synchronizationMapper->findById($synchronizationContract->getSynchronizationId()); + } + + // Lets check if we need to create or update + $update = false; + if($synchronizationContract->getTargetId()){ + $update = true; + } + + $type = $synchronizationContract->getTargetType(); + + switch($type){ + case 'register/schema': + // Setup the object service + $this->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()){ + $targetObject['id'] = $synchronizationContract->getTargetId(); + } + // Extract register and schema from the targetId + $targetId = $this->synchronization->getTargetId(); + list($register, $schema) = explode('/', $targetId); + + // Save the object to the target + $target = $this->objectService->saveObject($register, $schema, $targetObject); + // Get the id form the target object + $synchronizationContract->setTargetId($target->getUuid()); + break; + case 'api': + //@todo: implement + //$this->callService->put($targetObject); + break; + case 'database': + //@todo: implement + break; + } + } + + /** + * Get all the object from a source + * + * @param SynchronizationContract $synchronizationContract + * @return void + */ + public function getAllObjectsFromSource(Synchronization $synchronization) + { + switch($type){ + case 'register/schema': + // Setup the object service + $this->objectService = $this->containerInterface->get('OCA\OpenRegister\Service\ObjectService'); + + break; + case 'api': + + //@todo: implement + //$this->callService->put($targetObject); + break; + case 'database': + //@todo: implement + break; + } + } +} \ No newline at end of file diff --git a/src/entities/callLog/callLog.mock.ts b/src/entities/callLog/callLog.mock.ts new file mode 100644 index 000000000..fa56d5676 --- /dev/null +++ b/src/entities/callLog/callLog.mock.ts @@ -0,0 +1,37 @@ +import { CallLog } from './callLog' +import { TCallLog } from './callLog.types' + +export const mockCallLogData = (): TCallLog[] => [ + { + id: '5137a1e5-b54d-43ad-abd1-4b5bff5fcd3f', + sourceId: '4c3edd34-a90d-4d2a-8894-adb5836ecde8', + endpoint: '/api/users', + method: 'GET', + statusCode: 200, + requestHeaders: { 'Content-Type': 'application/json' }, + requestBody: null, + responseHeaders: { 'Content-Type': 'application/json' }, + responseBody: { users: [] }, + duration: 150, + error: null, + createdAt: '2023-06-01T12:00:00Z', + updatedAt: null, + }, + { + id: '4c3edd34-a90d-4d2a-8894-adb5836ecde8', + sourceId: '5137a1e5-b54d-43ad-abd1-4b5bff5fcd3f', + endpoint: '/api/posts', + method: 'POST', + statusCode: 201, + requestHeaders: { 'Content-Type': 'application/json' }, + requestBody: { title: 'New Post', content: 'This is a new post.' }, + responseHeaders: { 'Content-Type': 'application/json' }, + responseBody: { id: '123', message: 'Post created successfully' }, + duration: 200, + error: null, + createdAt: '2023-06-02T14:30:00Z', + updatedAt: null, + }, +] + +export const mockCallLog = (data: TCallLog[] = mockCallLogData()): TCallLog[] => data.map(item => new CallLog(item)) diff --git a/src/entities/callLog/callLog.ts b/src/entities/callLog/callLog.ts new file mode 100644 index 000000000..c5ccb7fc8 --- /dev/null +++ b/src/entities/callLog/callLog.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { SafeParseReturnType, z } from 'zod' +import { TCallLog } from './callLog.types' + +export class CallLog implements TCallLog { + + public id?: string + public sourceId: string + public endpoint: string + public method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + public statusCode: number + public requestHeaders?: object + public requestBody?: any + public responseHeaders?: object + public responseBody?: any + public duration: number + public error?: string | null + public createdAt: string + public updatedAt?: string | null + + constructor(callLog: TCallLog) { + this.id = callLog.id + this.sourceId = callLog.sourceId + this.endpoint = callLog.endpoint + this.method = callLog.method + this.statusCode = callLog.statusCode + this.requestHeaders = callLog.requestHeaders + this.requestBody = callLog.requestBody + this.responseHeaders = callLog.responseHeaders + this.responseBody = callLog.responseBody + this.duration = callLog.duration + this.error = callLog.error || null + this.createdAt = callLog.createdAt + this.updatedAt = callLog.updatedAt || null + } + + public validate(): SafeParseReturnType { + const schema = z.object({ + id: z.string().uuid().optional(), + sourceId: z.string().uuid(), + endpoint: z.string(), + method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']), + statusCode: z.number().int().positive(), + requestHeaders: z.record(z.any()).optional(), + requestBody: z.any().optional(), + responseHeaders: z.record(z.any()).optional(), + responseBody: z.any().optional(), + duration: z.number().positive(), + error: z.string().nullable().optional(), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime().nullable().optional() + }) + + return schema.safeParse({ ...this }) + } + +} diff --git a/src/entities/callLog/callLog.types.ts b/src/entities/callLog/callLog.types.ts new file mode 100644 index 000000000..6d700ef9a --- /dev/null +++ b/src/entities/callLog/callLog.types.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type TCallLog = { + id?: string + sourceId: string + endpoint: string + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + statusCode: number + requestHeaders?: object + requestBody?: any + responseHeaders?: object + responseBody?: any + duration: number + error?: string | null + createdAt: string + updatedAt?: string | null +} diff --git a/src/entities/callLog/callLogspec.ts b/src/entities/callLog/callLogspec.ts new file mode 100644 index 000000000..f289947f0 --- /dev/null +++ b/src/entities/callLog/callLogspec.ts @@ -0,0 +1,22 @@ +import { CallLog } from './callLog' +import { mockCallLog } from './callLog.mock' + +describe('CallLog Entity', () => { + it('create CallLog entity with full data', () => { + const callLog = new CallLog(mockCallLog()[0]) + + expect(callLog).toBeInstanceOf(CallLog) + expect(callLog).toEqual(mockCallLog()[0]) + + expect(callLog.validate().success).toBe(true) + }) + + it('create CallLog entity with partial data', () => { + const callLog = new CallLog(mockCallLog()[1]) + + expect(callLog).toBeInstanceOf(CallLog) + expect(callLog).toEqual(mockCallLog()[1]) + + expect(callLog.validate().success).toBe(true) + }) +}) diff --git a/src/entities/callLog/index.js b/src/entities/callLog/index.js new file mode 100644 index 000000000..7b97bc114 --- /dev/null +++ b/src/entities/callLog/index.js @@ -0,0 +1,4 @@ +export * from './callLog.ts' +export * from './callLog.types.ts' +export * from './callLog.mock.ts' + diff --git a/src/entities/index.js b/src/entities/index.js index 451cc630a..d4e34801c 100644 --- a/src/entities/index.js +++ b/src/entities/index.js @@ -1,8 +1,10 @@ /* eslint-disable import/export */ export * from './job/index.js' +export * from './jobLog/index.js' export * from './log/index.js' export * from './endpoint/index.js' export * from './webhook/index.js' export * from './mapping/index.js' export * from './synchronization/index.js' export * from './source/index.js' +export * from './callLog/index.js' diff --git a/src/entities/job/job.mock.ts b/src/entities/job/job.mock.ts index 9f5df9cdb..8eff4be51 100644 --- a/src/entities/job/job.mock.ts +++ b/src/entities/job/job.mock.ts @@ -6,17 +6,41 @@ export const mockJobData = (): TJob[] => [ id: '5137a1e5-b54d-43ad-abd1-4b5bff5fcd3f', name: 'Daily Backup', description: 'Performs a daily backup of the system', - version: '1.0.0', - crontab: '0 0 * * *', + jobClass: 'OCA\\OpenConnector\\Action\\BackupAction', + arguments: { backupType: 'full' }, + interval: 86400, // 24 hours in seconds + executionTime: 7200, // 2 hours in seconds + timeSensitive: true, + allowParallelRuns: false, isEnabled: true, + singleRun: false, + scheduleAfter: null, + userId: 'admin', + jobListId: 'daily-jobs', + lastRun: '2023-06-01T00:00:00Z', + nextRun: '2023-06-02T00:00:00Z', + created: '2023-01-01T00:00:00Z', + updated: '2023-06-01T00:00:00Z' }, { id: '4c3edd34-a90d-4d2a-8894-adb5836ecde8', name: 'Weekly Report', description: 'Generates and sends weekly reports', - version: '1.1.0', - crontab: '0 9 * * 1', + jobClass: 'OCA\\OpenConnector\\Action\\ReportAction', + arguments: { reportType: 'weekly' }, + interval: 604800, // 7 days in seconds + executionTime: 3600, // 1 hour in seconds + timeSensitive: false, + allowParallelRuns: false, isEnabled: true, + singleRun: false, + scheduleAfter: null, + userId: 'reporter', + jobListId: 'weekly-jobs', + lastRun: '2023-05-29T09:00:00Z', + nextRun: '2023-06-05T09:00:00Z', + created: '2023-01-01T00:00:00Z', + updated: '2023-05-29T09:00:00Z' }, ] diff --git a/src/entities/job/job.ts b/src/entities/job/job.ts index c0309c682..f74580605 100644 --- a/src/entities/job/job.ts +++ b/src/entities/job/job.ts @@ -6,62 +6,58 @@ export class Job implements TJob { public id: string public name: string public description: string | null - public reference: string | null - public version: string - public crontab: string + public jobClass: string + public arguments: object | null + public interval: number + public executionTime: number + public timeSensitive: boolean + public allowParallelRuns: boolean + public isEnabled: boolean + public singleRun: boolean + public scheduleAfter: string | null public userId: string | null - public throws: string[] - public data: object | null + public jobListId: string | null + public logRetention: number + public errorRetention: number public lastRun: string | null public nextRun: string | null - public isEnabled: boolean | null - public dateCreated: string | null - public dateModified: string | null - public listens: string[] - public conditions: object | null - public class: string | null - public priority: number - public async: boolean - public configuration: object | null - public isLockable: boolean - public locked: string | null - public lastRunTime: number | null - public status: boolean | null - public actionHandlerConfiguration: object | null + public created: string | null + public updated: string | null constructor(job: TJob) { this.id = job.id || '' this.name = job.name || '' this.description = job.description || null - this.reference = job.reference || null - this.version = job.version || '0.0.0' - this.crontab = job.crontab || '*/5 * * * *' + this.jobClass = job.jobClass || 'OCA\\OpenConnector\\Action\\PingAction' + this.arguments = job.arguments || null + this.interval = job.interval || 3600 + this.executionTime = job.executionTime || 3600 + this.timeSensitive = job.timeSensitive ?? true + this.allowParallelRuns = job.allowParallelRuns ?? false + this.isEnabled = job.isEnabled ?? true + this.singleRun = job.singleRun ?? false + this.scheduleAfter = job.scheduleAfter || null this.userId = job.userId || null - this.throws = job.throws || [] - this.data = job.data || null + this.jobListId = job.jobListId || null + this.logRetention = job.logRetention || 3600 + this.errorRetention = job.errorRetention || 86400 this.lastRun = job.lastRun || null this.nextRun = job.nextRun || null - this.isEnabled = job.isEnabled ?? true - this.dateCreated = job.dateCreated || null - this.dateModified = job.dateModified || null - this.listens = job.listens || [] - this.conditions = job.conditions || null - this.class = job.class || null - this.priority = job.priority || 1 - this.async = job.async || false - this.configuration = job.configuration || null - this.isLockable = job.isLockable || false - this.locked = job.locked || null - this.lastRunTime = job.lastRunTime || null - this.status = job.status || null - this.actionHandlerConfiguration = job.actionHandlerConfiguration || null + this.created = job.created || null + this.updated = job.updated || null } public validate(): SafeParseReturnType { const schema = z.object({ id: z.string().uuid(), name: z.string().max(255), - version: z.string(), + jobClass: z.string(), + interval: z.number().int().positive(), + executionTime: z.number().int().positive(), + timeSensitive: z.boolean(), + allowParallelRuns: z.boolean(), + isEnabled: z.boolean(), + singleRun: z.boolean(), }) return schema.safeParse({ ...this }) diff --git a/src/entities/job/job.types.ts b/src/entities/job/job.types.ts index 3f0394502..79b80e5d4 100644 --- a/src/entities/job/job.types.ts +++ b/src/entities/job/job.types.ts @@ -2,26 +2,21 @@ export type TJob = { id?: string name: string description?: string | null - reference?: string | null - version: string - crontab?: string + jobClass?: string + arguments?: object | null + interval?: number + executionTime?: number + timeSensitive?: boolean + allowParallelRuns?: boolean + isEnabled?: boolean + singleRun?: boolean + scheduleAfter?: string | null userId?: string | null - throws?: string[] - data?: object | null + jobListId?: string | null + logRetention?: number + errorRetention?: number lastRun?: string | null nextRun?: string | null - isEnabled?: boolean | null - dateCreated?: string | null - dateModified?: string | null - listens?: string[] - conditions?: object | null - class?: string | null - priority?: number - async?: boolean - configuration?: object | null - isLockable?: boolean - locked?: string | null - lastRunTime?: number | null - status?: boolean | null - actionHandlerConfiguration?: object | null + created?: string | null + updated?: string | null } diff --git a/src/entities/jobLog/index.js b/src/entities/jobLog/index.js new file mode 100644 index 000000000..df9f468fc --- /dev/null +++ b/src/entities/jobLog/index.js @@ -0,0 +1,4 @@ +export * from './jobLog.types.ts' +export * from './jobLog.types.ts' +export * from './jobLog.mock.ts' + diff --git a/src/entities/jobLog/jobLog.mock.ts b/src/entities/jobLog/jobLog.mock.ts new file mode 100644 index 000000000..08627d07b --- /dev/null +++ b/src/entities/jobLog/jobLog.mock.ts @@ -0,0 +1,31 @@ +import { JobLog } from './jobLog' +import { TJobLog } from './jobLog.types' + +export const mockJobLogData = (): TJobLog[] => [ + { + id: '5137a1e5-b54d-43ad-abd1-4b5bff5fcd3f', + jobId: 'job-001', + jobListId: 'list-001', + jobClass: 'OCA\\OpenConnector\\Action\\PingAction', + arguments: { url: 'https://example.com' }, + executionTime: 3600, + userId: 'user-001', + lastRun: '2023-05-01T12:00:00Z', + nextRun: '2023-05-02T12:00:00Z', + created: '2023-05-01T00:00:00Z', + }, + { + id: '4c3edd34-a90d-4d2a-8894-adb5836ecde8', + jobId: 'job-002', + jobListId: 'list-002', + jobClass: 'OCA\\OpenConnector\\Action\\BackupAction', + arguments: { destination: '/backup' }, + executionTime: 7200, + userId: 'user-002', + lastRun: '2023-05-01T00:00:00Z', + nextRun: '2023-05-08T00:00:00Z', + created: '2023-04-30T00:00:00Z', + }, +] + +export const mockJobLog = (data: TJobLog[] = mockJobLogData()): JobLog[] => data.map(item => new JobLog(item)) diff --git a/src/entities/jobLog/jobLog.spec.ts b/src/entities/jobLog/jobLog.spec.ts new file mode 100644 index 000000000..38906423a --- /dev/null +++ b/src/entities/jobLog/jobLog.spec.ts @@ -0,0 +1,22 @@ +import { JobLog } from './jobLog' +import { mockJobLog } from './jobLog.mock' + +describe('JobLog Entity', () => { + it('create JobLog entity with full data', () => { + const jobLog = new JobLog(mockJobLog()[0]) + + expect(jobLog).toBeInstanceOf(JobLog) + expect(jobLog).toEqual(mockJobLog()[0]) + + expect(jobLog.validate().success).toBe(true) + }) + + it('create JobLog entity with partial data', () => { + const jobLog = new JobLog(mockJobLog()[1]) + + expect(jobLog).toBeInstanceOf(JobLog) + expect(jobLog).toEqual(mockJobLog()[1]) + + expect(jobLog.validate().success).toBe(true) + }) +}) diff --git a/src/entities/jobLog/jobLog.ts b/src/entities/jobLog/jobLog.ts new file mode 100644 index 000000000..cc9393b24 --- /dev/null +++ b/src/entities/jobLog/jobLog.ts @@ -0,0 +1,47 @@ +import { SafeParseReturnType, z } from 'zod' +import { TJobLog } from './jobLog.types' + +export class JobLog implements TJobLog { + + public id?: string + public jobId?: string + public jobListId?: string + public jobClass?: string + public arguments?: object | null + public executionTime?: number + public userId?: string | null + public lastRun?: string | null + public nextRun?: string | null + public created?: string | null + + constructor(jobLog: TJobLog) { + this.id = jobLog.id + this.jobId = jobLog.jobId + this.jobListId = jobLog.jobListId + this.jobClass = jobLog.jobClass + this.arguments = jobLog.arguments + this.executionTime = jobLog.executionTime + this.userId = jobLog.userId + this.lastRun = jobLog.lastRun + this.nextRun = jobLog.nextRun + this.created = jobLog.created + } + + public validate(): SafeParseReturnType { + const schema = z.object({ + id: z.string().uuid().optional(), + jobId: z.string().optional(), + jobListId: z.string().optional(), + jobClass: z.string().optional(), + arguments: z.record(z.any()).nullable().optional(), + executionTime: z.number().optional(), + userId: z.string().nullable().optional(), + lastRun: z.string().nullable().optional(), + nextRun: z.string().nullable().optional(), + created: z.string().nullable().optional() + }) + + return schema.safeParse({ ...this }) + } + +} diff --git a/src/entities/jobLog/jobLog.types.ts b/src/entities/jobLog/jobLog.types.ts new file mode 100644 index 000000000..290801631 --- /dev/null +++ b/src/entities/jobLog/jobLog.types.ts @@ -0,0 +1,12 @@ +export type TJobLog = { + id?: string + jobId?: string + jobListId?: string + jobClass?: string + arguments?: object | null + executionTime?: number + userId?: string | null + lastRun?: string | null + nextRun?: string | null + created?: string | null +} diff --git a/src/entities/source/source.ts b/src/entities/source/source.ts index 073c1d857..52950d344 100644 --- a/src/entities/source/source.ts +++ b/src/entities/source/source.ts @@ -33,6 +33,8 @@ export class Source implements TSource { public configuration: object | null public endpointsConfig: object | null public status: string + public logRetention: number + public errorRetention: number public lastCall: string | null public lastSync: string | null public objectCount: number @@ -70,6 +72,8 @@ export class Source implements TSource { this.configuration = source.configuration || null this.endpointsConfig = source.endpointsConfig || null this.status = source.status || 'No calls have been made yet to this source' + this.logRetention = source.logRetention || 3600 + this.errorRetention = source.errorRetention || 86400 this.lastCall = source.lastCall || null this.lastSync = source.lastSync || null this.objectCount = source.objectCount || 0 diff --git a/src/entities/source/source.types.ts b/src/entities/source/source.types.ts index 72fe75698..9a9fc1d15 100644 --- a/src/entities/source/source.types.ts +++ b/src/entities/source/source.types.ts @@ -29,6 +29,8 @@ export type TSource = { configuration?: object | null endpointsConfig?: object | null status?: string + logRetention?: number + errorRetention?: number lastCall?: string | null lastSync?: string | null objectCount?: number diff --git a/src/modals/Job/EditJob.vue b/src/modals/Job/EditJob.vue index f61611537..59d0d1edb 100644 --- a/src/modals/Job/EditJob.vue +++ b/src/modals/Job/EditJob.vue @@ -16,9 +16,9 @@ import { jobStore, navigationStore } from '../../store/store.js'
@@ -27,8 +27,9 @@ import { jobStore, navigationStore } from '../../store/store.js' :value.sync="jobStore.jobItem.description" />
- - +
@@ -74,6 +75,11 @@ export default { }, data() { return { + sourceItem: { + name: '', + description: '', + location: '', + }, success: false, loading: false, error: false, diff --git a/src/store/modules/job.js b/src/store/modules/job.js index e147b8c49..e808c0e66 100644 --- a/src/store/modules/job.js +++ b/src/store/modules/job.js @@ -6,7 +6,10 @@ export const useJobStore = defineStore( 'job', { state: () => ({ jobItem: false, + jobRun: false, jobList: [], + jobLog: false, + jobLogs: [], }), actions: { setJobItem(jobItem) { @@ -59,6 +62,21 @@ export const useJobStore = defineStore( throw err } }, + // New function to get source logs + async refreshJobLogs() { + const endpoint = `/index.php/apps/openconnector/api/jobs-logs/${this.jobItem.id}` + try { + const response = await fetch(endpoint, { + method: 'GET', + }) + const data = await response.json() + this.setJobLogs(data) + return data + } catch (err) { + console.error(err) + throw err + } + }, // Delete a job deleteJob() { if (!this.jobItem || !this.jobItem.id) { diff --git a/src/store/modules/source.js b/src/store/modules/source.js index ce5db6475..b7f561825 100644 --- a/src/store/modules/source.js +++ b/src/store/modules/source.js @@ -8,6 +8,8 @@ export const useSourceStore = defineStore( sourceItem: false, sourceTest: false, sourceList: [], + sourceLog: false, + sourceLogs: [], }), actions: { setSourceItem(sourceItem) { @@ -24,6 +26,14 @@ export const useSourceStore = defineStore( ) console.log('Source list set to ' + sourceList.length + ' items') }, + setSourceLog(sourceLog) { + this.sourceLog = sourceLog + console.log('Source log set') + }, + setSourceLogs(sourceLogs) { + this.sourceLogs = sourceLogs + console.log('Source logs set to ' + sourceLogs.length + ' items') + }, /* istanbul ignore next */ // ignore this for Jest until moved into a service async refreshSourceList(search = null) { // @todo this might belong in a service? @@ -64,6 +74,21 @@ export const useSourceStore = defineStore( throw err } }, + // New function to get source logs + async refreshSourceLogs() { + const endpoint = `/index.php/apps/openconnector/api/sources-logs/${this.sourceItem.id}` + try { + const response = await fetch(endpoint, { + method: 'GET', + }) + const data = await response.json() + this.setSourceLogs(data) + return data + } catch (err) { + console.error(err) + throw err + } + }, // Delete a source deleteSource() { if (!this.sourceItem || !this.sourceItem.id) { diff --git a/src/views/Job/JobDetails.vue b/src/views/Job/JobDetails.vue index 19c7e3837..e0c44769b 100644 --- a/src/views/Job/JobDetails.vue +++ b/src/views/Job/JobDetails.vue @@ -58,6 +58,9 @@ export default { Pencil, TrashCanOutline, }, + mounted() { + jobStore.refreshJobLogs() + }, } diff --git a/src/views/Job/JobsList.vue b/src/views/Job/JobsList.vue index a83803b8b..b15cd920a 100644 --- a/src/views/Job/JobsList.vue +++ b/src/views/Job/JobsList.vue @@ -50,13 +50,13 @@ import { jobStore, navigationStore, searchStore } from '../../store/store.js' - Bewerken + Edit - Verwijderen + Delete diff --git a/src/views/Source/SourceDetails.vue b/src/views/Source/SourceDetails.vue index 68ae1c625..ffe5cc2bc 100644 --- a/src/views/Source/SourceDetails.vue +++ b/src/views/Source/SourceDetails.vue @@ -86,10 +86,10 @@ import { sourceStore, navigationStore } from '../../store/store.js' -
- +
-
+
No logs found
@@ -137,6 +137,9 @@ export default { TrashCanOutline, Sync, }, + mounted() { + sourceStore.refreshSourceLogs() + }, }