diff --git a/app/plugins/Transmogrify/config/schema/tables.json b/app/plugins/Transmogrify/config/schema/tables.json index d35bbfac6..803002e45 100644 --- a/app/plugins/Transmogrify/config/schema/tables.json +++ b/app/plugins/Transmogrify/config/schema/tables.json @@ -188,6 +188,7 @@ "source": "cm_names", "displayField": "id", "booleans": ["primary_name"], + "sqlSelect": "mveaSqlSelect", "fieldMap": { "co_person_id": "person_id", "org_identity_id": "external_identity_id", @@ -195,33 +196,11 @@ "type": null } }, - "ad_hoc_attributes": { - "source": "cm_ad_hoc_attributes", - "displayField": "id", - "fieldMap": { - "co_person_role_id": "person_role_id", - "org_identity_id": "external_identity_id", - "co_department_id": null, - "organization_id": null - }, - "postTable": "migrateExtendedAttributesToAdHocAttributes" - }, - "addresses": { - "source": "cm_addresses", - "displayField": "id", - "fieldMap": { - "co_person_role_id": "person_role_id", - "org_identity_id": "external_identity_id", - "type_id": "&mapAddressType", - "type": null, - "co_department_id": null, - "organization_id": null - } - }, "email_addresses": { "source": "cm_email_addresses", "displayField": "id", "booleans": ["verified"], + "sqlSelect": "mveaSqlSelect", "fieldMap": { "co_person_id": "person_id", "org_identity_id": "external_identity_id", @@ -235,6 +214,7 @@ "source": "cm_identifiers", "displayField": "id", "booleans": ["login"], + "sqlSelect": "mveaSqlSelect", "fieldMap": { "co_group_id": "group_id", "co_person_id": "person_id", @@ -247,25 +227,52 @@ }, "preRow": "mapLoginIdentifiers" }, - "telephone_numbers": { - "source": "cm_telephone_numbers", + "urls": { + "source": "cm_urls", + "displayField": "id", + "sqlSelect": "mveaSqlSelect", + "fieldMap": { + "co_person_id": "person_id", + "org_identity_id": "external_identity_id", + "type_id": "&mapUrlType", + "type": null, + "co_department_id": null, + "organization_id": null + } + }, + "ad_hoc_attributes": { + "source": "cm_ad_hoc_attributes", "displayField": "id", + "sqlSelect": "mveaSqlSelect", "fieldMap": { "co_person_role_id": "person_role_id", "org_identity_id": "external_identity_id", - "type_id": "&mapTelephoneType", + "co_department_id": null, + "organization_id": null + }, + "postTable": "migrateExtendedAttributesToAdHocAttributes" + }, + "addresses": { + "source": "cm_addresses", + "displayField": "id", + "sqlSelect": "mveaSqlSelect", + "fieldMap": { + "co_person_role_id": "person_role_id", + "org_identity_id": "external_identity_id", + "type_id": "&mapAddressType", "type": null, "co_department_id": null, "organization_id": null } }, - "urls": { - "source": "cm_urls", + "telephone_numbers": { + "source": "cm_telephone_numbers", "displayField": "id", + "sqlSelect": "mveaSqlSelect", "fieldMap": { - "co_person_id": "person_id", + "co_person_role_id": "person_role_id", "org_identity_id": "external_identity_id", - "type_id": "&mapUrlType", + "type_id": "&mapTelephoneType", "type": null, "co_department_id": null, "organization_id": null diff --git a/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php index 84f3beebe..309649b18 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/CacheTrait.php @@ -116,7 +116,11 @@ protected function findCoId(array $row): int return $coId; } - throw new \InvalidArgumentException('CO not found for record'); + // For the multiple value attributes we are going to get a lot of misses + // because we only move one org identity and only the latest one. No revisions. + // Which means that all the values that belong to deleted org identities are going + // to be misses. + throw new \InvalidArgumentException("CO not found for record"); } /** diff --git a/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php b/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php index ee621f9c4..2b90c7c66 100644 --- a/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php +++ b/app/plugins/Transmogrify/src/Lib/Traits/TypeMapperTrait.php @@ -30,6 +30,7 @@ namespace Transmogrify\Lib\Traits; use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Transmogrify\Lib\Util\RawSqlQueries; use Cake\Utility\Inflector; use Transmogrify\Lib\Traits\CacheTrait; @@ -71,10 +72,10 @@ trait TypeMapperTrait * Map address type to corresponding type ID * * @param array $row Row data containing address type - * @return int Mapped type ID + * @return int|null Mapped type ID * @since COmanage Registry v5.0.0 */ - protected function mapAddressType(array $row): int + protected function mapAddressType(array $row): ?int { return $this->mapType($row, 'Addresses.type', $this->findCoId($row)); } @@ -101,10 +102,10 @@ protected function mapAffiliationType(array $row, ?int $coId = null): ?int * Map email type to corresponding type ID * * @param array $row Row data containing email type - * @return int Mapped type ID + * @return int|null Mapped type ID * @since COmanage Registry v5.0.0 */ - protected function mapEmailType(array $row): int + protected function mapEmailType(array $row): ?int { return $this->mapType($row, 'EmailAddresses.type', $this->findCoId($row)); } @@ -136,22 +137,84 @@ protected function mapExtendedType(array $row): string * Map identifier type to corresponding type ID * * @param array $row Row data containing identifier type - * @return int Mapped type ID + * @return int|null Mapped type ID * @since COmanage Registry v5.0.0 */ - protected function mapIdentifierType(array $row): int + protected function mapIdentifierType(array $row): ?int { return $this->mapType($row, 'Identifiers.type', $this->findCoId($row)); } + /** + * Map login identifiers, in accordance with the configuration. + * + * @since COmanage Registry v5.0.0 + * @param array $origRow Row of table data (original data) + * @param array $row Row of table data (post fixes) + * @throws \InvalidArgumentException + */ + protected function mapLoginIdentifiers(array $origRow, array &$row): void { + // There might be multiple reasons to copy the row, but we only want to + // copy it once. + $copyRow = false; + + if(!empty($origRow['org_identity_id'])) { + if($this->args->getOption('login-identifier-copy') + && $origRow['login']) { + $copyRow = true; + } + + // Note the argument here is the old v4 string (eg "eppn") and not the + // PE foreign key + if($this->args->getOption('login-identifier-type') + && $origRow['type'] == $this->args->getOption('login-identifier-type')) { + $copyRow = true; + } + + // Identifiers attached to External Identities do not have login flags in PE + $row['login'] = false; + } + + if($copyRow) { + // Find the Person ID associated with this External Identity ID + + if(!empty($this->cache['external_identities']['id'][ $origRow['org_identity_id'] ]['person_id'])) { + // Insert a new row attached to the Person, leave the original record + // (ie: $row) untouched + + $copiedRow = [ + 'person_id' => $this->map_org_identity_co_person_id(['id' => $origRow['org_identity_id']]), + 'identifier' => $origRow['identifier'], + 'type_id' => $this->map_identifier_type($origRow), + 'status' => $origRow['status'], + 'login' => true, + 'created' => $origRow['created'], + 'modified' => $origRow['modified'] + ]; + + // Set up changelog and fix booleans + $this->fixChangelog('identifiers', $copiedRow, true); + $this->fixBooleans('identifiers', $copiedRow); + + try { + $tableName = 'identifiers'; + $qualifiedTableName = $this->outconn->qualifyTableName($tableName); + $this->outconn->insert($qualifiedTableName, $copiedRow); + } catch (UniqueConstraintViolationException $e) { + $this->io->warning("record already exists: " . print_r($copiedRow, true)); + } + } + } + } + /** * Map name type to corresponding type ID * * @param array $row Row data containing name type - * @return int Mapped type ID + * @return int|null Mapped type ID * @since COmanage Registry v5.0.0 */ - protected function mapNameType(array $row): int + protected function mapNameType(array $row): ?int { return $this->mapType($row, 'Names.type', $this->findCoId($row)); } @@ -207,7 +270,7 @@ protected function mapOrgIdentitycoPersonId(array $row): ?int $this->cache['org_identities']['co_people'] = []; // Build cache on first use - $this->io->info('Populating org identity map...'); + $this->io->verbose('Populating org identity map...'); $tableName = 'cm_co_org_identity_links'; $changelogFK = 'co_org_identity_link_id'; @@ -249,7 +312,6 @@ protected function mapOrgIdentitycoPersonId(array $row): ?int } if (!empty($this->cache['org_identities']['co_people'][$rowId])) { - // XXX OrgIdentities with no org identity link are not supported in v5 // Return the record with the highest revision number $revisions = $this->cache['org_identities']['co_people'][$rowId]; $rev = max(array_keys($revisions)); @@ -277,10 +339,10 @@ protected function mapServerTypeToPlugin(array $row): ?string * Map telephone type to corresponding type ID * * @param array $row Row data containing telephone type - * @return int Mapped type ID + * @return int|null Mapped type ID * @since COmanage Registry v5.0.0 */ - protected function mapTelephoneType(array $row): int + protected function mapTelephoneType(array $row): ?int { return $this->mapType($row, 'TelephoneNumbers.type', $this->findCoId($row)); } @@ -323,10 +385,10 @@ protected function mapType(array $row, string $type, int $coId, string $attr = ' * Map URL type to corresponding type ID * * @param array $row Row data containing URL type - * @return int Mapped type ID + * @return int|null Mapped type ID * @since COmanage Registry v5.0.0 */ - protected function mapUrlType(array $row): int + protected function mapUrlType(array $row): ?int { return $this->mapType($row, 'Urls.type', $this->findCoId($row)); } diff --git a/app/plugins/Transmogrify/src/Lib/Util/RawSqlQueries.php b/app/plugins/Transmogrify/src/Lib/Util/RawSqlQueries.php index 28b7ecf2c..2ce1538d8 100644 --- a/app/plugins/Transmogrify/src/Lib/Util/RawSqlQueries.php +++ b/app/plugins/Transmogrify/src/Lib/Util/RawSqlQueries.php @@ -192,6 +192,19 @@ public static function roleSqlSelect(string $tableName, bool $isMySQL): string { return RawSqlQueries::ROLE_SQL_SELECT; } + + /** + * Builds SQL query to select Multiple Value Element Attributes (MVEAs) for valid org identities + * + * @param string $tableName Name of the database table containing MVEAs + * @param bool $isMySQL Whether the target database is MySQL (true) or PostgreSQL (false) + * @return string SQL query string to select MVEA rows that are linked to valid org identities + * @since COmanage Registry v5.2.0 + */ + public static function mveaSqlSelect(string $tableName, bool $isMySQL): string { + return str_replace('{table}', $tableName, RawSqlQueries::MVEA_SQL_SELECT); + } + /** * Return SQL used to select Organization Identities from inbound database. * @@ -632,4 +645,39 @@ public static function orgidentitiesSqlSelect(string $tableName, bool $isMySQL): FROM cm_co_groups ORDER BY reason; SQL; + + + /** + * SQL template for selecting Multiple Value Element Attributes (MVEAs) that are linked to valid org identities + * Filters records to only include those with: + * - Valid person or org identity references + * - Org identities that have valid non-historical links to people + * + * @since COmanage Registry v5.2.0 + */ + final const MVEA_SQL_SELECT = <<