Skip to content
Merged
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
4 changes: 2 additions & 2 deletions lib/Service/ConfigurationHandlers/RuleHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function export(Entity $entity, array $mappings, array &$mappingIds = [])
*/
private function convertIdsToSlugs(array $config, array $mappings, array &$mappingIds = []): array
{
$entityTypes = ['source', 'job', 'endpoint', 'mapping', 'register', 'schema'];
$entityTypes = ['source', 'job', 'endpoint', 'mapping', 'register', 'schema', 'synchronization'];

foreach ($config as $key => $value) {
if (is_array($value)) {
Expand Down Expand Up @@ -134,7 +134,7 @@ public function import(array $data, array $mappings): Entity
*/
private function convertSlugsToIds(array $config, array $mappings): array
{
$entityTypes = ['source', 'job', 'endpoint', 'mapping', 'register', 'schema'];
$entityTypes = ['source', 'job', 'endpoint', 'mapping', 'register', 'schema', 'synchronization'];

foreach ($config as $key => $value) {
if (is_array($value)) {
Expand Down
102 changes: 102 additions & 0 deletions lib/Service/ConfigurationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use OCA\OpenConnector\Service\ConfigurationHandlers\JobHandler;
use OCA\OpenConnector\Service\ConfigurationHandlers\SourceHandler;
use OCA\OpenConnector\Service\ConfigurationHandlers\RuleHandler;
use OCP\AppFramework\Db\Entity;

/**
* Class ConfigurationService
Expand Down Expand Up @@ -218,6 +219,93 @@ private function resetMappings(): void
$this->mappings['schema']['slugToId'] = $this->schemaMapper->getSlugToIdMap();
}

/**
* Register a freshly imported entity in the in-memory slug/ID maps.
*
* This keeps references created earlier in the same import run available
* to later entities without requiring a second import pass.
*
* @param string $entityType Mapping bucket name
* @param Entity $entity Imported entity
*
* @return void
*/
private function registerImportedEntityMapping(string $entityType, Entity $entity): void
{
if (isset($this->mappings[$entityType]) === false) {
return;
}

if (method_exists($entity, 'getSlug') === false) {
return;
}

$id = $entity->getId();
$slug = $entity->getSlug();

if ($id === null || $slug === null || $slug === '') {
return;
}

$id = (string) $id;

$this->mappings[$entityType]['idToSlug'][$id] = $slug;
$this->mappings[$entityType]['slugToId'][$slug] = $id;
}

/**
* Ensure imported component payloads always carry their component-key slug.
*
* Some exports reference related entities by component key even when the nested
* payload is missing or inconsistent about its own slug field. Normalizing the
* slug up front keeps first-pass imports referentially stable.
*
* @param array $data Component payload
* @param string $slug Component key
*
* @return array
*/
private function withComponentSlug(array $data, string $slug): array
{
if (($data['slug'] ?? null) === null || $data['slug'] === '') {
$data['slug'] = $slug;
}

return $data;
}

/**
* Re-run import handlers for entity types whose dependencies are imported later
* in the ordered pass, so those references are resolved with the complete mapping set.
*
* Rules (step 3) and endpoints (step 4) are imported before synchronizations (step 5),
* so any references they hold to synchronizations cannot be resolved in the first pass.
* Synchronizations and jobs are imported last and have no such forward dependencies.
*
* Each handler is idempotent (upsert by slug), so re-running it is safe.
*
* @param array<string,mixed> $components Original imported components
* @param array<string,array<string,Entity>> $result Imported entity result map
*
* @return void
*/
private function reconcileImportedReferences(array $components, array &$result): void
{
if (isset($components['rules']) && is_array($components['rules'])) {
foreach ($components['rules'] as $ruleSlug => $ruleData) {
$ruleData = $this->withComponentSlug($ruleData, $ruleSlug);
$result['rules'][$ruleSlug] = $this->handlers['rule']->import($ruleData, $this->mappings);
}
}

if (isset($components['endpoints']) && is_array($components['endpoints'])) {
foreach ($components['endpoints'] as $endpointSlug => $endpointData) {
$endpointData = $this->withComponentSlug($endpointData, $endpointSlug);
$result['endpoints'][$endpointSlug] = $this->handlers['endpoint']->import($endpointData, $this->mappings);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Concern — reconcileImportedReferences doesn't re-register updated endpoint mappings after the second-pass import

After re-importing endpoints here, registerImportedEntityMapping is not called on the updated entity. If the upsert produces a freshly-assigned ID (e.g. the first-pass import failed to fully resolve the entity and the second pass creates it), $this->mappings['endpoint'] will be stale.

No current downstream step reads from the endpoint mapping after the reconcile pass runs, so there is no observable bug today. However, if reconcileImportedReferences is extended to include additional entity types that reference endpoints, the stale mapping will cause hard-to-trace failures.

Consider calling $this->registerImportedEntityMapping('endpoint', $result['endpoints'][$endpointSlug]) after this line.

}
}
}

/**
* Get all entities associated with a specific configuration ID, indexed by their slug.
*
Expand Down Expand Up @@ -753,51 +841,65 @@ public function importConfiguration(array $oas): array
// 1. Import sources first (no dependencies).
if (isset($components['sources'])) {
foreach ($components['sources'] as $sourceSlug => $sourceData) {
$sourceData = $this->withComponentSlug($sourceData, $sourceSlug);
$source = $this->handlers['source']->import($sourceData, $this->mappings);
$result['sources'][$sourceSlug] = $source;
$this->registerImportedEntityMapping('source', $source);
}
}

// 2. Import mappings (depends on sources).
if (isset($components['mappings'])) {
foreach ($components['mappings'] as $mappingSlug => $mappingData) {
$mappingData = $this->withComponentSlug($mappingData, $mappingSlug);
$mapping = $this->handlers['mapping']->import($mappingData, $this->mappings);
$result['mappings'][$mappingSlug] = $mapping;
$this->registerImportedEntityMapping('mapping', $mapping);
}
}

// 3. Import rules (depends on sources).
if (isset($components['rules'])) {
foreach ($components['rules'] as $ruleSlug => $ruleData) {
$ruleData = $this->withComponentSlug($ruleData, $ruleSlug);
$rule = $this->handlers['rule']->import($ruleData, $this->mappings);
$result['rules'][$ruleSlug] = $rule;
$this->registerImportedEntityMapping('rule', $rule);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Concern — registerImportedEntityMapping calls for 'rule' and 'job' are dead code

$this->mappings only contains buckets initialised by resetMappings() (source, mapping, endpoint, synchronization, and similar mapper-backed types). There is no 'rule' or 'job' bucket, so both calls hit the early-return guard (isset($this->mappings[$entityType]) === false) and silently no-op. The guard prevents corruption, but the calls imply rule and job slugs participate in cross-entity slug resolution — which they currently do not.

Same issue applies to the registerImportedEntityMapping('job', $job) call a few lines below.

Either remove both dead calls, or — if rule/job slugs are intended to be resolvable in the future — add the corresponding buckets to resetMappings().

}
}

// 4. Import endpoints (depends on sources and mappings).
if (isset($components['endpoints'])) {
foreach ($components['endpoints'] as $endpointSlug => $endpointData) {
$endpointData = $this->withComponentSlug($endpointData, $endpointSlug);
$endpoint = $this->handlers['endpoint']->import($endpointData, $this->mappings);
$result['endpoints'][$endpointSlug] = $endpoint;
$this->registerImportedEntityMapping('endpoint', $endpoint);
}
}

// 5. Import synchronizations (depends on sources, mappings, and endpoints).
if (isset($components['synchronizations'])) {
foreach ($components['synchronizations'] as $syncSlug => $syncData) {
$syncData = $this->withComponentSlug($syncData, $syncSlug);
$synchronization = $this->handlers['synchronization']->import($syncData, $this->mappings);
$result['synchronizations'][$syncSlug] = $synchronization;
$this->registerImportedEntityMapping('synchronization', $synchronization);
}
}

// 6. Import jobs (depends on synchronizations, endpoints, and sources).
if (isset($components['jobs'])) {
foreach ($components['jobs'] as $jobSlug => $jobData) {
$jobData = $this->withComponentSlug($jobData, $jobSlug);
$job = $this->handlers['job']->import($jobData, $this->mappings);
$result['jobs'][$jobSlug] = $job;
$this->registerImportedEntityMapping('job', $job);
}
}

$this->reconcileImportedReferences($components, $result);

return $result;
}
}
Loading