From 51e549477d0bd2ca6c09dd20e6cb16edaa9fb8f0 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Thu, 26 Sep 2024 11:40:53 +0200 Subject: [PATCH 01/16] First call service setup --- appinfo/routes.php | 1 + lib/Controller/SourcesController.php | 91 +++++++ lib/Db/Source.php | 340 +++++++++++++++++++++++++ lib/Service/AuthenticationService.php | 11 + lib/Service/CallService.php | 124 +++++++++ lib/Service/MappingService.php | 11 + lib/Service/SynchronizationService.php | 11 + 7 files changed, 589 insertions(+) create mode 100644 lib/Service/AuthenticationService.php create mode 100644 lib/Service/CallService.php create mode 100644 lib/Service/MappingService.php create mode 100644 lib/Service/SynchronizationService.php diff --git a/appinfo/routes.php b/appinfo/routes.php index c735c636d..961211545 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -9,5 +9,6 @@ ], 'routes' => [ ['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'], + ['name' => 'sources#test', 'url' => '/api/source-test/{id}', 'verb' => 'POST'], ], ]; diff --git a/lib/Controller/SourcesController.php b/lib/Controller/SourcesController.php index b464f6e72..21f710d47 100644 --- a/lib/Controller/SourcesController.php +++ b/lib/Controller/SourcesController.php @@ -4,6 +4,7 @@ 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 OCP\AppFramework\Controller; @@ -162,4 +163,94 @@ public function destroy(int $id): JSONResponse return new JSONResponse([]); } + + /** + * 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 = JSONResponse($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'] ?? 'POST'; + + // 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 + $response = $callService->call($source, $data); + + // Map it back to json + $responseObject = [ + 'requestUrl' => $response->getEffectiveUri()->__toString(), + 'requestMethod' => $response->getRequest()->getMethod(), + 'statusCode' => $response->getStatusCode(), + 'statusMessage' => $response->getReasonPhrase(), + 'responseTime' => $response->getTransferTime(), + 'size' => $response->getBody()->getSize(), + 'remoteIp' => $response->getHeaderLine('X-Real-IP') ?: $response->getHeaderLine('X-Forwarded-For') ?: null, + 'headers' => $response->getHeaders(), + 'body' => $response->getBody()->getContents(), + ]; + + return new JSONResponse($responseObject); + } } \ No newline at end of file diff --git a/lib/Db/Source.php b/lib/Db/Source.php index 22182d082..1eeb69873 100644 --- a/lib/Db/Source.php +++ b/lib/Db/Source.php @@ -150,4 +150,344 @@ public function jsonSerialize(): array 'test' => $this->test ]; } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): void + { + $this->name = $name; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): void + { + $this->description = $description; + } + + public function getReference(): ?string + { + return $this->reference; + } + + public function setReference(?string $reference): void + { + $this->reference = $reference; + } + + public function getVersion(): ?string + { + return $this->version; + } + + public function setVersion(?string $version): void + { + $this->version = $version; + } + + public function getLocation(): ?string + { + return $this->location; + } + + public function setLocation(?string $location): void + { + $this->location = $location; + } + + public function getIsEnabled(): ?bool + { + return $this->isEnabled; + } + + public function setIsEnabled(?bool $isEnabled): void + { + $this->isEnabled = $isEnabled; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(?string $type): void + { + $this->type = $type; + } + + public function getAuthorizationHeader(): ?string + { + return $this->authorizationHeader; + } + + public function setAuthorizationHeader(?string $authorizationHeader): void + { + $this->authorizationHeader = $authorizationHeader; + } + + public function getAuth(): ?string + { + return $this->auth; + } + + public function setAuth(?string $auth): void + { + $this->auth = $auth; + } + + public function getAuthenticationConfig(): ?array + { + return $this->authenticationConfig; + } + + public function setAuthenticationConfig(?array $authenticationConfig): void + { + $this->authenticationConfig = $authenticationConfig; + } + + public function getAuthorizationPassthroughMethod(): ?string + { + return $this->authorizationPassthroughMethod; + } + + public function setAuthorizationPassthroughMethod(?string $authorizationPassthroughMethod): void + { + $this->authorizationPassthroughMethod = $authorizationPassthroughMethod; + } + + public function getLocale(): ?string + { + return $this->locale; + } + + public function setLocale(?string $locale): void + { + $this->locale = $locale; + } + + public function getAccept(): ?string + { + return $this->accept; + } + + public function setAccept(?string $accept): void + { + $this->accept = $accept; + } + + public function getJwt(): ?string + { + return $this->jwt; + } + + public function setJwt(?string $jwt): void + { + $this->jwt = $jwt; + } + + public function getJwtId(): ?string + { + return $this->jwtId; + } + + public function setJwtId(?string $jwtId): void + { + $this->jwtId = $jwtId; + } + + public function getSecret(): ?string + { + return $this->secret; + } + + public function setSecret(?string $secret): void + { + $this->secret = $secret; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(?string $username): void + { + $this->username = $username; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(?string $password): void + { + $this->password = $password; + } + + public function getApikey(): ?string + { + return $this->apikey; + } + + public function setApikey(?string $apikey): void + { + $this->apikey = $apikey; + } + + public function getDocumentation(): ?string + { + return $this->documentation; + } + + public function setDocumentation(?string $documentation): void + { + $this->documentation = $documentation; + } + + public function getLoggingConfig(): ?array + { + return $this->loggingConfig; + } + + public function setLoggingConfig(?array $loggingConfig): void + { + $this->loggingConfig = $loggingConfig; + } + + public function getOas(): ?string + { + return $this->oas; + } + + public function setOas(?string $oas): void + { + $this->oas = $oas; + } + + public function getPaths(): ?array + { + return $this->paths; + } + + public function setPaths(?array $paths): void + { + $this->paths = $paths; + } + + public function getHeaders(): ?array + { + return $this->headers; + } + + public function setHeaders(?array $headers): void + { + $this->headers = $headers; + } + + public function getTranslationConfig(): ?array + { + return $this->translationConfig; + } + + public function setTranslationConfig(?array $translationConfig): void + { + $this->translationConfig = $translationConfig; + } + + public function getConfiguration(): ?array + { + return $this->configuration; + } + + public function setConfiguration(?array $configuration): void + { + $this->configuration = $configuration; + } + + public function getEndpointsConfig(): ?array + { + return $this->endpointsConfig; + } + + public function setEndpointsConfig(?array $endpointsConfig): void + { + $this->endpointsConfig = $endpointsConfig; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus(?string $status): void + { + $this->status = $status; + } + + public function getLastCall(): ?DateTime + { + return $this->lastCall; + } + + public function setLastCall(?DateTime $lastCall): void + { + $this->lastCall = $lastCall; + } + + public function getLastSync(): ?DateTime + { + return $this->lastSync; + } + + public function setLastSync(?DateTime $lastSync): void + { + $this->lastSync = $lastSync; + } + + public function getObjectCount(): ?int + { + return $this->objectCount; + } + + public function setObjectCount(?int $objectCount): void + { + $this->objectCount = $objectCount; + } + + public function getDateCreated(): ?DateTime + { + return $this->dateCreated; + } + + public function setDateCreated(?DateTime $dateCreated): void + { + $this->dateCreated = $dateCreated; + } + + public function getDateModified(): ?DateTime + { + return $this->dateModified; + } + + public function setDateModified(?DateTime $dateModified): void + { + $this->dateModified = $dateModified; + } + + public function getTest(): ?bool + { + return $this->test; + } + + public function setTest(?bool $test): void + { + $this->test = $test; + } } \ No newline at end of file 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 @@ +authenticationService = $authenticationService; + $this->mappingService = $mappingService; + $this->client = new Client([]); + + }//end __construct() /** + + /** + * 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 + ): Response + { + $this->source = $source; + + if ($this->source->getIsEnabled() === null || $this->source->getIsEnabled() === false) { + throw new HttpException('409', "This source is not enabled: {$this->source->getName()}"); + } + + if (empty($this->source->getLocation()) === true) { + throw new HttpException('409', "This source has no location: {$this->source->getName()}"); + } + + // 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'] = []; + } + + // 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); + + // Set the request info array + $requestInfo = [ + 'url' => $url, + 'method' => $method, + ]; + + // Let's log the call. + $this->source->setLastCall(new \DateTime()); + // @todo: save the source + + // Let's make the call. + try { + if ($asynchronous === false) { + $response = $this->client->request($method, $url, $config); + } else { + return $this->client->requestAsync($method, $url, $config); + } + } catch (ClientException $e) { + // @todo: log the error + } + + return $response; + } +} diff --git a/lib/Service/MappingService.php b/lib/Service/MappingService.php new file mode 100644 index 000000000..ab4b08c30 --- /dev/null +++ b/lib/Service/MappingService.php @@ -0,0 +1,11 @@ + Date: Thu, 26 Sep 2024 13:56:45 +0200 Subject: [PATCH 02/16] Add the other services --- composer.json | 6 +- composer.lock | 240 ++++++++++++++- lib/Service/JobService.php | 11 + lib/Service/MappingService.php | 409 ++++++++++++++++++++++++- lib/Service/SynchronizationService.php | 236 +++++++++++++- 5 files changed, 891 insertions(+), 11 deletions(-) create mode 100644 lib/Service/JobService.php 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/lib/Service/JobService.php b/lib/Service/JobService.php new file mode 100644 index 000000000..94e42ff65 --- /dev/null +++ b/lib/Service/JobService.php @@ -0,0 +1,11 @@ +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 index 49150a3d1..62803db60 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -2,10 +2,238 @@ namespace OCA\OpenConnector\Service; +use OCA\OpenConnector\Db\Source; +use OCA\OpenConnector\Db\Synchronization; +use OCA\OpenConnector\Service\CallService; +use OCA\OpenConnector\Service\MappingService; +use GuzzleHttp\Exception\GuzzleException; +use Twig\Error\LoaderError; +use Twig\Error\SyntaxError; +use Adbar\Dot; +use DateInterval; +use DateTime; - -class SynchronizationService +/** + * Service to synchronize resources between gateway and external sources. + * + * This service provides synchronization functionality to synchronize between sources and the gateway. + * + * @author Robert Zondervan, Barry Brands, Ruben van der Linde + * @license EUPL + * + * @category Service + */ +class NewSynchronizationService { - -} + + public function __construct( + private readonly GatewayResourceService $resourceService, + private readonly CallService $callService, + private readonly SynchronizationService $synchronizationService, + private readonly LoggerInterface $synchronizationLogger, + private readonly EntityManagerInterface $entityManager, + private readonly MappingService $mappingService, + ) { + + }//end __construct() + + + /** + * Executes the synchronization from source to gateway. + * Slightly edited clone of the SynchronizationService in the gateway. + * + * @param Synchronization $synchronization The synchronization to update + * @param array $sourceObject The object in the source + * @param bool $unsafe Unset attributes that are not included in the hydrator array when calling the hydrate function + * + * @throws GuzzleException + * @throws LoaderError + * @throws SyntaxError + * + * @return Synchronization The updated synchronization + */ + public function synchronizeFromSource(Synchronization $synchronization, array $sourceObject=[], bool $unsafe=false): Synchronization + { + + + public function __construct( + private readonly GatewayResourceService $resourceService, + private readonly CallService $callService, + private readonly SynchronizationService $synchronizationService, + private readonly LoggerInterface $synchronizationLogger, + private readonly EntityManagerInterface $entityManager, + private readonly MappingService $mappingService, + ) { + + }//end __construct() + + + /** + * Executes the synchronization from source to gateway. + * Slightly edited clone of the SynchronizationService in the gateway. + * + * @param Synchronization $synchronization The synchronization to update + * @param array $sourceObject The object in the source + * @param bool $unsafe Unset attributes that are not included in the hydrator array when calling the hydrate function + * + * @throws GuzzleException + * @throws LoaderError + * @throws SyntaxError + * + * @return Synchronization The updated synchronization + */ + public function synchronizeFromSource(Synchronization $synchronization, array $sourceObject=[], bool $unsafe=false): Synchronization + { + $this->synchronizationLogger->info("handleSync for Synchronization with id = {$synchronization->getId()->toString()}"); + + // create new object if no object exists + if (!$synchronization->getObject()) { + isset($this->io) && $this->io->text('creating new objectEntity'); + $this->synchronizationLogger->info('creating new objectEntity'); + $object = new ObjectEntity($synchronization->getEntity()); + $object->addSynchronization($synchronization); + $this->entityManager->persist($object); + $this->entityManager->persist($synchronization); + $oldDateModified = null; + } else { + $oldDateModified = $synchronization->getObject()->getDateModified()->getTimestamp(); + } + + $sourceObject = $sourceObject ?: $this->synchronizationService->getSingleFromSource($synchronization); + + if ($sourceObject === null) { + $this->synchronizationLogger->warning("Can not handle Synchronization with id = {$synchronization->getId()->toString()} if \$sourceObject === null"); + + return $synchronization; + } + + // Let check + $now = new DateTime(); + $synchronization->setLastChecked($now); + + $sha = hash('sha256', json_encode($sourceObject)); + + // Checking if data on source has changed. + if ($synchronization->getSha() === $sha) { + return $synchronization; + } + + // Counter + $counter = ($synchronization->getTryCounter() + 1); + if ($counter > 10000) { + $counter = 10000; + } + + $synchronization->setTryCounter($counter); + + // Set dont try before, expensional so in minutes 1,8,27,64,125,216,343,512,729,1000 + $addMinutes = pow($counter, 3); + if ($synchronization->getDontSyncBefore()) { + $dontTryBefore = $synchronization->getDontSyncBefore()->add(new DateInterval('PT'.$addMinutes.'M')); + } else { + $dontTryBefore = new DateTime(); + } + + $synchronization->setDontSyncBefore($dontTryBefore); + + if ($synchronization->getMapping()) { + $sourceObject = $this->mappingService->mapping($synchronization->getMapping(), $sourceObject); + } + + $synchronization->getObject()->hydrate($sourceObject, $unsafe); + + $synchronization->setSha($sha); + + $this->entityManager->persist($synchronization->getObject()); + $this->entityManager->persist($synchronization); + + if ($oldDateModified !== $synchronization->getObject()->getDateModified()->getTimestamp()) { + $date = new DateTime(); + (isset($this->io) ?? $this->io->text("set new dateLastChanged to {$date->format('d-m-YTH:i:s')}")); + $synchronization->setLastSynced(new DateTime()); + $synchronization->setTryCounter(0); + } else { + (isset($this->io) ?? $this->io->text("lastSynced is still {$synchronization->getObject()->getDateModified()->format('d-m-YTH:i:s')}")); + } + + return $synchronization; + + }//end synchronizeFromSource() + + + /** + * Fetch data from source in a way that is as abstract as possible at this time. + * + * @param array $configuration + * @param Source $source + * @return array + * @throws Exception + */ + public function getResults(array $configuration, Source $source): array + { + $response = $this->callService->call(source: $source, endpoint: $configuration['endpoint'], method: $configuration['method'], config: ['json' => $configuration['body']]); + + $result = $this->callService->decodeResponse(source: $source, response: $response, contentType: ($configuration['content-type'] ?? 'application/json')); + + $resultDot = new Dot($result); + + if ($resultDot->has(keys: $configuration['resultsPath']) === true) { + $return = $resultDot->get(key: $configuration['resultsPath']); + if ($return instanceof Dot) { + return $return->jsonSerialize(); + } else if (is_array($return)) { + return $return; + } + } + + throw new Exception('No cases found'); + + }//end getResults() + + + /** + * This function is designed to in time replace the existing syncCollectionHandler. + * At the moment it depends on the in-gateway SynchronizationService, and is one way with the source as the leading version. + * + * @param array $data + * @param array $configuration + * @return array + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function synchronizeCollectionHandler(array $data, array $configuration): array + { + $source = $this->resourceService->getSource(reference: $configuration['source'], pluginName: "common-gateway/vrijbrp-to-zgw-bundle"); + $schema = $this->resourceService->getSchema(reference: $configuration['schema'], pluginName: "common-gateway/vrijbrp-to-zgw-bundle"); + + if (isset($configuration['mapping']) === true) { + $mapping = $this->resourceService->getMapping(reference: $configuration['mapping'], pluginName: "common-gateway/vrijbrp-to-zgw-bundle"); + } + + try { + $dossiers = $this->getResults(configuration: $configuration, source: $source); + } catch (Exception $exception) { + $this->synchronizationLogger->warning(message: $exception->getMessage(), context: ['plugin' => 'common-gateway/vrijbrp-to-zgw-bundle']); + return $data; + } + + foreach ($dossiers as $dossier) { + $dossierDot = new Dot($dossier); + + $synchronization = $this->synchronizationService->findSyncBySource(source: $source, entity: $schema, sourceId: $dossierDot[$configuration['idField']], endpoint: $configuration['endpoint']); + + if ($synchronization->getMapping() === null && isset($mapping) === true) { + $synchronization->setMapping($mapping); + } + + try { + $this->synchronizeFromSource(synchronization: $synchronization, sourceObject: $dossier); + } catch (Exception $exception) { + $this->synchronizationLogger->error(message: $exception->getMessage(), context: ['plugin' => 'common-gateway/vrijbrp-to-zgw-bundle']); + } + } + + return $data; + + }//end synchronizeCollectionHandler() + }//end class From 9ec6a69bc1024ff9162771e8a1f4510e304391eb Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 27 Sep 2024 00:16:41 +0200 Subject: [PATCH 03/16] First checks on the call service --- lib/Controller/SourcesController.php | 24 +- lib/Db/CallLog.php | 78 +++++ lib/Db/CallLogMapper.php | 72 ++++ lib/Db/Source.php | 340 ------------------- lib/Migration/Version0Date20240926235025.php | 85 +++++ lib/Service/CallService.php | 199 +++++++---- src/modals/Source/EditSource.vue | 11 +- src/modals/TestSource/TestSource.vue | 26 +- src/store/modules/source.js | 36 +- src/views/Source/SourceDetails.vue | 4 +- 10 files changed, 421 insertions(+), 454 deletions(-) create mode 100644 lib/Db/CallLog.php create mode 100644 lib/Db/CallLogMapper.php create mode 100644 lib/Migration/Version0Date20240926235025.php diff --git a/lib/Controller/SourcesController.php b/lib/Controller/SourcesController.php index 21f710d47..3f675dbd8 100644 --- a/lib/Controller/SourcesController.php +++ b/lib/Controller/SourcesController.php @@ -188,7 +188,7 @@ public function test(CallService $callService,int $id): JSONResponse { // get the source try { - $source = JSONResponse($this->sourceMapper->find(id: (int) $id)); + $source = $this->sourceMapper->find(id: (int) $id); } catch (DoesNotExistException $exception) { return new JSONResponse(data: ['error' => 'Not Found'], statusCode: 404); } @@ -210,7 +210,7 @@ public function test(CallService $callService,int $id): JSONResponse } // Set method, default to POST if not provided - $method = $requestData['method'] ?? 'POST'; + $method = $requestData['method'] ?? 'GET'; // Set endpoint $endpoint = $requestData['endpoint'] ?? ''; @@ -236,21 +236,11 @@ public function test(CallService $callService,int $id): JSONResponse } // fire the call - $response = $callService->call($source, $data); - - // Map it back to json - $responseObject = [ - 'requestUrl' => $response->getEffectiveUri()->__toString(), - 'requestMethod' => $response->getRequest()->getMethod(), - 'statusCode' => $response->getStatusCode(), - 'statusMessage' => $response->getReasonPhrase(), - 'responseTime' => $response->getTransferTime(), - 'size' => $response->getBody()->getSize(), - 'remoteIp' => $response->getHeaderLine('X-Real-IP') ?: $response->getHeaderLine('X-Forwarded-For') ?: null, - 'headers' => $response->getHeaders(), - 'body' => $response->getBody()->getContents(), - ]; + + $time_start = microtime(true); + $callLog = $callService->call($source, $endpoint, $method, $config); + $time_end = microtime(true); - return new JSONResponse($responseObject); + return new JSONResponse($callLog->jsonSerialize()); } } \ No newline at end of file diff --git a/lib/Db/CallLog.php b/lib/Db/CallLog.php new file mode 100644 index 000000000..2557c27d8 --- /dev/null +++ b/lib/Db/CallLog.php @@ -0,0 +1,78 @@ +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'); + $this->addType('updatedAt', '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, + 'updatedAt' => $this->updatedAt, + ]; + } +} \ No newline at end of file diff --git a/lib/Db/CallLogMapper.php b/lib/Db/CallLogMapper.php new file mode 100644 index 000000000..0e57ffdef --- /dev/null +++ b/lib/Db/CallLogMapper.php @@ -0,0 +1,72 @@ +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); + } +} \ No newline at end of file diff --git a/lib/Db/Source.php b/lib/Db/Source.php index 1eeb69873..22182d082 100644 --- a/lib/Db/Source.php +++ b/lib/Db/Source.php @@ -150,344 +150,4 @@ public function jsonSerialize(): array 'test' => $this->test ]; } - - public function getName(): ?string - { - return $this->name; - } - - public function setName(?string $name): void - { - $this->name = $name; - } - - public function getDescription(): ?string - { - return $this->description; - } - - public function setDescription(?string $description): void - { - $this->description = $description; - } - - public function getReference(): ?string - { - return $this->reference; - } - - public function setReference(?string $reference): void - { - $this->reference = $reference; - } - - public function getVersion(): ?string - { - return $this->version; - } - - public function setVersion(?string $version): void - { - $this->version = $version; - } - - public function getLocation(): ?string - { - return $this->location; - } - - public function setLocation(?string $location): void - { - $this->location = $location; - } - - public function getIsEnabled(): ?bool - { - return $this->isEnabled; - } - - public function setIsEnabled(?bool $isEnabled): void - { - $this->isEnabled = $isEnabled; - } - - public function getType(): ?string - { - return $this->type; - } - - public function setType(?string $type): void - { - $this->type = $type; - } - - public function getAuthorizationHeader(): ?string - { - return $this->authorizationHeader; - } - - public function setAuthorizationHeader(?string $authorizationHeader): void - { - $this->authorizationHeader = $authorizationHeader; - } - - public function getAuth(): ?string - { - return $this->auth; - } - - public function setAuth(?string $auth): void - { - $this->auth = $auth; - } - - public function getAuthenticationConfig(): ?array - { - return $this->authenticationConfig; - } - - public function setAuthenticationConfig(?array $authenticationConfig): void - { - $this->authenticationConfig = $authenticationConfig; - } - - public function getAuthorizationPassthroughMethod(): ?string - { - return $this->authorizationPassthroughMethod; - } - - public function setAuthorizationPassthroughMethod(?string $authorizationPassthroughMethod): void - { - $this->authorizationPassthroughMethod = $authorizationPassthroughMethod; - } - - public function getLocale(): ?string - { - return $this->locale; - } - - public function setLocale(?string $locale): void - { - $this->locale = $locale; - } - - public function getAccept(): ?string - { - return $this->accept; - } - - public function setAccept(?string $accept): void - { - $this->accept = $accept; - } - - public function getJwt(): ?string - { - return $this->jwt; - } - - public function setJwt(?string $jwt): void - { - $this->jwt = $jwt; - } - - public function getJwtId(): ?string - { - return $this->jwtId; - } - - public function setJwtId(?string $jwtId): void - { - $this->jwtId = $jwtId; - } - - public function getSecret(): ?string - { - return $this->secret; - } - - public function setSecret(?string $secret): void - { - $this->secret = $secret; - } - - public function getUsername(): ?string - { - return $this->username; - } - - public function setUsername(?string $username): void - { - $this->username = $username; - } - - public function getPassword(): ?string - { - return $this->password; - } - - public function setPassword(?string $password): void - { - $this->password = $password; - } - - public function getApikey(): ?string - { - return $this->apikey; - } - - public function setApikey(?string $apikey): void - { - $this->apikey = $apikey; - } - - public function getDocumentation(): ?string - { - return $this->documentation; - } - - public function setDocumentation(?string $documentation): void - { - $this->documentation = $documentation; - } - - public function getLoggingConfig(): ?array - { - return $this->loggingConfig; - } - - public function setLoggingConfig(?array $loggingConfig): void - { - $this->loggingConfig = $loggingConfig; - } - - public function getOas(): ?string - { - return $this->oas; - } - - public function setOas(?string $oas): void - { - $this->oas = $oas; - } - - public function getPaths(): ?array - { - return $this->paths; - } - - public function setPaths(?array $paths): void - { - $this->paths = $paths; - } - - public function getHeaders(): ?array - { - return $this->headers; - } - - public function setHeaders(?array $headers): void - { - $this->headers = $headers; - } - - public function getTranslationConfig(): ?array - { - return $this->translationConfig; - } - - public function setTranslationConfig(?array $translationConfig): void - { - $this->translationConfig = $translationConfig; - } - - public function getConfiguration(): ?array - { - return $this->configuration; - } - - public function setConfiguration(?array $configuration): void - { - $this->configuration = $configuration; - } - - public function getEndpointsConfig(): ?array - { - return $this->endpointsConfig; - } - - public function setEndpointsConfig(?array $endpointsConfig): void - { - $this->endpointsConfig = $endpointsConfig; - } - - public function getStatus(): ?string - { - return $this->status; - } - - public function setStatus(?string $status): void - { - $this->status = $status; - } - - public function getLastCall(): ?DateTime - { - return $this->lastCall; - } - - public function setLastCall(?DateTime $lastCall): void - { - $this->lastCall = $lastCall; - } - - public function getLastSync(): ?DateTime - { - return $this->lastSync; - } - - public function setLastSync(?DateTime $lastSync): void - { - $this->lastSync = $lastSync; - } - - public function getObjectCount(): ?int - { - return $this->objectCount; - } - - public function setObjectCount(?int $objectCount): void - { - $this->objectCount = $objectCount; - } - - public function getDateCreated(): ?DateTime - { - return $this->dateCreated; - } - - public function setDateCreated(?DateTime $dateCreated): void - { - $this->dateCreated = $dateCreated; - } - - public function getDateModified(): ?DateTime - { - return $this->dateModified; - } - - public function setDateModified(?DateTime $dateModified): void - { - $this->dateModified = $dateModified; - } - - public function getTest(): ?bool - { - return $this->test; - } - - public function setTest(?bool $test): void - { - $this->test = $test; - } } \ No newline at end of file diff --git a/lib/Migration/Version0Date20240926235025.php b/lib/Migration/Version0Date20240926235025.php new file mode 100644 index 000000000..e71cc8f70 --- /dev/null +++ b/lib/Migration/Version0Date20240926235025.php @@ -0,0 +1,85 @@ +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('updated_at', 'datetime', [ + 'notnull' => true, + 'default' => 'CURRENT_TIMESTAMP' + ]); + + $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'); + } + + return $schema; + } +} \ No newline at end of file diff --git a/lib/Service/CallService.php b/lib/Service/CallService.php index 6d4357a2c..45386d5fd 100644 --- a/lib/Service/CallService.php +++ b/lib/Service/CallService.php @@ -12,113 +12,162 @@ use GuzzleHttp\Exception\ServerException; use GuzzleHttp\Promise\Promise; use GuzzleHttp\Psr7\Response; +use OCA\OpenConnector\Db\CallLog; +use OCA\OpenConnector\Db\CallLogMapper; class CallService { - /** - * The constructor sets al needed variables. - * - * @param AuthenticationService $authenticationService The authentication service - * @param MappingService $mappingService The mapping service - */ - public function __construct( - AuthenticationService $authenticationService, - MappingService $mappingService, - LoggerInterface $callLogger - ) { - $this->authenticationService = $authenticationService; - $this->mappingService = $mappingService; - $this->client = new Client([]); - - }//end __construct() /** + private $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 - ): Response + * The constructor sets al needed variables. + * + * @param AuthenticationService $authenticationService The authentication service + * @param MappingService $mappingService The mapping service + */ + public function __construct(CallLogMapper $callLogMapper) { - $this->source = $source; + $this->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) { - throw new HttpException('409', "This source is not enabled: {$this->source->getName()}"); - } + // 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) { - throw new HttpException('409', "This source has no location: {$this->source->getName()}"); - } + 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()); - } + 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']; - } + $overwriteContentType = $config['headers']['Content-Type']; + } // decapiitilized fall back for content-type - if (isset($config['headers']['content-type']) === true) { - $overwriteContentType = $config['headers']['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]; - } + 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'] = []; - } + 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); - - // Set the request info array - $requestInfo = [ - 'url' => $url, - 'method' => $method, - ]; + // 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. + // 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 (ClientException $e) { - // @todo: log the error + 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(); } - return $response; + $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()); + $callLog->setUpdatedAt(new \DateTime()); + + $this->callLogMapper->insert($callLog); + + return $callLog; } } diff --git a/src/modals/Source/EditSource.vue b/src/modals/Source/EditSource.vue index 34d8a17fd..5567c8b97 100644 --- a/src/modals/Source/EditSource.vue +++ b/src/modals/Source/EditSource.vue @@ -30,19 +30,20 @@ import { sourceStore, navigationStore } from '../../store/store.js' + id="location" + label="location*" + :value.sync="sourceItem.location" /> Test connection + + +

The connection to the source was successful.

+
+ +

An error occurred while testing the connection: {{ sourceStore.sourceTest ? sourceStore.sourceTest.response.statusMessage : error }}

+
+ +
+

Status: {{ sourceStore.sourceTest.response.statusMessage }} ({{ sourceStore.sourceTest.response.statusCode }})

+

Response time: {{ sourceStore.sourceTest.response.responseTime }} (Milliseconds)

+

Size: {{ sourceStore.sourceTest.response.size }} (Bytes)

+

Remote IP: {{ sourceStore.sourceTest.response.remoteIp }}

+

Headers: {{ sourceStore.sourceTest.response.headers }}

+

Body: {{ sourceStore.sourceTest.response.body }}

+
@@ -58,6 +73,7 @@ import { NcLoadingIcon, NcTextField, NcTextArea, + NcNoteCard, } from '@nextcloud/vue' import Sync from 'vue-material-design-icons/Sync.vue' @@ -70,6 +86,7 @@ export default { NcLoadingIcon, NcTextField, NcTextArea, + NcNoteCard, }, data() { return { @@ -99,7 +116,7 @@ export default { { id: 'PUT', label: 'PUT' }, { id: 'DELETE', label: 'DELETE' }, ], - value: { id: 'POST', label: 'POST' }, + value: { id: 'GET', label: 'GET' }, }, } @@ -129,11 +146,12 @@ export default { // Close modal or show success message this.success = true this.loading = false - setTimeout(this.closeModal, 2000) + this.error = false } catch (error) { this.loading = false this.success = false this.error = error.message || 'Er is een fout opgetreden bij het opslaan van de bron' + sourceStore.setSourceTest(false) } }, }, diff --git a/src/store/modules/source.js b/src/store/modules/source.js index 3e7c4e257..ce5db6475 100644 --- a/src/store/modules/source.js +++ b/src/store/modules/source.js @@ -6,6 +6,7 @@ export const useSourceStore = defineStore( 'source', { state: () => ({ sourceItem: false, + sourceTest: false, sourceList: [], }), actions: { @@ -13,6 +14,10 @@ export const useSourceStore = defineStore( this.sourceItem = sourceItem && new Source(sourceItem) console.log('Active source item set to ' + sourceItem) }, + setSourceTest(sourceTest) { + this.sourceTest = sourceTest + console.log('Source test set to ' + sourceTest) + }, setSourceList(sourceList) { this.sourceList = sourceList.map( (sourceItem) => new Source(sourceItem), @@ -81,29 +86,34 @@ export const useSourceStore = defineStore( }) }, // Test a source - testSource(sourceItem) { - if (!sourceItem) { + testSource(testSourceItem) { + if (!this.sourceItem) { throw new Error('No source item to test') } + if (!testSourceItem) { + throw new Error('No testobject to test') + } console.log('Testing source...') - const endpoint = `/index.php/apps/openconnector/api/source-test/${sourceItem.id}` + const endpoint = `/index.php/apps/openconnector/api/source-test/${this.sourceItem.id}` return fetch(endpoint, { - method: sourceItem.method, - body: sourceItem.body, + method: 'POST', headers: { - 'x-endpoint': sourceItem.endpoint, - 'x-method': sourceItem.method, + 'Content-Type': 'application/json', }, + body: JSON.stringify(testSourceItem), }) - .then((response) => { - console.log('response', response) - this.refreshSourceList() + .then((response) => response.json()) + .then((data) => { + this.setSourceTest(data) + console.log('Source tested') + // Refresh the source list + return this.refreshSourceList() }) .catch((err) => { - console.error('Error testing source:', err) + console.error('Error saving source:', err) throw err }) }, @@ -129,6 +139,10 @@ export const useSourceStore = defineStore( } }) + // remove the dateCreated and dateModified fields + delete sourceToSave.dateCreated + delete sourceToSave.dateModified + return fetch( endpoint, { diff --git a/src/views/Source/SourceDetails.vue b/src/views/Source/SourceDetails.vue index 6c2e10ef3..68ae1c625 100644 --- a/src/views/Source/SourceDetails.vue +++ b/src/views/Source/SourceDetails.vue @@ -39,8 +39,8 @@ import { sourceStore, navigationStore } from '../../store/store.js'
- URL: -

{{ sourceStore.sourceItem.url }}

+ location: +

{{ sourceStore.sourceItem.location }}

From 76f229da649f78def421763f033ecb19e1a463b8 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 27 Sep 2024 00:34:22 +0200 Subject: [PATCH 04/16] Add route for getting the logs for a source --- appinfo/routes.php | 1 + lib/Controller/SourcesController.php | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/appinfo/routes.php b/appinfo/routes.php index 961211545..ef695ea17 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -10,5 +10,6 @@ 'routes' => [ ['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'], ['name' => 'sources#test', 'url' => '/api/source-test/{id}', 'verb' => 'POST'], + ['name' => 'sources#logs', 'url' => '/api/sources/{id}/logs', 'verb' => 'GET'], ], ]; diff --git a/lib/Controller/SourcesController.php b/lib/Controller/SourcesController.php index 3f675dbd8..74de6c527 100644 --- a/lib/Controller/SourcesController.php +++ b/lib/Controller/SourcesController.php @@ -164,6 +164,28 @@ 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 * From d534a35b3d7950470aea87a03927d139c82eca2d Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 27 Sep 2024 11:43:12 +0200 Subject: [PATCH 05/16] Wip Jobs - Refactor - Service - Loging --- lib/Db/CallLog.php | 3 - lib/Db/Job.php | 78 +++++------------- lib/Db/JobLog.php | 75 +++++++++++++++++ lib/Db/JobLogMapper.php | 72 +++++++++++++++++ lib/Migration/Version0Date20240826193657.php | 69 +++++++++++----- lib/Migration/Version0Date20240926235025.php | 85 -------------------- src/modals/Job/EditJob.vue | 9 ++- 7 files changed, 218 insertions(+), 173 deletions(-) create mode 100644 lib/Db/JobLog.php create mode 100644 lib/Db/JobLogMapper.php delete mode 100644 lib/Migration/Version0Date20240926235025.php diff --git a/lib/Db/CallLog.php b/lib/Db/CallLog.php index 2557c27d8..25139a062 100644 --- a/lib/Db/CallLog.php +++ b/lib/Db/CallLog.php @@ -16,7 +16,6 @@ class CallLog extends Entity implements JsonSerializable protected ?int $actionId = null; protected ?int $synchronizationId = null; protected ?DateTime $createdAt = null; - protected ?DateTime $updatedAt = null; public function __construct() { $this->addType('statusCode', 'integer'); @@ -27,7 +26,6 @@ public function __construct() { $this->addType('actionId', 'integer'); $this->addType('synchronizationId', 'integer'); $this->addType('createdAt', 'datetime'); - $this->addType('updatedAt', 'datetime'); } public function getJsonFields(): array @@ -72,7 +70,6 @@ public function jsonSerialize(): array 'actionId' => $this->actionId, 'synchronizationId' => $this->synchronizationId, 'createdAt' => $this->createdAt, - 'updatedAt' => $this->updatedAt, ]; } } \ No newline at end of file diff --git a/lib/Db/Job.php b/lib/Db/Job.php index c75a7ba8d..380667233 100644 --- a/lib/Db/Job.php +++ b/lib/Db/Job.php @@ -10,54 +10,26 @@ 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 ?integer $interval = null; + protected ?bool $timeSensitive = true; + protected ?bool $allowParallelRuns = false; + protected ?bool $isEnabled = true; 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 ?DateTime $created = null; + protected ?DateTime $updated = null; 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('interval', 'integer'); + $this->addType('timeSensitive', 'boolean'); + $this->addType('allowParallelRuns', 'boolean'); + $this->addType('isEnabled', 'boolean'); $this->addType('userId', 'string'); - $this->addType('throws', 'string'); $this->addType('data', 'json'); - $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 +68,14 @@ public function jsonSerialize(): array 'id' => $this->id, 'name' => $this->name, 'description' => $this->description, - 'reference' => $this->reference, - 'version' => $this->version, - 'crontab' => $this->crontab, + 'interval' => $this->interval, + 'timeSensitive' => $this->timeSensitive, + 'allowParallelRuns' => $this->allowParallelRuns, + 'isEnabled' => $this->isEnabled, 'userId' => $this->userId, - 'throws' => $this->throws, 'data' => $this->data, - '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..25139a062 --- /dev/null +++ b/lib/Db/JobLog.php @@ -0,0 +1,75 @@ +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/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/Migration/Version0Date20240826193657.php b/lib/Migration/Version0Date20240826193657.php index cf6e160af..a160537f0 100644 --- a/lib/Migration/Version0Date20240826193657.php +++ b/lib/Migration/Version0Date20240826193657.php @@ -46,28 +46,14 @@ 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('interval', Types::INTEGER, ['notnull' => true]); + $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('user_id', Types::STRING, ['notnull' => false, 'length' => 255]); - $table->addColumn('throws', Types::TEXT, ['notnull' => false]); $table->addColumn('data', Types::TEXT, ['notnull' => false]); - $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->addColumn('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); + $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); } @@ -181,7 +167,48 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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_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->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'); + } return $schema; } diff --git a/lib/Migration/Version0Date20240926235025.php b/lib/Migration/Version0Date20240926235025.php deleted file mode 100644 index e71cc8f70..000000000 --- a/lib/Migration/Version0Date20240926235025.php +++ /dev/null @@ -1,85 +0,0 @@ -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('updated_at', 'datetime', [ - 'notnull' => true, - 'default' => 'CURRENT_TIMESTAMP' - ]); - - $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'); - } - - return $schema; - } -} \ No newline at end of file diff --git a/src/modals/Job/EditJob.vue b/src/modals/Job/EditJob.vue index f61611537..97d7d0ad8 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" />
- - +
From 173cb4d5f574674d42597fcc075a4fd7f3382d56 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 27 Sep 2024 13:12:14 +0200 Subject: [PATCH 06/16] Very small but essential fixes Job cretion now works --- lib/Db/Job.php | 2 +- src/modals/Job/EditJob.vue | 7 ++++++- src/views/Job/JobsList.vue | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/Db/Job.php b/lib/Db/Job.php index 380667233..6caf5635e 100644 --- a/lib/Db/Job.php +++ b/lib/Db/Job.php @@ -10,7 +10,7 @@ class Job extends Entity implements JsonSerializable { protected ?string $name = null; protected ?string $description = null; - protected ?integer $interval = null; + protected ?int $interval = null; protected ?bool $timeSensitive = true; protected ?bool $allowParallelRuns = false; protected ?bool $isEnabled = true; diff --git a/src/modals/Job/EditJob.vue b/src/modals/Job/EditJob.vue index 97d7d0ad8..59d0d1edb 100644 --- a/src/modals/Job/EditJob.vue +++ b/src/modals/Job/EditJob.vue @@ -29,7 +29,7 @@ import { jobStore, navigationStore } from '../../store/store.js'
+ :value.sync="jobStore.jobItem.interval" />
@@ -75,6 +75,11 @@ export default { }, data() { return { + sourceItem: { + name: '', + description: '', + location: '', + }, success: false, loading: false, error: false, 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 From 7b5da788aae523e6c43e3599c732508fdcc03158 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 29 Sep 2024 22:18:13 +0200 Subject: [PATCH 07/16] Nexcloud background jobs as actions Converting nexcloud background jobs to actions --- lib/Cron/ActionTask.php | 70 ++++++++++++++++++++++++++ lib/Cron/LogCleanUpTask.php | 15 ++++++ lib/Db/CallLogMapper.php | 7 +++ lib/Db/Job.php | 38 ++++++++++---- lib/Service/JobService.php | 46 ++++++++++++++++- lib/Service/SynchronizationService.php | 1 - 6 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 lib/Cron/ActionTask.php create mode 100644 lib/Cron/LogCleanUpTask.php diff --git a/lib/Cron/ActionTask.php b/lib/Cron/ActionTask.php new file mode 100644 index 000000000..292e6cabd --- /dev/null +++ b/lib/Cron/ActionTask.php @@ -0,0 +1,70 @@ +callService = $callService; + $this->sourceMapper = $sourceMapper; + $this->jobMapper = $jobMapper; + // 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(array $arguments) + { + // lets get the job + $job = $this->jobMapper->find($arguments['jobId']); + // If the job is not enabled, we don't need to do anything + if (!$job->isEnabled()) { + 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; + } + + // For now we only have one action, so this is a bit overkill, but it's a good starting point + $source = $this->sourceMapper->find($arguments['sourceId']); + $this->callService->call($source); + + // @todo: instead get the actual call an run that + + // Update the job + $job->setLastRun($this->time->getTime()); + $job->setNextRun($this->time->getTime() + $job->getInterval()); + $this->jobMapper->update($job); + } + +} diff --git a/lib/Cron/LogCleanUpTask.php b/lib/Cron/LogCleanUpTask.php new file mode 100644 index 000000000..a2e109412 --- /dev/null +++ b/lib/Cron/LogCleanUpTask.php @@ -0,0 +1,15 @@ +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 6caf5635e..45001a5f7 100644 --- a/lib/Db/Job.php +++ b/lib/Db/Job.php @@ -10,24 +10,36 @@ class Job extends Entity implements JsonSerializable { protected ?string $name = null; protected ?string $description = null; - protected ?int $interval = null; - protected ?bool $timeSensitive = true; - protected ?bool $allowParallelRuns = false; - protected ?bool $isEnabled = true; - protected ?string $userId = null; - protected ?array $data = null; - protected ?DateTime $created = null; - protected ?DateTime $updated = null; + protected ?string $jobClass = 'OCA\OpenConnector\Cron\ActionTask'; + 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 ?DateTime $scheduleAfter = null; // if the job should be executed after a certain date and time + protected ?string $userId = null; // the uner wich the job is running for security reasons + protected ?string $jobListId = null; // the id of the job in the job list + 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('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('scheduleAfter', 'datetime'); $this->addType('userId', 'string'); - $this->addType('data', 'json'); + $this->addType('jobListId', 'string'); + $this->addType('lastRun', 'datetime'); + $this->addType('nextRun', 'datetime'); $this->addType('created', 'datetime'); $this->addType('updated', 'datetime'); } @@ -68,12 +80,18 @@ public function jsonSerialize(): array 'id' => $this->id, 'name' => $this->name, 'description' => $this->description, + 'jobClass' => $this->jobClass, + 'arguments' => $this->arguments, 'interval' => $this->interval, + 'executionTime' => $this->executionTime, 'timeSensitive' => $this->timeSensitive, 'allowParallelRuns' => $this->allowParallelRuns, 'isEnabled' => $this->isEnabled, + 'scheduleAfter' => $this->scheduleAfter, 'userId' => $this->userId, - 'data' => $this->data, + 'jobListId' => $this->jobListId, + 'lastRun' => $this->lastRun, + 'nextRun' => $this->nextRun, 'created' => $this->created, 'updated' => $this->updated, ]; diff --git a/lib/Service/JobService.php b/lib/Service/JobService.php index 94e42ff65..d00987c3a 100644 --- a/lib/Service/JobService.php +++ b/lib/Service/JobService.php @@ -2,10 +2,54 @@ namespace OCA\OpenConnector\Service; + +use OCA\OpenConnector\Cron\ActionTask; use OCA\OpenConnector\Db\Job; +use OCA\OpenConnector\Db\JobMapper; +use OCP\BackgroundJob\IJobList; class JobService { - + private IJobList $jobList; + private JobMapper $jobMapper; + + public function __construct( IJobList $jobList, JobMapper $jobMapper) { + + $this->jobList = $jobList; + $this->jobMapper = $jobMapper; + } + + public function scheduleJob(Job $job): Job + { + // Lets first check if the job should be disabled + if (!$job->isEnabled() || $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 + $actionTask = new ActionTask(); + $arguments = $job->getArguments(); + $arguments['jobId'] = $job->getId(); + + if(!$job->getScheduleAfter()) { + $iJob = $this->jobList->add($actionTask::class, $arguments); + } else { + $runAfter = $job->getScheduleAfter()->getTimestamp(); + $iJob = $this->jobList->scheduleAfter($actionTask::class, $runAfter, $arguments); + } + + // Save the job to the database + $job->setJobListId($iJob->getId()); + return $this->jobMapper->save($job); + // $this->jobList->add($job->getJobClass(), $job->getArguments()); + } } diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php index 62803db60..ac4c06dbf 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -26,7 +26,6 @@ class NewSynchronizationService { - public function __construct( private readonly GatewayResourceService $resourceService, private readonly CallService $callService, From 71061493c72deb87e6c25ca11fddcd9d1c2ba337 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 29 Sep 2024 22:31:23 +0200 Subject: [PATCH 08/16] Add loging to the jobs --- lib/Cron/ActionTask.php | 37 ++++++++++++++++++++++++---- lib/Db/Job.php | 7 ++++-- lib/Db/JobLog.php | 53 ++++++++++++++++++++++------------------- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/lib/Cron/ActionTask.php b/lib/Cron/ActionTask.php index 292e6cabd..7bca627a4 100644 --- a/lib/Cron/ActionTask.php +++ b/lib/Cron/ActionTask.php @@ -5,6 +5,8 @@ use OCA\MyApp\Service\CallService; use OCA\MyApp\DB\SourceMapper; use OCA\MyApp\DB\JobMapper; +use OCA\MyApp\DB\JobLog; +use OCA\MyApp\DB\JobLogMapper; use OCP\BackgroundJob\TimedJob; use OCP\AppFramework\Utility\ITimeFactory; @@ -18,17 +20,20 @@ class ActionTask extends TimedJob private CallService $callService; private SourceMapper $sourceMapper; private JobMapper $jobMapper; - + private JobLogMapper $jobLogMapper; + public function __construct( ITimeFactory $time, CallService $callService, SourceMapper $sourceMapper, - JobMapper $jobMapper + JobMapper $jobMapper, + JobLogMapper $jobLogMapper ) { parent::__construct($time); $this->callService = $callService; $this->sourceMapper = $sourceMapper; $this->jobMapper = $jobMapper; + $this->jobLogMapper = $jobLogMapper; // Run every 5 minutes $this->setInterval(300); @@ -55,16 +60,40 @@ public function run(array $arguments) return; } + $time_start = microtime(true); + // For now we only have one action, so this is a bit overkill, but it's a good starting point - $source = $this->sourceMapper->find($arguments['sourceId']); - $this->callService->call($source); + if (isset($arguments['sourceId']) && is_int($arguments['sourceId'])) { + $source = $this->sourceMapper->find($arguments['sourceId']); + $this->callService->call($source); + } // @todo: instead get the actual call an run that + $time_end = microtime(true); + $executionTime = $time_end - $time_start; + + // 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); } } diff --git a/lib/Db/Job.php b/lib/Db/Job.php index 45001a5f7..2d7e90271 100644 --- a/lib/Db/Job.php +++ b/lib/Db/Job.php @@ -10,15 +10,16 @@ class Job extends Entity implements JsonSerializable { protected ?string $name = null; protected ?string $description = null; - protected ?string $jobClass = 'OCA\OpenConnector\Cron\ActionTask'; + 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 uner wich the job is running for security reasons + 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 ?DateTime $lastRun = null; // the last time the job was run protected ?DateTime $nextRun = null; // the next time the job will be run @@ -35,6 +36,7 @@ public function __construct() { $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('jobListId', 'string'); @@ -87,6 +89,7 @@ public function jsonSerialize(): array 'timeSensitive' => $this->timeSensitive, 'allowParallelRuns' => $this->allowParallelRuns, 'isEnabled' => $this->isEnabled, + 'singleRun' => $this->singleRun, 'scheduleAfter' => $this->scheduleAfter, 'userId' => $this->userId, 'jobListId' => $this->jobListId, diff --git a/lib/Db/JobLog.php b/lib/Db/JobLog.php index 25139a062..7730c5bc9 100644 --- a/lib/Db/JobLog.php +++ b/lib/Db/JobLog.php @@ -6,26 +6,28 @@ use JsonSerializable; use OCP\AppFramework\Db\Entity; -class CallLog extends Entity implements JsonSerializable +class JobLog extends Entity implements JsonSerializable { - protected ?int $statusCode = null; - protected ?string $statusMessage = null; - protected ?array $request = null; - protected ?array $response = null; - protected ?int $sourceId = null; - protected ?int $actionId = null; - protected ?int $synchronizationId = null; - protected ?DateTime $createdAt = null; + protected ?string $jobId = null; // the id of the job in the job + protected ?string $jobListId = null; // the id of the job in the job list + protected ?string $jobClass = 'OCA\OpenConnector\Action\PingAction'; + protected ?array $arguments = null; + protected ?int $executionTime = 3600; // the execution time in seconds + protected ?string $userId = null; // the user which the job is running for security reasons + 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 public function __construct() { - $this->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'); + $this->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 @@ -62,14 +64,15 @@ 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, + '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 From ef9046f95603bf8f4a5b0547e8abe49180f108de Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 29 Sep 2024 23:18:40 +0200 Subject: [PATCH 09/16] Putting it al together --- lib/Controller/JobsController.php | 73 ++++++++++++++++++-- lib/Cron/ActionTask.php | 3 + lib/Migration/Version0Date20240826193657.php | 29 +++++++- src/entities/callLog/callLog.mock.ts | 37 ++++++++++ src/entities/callLog/callLog.ts | 57 +++++++++++++++ src/entities/callLog/callLog.types.ts | 16 +++++ src/entities/callLog/callLogspec.ts | 22 ++++++ src/entities/callLog/index.js | 4 ++ src/entities/index.js | 2 + src/entities/job/job.mock.ts | 32 +++++++-- src/entities/job/job.ts | 70 +++++++++---------- src/entities/job/job.types.ts | 31 ++++----- src/entities/jobLog/index.js | 4 ++ src/entities/jobLog/jobLog.mock.ts | 31 +++++++++ src/entities/jobLog/jobLog.spec.ts | 22 ++++++ src/entities/jobLog/jobLog.ts | 47 +++++++++++++ src/entities/jobLog/jobLog.types.ts | 12 ++++ 17 files changed, 424 insertions(+), 68 deletions(-) create mode 100644 src/entities/callLog/callLog.mock.ts create mode 100644 src/entities/callLog/callLog.ts create mode 100644 src/entities/callLog/callLog.types.ts create mode 100644 src/entities/callLog/callLogspec.ts create mode 100644 src/entities/callLog/index.js create mode 100644 src/entities/jobLog/index.js create mode 100644 src/entities/jobLog/jobLog.mock.ts create mode 100644 src/entities/jobLog/jobLog.spec.ts create mode 100644 src/entities/jobLog/jobLog.ts create mode 100644 src/entities/jobLog/jobLog.types.ts 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/Cron/ActionTask.php b/lib/Cron/ActionTask.php index 7bca627a4..d8c3a4c19 100644 --- a/lib/Cron/ActionTask.php +++ b/lib/Cron/ActionTask.php @@ -94,6 +94,9 @@ public function run(array $arguments) $jobLog->setNextRun($job->getNextRun()); $jobLog->setExecutionTime($executionTime); $this->jobLogMapper->insert($jobLog); + + // Lets report back about what we have just done + return $jobLog; } } diff --git a/lib/Migration/Version0Date20240826193657.php b/lib/Migration/Version0Date20240826193657.php index a160537f0..92480481c 100644 --- a/lib/Migration/Version0Date20240826193657.php +++ b/lib/Migration/Version0Date20240826193657.php @@ -46,12 +46,19 @@ 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('interval', Types::INTEGER, ['notnull' => true]); + $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('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('created', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->addColumn('updated', Types::DATETIME, ['notnull' => true, 'default' => 'CURRENT_TIMESTAMP']); $table->setPrimaryKey(['id']); @@ -210,6 +217,24 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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->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'); + } + return $schema; } 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..cf4ceb2ea 100644 --- a/src/entities/job/job.ts +++ b/src/entities/job/job.ts @@ -6,62 +6,54 @@ 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 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.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..2a7e509c0 100644 --- a/src/entities/job/job.types.ts +++ b/src/entities/job/job.types.ts @@ -2,26 +2,19 @@ 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 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 +} From 3b347b9c0f504c8d57cbe04e203d1cc4699123a8 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 30 Sep 2024 00:09:30 +0200 Subject: [PATCH 10/16] Testing for bugs --- appinfo/routes.php | 4 +++- lib/Cron/ActionTask.php | 30 +++++++++++++++++------------- lib/Cron/LogCleanUpTask.php | 2 +- lib/Service/CallService.php | 1 - lib/Service/JobService.php | 15 ++++++++------- src/store/modules/job.js | 3 +++ src/store/modules/source.js | 25 +++++++++++++++++++++++++ 7 files changed, 57 insertions(+), 23 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index ef695ea17..a07197c6d 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -10,6 +10,8 @@ 'routes' => [ ['name' => 'dashboard#page', 'url' => '/', 'verb' => 'GET'], ['name' => 'sources#test', 'url' => '/api/source-test/{id}', 'verb' => 'POST'], - ['name' => 'sources#logs', 'url' => '/api/sources/{id}/logs', 'verb' => 'GET'], + ['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/lib/Cron/ActionTask.php b/lib/Cron/ActionTask.php index d8c3a4c19..6a0a320e8 100644 --- a/lib/Cron/ActionTask.php +++ b/lib/Cron/ActionTask.php @@ -2,11 +2,11 @@ namespace OCA\OpenConnector\Cron; -use OCA\MyApp\Service\CallService; -use OCA\MyApp\DB\SourceMapper; -use OCA\MyApp\DB\JobMapper; -use OCA\MyApp\DB\JobLog; -use OCA\MyApp\DB\JobLogMapper; +use OCA\OpenConnector\Service\CallService; +use OCA\OpenConnector\Db\SourceMapper; +use OCA\OpenConnector\Db\JobMapper; +use OCA\OpenConnector\Db\JobLog; +use OCA\OpenConnector\Db\JobLogMapper; use OCP\BackgroundJob\TimedJob; use OCP\AppFramework\Utility\ITimeFactory; @@ -35,21 +35,21 @@ public function __construct( $this->jobMapper = $jobMapper; $this->jobLogMapper = $jobLogMapper; // Run every 5 minutes - $this->setInterval(300); + //$this->setInterval(300); // Delay until low-load time - $this->setTimeSensitivity(\OCP\BackgroundJob\IJob::TIME_SENSITIVE); + //$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); + //$this->setAllowParallelRuns(false); } //@todo: make this a bit more generic :') - public function run(array $arguments) + public function run($argument) { // lets get the job - $job = $this->jobMapper->find($arguments['jobId']); + $job = $this->jobMapper->find($argument['jobId']); // If the job is not enabled, we don't need to do anything if (!$job->isEnabled()) { return; @@ -63,8 +63,12 @@ public function run(array $arguments) $time_start = microtime(true); // 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($arguments['sourceId'])) { - $source = $this->sourceMapper->find($arguments['sourceId']); + 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); } @@ -96,7 +100,7 @@ public function run(array $arguments) $this->jobLogMapper->insert($jobLog); // Lets report back about what we have just done - return $jobLog; + return; } } diff --git a/lib/Cron/LogCleanUpTask.php b/lib/Cron/LogCleanUpTask.php index a2e109412..0af048ad8 100644 --- a/lib/Cron/LogCleanUpTask.php +++ b/lib/Cron/LogCleanUpTask.php @@ -2,7 +2,7 @@ namespace OCA\OpenConnector\Cron; -use OCA\MyApp\DB\CallLogMapper; +use OCA\OpenConnector\Db\CallLogMapper; use OCP\BackgroundJob\TimedJob; use OCP\AppFramework\Utility\ITimeFactory; diff --git a/lib/Service/CallService.php b/lib/Service/CallService.php index 45386d5fd..590028d85 100644 --- a/lib/Service/CallService.php +++ b/lib/Service/CallService.php @@ -164,7 +164,6 @@ public function call( $callLog->setRequest($data['request']); $callLog->setResponse($data['response']); $callLog->setCreatedAt(new \DateTime()); - $callLog->setUpdatedAt(new \DateTime()); $this->callLogMapper->insert($callLog); diff --git a/lib/Service/JobService.php b/lib/Service/JobService.php index d00987c3a..2d1067e38 100644 --- a/lib/Service/JobService.php +++ b/lib/Service/JobService.php @@ -13,16 +13,17 @@ class JobService private IJobList $jobList; private JobMapper $jobMapper; - public function __construct( IJobList $jobList, JobMapper $jobMapper) { + public function __construct( IJobList $jobList, JobMapper $jobMapper, ActionTask $actionTask) { $this->jobList = $jobList; $this->jobMapper = $jobMapper; + $this->actionTask = $actionTask; } public function scheduleJob(Job $job): Job { // Lets first check if the job should be disabled - if (!$job->isEnabled() || $job->getJobListId()) { + if (!$job->getIsEnabled() || $job->getJobListId()) { $this->jobList->removeById($job->getId()); $job->setJobListId(null); @@ -35,20 +36,20 @@ public function scheduleJob(Job $job): Job } // Oke this is a new job lets schedule it - $actionTask = new ActionTask(); $arguments = $job->getArguments(); $arguments['jobId'] = $job->getId(); if(!$job->getScheduleAfter()) { - $iJob = $this->jobList->add($actionTask::class, $arguments); + $iJob = $this->jobList->add($this->actionTask::class, $arguments); } else { $runAfter = $job->getScheduleAfter()->getTimestamp(); - $iJob = $this->jobList->scheduleAfter($actionTask::class, $runAfter, $arguments); + $iJob = $this->jobList->scheduleAfter($this->actionTask::class, $runAfter, $arguments); } // Save the job to the database - $job->setJobListId($iJob->getId()); - return $this->jobMapper->save($job); + // @todo how do we get the job id? + ///$job->setJobListId($iJob->getId()); + return $this->jobMapper->update($job); // $this->jobList->add($job->getJobClass(), $job->getArguments()); } diff --git a/src/store/modules/job.js b/src/store/modules/job.js index e147b8c49..faf0b2de0 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) { diff --git a/src/store/modules/source.js b/src/store/modules/source.js index ce5db6475..bbbd43ac4 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 getSourceLogs(id) { + const endpoint = `/index.php/apps/openconnector/api/sources-logs/${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) { From f9f89e1dc9054eb7b5e71606c6ab0575f1a82bf2 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 30 Sep 2024 00:59:57 +0200 Subject: [PATCH 11/16] Bit more bug fixes --- lib/Controller/SourcesController.php | 4 +++- lib/Cron/ActionTask.php | 30 +++++++++++++++-------- lib/Service/JobService.php | 33 ++++++++++++++++++++++---- lib/Service/SynchronizationService.php | 24 ------------------- src/store/modules/job.js | 15 ++++++++++++ src/store/modules/source.js | 4 ++-- src/views/Job/JobDetails.vue | 3 +++ src/views/Source/SourceDetails.vue | 15 +++++++----- 8 files changed, 81 insertions(+), 47 deletions(-) diff --git a/lib/Controller/SourcesController.php b/lib/Controller/SourcesController.php index 74de6c527..ba757392d 100644 --- a/lib/Controller/SourcesController.php +++ b/lib/Controller/SourcesController.php @@ -7,6 +7,7 @@ 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; @@ -26,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); diff --git a/lib/Cron/ActionTask.php b/lib/Cron/ActionTask.php index 6a0a320e8..b7cf53f72 100644 --- a/lib/Cron/ActionTask.php +++ b/lib/Cron/ActionTask.php @@ -48,10 +48,20 @@ public function __construct( //@todo: make this a bit more generic :') public function run($argument) { - // lets get the job - $job = $this->jobMapper->find($argument['jobId']); + // 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->isEnabled()) { + if (!$job->getIsEnabled()) { return; } @@ -75,7 +85,7 @@ public function run($argument) // @todo: instead get the actual call an run that $time_end = microtime(true); - $executionTime = $time_end - $time_start; + $executionTime = ( $time_end - $time_start ) * 1000; // deal with single run if ($job->isSingleRun()) { @@ -84,9 +94,9 @@ public function run($argument) // Update the job - $job->setLastRun($this->time->getTime()); - $job->setNextRun($this->time->getTime() + $job->getInterval()); - $this->jobMapper->update($job); + //$job->setLastRun($this->time->getTime()); + //$job->setNextRun($this->time->getTime() + $job->getInterval()); + //$this->jobMapper->update($job); // Log the job $jobLog = new JobLog(); @@ -94,9 +104,9 @@ public function run($argument) $jobLog->setJobClass($job->getJobClass()); $jobLog->setJobListId($job->getJobListId()); $jobLog->setArguments($job->getArguments()); - $jobLog->setLastRun($job->getLastRun()); - $jobLog->setNextRun($job->getNextRun()); - $jobLog->setExecutionTime($executionTime); + //$jobLog->setLastRun($job->getLastRun()); + //$jobLog->setNextRun($job->getNextRun()); + //$jobLog->setExecutionTime($executionTime); $this->jobLogMapper->insert($jobLog); // Lets report back about what we have just done diff --git a/lib/Service/JobService.php b/lib/Service/JobService.php index 2d1067e38..0ffc4c09e 100644 --- a/lib/Service/JobService.php +++ b/lib/Service/JobService.php @@ -7,17 +7,20 @@ use OCA\OpenConnector\Db\Job; use OCA\OpenConnector\Db\JobMapper; use OCP\BackgroundJob\IJobList; +use OCP\IDBConnection; class JobService { private IJobList $jobList; private JobMapper $jobMapper; + private IDBConnection $connection; - public function __construct( IJobList $jobList, JobMapper $jobMapper, ActionTask $actionTask) { + public function __construct( IJobList $jobList, JobMapper $jobMapper, ActionTask $actionTask, IDBConnection $connection) { $this->jobList = $jobList; $this->jobMapper = $jobMapper; $this->actionTask = $actionTask; + $this->connection = $connection; } public function scheduleJob(Job $job): Job @@ -47,10 +50,32 @@ public function scheduleJob(Job $job): Job } // Save the job to the database - // @todo how do we get the job id? - ///$job->setJobListId($iJob->getId()); + $job->setJobListId($this->getJobListId($this->actionTask::class, $arguments)); return $this->jobMapper->update($job); - // $this->jobList->add($job->getJobClass(), $job->getArguments()); } + /** + * check if a job is in the list + * + * @param IJob|class-string $job + * @param mixed $argument + */ + public function getJobListId($job, $argument): int|null { + $class = ($job instanceof IJob) ? get_class($job) : $job; + $arguments = json_encode($arguments); + + $query = $this->connection->getQueryBuilder(); + $query->select('id') + ->from('jobs') + ->where($query->expr()->eq('class', $query->createNamedParameter($class))) + ->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(hash('sha256', $arguments)))) + ->setMaxResults(1); + + $result = $query->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + return $row['id'] ?? null; + } + } diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php index ac4c06dbf..26a876a4d 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -13,29 +13,6 @@ use DateInterval; use DateTime; -/** - * Service to synchronize resources between gateway and external sources. - * - * This service provides synchronization functionality to synchronize between sources and the gateway. - * - * @author Robert Zondervan, Barry Brands, Ruben van der Linde - * @license EUPL - * - * @category Service - */ -class NewSynchronizationService -{ - - public function __construct( - private readonly GatewayResourceService $resourceService, - private readonly CallService $callService, - private readonly SynchronizationService $synchronizationService, - private readonly LoggerInterface $synchronizationLogger, - private readonly EntityManagerInterface $entityManager, - private readonly MappingService $mappingService, - ) { - - }//end __construct() /** @@ -55,7 +32,6 @@ public function __construct( public function synchronizeFromSource(Synchronization $synchronization, array $sourceObject=[], bool $unsafe=false): Synchronization { - public function __construct( private readonly GatewayResourceService $resourceService, private readonly CallService $callService, diff --git a/src/store/modules/job.js b/src/store/modules/job.js index faf0b2de0..e808c0e66 100644 --- a/src/store/modules/job.js +++ b/src/store/modules/job.js @@ -62,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 bbbd43ac4..b7f561825 100644 --- a/src/store/modules/source.js +++ b/src/store/modules/source.js @@ -75,8 +75,8 @@ export const useSourceStore = defineStore( } }, // New function to get source logs - async getSourceLogs(id) { - const endpoint = `/index.php/apps/openconnector/api/sources-logs/${id}` + async refreshSourceLogs() { + const endpoint = `/index.php/apps/openconnector/api/sources-logs/${this.sourceItem.id}` try { const response = await fetch(endpoint, { method: 'GET', 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/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() + }, } From fde42a7ef4ed4e4a7373629e976f27c733a7668e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 30 Sep 2024 09:08:41 +0200 Subject: [PATCH 12/16] Setting up log retention --- lib/Cron/ActionTask.php | 7 +++++-- lib/Db/Job.php | 6 ++++++ lib/Db/Source.php | 6 ++++++ lib/Migration/Version0Date20240826193657.php | 6 ++++++ lib/Service/JobService.php | 15 +++++++++------ src/entities/job/job.ts | 4 ++++ src/entities/job/job.types.ts | 2 ++ src/entities/source/source.ts | 4 ++++ src/entities/source/source.types.ts | 2 ++ 9 files changed, 44 insertions(+), 8 deletions(-) diff --git a/lib/Cron/ActionTask.php b/lib/Cron/ActionTask.php index b7cf53f72..9425b2e44 100644 --- a/lib/Cron/ActionTask.php +++ b/lib/Cron/ActionTask.php @@ -9,6 +9,7 @@ use OCA\OpenConnector\Db\JobLogMapper; use OCP\BackgroundJob\TimedJob; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; /** * This class is used to run the action tasks for the OpenConnector app. It hooks into the cron job list and runs the classes that are set as the job class in the job. @@ -21,19 +22,21 @@ class ActionTask extends TimedJob private SourceMapper $sourceMapper; private JobMapper $jobMapper; private JobLogMapper $jobLogMapper; - + private IJobList $jobList; public function __construct( ITimeFactory $time, CallService $callService, SourceMapper $sourceMapper, JobMapper $jobMapper, - JobLogMapper $jobLogMapper + JobLogMapper $jobLogMapper, + IJobList $jobList ) { parent::__construct($time); $this->callService = $callService; $this->sourceMapper = $sourceMapper; $this->jobMapper = $jobMapper; $this->jobLogMapper = $jobLogMapper; + $this->jobList = $jobList; // Run every 5 minutes //$this->setInterval(300); diff --git a/lib/Db/Job.php b/lib/Db/Job.php index 2d7e90271..3b7fdbba7 100644 --- a/lib/Db/Job.php +++ b/lib/Db/Job.php @@ -21,6 +21,8 @@ class Job extends Entity implements JsonSerializable 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 @@ -40,6 +42,8 @@ public function __construct() { $this->addType('scheduleAfter', 'datetime'); $this->addType('userId', 'string'); $this->addType('jobListId', 'string'); + $this->addType('logRetention', 'integer'); + $this->addType('errorRetention', 'integer'); $this->addType('lastRun', 'datetime'); $this->addType('nextRun', 'datetime'); $this->addType('created', 'datetime'); @@ -93,6 +97,8 @@ public function jsonSerialize(): array 'scheduleAfter' => $this->scheduleAfter, 'userId' => $this->userId, 'jobListId' => $this->jobListId, + 'logRetention' => $this->logRetention, + 'errorRetention' => $this->errorRetention, 'lastRun' => $this->lastRun, 'nextRun' => $this->nextRun, 'created' => $this->created, 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/Migration/Version0Date20240826193657.php b/lib/Migration/Version0Date20240826193657.php index 92480481c..9c339acd6 100644 --- a/lib/Migration/Version0Date20240826193657.php +++ b/lib/Migration/Version0Date20240826193657.php @@ -59,6 +59,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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('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']); @@ -147,6 +149,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']); @@ -209,6 +213,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt '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'); @@ -229,6 +234,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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'); diff --git a/lib/Service/JobService.php b/lib/Service/JobService.php index 0ffc4c09e..d4e148f49 100644 --- a/lib/Service/JobService.php +++ b/lib/Service/JobService.php @@ -49,26 +49,29 @@ public function scheduleJob(Job $job): Job $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 - $job->setJobListId($this->getJobListId($this->actionTask::class, $arguments)); return $this->jobMapper->update($job); } - /** - * check if a job is in the list + /** + * 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, $argument): int|null { + public function getJobListId($job): int|null { $class = ($job instanceof IJob) ? get_class($job) : $job; - $arguments = json_encode($arguments); $query = $this->connection->getQueryBuilder(); $query->select('id') ->from('jobs') ->where($query->expr()->eq('class', $query->createNamedParameter($class))) - ->andWhere($query->expr()->eq('argument_hash', $query->createNamedParameter(hash('sha256', $arguments)))) + ->orderBy('id', 'DESC') ->setMaxResults(1); $result = $query->executeQuery(); diff --git a/src/entities/job/job.ts b/src/entities/job/job.ts index cf4ceb2ea..f74580605 100644 --- a/src/entities/job/job.ts +++ b/src/entities/job/job.ts @@ -17,6 +17,8 @@ export class Job implements TJob { public scheduleAfter: string | null public userId: string | null public jobListId: string | null + public logRetention: number + public errorRetention: number public lastRun: string | null public nextRun: string | null public created: string | null @@ -37,6 +39,8 @@ export class Job implements TJob { this.scheduleAfter = job.scheduleAfter || null this.userId = job.userId || 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.created = job.created || null diff --git a/src/entities/job/job.types.ts b/src/entities/job/job.types.ts index 2a7e509c0..79b80e5d4 100644 --- a/src/entities/job/job.types.ts +++ b/src/entities/job/job.types.ts @@ -13,6 +13,8 @@ export type TJob = { scheduleAfter?: string | null userId?: string | null jobListId?: string | null + logRetention?: number + errorRetention?: number 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 From 1a7bfcd640bb30408723819c2cb04862bf1b38a1 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 30 Sep 2024 12:44:04 +0200 Subject: [PATCH 13/16] Move the ping to its own action --- lib/Action/PingAction.php | 41 +++++++++++++++++++++++++++++++++++++++ lib/Cron/ActionTask.php | 29 ++++++++------------------- 2 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 lib/Action/PingAction.php 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/Cron/ActionTask.php b/lib/Cron/ActionTask.php index 9425b2e44..b198c525d 100644 --- a/lib/Cron/ActionTask.php +++ b/lib/Cron/ActionTask.php @@ -2,13 +2,12 @@ namespace OCA\OpenConnector\Cron; -use OCA\OpenConnector\Service\CallService; -use OCA\OpenConnector\Db\SourceMapper; use OCA\OpenConnector\Db\JobMapper; use OCA\OpenConnector\Db\JobLog; use OCA\OpenConnector\Db\JobLogMapper; use OCP\BackgroundJob\TimedJob; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\AppFramework\Utility\IContainer; use OCP\BackgroundJob\IJobList; /** @@ -18,25 +17,22 @@ */ class ActionTask extends TimedJob { - private CallService $callService; - private SourceMapper $sourceMapper; private JobMapper $jobMapper; private JobLogMapper $jobLogMapper; private IJobList $jobList; + private IContainer $iContainer; public function __construct( ITimeFactory $time, - CallService $callService, - SourceMapper $sourceMapper, JobMapper $jobMapper, JobLogMapper $jobLogMapper, - IJobList $jobList + IJobList $jobList, + IContainer $iContainer ) { parent::__construct($time); - $this->callService = $callService; - $this->sourceMapper = $sourceMapper; $this->jobMapper = $jobMapper; $this->jobLogMapper = $jobLogMapper; $this->jobList = $jobList; + $this->iContainer = $iContainer; // Run every 5 minutes //$this->setInterval(300); @@ -74,19 +70,10 @@ public function run($argument) } $time_start = microtime(true); - - // 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); - } - - // @todo: instead get the actual call an run that + $action = $this->iContainer->get($job->getClass()); + $action->run($job->getArguments()); + $time_end = microtime(true); $executionTime = ( $time_end - $time_start ) * 1000; From 4379e065bbdbe63e8bf0c9c54faec672bec77799 Mon Sep 17 00:00:00 2001 From: Remko Date: Mon, 30 Sep 2024 14:46:59 +0200 Subject: [PATCH 14/16] merge dev --- docker-compose.yml | 13 ++++++++++--- src/modals/Source/EditSource.vue | 1 - 2 files changed, 10 insertions(+), 4 deletions(-) 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/src/modals/Source/EditSource.vue b/src/modals/Source/EditSource.vue index b5bf7d6db..2c00bb9ca 100644 --- a/src/modals/Source/EditSource.vue +++ b/src/modals/Source/EditSource.vue @@ -30,7 +30,6 @@ import { sourceStore, navigationStore } from '../../store/store.js' From 13353307cd298aac47bb0522d4f9af1e799d9526 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 30 Sep 2024 15:34:02 +0200 Subject: [PATCH 15/16] Setting Synchronyzation The actual action and call --- lib/Action/EventAction.php | 33 ++ lib/Action/SynchronizationAction.php | 56 +++ lib/Db/Log.php | 129 ------- lib/Db/LogMapper.php | 74 ---- lib/Db/Synchronization.php | 112 +++--- lib/Db/SynchronizationContract.php | 105 ++++++ lib/Db/SynchronizationContractLog.php | 78 ++++ lib/Db/SynchronizationContractMapper.php | 108 ++++++ lib/Migration/Version0Date20240826193657.php | 107 +++--- lib/Service/SynchronizationService.php | 369 +++++++++---------- 10 files changed, 674 insertions(+), 497 deletions(-) create mode 100644 lib/Action/EventAction.php create mode 100644 lib/Action/SynchronizationAction.php delete mode 100644 lib/Db/Log.php delete mode 100644 lib/Db/LogMapper.php create mode 100644 lib/Db/SynchronizationContract.php create mode 100644 lib/Db/SynchronizationContractLog.php create mode 100644 lib/Db/SynchronizationContractMapper.php 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/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/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/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 9c339acd6..e7570f1f4 100644 --- a/lib/Migration/Version0Date20240826193657.php +++ b/lib/Migration/Version0Date20240826193657.php @@ -66,38 +66,6 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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->setPrimaryKey(['id']); - } - if (!$schema->hasTable('openconnector_mappings')) { $table = $schema->createTable('openconnector_mappings'); $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'length' => 20]); @@ -159,26 +127,33 @@ 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'); @@ -241,6 +216,36 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt $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/SynchronizationService.php b/lib/Service/SynchronizationService.php index 26a876a4d..0683682a3 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -4,9 +4,13 @@ use OCA\OpenConnector\Db\Source; use OCA\OpenConnector\Db\Synchronization; +use OCA\OpenConnector\Db\SynchronizationMapper; +use OCA\OpenConnector\Db\SynchronizationContract; +use OCA\OpenConnector\Db\SynchronizationContractMapper; use OCA\OpenConnector\Service\CallService; use OCA\OpenConnector\Service\MappingService; -use GuzzleHttp\Exception\GuzzleException; + +use OCP\AppFramework\Utility\IContainer; use Twig\Error\LoaderError; use Twig\Error\SyntaxError; use Adbar\Dot; @@ -14,201 +18,182 @@ use DateTime; +class SynchronizationService +{ + public $CallService; + public $MappingService; + public $container; + public $synchronization; + public $synchronizationMapper; + public $synchronizationContractMapper; + public $objectService; + + + public function __construct( + CallService $callService, + MappingService $mappingService, + IContainer $container, + SynchronizationMapper $synchronizationMapper, + SynchronizationContractMapper $synchronizationContractMapper + ) { + $this->callService = $callService; + $this->mappingService = $mappingService; + $this->container = $container; + $this->synchronizationMapper = $synchronizationMapper; + $this->synchronizationContractMapper = $synchronizationContractMapper; + } /** - * Executes the synchronization from source to gateway. - * Slightly edited clone of the SynchronizationService in the gateway. - * - * @param Synchronization $synchronization The synchronization to update - * @param array $sourceObject The object in the source - * @param bool $unsafe Unset attributes that are not included in the hydrator array when calling the hydrate function - * - * @throws GuzzleException - * @throws LoaderError - * @throws SyntaxError - * - * @return Synchronization The updated synchronization + * Synchronizes a given synchronization (or a complete source). + * + * @param Synchronization $synchronization + * @return void */ - public function synchronizeFromSource(Synchronization $synchronization, array $sourceObject=[], bool $unsafe=false): Synchronization + public function synchronize(Synchronization $synchronization) { + $this->synchronization = $synchronization; + $objectList = []; - public function __construct( - private readonly GatewayResourceService $resourceService, - private readonly CallService $callService, - private readonly SynchronizationService $synchronizationService, - private readonly LoggerInterface $synchronizationLogger, - private readonly EntityManagerInterface $entityManager, - private readonly MappingService $mappingService, - ) { - - }//end __construct() - - - /** - * Executes the synchronization from source to gateway. - * Slightly edited clone of the SynchronizationService in the gateway. - * - * @param Synchronization $synchronization The synchronization to update - * @param array $sourceObject The object in the source - * @param bool $unsafe Unset attributes that are not included in the hydrator array when calling the hydrate function - * - * @throws GuzzleException - * @throws LoaderError - * @throws SyntaxError - * - * @return Synchronization The updated synchronization - */ - public function synchronizeFromSource(Synchronization $synchronization, array $sourceObject=[], bool $unsafe=false): Synchronization - { - $this->synchronizationLogger->info("handleSync for Synchronization with id = {$synchronization->getId()->toString()}"); - - // create new object if no object exists - if (!$synchronization->getObject()) { - isset($this->io) && $this->io->text('creating new objectEntity'); - $this->synchronizationLogger->info('creating new objectEntity'); - $object = new ObjectEntity($synchronization->getEntity()); - $object->addSynchronization($synchronization); - $this->entityManager->persist($object); - $this->entityManager->persist($synchronization); - $oldDateModified = null; - } else { - $oldDateModified = $synchronization->getObject()->getDateModified()->getTimestamp(); - } - - $sourceObject = $sourceObject ?: $this->synchronizationService->getSingleFromSource($synchronization); - - if ($sourceObject === null) { - $this->synchronizationLogger->warning("Can not handle Synchronization with id = {$synchronization->getId()->toString()} if \$sourceObject === null"); - - return $synchronization; - } - - // Let check - $now = new DateTime(); - $synchronization->setLastChecked($now); - - $sha = hash('sha256', json_encode($sourceObject)); - - // Checking if data on source has changed. - if ($synchronization->getSha() === $sha) { - return $synchronization; - } - - // Counter - $counter = ($synchronization->getTryCounter() + 1); - if ($counter > 10000) { - $counter = 10000; - } - - $synchronization->setTryCounter($counter); - - // Set dont try before, expensional so in minutes 1,8,27,64,125,216,343,512,729,1000 - $addMinutes = pow($counter, 3); - if ($synchronization->getDontSyncBefore()) { - $dontTryBefore = $synchronization->getDontSyncBefore()->add(new DateInterval('PT'.$addMinutes.'M')); - } else { - $dontTryBefore = new DateTime(); - } - - $synchronization->setDontSyncBefore($dontTryBefore); - - if ($synchronization->getMapping()) { - $sourceObject = $this->mappingService->mapping($synchronization->getMapping(), $sourceObject); - } - - $synchronization->getObject()->hydrate($sourceObject, $unsafe); - - $synchronization->setSha($sha); - - $this->entityManager->persist($synchronization->getObject()); - $this->entityManager->persist($synchronization); - - if ($oldDateModified !== $synchronization->getObject()->getDateModified()->getTimestamp()) { - $date = new DateTime(); - (isset($this->io) ?? $this->io->text("set new dateLastChanged to {$date->format('d-m-YTH:i:s')}")); - $synchronization->setLastSynced(new DateTime()); - $synchronization->setTryCounter(0); - } else { - (isset($this->io) ?? $this->io->text("lastSynced is still {$synchronization->getObject()->getDateModified()->format('d-m-YTH:i:s')}")); - } - - return $synchronization; - - }//end synchronizeFromSource() - - - /** - * Fetch data from source in a way that is as abstract as possible at this time. - * - * @param array $configuration - * @param Source $source - * @return array - * @throws Exception - */ - public function getResults(array $configuration, Source $source): array - { - $response = $this->callService->call(source: $source, endpoint: $configuration['endpoint'], method: $configuration['method'], config: ['json' => $configuration['body']]); - - $result = $this->callService->decodeResponse(source: $source, response: $response, contentType: ($configuration['content-type'] ?? 'application/json')); - - $resultDot = new Dot($result); - - if ($resultDot->has(keys: $configuration['resultsPath']) === true) { - $return = $resultDot->get(key: $configuration['resultsPath']); - if ($return instanceof Dot) { - return $return->jsonSerialize(); - } else if (is_array($return)) { - return $return; - } + 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); } - - throw new Exception('No cases found'); - - }//end getResults() - - - /** - * This function is designed to in time replace the existing syncCollectionHandler. - * At the moment it depends on the in-gateway SynchronizationService, and is one way with the source as the leading version. - * - * @param array $data - * @param array $configuration - * @return array - * @throws \GuzzleHttp\Exception\GuzzleException - */ - public function synchronizeCollectionHandler(array $data, array $configuration): array - { - $source = $this->resourceService->getSource(reference: $configuration['source'], pluginName: "common-gateway/vrijbrp-to-zgw-bundle"); - $schema = $this->resourceService->getSchema(reference: $configuration['schema'], pluginName: "common-gateway/vrijbrp-to-zgw-bundle"); - - if (isset($configuration['mapping']) === true) { - $mapping = $this->resourceService->getMapping(reference: $configuration['mapping'], pluginName: "common-gateway/vrijbrp-to-zgw-bundle"); - } - - try { - $dossiers = $this->getResults(configuration: $configuration, source: $source); - } catch (Exception $exception) { - $this->synchronizationLogger->warning(message: $exception->getMessage(), context: ['plugin' => 'common-gateway/vrijbrp-to-zgw-bundle']); - return $data; - } - - foreach ($dossiers as $dossier) { - $dossierDot = new Dot($dossier); - - $synchronization = $this->synchronizationService->findSyncBySource(source: $source, entity: $schema, sourceId: $dossierDot[$configuration['idField']], endpoint: $configuration['endpoint']); - - if ($synchronization->getMapping() === null && isset($mapping) === true) { - $synchronization->setMapping($mapping); - } - - try { - $this->synchronizeFromSource(synchronization: $synchronization, sourceObject: $dossier); - } catch (Exception $exception) { - $this->synchronizationLogger->error(message: $exception->getMessage(), context: ['plugin' => 'common-gateway/vrijbrp-to-zgw-bundle']); + + $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->iContainer->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(); } - } - - return $data; - - }//end synchronizeCollectionHandler() - }//end class + // 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': + $this->callService->put($targetObject); + break; + case 'database': + $this->callService->put($targetObject); + 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->iContainer->get('OCA\OpenRegister\Service\ObjectService'); + + break; + case 'api': + $this->callService->put($targetObject); + break; + case 'database': + $this->callService->put($targetObject); + break; + } + } +} \ No newline at end of file From 76ad515a7ba37ffecc49fbc4128e79beb4f4564e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 30 Sep 2024 16:28:47 +0200 Subject: [PATCH 16/16] Fixes on the container interface --- lib/Cron/ActionTask.php | 11 ++++---- lib/Service/SynchronizationService.php | 35 ++++++++++++++------------ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/Cron/ActionTask.php b/lib/Cron/ActionTask.php index b198c525d..96fdba98b 100644 --- a/lib/Cron/ActionTask.php +++ b/lib/Cron/ActionTask.php @@ -7,8 +7,8 @@ use OCA\OpenConnector\Db\JobLogMapper; use OCP\BackgroundJob\TimedJob; use OCP\AppFramework\Utility\ITimeFactory; -use OCP\AppFramework\Utility\IContainer; use OCP\BackgroundJob\IJobList; +use Psr\Container\ContainerInterface; /** * This class is used to run the action tasks for the OpenConnector app. It hooks into the cron job list and runs the classes that are set as the job class in the job. @@ -20,19 +20,20 @@ class ActionTask extends TimedJob private JobMapper $jobMapper; private JobLogMapper $jobLogMapper; private IJobList $jobList; - private IContainer $iContainer; + private ContainerInterface $containerInterface; + public function __construct( ITimeFactory $time, JobMapper $jobMapper, JobLogMapper $jobLogMapper, IJobList $jobList, - IContainer $iContainer + ContainerInterface $containerInterface ) { parent::__construct($time); $this->jobMapper = $jobMapper; $this->jobLogMapper = $jobLogMapper; $this->jobList = $jobList; - $this->iContainer = $iContainer; + $this->containerInterface = $containerInterface; // Run every 5 minutes //$this->setInterval(300); @@ -71,7 +72,7 @@ public function run($argument) $time_start = microtime(true); - $action = $this->iContainer->get($job->getClass()); + $action = $this->containerInterface->get($job->getClass()); $action->run($job->getArguments()); $time_end = microtime(true); diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php index 0683682a3..88507b195 100644 --- a/lib/Service/SynchronizationService.php +++ b/lib/Service/SynchronizationService.php @@ -10,7 +10,7 @@ use OCA\OpenConnector\Service\CallService; use OCA\OpenConnector\Service\MappingService; -use OCP\AppFramework\Utility\IContainer; +use Psr\Container\ContainerInterface; use Twig\Error\LoaderError; use Twig\Error\SyntaxError; use Adbar\Dot; @@ -20,25 +20,25 @@ class SynchronizationService { - public $CallService; - public $MappingService; - public $container; - public $synchronization; - public $synchronizationMapper; - public $synchronizationContractMapper; - public $objectService; + private CallService $callService; + private MappingService $mappingService; + private ContainerInterface $containerInterface; + private Synchronization $synchronization; + private SynchronizationMapper $synchronizationMapper; + private SynchronizationContractMapper $synchronizationContractMapper; + private ObjectService $objectService; public function __construct( CallService $callService, MappingService $mappingService, - IContainer $container, + ContainerInterface $containerInterface, SynchronizationMapper $synchronizationMapper, SynchronizationContractMapper $synchronizationContractMapper ) { $this->callService = $callService; $this->mappingService = $mappingService; - $this->container = $container; + $this->containerInterface = $containerInterface; $this->synchronizationMapper = $synchronizationMapper; $this->synchronizationContractMapper = $synchronizationContractMapper; } @@ -151,7 +151,7 @@ public function updateTarget(SynchronizationContract $synchronizationContract, a switch($type){ case 'register/schema': // Setup the object service - $this->objectService = $this->iContainer->get('OCA\OpenRegister\Service\ObjectService'); + $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(); @@ -166,10 +166,11 @@ public function updateTarget(SynchronizationContract $synchronizationContract, a $synchronizationContract->setTargetId($target->getUuid()); break; case 'api': - $this->callService->put($targetObject); + //@todo: implement + //$this->callService->put($targetObject); break; case 'database': - $this->callService->put($targetObject); + //@todo: implement break; } } @@ -185,14 +186,16 @@ public function getAllObjectsFromSource(Synchronization $synchronization) switch($type){ case 'register/schema': // Setup the object service - $this->objectService = $this->iContainer->get('OCA\OpenRegister\Service\ObjectService'); + $this->objectService = $this->containerInterface->get('OCA\OpenRegister\Service\ObjectService'); break; case 'api': - $this->callService->put($targetObject); + + //@todo: implement + //$this->callService->put($targetObject); break; case 'database': - $this->callService->put($targetObject); + //@todo: implement break; } }