Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added source hash mapping #129

Open
wants to merge 3 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/Db/Synchronization.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Synchronization extends Entity implements JsonSerializable
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 $sourceHashMapping = null; // The mapping id of the mapping that we map the object to for hashing.
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
Expand Down Expand Up @@ -46,6 +47,7 @@ public function __construct() {
$this->addType('sourceId', 'string');
$this->addType('sourceType', 'string');
$this->addType('sourceHash', 'string');
$this->addType('sourceHashMapping', 'string');
$this->addType('sourceTargetMapping', 'string');
$this->addType('sourceConfig', 'json');
$this->addType('sourceLastChanged', 'datetime');
Expand Down Expand Up @@ -121,6 +123,7 @@ public function jsonSerialize(): array
'sourceId' => $this->sourceId,
'sourceType' => $this->sourceType,
'sourceHash' => $this->sourceHash,
'sourceHashMapping' => $this->sourceHashMapping,
'sourceTargetMapping' => $this->sourceTargetMapping,
'sourceConfig' => $this->sourceConfig,
'sourceLastChanged' => isset($this->sourceLastChanged) === true ? $this->sourceLastChanged->format('c') : null,
Expand Down
62 changes: 62 additions & 0 deletions lib/Migration/Version1Date20241210100000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\OpenConnector\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

/**
* FIXME Auto-generated migration step: Please modify to your needs!
*/
class Version1Date20241210100000 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/**
* @var ISchemaWrapper $schema
*/
$schema = $schemaClosure();

if ($schema->hasTable('openconnector_synchronizations') === true) {
$table = $schema->getTable('openconnector_synchronizations');

if ($table->hasColumn('sourceHashMapping') === false) {
$table->dropColumn('sourceHashMapping');
$table->addColumn('source_hash_mapping', Types::STRING)->setNotnull(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks weird, please explain why you are doing this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to be able to map before hashing

}
}

return $schema;
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
}
}
69 changes: 56 additions & 13 deletions lib/Service/SynchronizationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,57 @@ private function fetchExtraDataForObject(Synchronization $synchronization, array
return $extraData;
}

/**
* Fetches multiple extra data entries for an object based on the source configuration.
*
* This method iterates through a list of extra data configurations, fetches the additional data for each configuration,
* and merges it with the original object.
*
* @param Synchronization $synchronization The synchronization instance containing configuration details.
* @param array $sourceConfig The source configuration containing extra data retrieval settings.
* @param array $object The original object for which extra data needs to be fetched.
*
* @return array The updated object with all fetched extra data merged into it.
*/
private function fetchMultipleExtraData(Synchronization $synchronization, array $sourceConfig, array $object): array
{
if (isset($sourceConfig[$this::EXTRA_DATA_CONFIGS_LOCATION]) === true) {
foreach ($sourceConfig[$this::EXTRA_DATA_CONFIGS_LOCATION] as $extraDataConfig) {
$object = array_merge($object, $this->fetchExtraDataForObject($synchronization, $extraDataConfig, $object));
}
}

return $object;
}

/**
* Maps a given object using a source hash mapping configuration.
*
* This function retrieves a hash mapping configuration for a synchronization instance, if available,
* and applies it to the input object using the mapping service.
*
* @param Synchronization $synchronization The synchronization instance containing the hash mapping configuration.
* @param array $object The input object to be mapped.
*
* @return array The mapped object, or the original object if no mapping is found.
*/
private function mapHashObject(Synchronization $synchronization, array $object): array
{
if (empty($synchronization->getSourceHashMapping()) === false) {
try {
$sourceHashMapping = $this->mappingMapper->find(id: $synchronization->getSourceHashMapping());
} catch (DoesNotExistException $exception) {
return new Exception($exception->getMessage());
}

// Execute mapping if found
if ($sourceHashMapping) {
return $this->mappingService->executeMapping(mapping: $sourceHashMapping, input: $object);
}
}

return $object;
}

/**
* Synchronize a contract
Expand All @@ -314,27 +365,19 @@ public function synchronizeContract(SynchronizationContract $synchronizationCont
$sourceConfig = $this->callService->applyConfigDot($synchronization->getSourceConfig());

// Check if extra data needs to be fetched
if (isset($sourceConfig[$this::EXTRA_DATA_CONFIGS_LOCATION]) === true) {
foreach ($sourceConfig[$this::EXTRA_DATA_CONFIGS_LOCATION] as $extraDataConfig) {
$object = array_merge($object, $this->fetchExtraDataForObject($synchronization, $extraDataConfig, $object));
}
}

// @TODO: This should be unset through pre-mapping
if(isset($object['d']['vti_x005f_dirlateststamp']) === true) {
unset($object['d']['vti_x005f_dirlateststamp']);
}

$object = $this->fetchMultipleExtraData(synchronization: $synchronization, sourceConfig: $sourceConfig, object: $object);

// Get mapped hash object (some fields can make it look the object has changed even if it hasn't)
$hashObject = $this->mapHashObject(synchronization: $synchronization, object: $object);
// Let create a source hash for the object
$originHash = md5(serialize($object));
$originHash = md5(serialize($hashObject));

// Let's prevent pointless updates @todo account for omnidirectional sync, unless the config has been updated since last check then we do want to rebuild and check if the tagert object has changed
if ($originHash === $synchronizationContract->getOriginHash() && $synchronization->getUpdated() < $synchronizationContract->getSourceLastChecked()) {
// The object has not changed and the config has not been updated since last check
return $synchronizationContract;
}

// The object has changed, oke let do mappig and bla die bla
$synchronizationContract->setOriginHash($originHash);
$synchronizationContract->setSourceLastChanged(new DateTime());
Expand Down
2 changes: 2 additions & 0 deletions src/entities/synchronization/synchronization.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const mockSynchronizationData = (): TSynchronization[] => [
sourceId: 'source1',
sourceType: 'api',
sourceHash: 'source1',
sourceHashMapping: '1',
sourceTargetMapping: 'source1',
sourceConfig: {},
sourceLastChanged: '2023-05-01T12:00:00Z',
Expand All @@ -32,6 +33,7 @@ export const mockSynchronizationData = (): TSynchronization[] => [
sourceId: 'source2',
sourceType: 'api',
sourceHash: 'source2',
sourceHashMapping: '1',
sourceTargetMapping: 'source2',
sourceConfig: {},
sourceLastChanged: '2023-05-02T12:00:00Z',
Expand Down
3 changes: 3 additions & 0 deletions src/entities/synchronization/synchronization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati
public sourceId: string
public sourceType: string
public sourceHash: string
public sourceHashMapping: string
public sourceTargetMapping: string
public sourceConfig: Record<string, string>
public sourceLastChanged: string
Expand All @@ -35,6 +36,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati
sourceId: synchronization.sourceId || '',
sourceType: synchronization.sourceType || '',
sourceHash: synchronization.sourceHash || '',
sourceHashMapping: synchronization.sourceHashMapping || '',
sourceTargetMapping: synchronization.sourceTargetMapping || '',
sourceConfig: synchronization.sourceConfig || {},
sourceLastChanged: synchronization.sourceLastChanged || '',
Expand Down Expand Up @@ -63,6 +65,7 @@ export class Synchronization extends ReadonlyBaseClass implements TSynchronizati
sourceId: z.string(),
sourceType: z.string(),
sourceHash: z.string(),
sourceHashMapping: z.string(),
sourceTargetMapping: z.string(),
sourceConfig: z.record(z.string(), z.string()),
sourceLastChanged: z.string(),
Expand Down
1 change: 1 addition & 0 deletions src/entities/synchronization/synchronization.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type TSynchronization = {
sourceId: string
sourceType: string
sourceHash: string
sourceHashMapping: string
sourceTargetMapping: string
sourceConfig: Record<string, string>
sourceLastChanged: string
Expand Down
19 changes: 17 additions & 2 deletions src/modals/Synchronization/EditSynchronization.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,15 @@ import { synchronizationStore, navigationStore, sourceStore, mappingStore } from
:loading="sourcesLoading"
input-label="Source ID" />

<NcSelect v-bind="sourceTargetMappingOptions"
v-model="sourceTargetMappingOptions.hashValue"
:loading="sourceTargetMappingLoading"
input-label="Source hash mapping" />

<NcSelect v-bind="sourceTargetMappingOptions"
v-model="sourceTargetMappingOptions.sourceValue"
:loading="sourceTargetMappingLoading"
input-label="sourceTargetMapping" />
input-label="Source target mapping" />

<NcTextField :value.sync="synchronizationItem.sourceConfig.idPosition"
label="(optional) Position of id in source object" />
Expand Down Expand Up @@ -135,7 +140,7 @@ import { synchronizationStore, navigationStore, sourceStore, mappingStore } from
<NcSelect v-bind="sourceTargetMappingOptions"
v-model="sourceTargetMappingOptions.targetValue"
:loading="sourceTargetMappingLoading"
input-label="targetSourceMapping" />
input-label="Target source mapping" />
</form>

<NcButton v-if="!success"
Expand Down Expand Up @@ -208,6 +213,7 @@ export default {
headers: {},
query: {},
},
sourceHashMapping: '',
sourceTargetMapping: '',
targetId: '',
targetType: 'register/schema',
Expand All @@ -234,6 +240,7 @@ export default {
sourceTargetMappingLoading: false, // Indicates if the mappings are loading
sourceTargetMappingOptions: { // A list of mappings
options: [],
hashValue: null,
sourceValue: null,
targetValue: null,
},
Expand Down Expand Up @@ -340,12 +347,19 @@ export default {
.then(({ entities }) => {
const activeSourceMapping = entities.find(mapping => mapping.id.toString() === this.synchronizationItem.sourceTargetMapping.toString())
const activeTargetMapping = entities.find(mapping => mapping.id.toString() === this.synchronizationItem.targetSourceMapping.toString())
const sourceHashMapping = entities.find(mapping => mapping.id.toString() === this.synchronizationItem.sourceHashMapping.toString())

this.sourceTargetMappingOptions = {
options: entities.map(mapping => ({
label: mapping.name,
id: mapping.id,
})),
hashValue: sourceHashMapping
? {
label: sourceHashMapping.name,
id: sourceHashMapping.id,
}
: null,
sourceValue: activeSourceMapping
? {
label: activeSourceMapping.name,
Expand Down Expand Up @@ -528,6 +542,7 @@ export default {
...this.synchronizationItem,
sourceId: this.sourceOptions.sourceValue?.id || null,
sourceType: this.typeOptions.value?.id || null,
sourceHashMapping: this.sourceTargetMappingOptions.hashValue?.id || null,
sourceTargetMapping: this.sourceTargetMappingOptions.sourceValue?.id || null,
targetType: this.targetTypeOptions.value?.id || null,
targetId: targetId || null,
Expand Down
4 changes: 4 additions & 0 deletions src/views/Synchronization/SynchronizationDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ import { synchronizationStore, navigationStore, logStore } from '../../store/sto
<b>Source Hash:</b>
<p>{{ synchronizationStore.synchronizationItem.sourceHash || 'N/A' }}</p>
</div>
<div class="gridContent gridFullWidth">
<b>Source Hash mapping id:</b>
<p>{{ synchronizationStore.synchronizationItem.sourceHashMapping || 'N/A' }}</p>
</div>
<div class="gridContent gridFullWidth">
<b>Source Last Changed:</b>
<p>{{ synchronizationStore.synchronizationItem.sourceLastChanged || 'N/A' }}</p>
Expand Down
Loading