diff --git a/.github/workflows/pull-request-from-branch-check.yaml b/.github/workflows/pull-request-from-branch-check.yaml
new file mode 100644
index 0000000..77d1970
--- /dev/null
+++ b/.github/workflows/pull-request-from-branch-check.yaml
@@ -0,0 +1,18 @@
+name: Main Branch Protection
+
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ check-branch:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check branch
+ run: |
+ if [[ ${GITHUB_HEAD_REF} != development ]] && [[ ${GITHUB_HEAD_REF} != documentation ]] && ! [[ ${GITHUB_HEAD_REF} =~ ^hotfix/ ]];
+ then
+ echo "Error: Pull request must come from 'development', 'documentation' or 'hotfix/' branch"
+ exit 1
+ fi
diff --git a/.github/workflows/pull-request-lint-check.yaml b/.github/workflows/pull-request-lint-check.yaml
new file mode 100644
index 0000000..4a8c874
--- /dev/null
+++ b/.github/workflows/pull-request-lint-check.yaml
@@ -0,0 +1,21 @@
+name: Lint Check
+
+on:
+ pull_request:
+ branches:
+ - development
+ - main
+
+jobs:
+ lint-check:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Install dependencies
+ run: npm i
+
+ - name: Linting
+ run: npm run lint
diff --git a/README.md b/README.md
index baa1e16..fa2d566 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
# OpenConector
Provides gateway and service bus functionality like mapping, translation and synchronisation of data
+
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 27548e5..f96805d 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -22,6 +22,8 @@
['name' => 'synchronizations#test', 'url' => '/api/synchronizations-test/{id}', 'verb' => 'POST'],
// Mapping endpoints
['name' => 'mappings#test', 'url' => '/api/mappings/test', 'verb' => 'POST'],
+ ['name' => 'mappings#saveObject', 'url' => '/api/mappings/objects', 'verb' => 'POST'],
+ ['name' => 'mappings#getObjects', 'url' => '/api/mappings/objects', 'verb' => 'GET'],
// Running endpoints
['name' => 'endpoints#run', 'url' => '/api/v1/{endpoint}', 'verb' => 'GET'],
['name' => 'endpoints#run', 'url' => '/api/v1/{endpoint}', 'verb' => 'PUT'],
diff --git a/docs/authentication.md b/docs/authentication.md
new file mode 100644
index 0000000..d3bb763
--- /dev/null
+++ b/docs/authentication.md
@@ -0,0 +1,88 @@
+# Authentication on sources
+
+In order to authenticate on other sources there are possibilities based upon the way the source expects autentication parameters.
+These parameters can be set in the source configuration. For example, if the source expects an API key in the headers, we can set the parameter `headers.Authorization` with the API key as value.
+
+Usually, sources tend to use dynamic authorization parameters in order to prevent the same authentication parameter from being used by adversaries that catch a call and deduce the parameter.
+
+At the moment, OpenConnector supports two dynamic authentication methods, OAuth and JWT Bearers.
+
+## OAuth
+
+To use OAuth we put in our Authorization header the following value:
+```twig
+Bearer {{ oauthToken(source) }}
+```
+This will impose an OAuth 2.0 access token after `Bearer` if the field `authenticationConfig` contains correct values.
+OpenConnector supports the OAuth 2.0 protocol with client credentials and password credentials as grant_types.
+
+>[!NOTE]
+> TODO: How to add authenticationConfig parameters in frontend
+
+When using OAuth, OpenConnector supports the following parameters:
+
+### Standard parameters
+* `grant_type`: The type of grant we have to use at the source. Supported are `client_credentials` and `password`
+* `scope`: The scope(s) needed to perform the requests we want to do in the API.
+* `tokenUrl`: The URL used to fetch the actual access token. Usually this url can be recognised by its path ending on `/oauth/token`
+* `authentication`: Location of the credentials, either `body` for credentials included in the request body, or `basic_auth` when the credentials have to be sent as a basic_auth header.
+ > Only used when `grant_type` is `client_credentials`
+* `client_id`: The client id of the OAuth client
+ > Only used when `grant_type` is `client_credentials`
+* `client_secret`: The secret for the OAuth client
+ > Only used when `grant_type` is `client_credentials`
+* `username`: The username for the OAuth client
+ > Only used when `grant_type` is `password`
+* `password`: The password for the OAuth client
+ > Only used when `grant_type` is `password`
+
+This results in the following example:
+```json
+{
+ "grant_type": "client_credentials",
+ "scope": "api",
+ "authentication": "body",
+ "tokenUrl": "https://example.com/oauth/token",
+ "client_id": "test-client",
+ "client_secret": "some secret value"
+}
+```
+### Custom parameters
+
+> [!WARNING]
+> Custom parameters are currently in beta, it is not recommended to use them in production environments.
+
+At the moment, OpenConnector is tested with the following custom parameters:
+
+* `client_assertion_type`, only meaningful at the moment when value is set to `urn:ietf:params:oauth:client-assertion-type:jwt-bearer`. When this is set (for Microsoft authentications) the following fields are needed to generate the `client-assertion`-field
+ - `private_key`: The base64 encoded private key of the certificate uploaded to Microsoft.
+ - `x5t`: The base64 encoded sha1 fingerprint of the uploaded certificate, generated by running the following command:
+
+ ```bash
+ echo $(openssl x509 -in certificate.crt -fingerprint -noout) | sed 's/SHA1 Fingerprint=//g' | sed 's/://g' | xxd -r -ps | base64`)
+ ```
+
+ - `payload`: The payload of the JWT generated as `client_assertion`, this can contain Twig variables to render, for example to set timestamps in the JWT payload.
+
+## JWT Bearer
+
+A second supported way of using dynamic authentication is setting a JWT Bearer. This means setting a header or query parameter with a JWT token.
+
+This can for example be used by setting an Authorization header with the following value:
+```twig
+Bearer {{ jwtToken(source) }}
+```
+
+This will impose a JWT token after the bearer. For this, the `authenticationConfig` field of the source needs to contain the following fields:
+* `algorithm`: The algorithm that should be used to generate the JWT. Supported are `HS256`, `HS384` and `HS512` for HMAC algorithms, `RS256`, `RS384`, `RS512` and `PS256` for RSA algorithms.
+* `secret`: The secret used for the JWT. This can either be a HMAC shared secret, or a RSA private key in base64 encoding.
+* `payload`: The payload of your JWT, json_encoded.
+
+This results in the following example for the `authenticationConfig` parameter in i.e. an OpenZaak source.
+```json
+{
+ "algorithm": "HS256",
+ "secret": "YOUR_256BIT_(32BYTE)_HMAC_SECRET",
+ "payload": "{\"iss\":\"my_zgw_client\",\"iat\":{{ 'now'|date('U') }},\"client_id\":\"my_zgw_client\",\"user_id\":\"my_zgw_client\",\"user_representation\":\"me@company.com\",\"aud\":\"my_zgw_client\"}"
+}
+```
diff --git a/lib/Controller/MappingsController.php b/lib/Controller/MappingsController.php
index f3ab903..190d83b 100644
--- a/lib/Controller/MappingsController.php
+++ b/lib/Controller/MappingsController.php
@@ -16,6 +16,8 @@
use OCP\IAppConfig;
use OCP\IRequest;
use OCP\IURLGenerator;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\NotFoundExceptionInterface;
class MappingsController extends Controller
{
@@ -31,7 +33,8 @@ public function __construct(
IRequest $request,
private readonly IAppConfig $config,
private readonly MappingMapper $mappingMapper,
- private readonly MappingService $mappingService
+ private readonly MappingService $mappingService,
+ private readonly ObjectService $objectService
)
{
parent::__construct($appName, $request);
@@ -125,17 +128,17 @@ public function create(): JSONResponse
return new JSONResponse($this->mappingMapper->createFromArray(object: $data));
}
- /**
- * Updates an existing mapping
- *
- * This method updates an existing mapping based on its ID.
- *
- * @NoAdminRequired
- * @NoCSRFRequired
- *
- * @param string $id The ID of the mapping to update
- * @return JSONResponse A JSON response containing the updated mapping details
- */
+ /**
+ * Updates an existing mapping
+ *
+ * This method updates an existing mapping based on its ID.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @param int $id The ID of the mapping to update
+ * @return JSONResponse A JSON response containing the updated mapping details
+ */
public function update(int $id): JSONResponse
{
$data = $this->request->getParams();
@@ -151,17 +154,18 @@ public function update(int $id): JSONResponse
return new JSONResponse($this->mappingMapper->updateFromArray(id: (int) $id, object: $data));
}
- /**
- * Deletes a mapping
- *
- * This method deletes a mapping based on its ID.
- *
- * @NoAdminRequired
- * @NoCSRFRequired
- *
- * @param string $id The ID of the mapping to delete
- * @return JSONResponse An empty JSON response
- */
+ /**
+ * Deletes a mapping
+ *
+ * This method deletes a mapping based on its ID.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @param int $id The ID of the mapping to delete
+ * @return JSONResponse An empty JSON response
+ * @throws \OCP\DB\Exception
+ */
public function destroy(int $id): JSONResponse
{
$this->mappingMapper->delete($this->mappingMapper->find((int) $id));
@@ -169,42 +173,47 @@ public function destroy(int $id): JSONResponse
return new JSONResponse([]);
}
- /**
- * Tests a mapping
- *
- * This method tests a mapping with provided input data and optional schema validation.
- *
- * @NoAdminRequired
- * @NoCSRFRequired
- *
- * @return JSONResponse A JSON response containing the test results
- *
- * @example
- * Request:
- * {
- * "inputObject": "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john@example.com\"}",
- * "mapping": {
- * "mapping": {
- * "fullName":"{{name}}",
- * "userAge":"{{age}}",
- * "contactEmail":"{{email}}"
- * }
- * },
- * "schema": "user_schema_id",
- * "validation": true
- * }
- *
- * Response:
- * {
- * "resultObject": {
- * "fullName": "John Doe",
- * "userAge": 30,
- * "contactEmail": "john@example.com"
- * },
- * "isValid": true,
- * "validationErrors": []
- * }
- */
+ /**
+ * Tests a mapping
+ *
+ * This method tests a mapping with provided input data and optional schema validation.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @param ObjectService $objectService
+ * @param IURLGenerator $urlGenerator
+ *
+ * @return JSONResponse A JSON response containing the test results
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ *
+ * @example
+ * Request:
+ * {
+ * "inputObject": "{\"name\":\"John Doe\",\"age\":30,\"email\":\"john@example.com\"}",
+ * "mapping": {
+ * "mapping": {
+ * "fullName":"{{name}}",
+ * "userAge":"{{age}}",
+ * "contactEmail":"{{email}}"
+ * }
+ * },
+ * "schema": "user_schema_id",
+ * "validation": true
+ * }
+ *
+ * Response:
+ * {
+ * "resultObject": {
+ * "fullName": "John Doe",
+ * "userAge": 30,
+ * "contactEmail": "john@example.com"
+ * },
+ * "isValid": true,
+ * "validationErrors": []
+ * }
+ */
public function test(ObjectService $objectService, IURLGenerator $urlGenerator): JSONResponse
{
$openRegisters = $objectService->getOpenRegisters();
@@ -291,4 +300,57 @@ public function test(ObjectService $objectService, IURLGenerator $urlGenerator):
'validationErrors' => $validationErrors
]);
}
+
+ /**
+ * Saves a mapping object
+ *
+ * This method saves a mapping object based on POST data.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @return JSONResponse|null
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function saveObject(): ?JSONResponse
+ {
+ // Check if the OpenRegister service is available
+ $openRegisters = $this->objectService->getOpenRegisters();
+ if ($openRegisters !== null) {
+ $data = $this->request->getParams();
+ return new JSONResponse($openRegisters->saveObject($data['register'], $data['schema'], $data['object']));
+ }
+
+ return null;
+ }
+
+ /**
+ * Retrieves a list of objects to map to
+ *
+ * This method retrieves a list of objects to map to based on GET data.
+ *
+ * @NoAdminRequired
+ * @NoCSRFRequired
+ *
+ * @return JSONResponse
+ * @throws ContainerExceptionInterface
+ * @throws NotFoundExceptionInterface
+ */
+ public function getObjects(): JSONResponse
+ {
+ // Check if the OpenRegister service is available
+ $openRegisters = $this->objectService->getOpenRegisters();
+ $data = [];
+ if ($openRegisters !== null) {
+ $data['openRegisters'] = true;
+ $data['availableRegisters'] = $openRegisters->getRegisters();
+ }
+ else {
+ $data['openRegisters'] = false;
+ }
+
+ return new JSONResponse($data);
+
+ }
}
diff --git a/lib/Db/CallLog.php b/lib/Db/CallLog.php
index c8c40c8..9dc69b5 100644
--- a/lib/Db/CallLog.php
+++ b/lib/Db/CallLog.php
@@ -80,8 +80,9 @@ public function jsonSerialize(): array
'synchronizationId' => $this->synchronizationId,
'userId' => $this->userId,
'sessionId' => $this->sessionId,
- 'expires' => $this->expires,
- 'created' => $this->created,
+ 'expires' => isset($this->expires) ? $this->expires->format('c') : null,
+ 'created' => isset($this->created) ? $this->created->format('c') : null,
+
];
}
}
diff --git a/lib/Service/AuthenticationService.php b/lib/Service/AuthenticationService.php
index b6518ed..b59e41b 100644
--- a/lib/Service/AuthenticationService.php
+++ b/lib/Service/AuthenticationService.php
@@ -327,6 +327,7 @@ public function fetchJWTToken (array $configuration): string
}
$payload = $this->getJWTPayload($configuration);
+
$jwk = $this->getJWK($configuration);
if ($jwk === null) {
diff --git a/lib/Service/CallService.php b/lib/Service/CallService.php
index 4e7ca38..4a86c60 100644
--- a/lib/Service/CallService.php
+++ b/lib/Service/CallService.php
@@ -197,6 +197,11 @@ public function call(
// Set authentication if needed. @todo: create the authentication service
//$createCertificates && $this->getCertificate($config);
+ // Make sure to filter out all the authentication variables / secrets.
+ $config = array_filter($config, function ($key) {
+ return str_contains(strtolower($key), 'authentication') === false;
+ }, ARRAY_FILTER_USE_KEY);
+
// Let's log the call.
$this->source->setLastCall(new \DateTime());
// @todo: save the source
diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php
index ca02cf1..eb5f2c0 100644
--- a/lib/Service/ObjectService.php
+++ b/lib/Service/ObjectService.php
@@ -6,6 +6,7 @@
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
+use GuzzleHttp\Exception\GuzzleException;
use OCP\App\IAppManager;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
@@ -49,7 +50,7 @@ public function getClient(array $config): Client
* @param array $config The configuration that should be used by the call.
*
* @return array The resulting object.
- * @throws \GuzzleHttp\Exception\GuzzleException
+ * @throws GuzzleException
*/
public function saveObject(array $data, array $config): array
{
@@ -81,7 +82,7 @@ public function saveObject(array $data, array $config): array
*
* @return array The objects found for given filters.
*
- * @throws \GuzzleHttp\Exception\GuzzleException
+ * @throws GuzzleException
*/
public function findObjects(array $filters, array $config): array
{
@@ -115,7 +116,7 @@ public function findObjects(array $filters, array $config): array
*
* @return array The resulting object.
*
- * @throws \GuzzleHttp\Exception\GuzzleException
+ * @throws GuzzleException
*/
public function findObject(array $filters, array $config): array
{
@@ -149,7 +150,7 @@ public function findObject(array $filters, array $config): array
*
* @return array The updated object.
*
- * @throws \GuzzleHttp\Exception\GuzzleException
+ * @throws GuzzleException
*/
public function updateObject(array $filters, array $update, array $config): array
{
@@ -181,7 +182,7 @@ public function updateObject(array $filters, array $update, array $config): arra
*
* @return array An empty array.
*
- * @throws \GuzzleHttp\Exception\GuzzleException
+ * @throws GuzzleException
*/
public function deleteObject(array $filters, array $config): array
{
@@ -206,7 +207,7 @@ public function deleteObject(array $filters, array $config): array
* @param array $pipeline The pipeline to use.
* @param array $config The configuration to use in the call.
* @return array
- * @throws \GuzzleHttp\Exception\GuzzleException
+ * @throws GuzzleException
*/
public function aggregateObjects(array $filters, array $pipeline, array $config):array
{
diff --git a/lib/Service/SynchronizationService.php b/lib/Service/SynchronizationService.php
index c4afe47..37fcb83 100644
--- a/lib/Service/SynchronizationService.php
+++ b/lib/Service/SynchronizationService.php
@@ -83,6 +83,9 @@ public function __construct(
*/
public function synchronize(Synchronization $synchronization, ?bool $isTest = false): array
{
+ if (empty($synchronization->getSourceId()) === true) {
+ throw new Exception('sourceId of synchronziation cannot be empty. Canceling synchronization..');
+ }
$objectList = $this->getAllObjectsFromSource(synchronization: $synchronization, isTest: $isTest);
@@ -151,7 +154,7 @@ private function getOriginId(Synchronization $synchronization, array $object): i
$sourceConfig = $synchronization->getSourceConfig();
// Check if a custom ID position is defined in the source configuration
- if (isset($sourceConfig['idPosition']) === true) {
+ if (isset($sourceConfig['idPosition']) === true && empty($sourceConfig['idPosition']) === false) {
// Override default with custom ID position from config
$originIdPosition = $sourceConfig['idPosition'];
}
@@ -381,7 +384,12 @@ public function getAllObjectsFromApi(Synchronization $synchronization, ?bool $is
// Make the initial API call
$response = $this->callService->call(source: $source, endpoint: $endpoint, method: 'GET', config: $config)->getResponse();
+ $lastHash = md5($response['body']);
$body = json_decode($response['body'], true);
+ if (empty($body) === true) {
+ // @todo log that we got a empty response
+ return [];
+ }
$objects = array_merge($objects, $this->getAllObjectsFromArray(array: $body, synchronization: $synchronization));
// Return a single object or empty array if in test mode
@@ -391,22 +399,31 @@ public function getAllObjectsFromApi(Synchronization $synchronization, ?bool $is
// Current page is 2 because the first call made above is page 1.
$currentPage = 2;
- $usedNextEndpoint = false;
+ $useNextEndpoint = false;
+ if (array_key_exists('next', $body)) {
+ $useNextEndpoint = true;
+ }
// Continue making API calls if there are more pages from 'next' the response body or if paginationQuery is set
- while($nextEndpoint = $this->getNextEndpoint(body: $body, url: $source->getLocation(), sourceConfig: $sourceConfig, currentPage: $currentPage)) {
- $usedNextEndpoint = true;
+ while($useNextEndpoint === true && $nextEndpoint = $this->getNextEndpoint(body: $body, url: $source->getLocation(), sourceConfig: $sourceConfig, currentPage: $currentPage)) {
// Do not pass $config here becuase it overwrites the query attached to nextEndpoint
$response = $this->callService->call(source: $source, endpoint: $nextEndpoint)->getResponse();
$body = json_decode($response['body'], true);
$objects = array_merge($objects, $this->getAllObjectsFromArray($body, $synchronization));
}
- if ($usedNextEndpoint === false) {
+ if ($useNextEndpoint === false) {
do {
$config = $this->getNextPage(config: $config, sourceConfig: $sourceConfig, currentPage: $currentPage);
$response = $this->callService->call(source: $source, endpoint: $endpoint, method: 'GET', config: $config)->getResponse();
+ $hash = md5($response['body']);
+
+ if($hash === $lastHash) {
+ break;
+ }
+
+ $lastHash = $hash;
$body = json_decode($response['body'], true);
if (empty($body) === true) {
@@ -523,7 +540,6 @@ public function getAllObjectsFromArray(array $array, Synchronization $synchroniz
*/
public function getNextlinkFromCall(array $body): ?string
{
- // Check if the 'next' key exists in the response body
return $body['next'] ?? null;
}
}
diff --git a/lib/Twig/AuthenticationRuntime.php b/lib/Twig/AuthenticationRuntime.php
index 34b59ac..1655229 100644
--- a/lib/Twig/AuthenticationRuntime.php
+++ b/lib/Twig/AuthenticationRuntime.php
@@ -2,9 +2,12 @@
namespace OCA\OpenConnector\Twig;
+use Adbar\Dot;
use GuzzleHttp\Exception\GuzzleException;
use OCA\OpenConnector\Db\Source;
use OCA\OpenConnector\Service\AuthenticationService;
+use Symfony\Component\HttpFoundation\Exception\BadRequestException;
+use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Twig\Extension\RuntimeExtensionInterface;
class AuthenticationRuntime implements RuntimeExtensionInterface
@@ -25,8 +28,12 @@ public function __construct(
*/
public function oauthToken(Source $source): string
{
+ $configuration = new Dot($source->getConfiguration(), true);
+
+ $authConfig = $configuration->get('authentication');
+
return $this->authService->fetchOAuthTokens(
- configuration: $source->getAuthenticationConfig()
+ configuration: $authConfig
);
}
@@ -39,8 +46,12 @@ public function oauthToken(Source $source): string
*/
public function jwtToken(Source $source): string
{
+ $configuration = new Dot($source->getConfiguration(), true);
+
+ $authConfig = $configuration->get('authentication');
+
return $this->authService->fetchJWTToken(
- configuration: $source->getAuthenticationConfig()
+ configuration: $authConfig
);
}
}
diff --git a/src/components/CreateEndpointDialog.vue b/src/components/CreateEndpointDialog.vue
index 0519ecb..e69de29 100644
--- a/src/components/CreateEndpointDialog.vue
+++ b/src/components/CreateEndpointDialog.vue
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/CreateWebhookDialog.vue b/src/components/CreateWebhookDialog.vue
index 0519ecb..e69de29 100644
--- a/src/components/CreateWebhookDialog.vue
+++ b/src/components/CreateWebhookDialog.vue
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/EditEndpointDialog.vue b/src/components/EditEndpointDialog.vue
index 0519ecb..e69de29 100644
--- a/src/components/EditEndpointDialog.vue
+++ b/src/components/EditEndpointDialog.vue
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/EditWebhookDialog.vue b/src/components/EditWebhookDialog.vue
index 0519ecb..e69de29 100644
--- a/src/components/EditWebhookDialog.vue
+++ b/src/components/EditWebhookDialog.vue
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/entities/callLog/index.js b/src/entities/callLog/index.js
index 7b97bc1..ec8b447 100644
--- a/src/entities/callLog/index.js
+++ b/src/entities/callLog/index.js
@@ -1,4 +1,3 @@
export * from './callLog.ts'
export * from './callLog.types.ts'
export * from './callLog.mock.ts'
-
diff --git a/src/entities/endpoint/index.js b/src/entities/endpoint/index.js
index d9efff0..5689c47 100644
--- a/src/entities/endpoint/index.js
+++ b/src/entities/endpoint/index.js
@@ -1,4 +1,3 @@
export * from './endpoint.ts'
export * from './endpoint.types.ts'
export * from './endpoint.mock.ts'
-
diff --git a/src/entities/index.js b/src/entities/index.js
index 9e9bf6e..99d426c 100644
--- a/src/entities/index.js
+++ b/src/entities/index.js
@@ -8,4 +8,4 @@ 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'
+export * from './callLog/index.js'
diff --git a/src/entities/jobLog/index.js b/src/entities/jobLog/index.js
index df9f468..2596894 100644
--- a/src/entities/jobLog/index.js
+++ b/src/entities/jobLog/index.js
@@ -1,4 +1,3 @@
export * from './jobLog.types.ts'
export * from './jobLog.types.ts'
export * from './jobLog.mock.ts'
-
diff --git a/src/entities/mapping/mapping.ts b/src/entities/mapping/mapping.ts
index 984092c..0574e29 100644
--- a/src/entities/mapping/mapping.ts
+++ b/src/entities/mapping/mapping.ts
@@ -13,7 +13,7 @@ export class Mapping extends ReadonlyBaseClass implements TMapping {
public readonly name: string
public readonly description: string
public readonly mapping: Record
- public readonly unset: any[]
+ public readonly unset: string[]
public readonly cast: Record
public readonly passThrough: boolean
public readonly dateCreated: string
@@ -47,7 +47,7 @@ export class Mapping extends ReadonlyBaseClass implements TMapping {
name: z.string().max(255),
description: z.string(),
mapping: z.record(z.any()),
- unset: z.array(z.any()),
+ unset: z.array(z.string()),
cast: z.record(z.any()),
passThrough: z.boolean(),
dateCreated: z.string().or(z.literal('')),
diff --git a/src/entities/mapping/mapping.types.ts b/src/entities/mapping/mapping.types.ts
index 92e69b5..5d7a9ed 100644
--- a/src/entities/mapping/mapping.types.ts
+++ b/src/entities/mapping/mapping.types.ts
@@ -7,7 +7,7 @@ export type TMapping = {
name: string
description: string
mapping: Record
- unset: any[]
+ unset: string[]
cast: Record
passThrough: boolean
dateCreated: string
diff --git a/src/entities/source/source.mock.ts b/src/entities/source/source.mock.ts
index d4aec98..3be4fa9 100644
--- a/src/entities/source/source.mock.ts
+++ b/src/entities/source/source.mock.ts
@@ -4,6 +4,7 @@ import { TSource } from './source.types'
export const mockSourceData = (): TSource[] => [
{
id: '5137a1e5-b54d-43ad-abd1-4b5bff5fcd3f',
+ uuid: '5137a1e5-b54d-43ad-abd1-4b5bff5fcd3f',
name: 'Test Source 1',
description: 'A test source for demonstration',
location: 'https://api.test1.com',
@@ -43,6 +44,7 @@ export const mockSourceData = (): TSource[] => [
},
{
id: '4c3edd34-a90d-4d2a-8894-adb5836ecde8',
+ uuid: '4c3edd34-a90d-4d2a-8894-adb5836ecde8',
name: 'Test Source 2',
description: 'Another test source',
location: 'https://api.test2.com',
diff --git a/src/entities/source/source.ts b/src/entities/source/source.ts
index f842a0c..7846c2d 100644
--- a/src/entities/source/source.ts
+++ b/src/entities/source/source.ts
@@ -7,6 +7,7 @@ import ReadonlyBaseClass from '../ReadonlyBaseClass.js'
export class Source extends ReadonlyBaseClass implements TSource {
public readonly id: string
+ public readonly uuid: string
public readonly name: string
public readonly description: string
public readonly reference: string
@@ -47,6 +48,7 @@ export class Source extends ReadonlyBaseClass implements TSource {
constructor(source: TSource) {
const processedSource: TSource = {
id: source.id || null,
+ uuid: source.uuid || '',
name: source.name || '',
description: source.description || '',
reference: source.reference || '',
@@ -91,6 +93,7 @@ export class Source extends ReadonlyBaseClass implements TSource {
public validate(): SafeParseReturnType {
const schema = z.object({
id: z.string().nullable(),
+ uuid: z.string(),
name: z.string().max(255),
description: z.string(),
reference: z.string(),
diff --git a/src/entities/source/source.types.ts b/src/entities/source/source.types.ts
index 1e4abd8..a47f21e 100644
--- a/src/entities/source/source.types.ts
+++ b/src/entities/source/source.types.ts
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export type TSource = {
id: string
+ uuid: string
name: string
description: string
reference: string
diff --git a/src/entities/synchronization/synchronization.ts b/src/entities/synchronization/synchronization.ts
index 700367e..c9bac36 100644
--- a/src/entities/synchronization/synchronization.ts
+++ b/src/entities/synchronization/synchronization.ts
@@ -12,7 +12,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati
public sourceType: string
public sourceHash: string
public sourceTargetMapping: string
- public sourceConfig: object
+ public sourceConfig: Record
public sourceLastChanged: string
public sourceLastChecked: string
public sourceLastSynced: string
@@ -20,7 +20,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati
public targetType: string
public targetHash: string
public targetSourceMapping: string
- public targetConfig: object
+ public targetConfig: Record
public targetLastChanged: string
public targetLastChecked: string
public targetLastSynced: string
@@ -64,7 +64,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati
sourceType: z.string(),
sourceHash: z.string(),
sourceTargetMapping: z.string(),
- sourceConfig: z.object({}),
+ sourceConfig: z.record(z.string(), z.string()),
sourceLastChanged: z.string(),
sourceLastChecked: z.string(),
sourceLastSynced: z.string(),
@@ -72,7 +72,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati
targetType: z.string(),
targetHash: z.string(),
targetSourceMapping: z.string(),
- targetConfig: z.object({}),
+ targetConfig: z.record(z.string(), z.string()),
targetLastChanged: z.string(),
targetLastChecked: z.string(),
targetLastSynced: z.string(),
diff --git a/src/entities/synchronization/synchronization.types.ts b/src/entities/synchronization/synchronization.types.ts
index e2e59bf..7dcf47a 100644
--- a/src/entities/synchronization/synchronization.types.ts
+++ b/src/entities/synchronization/synchronization.types.ts
@@ -6,7 +6,7 @@ export type TSynchronization = {
sourceType: string
sourceHash: string
sourceTargetMapping: string
- sourceConfig: object
+ sourceConfig: Record
sourceLastChanged: string
sourceLastChecked: string
sourceLastSynced: string
@@ -14,7 +14,7 @@ export type TSynchronization = {
targetType: string
targetHash: string
targetSourceMapping: string
- targetConfig: object
+ targetConfig: Record
targetLastChanged: string
targetLastChecked: string
targetLastSynced: string
diff --git a/src/modals/Endpoint/DeleteEndpoint.vue b/src/modals/Endpoint/DeleteEndpoint.vue
index 130e212..77691a5 100644
--- a/src/modals/Endpoint/DeleteEndpoint.vue
+++ b/src/modals/Endpoint/DeleteEndpoint.vue
@@ -8,7 +8,7 @@ import { endpointStore, navigationStore } from '../../store/store.js'
size="normal"
:can-close="false">
- Do you want to delete {{ endpointStore.endpointItem.name }}? This action cannot be undone.
+ Do you want to delete {{ endpointStore.endpointItem?.name }}? This action cannot be undone.
diff --git a/src/modals/JobArgument/DeleteJobArgument.vue b/src/modals/JobArgument/DeleteJobArgument.vue
index 75b779f..b41001e 100644
--- a/src/modals/JobArgument/DeleteJobArgument.vue
+++ b/src/modals/JobArgument/DeleteJobArgument.vue
@@ -22,7 +22,7 @@ import { navigationStore, jobStore } from '../../store/store.js'
Do you want to delete {{ jobStore.jobArgumentKey }}? This action cannot be undone.
-
+
@@ -94,11 +94,7 @@ export default {
this.success = true
// Wait for the user to read the feedback then close the model
- const self = this
- this.closeTimeoutFunc = setTimeout(function() {
- self.success = null
- navigationStore.setModal(false)
- }, 2000)
+ this.closeTimeoutFunc = setTimeout(this.closeModal, 2000)
})
.catch((err) => {
this.error = err
diff --git a/src/modals/Log/ViewSynchronizationContract.vue b/src/modals/Log/ViewSynchronizationContract.vue
index 7af566f..b0fe66b 100644
--- a/src/modals/Log/ViewSynchronizationContract.vue
+++ b/src/modals/Log/ViewSynchronizationContract.vue
@@ -13,18 +13,17 @@ import { logStore, navigationStore } from '../../store/store.js'
Standard
-
+ {{ openRegisterIsAvailable
+ ? 'Some features require Open Register to be installed'
+ : 'This either means that Open Register is not available on this server or you need to confirm your password' }}
+
The connection to the synchronization was successful.
-
-
-
An error occurred while testing the connection: {{ synchronizationStore.synchronizationTest ? synchronizationStore.synchronizationTest.response.statusMessage : error }}
+
+
An error occurred while testing the synchronization.
+
+
+
+
diff --git a/src/navigation/MainMenu.vue b/src/navigation/MainMenu.vue
index d2c0957..51cfb59 100644
--- a/src/navigation/MainMenu.vue
+++ b/src/navigation/MainMenu.vue
@@ -30,7 +30,11 @@ import { navigationStore } from '../store/store.js'
-
+
@@ -39,6 +43,8 @@ import { navigationStore } from '../store/store.js'
+
+
diff --git a/src/services/getValidISOstring.js b/src/services/getValidISOstring.js
index 34e937a..231912e 100644
--- a/src/services/getValidISOstring.js
+++ b/src/services/getValidISOstring.js
@@ -1,6 +1,8 @@
/**
* Converts a given date string or Date object to a valid ISO string.
*
+ * this function can double as a validator for ISO / date strings
+ *
* If the dateString is valid it will return the ISO string,
* if it is not a valid dateString it will return null.
*
diff --git a/src/store/modules/mapping.js b/src/store/modules/mapping.js
index 18c11a4..17b92b8 100644
--- a/src/store/modules/mapping.js
+++ b/src/store/modules/mapping.js
@@ -8,6 +8,7 @@ export const useMappingStore = defineStore('mapping', {
mappingList: [],
mappingMappingKey: null,
mappingCastKey: null,
+ mappingUnsetKey: null,
}),
actions: {
setMappingItem(mappingItem) {
@@ -28,6 +29,10 @@ export const useMappingStore = defineStore('mapping', {
this.mappingCastKey = mappingCastKey
console.log('Active mapping cast key set to ' + mappingCastKey)
},
+ setMappingUnsetKey(mappingUnsetKey) {
+ this.mappingUnsetKey = mappingUnsetKey
+ console.log('Active mapping unset key set to ' + mappingUnsetKey)
+ },
/* istanbul ignore next */ // ignore this for Jest until moved into a service
async refreshMappingList(search = null) {
// @todo this might belong in a service?
@@ -35,23 +40,17 @@ export const useMappingStore = defineStore('mapping', {
if (search !== null && search !== '') {
endpoint = endpoint + '?_search=' + search
}
- return fetch(endpoint, {
+
+ const response = await fetch(endpoint, {
method: 'GET',
})
- .then(
- (response) => {
- response.json().then(
- (data) => {
- this.setMappingList(data.results)
- },
- )
- },
- )
- .catch(
- (err) => {
- console.error(err)
- },
- )
+
+ const data = (await response.json()).results
+ const entities = data.map(mapping => new Mapping(mapping))
+
+ this.setMappingList(entities)
+
+ return { response, entities }
},
// New function to get a single mapping
async getMapping(id) {
@@ -178,5 +177,63 @@ export const useMappingStore = defineStore('mapping', {
return { response, data }
},
+ /**
+ * Get objects on a mapping from the endpoint.
+ *
+ * This method fetches objects related to a mapping from the specified API endpoint.
+ *
+ * @throws Will throw an error if the fetch operation fails.
+ * @return { Promise<{ response: Response, data: object }> } The response and data from the API.
+ */
+ async getMappingObjects() {
+ console.log('Fetching mapping objects...')
+
+ // Fetch objects related to a mapping from the API endpoint
+ const response = await fetch(
+ '/index.php/apps/openconnector/api/mappings/objects',
+ {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ )
+
+ // Parse the response data as JSON
+ const data = await response.json()
+
+ // Return the response and parsed data
+ return { response, data }
+ },
+ /**
+ * Save a mapping object to the endpoint.
+ *
+ * This method sends a mapping object to the specified API endpoint to be saved.
+ *
+ * @param { object } mappingObject - The mapping object to be saved.
+ * @return { Promise<{ response: Response, data: object }> } The response and data from the API.
+ * @throws Will throw an error if the save operation fails.
+ */
+ async saveMappingObject(mappingObject) {
+ console.log('Saving mapping object...')
+
+ // Send the mapping object to the API endpoint to be saved
+ const response = await fetch(
+ '/index.php/apps/openconnector/api/mappings/objects',
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(mappingObject),
+ },
+ )
+
+ // Parse the response data as JSON
+ const data = await response.json()
+
+ // Return the response and parsed data
+ return { response, data }
+ },
},
})
diff --git a/src/store/modules/source.js b/src/store/modules/source.js
index 9c2fa6a..7167d64 100644
--- a/src/store/modules/source.js
+++ b/src/store/modules/source.js
@@ -41,30 +41,29 @@ export const useSourceStore = defineStore(
this.sourceConfigurationKey = sourceConfigurationKey
console.log('Source configuration key set to ' + sourceConfigurationKey)
},
- /* istanbul ignore next */ // ignore this for Jest until moved into a service
+ /**
+ * Refreshes the source list by fetching data from the API.
+ *
+ * @param { string | null } search - The search query to filter sources.
+ * @return { Promise<{ response: Response, data: Array, entities: Array
@@ -195,6 +195,8 @@ import Update from 'vue-material-design-icons/Update.vue'
import Sync from 'vue-material-design-icons/Sync.vue'
import EyeOutline from 'vue-material-design-icons/EyeOutline.vue'
+import getValidISOstring from '../../services/getValidISOstring.js'
+
export default {
name: 'JobDetails',
components: {
diff --git a/src/views/Mapping/MappingDetails.vue b/src/views/Mapping/MappingDetails.vue
index 841d493..72257fb 100644
--- a/src/views/Mapping/MappingDetails.vue
+++ b/src/views/Mapping/MappingDetails.vue
@@ -33,6 +33,12 @@ import { mappingStore, navigationStore } from '../../store/store.js'
Add Cast
+
+
+
+
+ Add Unset
+
@@ -141,6 +147,42 @@ import { mappingStore, navigationStore } from '../../store/store.js'
No cast found
+
+