From 3a28c0d0f66952a18cb19a1e40cce964034961ce Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Fri, 28 Nov 2025 11:20:35 +0200 Subject: [PATCH] SqlSource,FileSource. Mapper and cache engine improvements. --- .../Transmogrify/config/schema/tables.json | 153 ++++++++++++------ .../src/Command/TransmogrifyCommand.php | 34 ++-- .../src/Lib/Traits/ManageDefaultsTrait.php | 22 +++ .../src/Lib/Traits/RowTransformationTrait.php | 9 ++ .../src/Lib/Traits/TypeMapperTrait.php | 84 +++++++++- 5 files changed, 228 insertions(+), 74 deletions(-) diff --git a/app/plugins/Transmogrify/config/schema/tables.json b/app/plugins/Transmogrify/config/schema/tables.json index 7c2a128c..430c6c39 100644 --- a/app/plugins/Transmogrify/config/schema/tables.json +++ b/app/plugins/Transmogrify/config/schema/tables.json @@ -2,9 +2,11 @@ "__COMMENT__": "Template for adding new table configurations to the Transmogrify mapping. Keys starting with double underscores (__) are ignored by processors and exist only for documentation.", "__EXAMPLE_TABLE_TEMPLATE__": { "__INSTRUCTIONS__": "Copy this object, rename the key to your logical table name (eg, 'my_items'), and adjust values. Do NOT leave this example enabled in production.", + "__INSTRUCTIONS__": "When mapping legacy type fields, call the function-based mapper (eg, &map...Type) before configuring null for the old column name. The mapper still needs the original source column value, and performNoMapping() will unset that column once it sees a null mapping. In other words, always place the null mapping for the old column after the new field’s function mapping so the mapper can read the legacy value before it is removed.", "source": "cm_my_items", "displayField": "name", "addChangelog": true, + "plugin": "MyItems", "booleans": ["is_active", "is_primary"], "cache": [ "co_id", @@ -196,6 +198,11 @@ "cache": ["co_id"], "fieldMap": { "name": "description", + "sync_affiliation_type_id": "&mapAffiliationType", + "sync_identifier_type_id": "&mapIdentifierType", + "match_identifier_type_id": "&mapIdentifierType", + "match_email_address_type_id": "&mapEmailType", + "co_pipeline_id": "pipeline_id", "sync_on_add": null, "sync_on_update": null, "sync_on_delete": null, @@ -205,13 +212,8 @@ "match_type": null, "sync_affiliation": null, "sync_identifier_type": null, - "sync_affiliation_type_id": "&mapAffiliationType", - "sync_identifier_type_id": "&mapIdentifierType", - "match_identifier_type_id": "&mapIdentifierType", - "match_email_address_type_id": "&mapEmailType", "establish_clusters": null, - "co_enrollment_flow_id": null, - "co_pipeline_id": "pipeline_id" + "co_enrollment_flow_id": null } }, "external_identity_sources": { @@ -222,13 +224,13 @@ "fieldMap": { "plugin": "&mapExternalIdentitySourcePlugin", "co_pipeline_id": "pipeline_id", + "org_identity_source_id": "external_identity_source_id", "sync_mode": null, "sync_query_mismatch_mode": null, "sync_query_skip_known": null, "sync_on_user_login": null, "eppn_identifier_type": null, - "eppn_suffix": null, - "org_identity_source_id": "external_identity_source_id" + "eppn_suffix": null } }, "orcid_sources": { @@ -263,17 +265,16 @@ "addChangelog": true, "fieldMap": { "org_identity_source_id": "external_identity_source_id", - "duplicate_mode": null, "redirect_on_duplicate": "redirect_on_duplicate", "sp_type": "sp_mode", "default_affiliation_type_id": "&mapAffiliationType", - "default_affiliation": null, "address_type_id": "&mapToDefaultAddressTypeId", "email_address_type_id": "&mapToDefaultEmailAddressTypeId", "name_type_id": "&mapToDefaultNameTypeId", "telephone_number_type_id": "&mapToDefaultTelephoneNumberTypeId", "env_o": "env_organization", "env_ou": "env_department", + "env_identifier_sorid": "env_identifier_sourcekey", "env_identifier_eppn_login": null, "env_identifier_eptid_login": null, "env_identifier_epuid_login": null, @@ -282,9 +283,10 @@ "env_identifier_orcid_login": null, "env_identifier_samlpairwiseid_login": null, "env_identifier_samlsubjectid_login": null, - "env_identifier_sorid": "env_identifier_sourcekey", "env_identifier_sorid_login": null, - "env_identifier_network_login": null + "env_identifier_network_login": null, + "duplicate_mode": null, + "default_affiliation": null } }, "api_sources": { @@ -313,6 +315,47 @@ }, "addChangelog": true }, + "file_sources": { + "source": "cm_file_sources", + "plugin": "FileConnector", + "displayField": "id", + "booleans": [ + "threshold_override" + ], + "fieldMap": { + "org_identity_source_id": "external_identity_source_id", + "filepath": "filename", + "format": "=C3", + "threshold_warn": "threshold_check" + }, + "addChangelog": true + }, + "sql_sources": { + "source": "cm_sql_sources", + "plugin": "SqlConnector", + "displayField": "id", + "booleans": [ + "threshold_override" + ], + "fieldMap": { + "org_identity_source_id": "external_identity_source_id", + "address_type_id": "&mapAddressType", + "email_address_type_id": "&mapEmailType", + "identifier_type_id": "&mapIdentifierType", + "name_type_id": "&mapNameType", + "pronouns_type_id": "&mapPronounsTypeDefault", + "telephone_number_type_id": "&mapTelephoneType", + "url_type_id": "&mapUrlType", + "__NOTE__": "Old columns removal should go after the function mappers", + "address_type": null, + "email_address_type": null, + "identifier_type": null, + "name_type": null, + "telephone_number_type": null, + "url_type": null + }, + "addChangelog": true + }, "__NOTES__": "DATA MIGRATIONS", "authentication_events": { "source": "cm_authentication_events", @@ -337,12 +380,12 @@ "co_person_id": "person_id", "co_person_role_id": "person_role_id", "affiliation_type_id": "&mapAffiliationType", - "affiliation": null, "manager_co_person_id": "manager_person_id", "sponsor_co_person_id": "sponsor_person_id", "o": "organization", "ou": "department", - "source_org_identity_id": null + "source_org_identity_id": null, + "affiliation": null } }, "external_identities": { @@ -352,7 +395,6 @@ "sqlSelect": "orgidentitiesSqlSelect", "postRow": "mapExternalIdentityToExternalIdentityRole", "fieldMap": { - "co_id": null, "person_id": "&mapOrgIdentityCoPersonId", "org_identity_id": "external_identity_id", "title": null, @@ -362,7 +404,8 @@ "manager_identifier": null, "sponsor_identifier": null, "valid_from": null, - "valid_through": null + "valid_through": null, + "co_id": null } }, "groups": { @@ -372,10 +415,10 @@ "booleans": ["nesting_mode_all", "open"], "preRow": "applyCheckGroupNameARRule", "fieldMap": { - "auto": null, "co_group_id": "group_id", "group_type": "?S", - "introduction": null + "introduction": null, + "auto": null }, "postTable": "createOwnersGroups" }, @@ -397,11 +440,11 @@ "fieldMap": { "co_group_id": "group_id", "co_person_id": "person_id", - "member": null, - "owner": null, "co_group_nesting_id": "group_nesting_id", "co_group_member_id": "group_member_id", - "source_org_identity_id": null + "source_org_identity_id": null, + "member": null, + "owner": null } }, "names": { @@ -514,12 +557,12 @@ "resolver_co_person_id": "resolver_person_id", "action": "&mapNotificationAction", "source_url": "source", + "email_body": "email_body_text", "source_controller": null, "source_action": null, "source_id": null, "source_arg0": null, - "source_val0": null, - "email_body": "email_body_text" + "source_val0": null } }, "history_records": { @@ -542,14 +585,14 @@ "displayField": "id", "fieldMap": { "job_type": "plugin", - "job_mode": null, "queue_time": "register_time", "complete_time": "finish_time", - "job_type_fk": null, "job_params": "parameters", "requeued_from_co_job_id": "requeued_from_job_id", "max_retry": null, - "max_retry_count": null + "max_retry_count": null, + "job_type_fk": null, + "job_mode": null }, "preRow": "validateJobIsTransmogrifiable" }, @@ -575,6 +618,12 @@ "fieldMap": { "authz_level": "authz_type", "authz_co_group_id": "authz_group_id", + "notification_co_group_id": "notification_group_id", + "redirect_on_finalize": "redirect_on_finalize", + "status": "=S", + "approval_template_id": "notification_message_template_id", + "finalization_template_id": "finalization_message_template_id", + "co_enrollment_flow_id": "enrollment_flow_id", "my_identity_shortcut": null, "match_policy": null, "match_server_id": null, @@ -588,8 +637,6 @@ "invitation_validity": null, "regenerate_expired_verification": null, "require_authn": null, - "notification_co_group_id": "notification_group_id", - "status": "=S", "notify_from": null, "verification_subject": null, "verification_body": null, @@ -598,26 +645,22 @@ "notify_on_approval": null, "approval_subject": null, "approval_body": null, - "approval_template_id": "notification_message_template_id", "approver_template_id": null, "denial_template_id": null, "notify_on_finalize": null, - "finalization_template_id": "finalization_message_template_id", "introduction_text": null, "conclusion_text": null, "introduction_text_pa": null, "t_and_c_mode": null, "redirect_on_submit": null, "redirect_on_confirm": null, - "redirect_on_finalize": "redirect_on_finalize", "return_url_allowlist": null, "ignore_authoritative": null, "duplicate_mode": null, "co_theme_id": null, "theme_stacking": null, "establish_authenticators": null, - "establish_cluster_accounts": null, - "co_enrollment_flow_id": "enrollment_flow_id" + "establish_cluster_accounts": null } }, "petitions": { @@ -626,26 +669,26 @@ "canSkip": "true", "fieldMap": { "co_enrollment_flow_id": "enrollment_flow_id", - "co_id": null, "cou_id": "cou_id", "status": "&mapPetitionStatus", - "token": null, - "enrollee_org_identity_id": null, - "archived_org_identity_id": null, "enrollee_co_person_id": "enrollee_person_id", - "enrollee_co_person_role_id": null, "petitioner_co_person_id": "petitioner_person_id", - "sponsor_co_person_id": null, - "approver_co_person_id": null, - "co_invite_id": null, - "vetting_request_id": null, "authenticated_identifier": "petitioner_identifier", "reference_identifier": "enrollee_identifier", + "co_petition_id": "petition_id", "petitioner_token": null, "enrollee_token": null, "return_url": null, "approver_comment": null, - "co_petition_id": "petition_id" + "sponsor_co_person_id": null, + "approver_co_person_id": null, + "co_invite_id": null, + "vetting_request_id": null, + "enrollee_org_identity_id": null, + "archived_org_identity_id": null, + "enrollee_co_person_role_id": null, + "token": null, + "co_id": null } }, "petition_meta_hist_recs": { @@ -653,20 +696,20 @@ "displayField": "id", "fieldMap": { "co_enrollment_flow_id": "enrollment_flow_id", - "co_id": null, - "cou_id": null, "historic_petition_viewer_id" :"&mapHistoricPetitionViewerId", "enrollee_org_identity_id": "enrollee_org_identity_id", "archived_org_identity_id": "archived_org_identity_id", - "enrollee_co_person_id": null, "enrollee_co_person_role_id": "enrollee_person_role_id", - "petitioner_co_person_id": null, "sponsor_co_person_id": "sponsor_person_id", "approver_co_person_id": "approver_person_id", + "co_petition_id": "petition_id", + "co_id": null, + "cou_id": null, + "status": null, "authenticated_identifier": null, "reference_identifier": null, - "status": null, - "co_petition_id": "petition_id" + "petitioner_co_person_id": null, + "enrollee_co_person_id": null } }, "petition_hist_attrs": { @@ -690,10 +733,18 @@ "org_identity_source_id": "external_identity_source_id", "sorid": "source_key", "org_identity_id": "external_identity_id", - "co_petition_id": null, "co_person_id": "adopted_person_id", "reference_identifier": "reference_identifier", - "org_identity_source_record_id": "ext_identity_source_record_id" + "org_identity_source_record_id": "ext_identity_source_record_id", + "co_petition_id": null + } + }, + "api_source_records": { + "source": "cm_api_source_records", + "displayField": "id", + "plugin": "ApiConnector", + "fieldMap": { + "sorid": "source_key" } } } diff --git a/app/plugins/Transmogrify/src/Command/TransmogrifyCommand.php b/app/plugins/Transmogrify/src/Command/TransmogrifyCommand.php index 8e7aec9b..4b4ec136 100644 --- a/app/plugins/Transmogrify/src/Command/TransmogrifyCommand.php +++ b/app/plugins/Transmogrify/src/Command/TransmogrifyCommand.php @@ -287,7 +287,8 @@ public function execute(Arguments $args, ConsoleIo $io): int } } - foreach(array_keys($this->tables) as $t) { + $allTables = array_keys($this->tables); + foreach ($allTables as $t) { // Check per-table skip configuration and optionally prompt user $canSkipCfg = $this->tables[$t]['canSkip'] ?? null; if (filter_var($canSkipCfg, FILTER_VALIDATE_BOOLEAN)) { @@ -327,7 +328,7 @@ public function execute(Arguments $args, ConsoleIo $io): int **/ // Check if source table exists and warn if not present - if(!empty($this->tables[$t]['source'])) { + if (!empty($this->tables[$t]['source'])) { $src = $this->tables[$t]['source']; if (!$this->tableExists($src)) { $this->cmdPrinter->warning("Source table '$src' does not exist in source database, skipping table '$t'"); @@ -336,7 +337,7 @@ public function execute(Arguments $args, ConsoleIo $io): int } // Skip a table if already contains data - if($Model->find()->count() > 0) { + if ($Model->find()->count() > 0) { $outboundTableEmpty = false; $this->cmdPrinter->warning("Table (" . $t . ") is not empty. We will not overwrite existing data."); } @@ -354,14 +355,14 @@ public function execute(Arguments $args, ConsoleIo $io): int // Mark the table as skipped if it is not empty and not selected $this->cache['skipInsert'][$outboundQualifiedTableName] = !$outboundTableEmpty || $skipTableTransmogrification; - + $this->cache['current'] = $outboundQualifiedTableName; /* * End of checks */ // Configure sequence ID for the target table - if(!RawSqlQueries::setSequenceId( + if (!RawSqlQueries::setSequenceId( $this->inconn, $this->outconn, $this->tables[$t]['source'], @@ -376,7 +377,7 @@ public function execute(Arguments $args, ConsoleIo $io): int $this->runPreTableHook($t); // Step 8: Build and execute query to fetch all source records - $insql = match(true) { + $insql = match (true) { !empty($this->tables[$t]['sqlSelect']) => $this->runSqlSelectHook($t, $inboundQualifiedTableName), default => RawSqlQueries::buildSelectAllOrderedById($inboundQualifiedTableName) }; @@ -402,8 +403,9 @@ public function execute(Arguments $args, ConsoleIo $io): int $this->cache['warns'] = 0; while ($row = $stmt->fetchAssociative()) { - if(!empty($row[ $this->tables[$t]['displayField'] ])) { - $this->cmdPrinter->verbose("$t " . $row[ $this->tables[$t]['displayField'] ]); + if (!empty($row[$this->tables[$t]['displayField']])) { + $displayMessage = "$t " . $row[$this->tables[$t]['displayField']]; + $this->cmdPrinter->verbose($displayMessage); } try { @@ -428,7 +430,7 @@ public function execute(Arguments $args, ConsoleIo $io): int $this->mapLegacyFieldNames($t, $row); // Insert the transformed row into the target database - if($this->cache['skipInsert'][$outboundQualifiedTableName] === false) { + if ($this->cache['skipInsert'][$outboundQualifiedTableName] === false) { // Check if a parent record for this row was previously rejected; if so, skip this insert if ($this->skipIfRejectedParent(currentTable: $t, row: $row)) { continue; @@ -440,8 +442,8 @@ public function execute(Arguments $args, ConsoleIo $io): int } // Store row data in cache for potential later use - $this->cacheResults($t, $row); - } catch(ForeignKeyConstraintViolationException $e) { + $this->cacheResults($t, $row, $origRow); + } catch (ForeignKeyConstraintViolationException $e) { // A foreign key associated with this record did not load, so we can't // load this record. This can happen, eg, because the source_field_id // did not load, perhaps because it was associated with an Org Identity @@ -453,8 +455,7 @@ public function execute(Arguments $args, ConsoleIo $io): int } $this->cmdPrinter->warning("Skipping $t record " . (string)$rowIdLabel . " due to invalid foreign key: " . $e->getMessage()); $this->cmdPrinter->pause(); - } - catch(\InvalidArgumentException $e) { + } catch(\InvalidArgumentException $e) { // If we can't find a value for mapping we skip the record // (ie: mapLegacyFieldNames basically requires a successful mapping) $this->cache['warns'] += 1; @@ -464,8 +465,7 @@ public function execute(Arguments $args, ConsoleIo $io): int } $this->cmdPrinter->warning("Skipping $t record " . (string)$rowIdLabel . ": " . $e->getMessage()); $this->cmdPrinter->pause(); - } - catch(\Exception $e) { + } catch(\Exception $e) { $this->cache['error'] += 1; if (isset($row['id'])) { $this->cache['rejected'][$outboundQualifiedTableName][$row['id']] = $row; @@ -490,7 +490,7 @@ public function execute(Arguments $args, ConsoleIo $io): int $this->cmdPrinter->error(sprintf('Errors: %d', $this->cache['error'])); // Execute any post-processing hooks for the table - if($this->cache['skipInsert'][$outboundQualifiedTableName] === false) { + if ($this->cache['skipInsert'][$outboundQualifiedTableName] === false) { $this->cmdPrinter->out('Running post-table hook for ' . $t); $this->runPostTableHook($t); } @@ -510,7 +510,7 @@ public function execute(Arguments $args, ConsoleIo $io): int if (isset($tables[$currentIndex + 1])) { $this->cmdPrinter->info("Next table to process: " . $tables[$currentIndex + 1]); } else { - $this->cmdPrinter->out(PHP_EOL. "Table import complete. Exiting."); + $this->cmdPrinter->out(PHP_EOL . "Table import complete. Exiting."); } $this->cmdPrinter->pause(); diff --git a/app/plugins/Transmogrify/src/Lib/Traits/ManageDefaultsTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/ManageDefaultsTrait.php index 0682edc4..95bb2fc0 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/ManageDefaultsTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/ManageDefaultsTrait.php @@ -236,6 +236,28 @@ protected function insertPronounTypes(): void foreach(array_keys($this->cache['cos']['id']) as $coId) { $Types->addDefault($coId, 'Pronouns.type'); } + + // After inserting the default Pronoun types, populate the type cache so that + // subsequent calls to mapType() for Pronouns.type can resolve the IDs. + $pronounTypes = $Types->find() + ->where([ + 'attribute' => 'Pronouns.type', + 'co_id IN' => array_keys($this->cache['cos']['id']), + ]) + ->all(); + + foreach ($pronounTypes as $type) { + $row = [ + 'id' => $type->id, + 'co_id' => $type->co_id, + 'attribute' => $type->attribute, + 'value' => $type->value, + ]; + + // This will populate: + // $this->cache['types']['co_id+attribute+value+']["+Pronouns.type++"] = + $this->cacheCompositeKey('types', $row, ['co_id', 'attribute', 'value']); + } } /** diff --git a/app/plugins/Transmogrify/src/Lib/Traits/RowTransformationTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/RowTransformationTrait.php index 25da7e1f..9e0bae45 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/RowTransformationTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/RowTransformationTrait.php @@ -215,6 +215,15 @@ protected function populateChangelogDefaults(string $table, array &$row, bool $f /** * Map fields that have been renamed from Registry Classic to Registry PE. * + * IMPORTANT!!! + * + * When mapping legacy type fields, call the function-based mapper (eg, &map...Type) before configuring null + * for the old column name. The mapper still needs the original source column value, and performNoMapping() + * will unset that column once it sees a null mapping. In other words, always place the null mapping for + * the old column after the new field’s function mapping so the mapper can read the legacy value + * before it is removed. + * In general, keep all the `unset`, the keys with value null, at the bottom of the tables.json configuration + * * @since COmanage Registry v5.0.0 * @param string $table Table Name * @param array $row Row of attributes, fixed in place diff --git a/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php index 78ef9a96..912917af 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php @@ -164,7 +164,16 @@ protected function findAdoptedPersonByLinkedOrgIdentityId(array &$origRow, array */ protected function mapAddressType(array $row): ?int { - return $this->mapType($row, 'Addresses.type', $this->findCoId($row)); + $type = 'type'; + if (isset($row['address_type'])) { + $type = "address_type"; + } + return $this->mapType( + $row, + 'Addresses.type', + $this->findCoId($row), + $type + ); } /** @@ -184,6 +193,11 @@ protected function mapAffiliationType(array $row, ?int $coId = null): ?int $type = 'sync_affiliation'; } + // No value assigned for the affiliation, return null + if (empty($row[$type])) { + return null; + } + return $this->mapType( $row, 'PersonRoles.affiliation_type', @@ -315,6 +329,13 @@ protected function mapEmailType(array $row): ?int $type = 'type'; if (isset($row['match_type']) && $row['match_strategy'] === MatchStrategyEnum::EmailAddress) { $type = 'match_type'; + } else if (isset($row['email_address_type'])) { + $type = 'email_address_type'; + } + + // No value assigned for the email address, return null + if (empty($row[$type])) { + return null; } return $this->mapType( @@ -405,6 +426,13 @@ protected function mapIdentifierType(array $row): ?int $type = 'sync_identifier_type'; } else if (isset($row['match_type']) && $row['match_strategy'] === MatchStrategyEnum::Identifier) { $type = 'match_type'; + } else if (isset($row['identifier_type'])) { + $type = 'identifier_type'; + } + + // No value assigned for the identifier, return null + if (empty($row[$type])) { + return null; } return $this->mapType( @@ -549,7 +577,17 @@ protected function mapLoginIdentifiers(array $origRow, array &$row): void { */ protected function mapNameType(array $row): ?int { - return $this->mapType($row, 'Names.type', $this->findCoId($row)); + $type = 'type'; + if (isset($row['name_type'])) { + $type = 'name_type'; + } + + return $this->mapType( + $row, + 'Names.type', + $this->findCoId($row), + $type + ); } /** @@ -693,6 +731,20 @@ protected function mapPetitionStatus(array $row): ?string }; } + /** + * Get a default Pronoun type ID + * + * @param array $row Row data containing name type + * @return int|null Mapped type ID + * @since COmanage Registry v5.2.0 + */ + protected function mapPronounsTypeDefault(array $row): ?int + { + $row['type'] = 'default'; + + return $this->mapType($row, 'Pronouns.type', $this->findCoId($row)); + } + /** * Map v4 person status codes to v5 StatusEnum values @@ -758,7 +810,17 @@ protected function mapServerTypeToPlugin(array $row): ?string */ protected function mapTelephoneType(array $row): ?int { - return $this->mapType($row, 'TelephoneNumbers.type', $this->findCoId($row)); + $type = 'type'; + if (isset($row['telephone_number_type'])) { + $type = 'telephone_number_type'; + } + + return $this->mapType( + $row, + 'TelephoneNumbers.type', + $this->findCoId($row), + $type + ); } /** @@ -784,8 +846,8 @@ protected function mapType(array $row, string $type, int $coId, string $attr = ' $key = $coId . "+" . $type . "+" . $value . "+"; if(empty($this->cache['types']['co_id+attribute+value+'][$key])) { if ( - !isset($this->cache['cos'][$coId]) - || ($this->cache['cos'][$coId]['status'] && in_array($this->cache['cos'][$coId]['status'], ['TR'])) + !isset($this->cache['cos']['id'][$coId]) + || ($this->cache['cos']['id'][$coId]['status'] && in_array($this->cache['cos']['id'][$coId]['status'], ['TR'])) ) { // This CO has been deleted, so we can't map the type. We will return null return null; @@ -804,6 +866,16 @@ protected function mapType(array $row, string $type, int $coId, string $attr = ' */ protected function mapUrlType(array $row): ?int { - return $this->mapType($row, 'Urls.type', $this->findCoId($row)); + $type = 'type'; + if (isset($row['url_type'])) { + $type = 'url_type'; + } + + return $this->mapType( + $row, + 'Urls.type', + $this->findCoId($row), + $type + ); } }