From f3f3443be76328cda3397bba2dcdcf8e0183b7fe Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 6 Dec 2024 10:30:50 +0100 Subject: [PATCH 1/6] fix lint warning --- .../two-factor-monitoring/__tests__/TwoFactorTest.data.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/domain/usecases/user-monitoring/two-factor-monitoring/__tests__/TwoFactorTest.data.ts b/src/domain/usecases/user-monitoring/two-factor-monitoring/__tests__/TwoFactorTest.data.ts index 45ef694..78d08c7 100644 --- a/src/domain/usecases/user-monitoring/two-factor-monitoring/__tests__/TwoFactorTest.data.ts +++ b/src/domain/usecases/user-monitoring/two-factor-monitoring/__tests__/TwoFactorTest.data.ts @@ -1,4 +1,3 @@ -import { UserMonitoringProgramMetadata } from "domain/entities/user-monitoring/common/UserMonitoringProgramMetadata"; import { TwoFactorUser } from "domain/entities/user-monitoring/two-factor-monitoring/TwoFactorUser"; import { TwoFactorUserOptions } from "domain/entities/user-monitoring/two-factor-monitoring/TwoFactorUserOptions"; From 44e998d652f4b538257f6996be6df84a785493c0 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 6 Dec 2024 10:34:42 +0100 Subject: [PATCH 2/6] Add new script: users rename-username from a old->new username mapping --- package.json | 1 + src/data/UsernameRenameSqlRepository.ts | 62 +++++++++ src/data/sql/rename-usernames.sql | 129 ++++++++++++++++++ src/domain/entities/UsernameRename.ts | 4 + .../repositories/UsernameRenameRepository.ts | 5 + src/domain/usecases/RenameUsernamesUseCase.ts | 10 ++ src/scripts/commands/users.ts | 46 ++++++- yarn.lock | 37 ++++- 8 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 src/data/UsernameRenameSqlRepository.ts create mode 100644 src/data/sql/rename-usernames.sql create mode 100644 src/domain/entities/UsernameRename.ts create mode 100644 src/domain/repositories/UsernameRenameRepository.ts create mode 100644 src/domain/usecases/RenameUsernamesUseCase.ts diff --git a/package.json b/package.json index 92ce2ea..0a6fba4 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "loglevel": "^1.8.0", "luxon": "3.4.2", "nodemailer": "^6.7.5", + "psqlformat": "^1.21.0", "purify-ts": "2.0.1", "random-seed": "^0.3.0", "simple-node-logger": "^21.8.12", diff --git a/src/data/UsernameRenameSqlRepository.ts b/src/data/UsernameRenameSqlRepository.ts new file mode 100644 index 0000000..450b07e --- /dev/null +++ b/src/data/UsernameRenameSqlRepository.ts @@ -0,0 +1,62 @@ +import { Path } from "domain/entities/Base"; +import { UsernameRename } from "domain/entities/UsernameRename"; +import path from "path"; +import fs from "fs"; +import { UsernameRenameRepository } from "domain/repositories/UsernameRenameRepository"; +import logger from "utils/log"; +import _ from "lodash"; +import * as psqlformat from "psqlformat"; + +export class UsernameRenameSqlRepository implements UsernameRenameRepository { + constructor(private sqlFile: Path) {} + + async run(mapping: UsernameRename[], options: { dryRun: boolean }): Promise { + if (_.isEmpty(mapping)) { + logger.warn("No usernames to rename"); + return; + } + + const sqlMapping = getSqlForTemporalMappingTable(mapping); + const sqlRename = getRenamingSql(); + + const fullSql = [ + sqlMapping, // + "BEGIN;", + sqlRename, + options.dryRun ? "ROLLBACK;" : "COMMIT;", + ].join("\n"); + + const formattedSql = formatSql(fullSql); + logger.info(`Writing SQL: ${this.sqlFile}`); + fs.writeFileSync(this.sqlFile, formattedSql + "\n"); + } +} + +function getRenamingSql() { + const sqlPath = path.join(__dirname, "./sql", "rename-usernames.sql"); + logger.debug(`Template SQL: ${sqlPath}`); + const sqlRename = fs.readFileSync(sqlPath, "utf8"); + return sqlRename; +} + +function getSqlForTemporalMappingTable(mapping: UsernameRename[]) { + return ` + CREATE TEMP TABLE + username_mapping (old_username TEXT, new_username TEXT); + INSERT INTO + username_mapping (old_username, new_username) + VALUES + ${mapping.map(x => `('${x.from}', '${x.to}')`).join(",\n")} + ; + `; +} + +function formatSql(fullSql: string) { + return psqlformat.formatSql(fullSql, { + commaStart: false, + commaEnd: true, + commaBreak: false, + formatType: true, + noSpaceFunction: true, + }); +} diff --git a/src/data/sql/rename-usernames.sql b/src/data/sql/rename-usernames.sql new file mode 100644 index 0000000..9e25885 --- /dev/null +++ b/src/data/sql/rename-usernames.sql @@ -0,0 +1,129 @@ +-- Functions +CREATE OR REPLACE FUNCTION update_username_string(table_name TEXT, column_name TEXT) RETURNS VOID AS $$ +DECLARE rows_updated INTEGER; +BEGIN EXECUTE format( + 'UPDATE %I + SET %I = username_mapping.new_username + FROM username_mapping + WHERE %I.%I = username_mapping.old_username', + table_name, + column_name, + table_name, + column_name +); +GET DIAGNOSTICS rows_updated = ROW_COUNT; +RAISE NOTICE '%.%: %', +table_name, +column_name, +rows_updated; +END; +$$ LANGUAGE plpgsql; +-- +CREATE OR REPLACE FUNCTION update_username_values() RETURNS VOID AS $$ +DECLARE rows_updated INTEGER; +BEGIN +UPDATE datavalue dv +SET value = username_mapping.new_username +FROM username_mapping, + dataelement de +WHERE dv.dataelementid = de.dataelementid + AND de.valuetype = 'USERNAME' + AND dv.value = username_mapping.old_username; +GET DIAGNOSTICS rows_updated = ROW_COUNT; +RAISE NOTICE 'datavalue.value[dataelement.type="USERNAME"]: %', +rows_updated; +END; +$$ LANGUAGE plpgsql; +--- +CREATE OR REPLACE FUNCTION update_event_datavalues() RETURNS VOID AS $$ +DECLARE rows_updated INTEGER; +BEGIN +UPDATE programstageinstance +SET eventdatavalues = jsonb_set( + eventdatavalues, + array [de.uid, 'value'], + to_jsonb(um.new_username::text) + ) +FROM dataelement de, + username_mapping um +WHERE de.valuetype = 'USERNAME' + AND programstageinstance.eventdatavalues->de.uid->>'value' = um.old_username; +GET DIAGNOSTICS rows_updated = ROW_COUNT; +RAISE NOTICE 'programstageinstance.eventdatavalues[dataelement.type="USERNAME"]: %', +rows_updated; +END; +$$ LANGUAGE plpgsql; +-- +CREATE OR REPLACE FUNCTION update_tracked_entity_attributes_values() RETURNS VOID AS $$ +DECLARE rows_updated INTEGER; +BEGIN WITH updated_values AS ( + SELECT teav.trackedentityattributeid, + username_mapping.new_username + FROM trackedentityattributevalue teav + JOIN trackedentityattribute tea ON teav.trackedentityattributeid = tea.trackedentityattributeid + JOIN username_mapping ON teav.value = username_mapping.old_username + WHERE tea.valuetype = 'USERNAME' +) +UPDATE trackedentityattributevalue teav +SET value = updated_values.new_username +FROM updated_values +WHERE teav.trackedentityattributeid = updated_values.trackedentityattributeid; +GET DIAGNOSTICS rows_updated = ROW_COUNT; +RAISE NOTICE 'trackedentityattributevalue.value[trackedentityattribute.type="USERNAME"]: %', +rows_updated; +END; +$$ LANGUAGE plpgsql; +-- +CREATE OR REPLACE FUNCTION update_username_jsonb( +table_name TEXT, +column_name TEXT +) RETURNS VOID AS $$ +DECLARE rows_updated INTEGER; +BEGIN EXECUTE format( + 'UPDATE %I + SET %I = jsonb_set(%I, ''{username}'', to_jsonb(username_mapping.new_username::TEXT), true) + FROM username_mapping + WHERE %I->>''username'' = username_mapping.old_username', + table_name, + column_name, + column_name, + column_name +); +GET DIAGNOSTICS rows_updated = ROW_COUNT; +RAISE NOTICE '%.%[username]: % rows updated', +table_name, +column_name, +rows_updated; +END; +$$ LANGUAGE plpgsql; +-- Actions +SELECT update_username_string('completedatasetregistration', 'lastupdatedby'); +SELECT update_username_string('completedatasetregistration', 'storedby'); +SELECT update_username_string('datastatisticsevent', 'username'); +SELECT update_username_string('datavalue', 'storedby'); +SELECT update_username_string('deletedobject', 'deleted_by'); +SELECT update_username_string('externalnotificationlogentry', 'triggerby'); +SELECT update_username_string('potentialduplicate', 'createdbyusername'); +SELECT update_username_string('potentialduplicate', 'lastupdatebyusername'); +SELECT update_username_string('programinstance', 'completedby'); +SELECT update_username_jsonb('programinstance', 'createdbyuserinfo'); +SELECT update_username_jsonb('programinstance', 'lastupdatedbyuserinfo'); +SELECT update_username_string('programinstance', 'storedby'); +SELECT update_username_string('programownershiphistory', 'createdby'); +SELECT update_username_string('programstageinstance', 'completedby'); +SELECT update_username_jsonb('programstageinstance', 'createdbyuserinfo'); +SELECT update_username_jsonb('programstageinstance', 'lastupdatedbyuserinfo'); +SELECT update_username_string('programstageinstance', 'storedby'); +SELECT update_username_string('programtempownershipaudit', 'accessedby'); +SELECT update_username_string('trackedentityattributevalue', 'storedby'); +SELECT update_username_string('trackedentityattributevalueaudit', 'modifiedby'); +SELECT update_username_string('trackedentitydatavalueaudit', 'modifiedby'); +SELECT update_username_jsonb('trackedentityinstance', 'createdbyuserinfo'); +SELECT update_username_jsonb('trackedentityinstance', 'lastupdatedbyuserinfo'); +SELECT update_username_string('trackedentityinstance', 'storedby'); +SELECT update_username_string('trackedentityinstanceaudit', 'accessedby'); +SELECT update_username_string('trackedentityprogramowner', 'createdby'); +SELECT update_username_string('userinfo', 'username'); +SELECT update_username_values(); +SELECT update_event_datavalues(); +SELECT update_tracked_entity_attributes_values(); diff --git a/src/domain/entities/UsernameRename.ts b/src/domain/entities/UsernameRename.ts new file mode 100644 index 0000000..02fcfef --- /dev/null +++ b/src/domain/entities/UsernameRename.ts @@ -0,0 +1,4 @@ +export type UsernameRename = { + from: string; + to: string; +}; diff --git a/src/domain/repositories/UsernameRenameRepository.ts b/src/domain/repositories/UsernameRenameRepository.ts new file mode 100644 index 0000000..250bdb1 --- /dev/null +++ b/src/domain/repositories/UsernameRenameRepository.ts @@ -0,0 +1,5 @@ +import { UsernameRename } from "domain/entities/UsernameRename"; + +export interface UsernameRenameRepository { + run(mapping: UsernameRename[], options: { dryRun: boolean }): Promise; +} diff --git a/src/domain/usecases/RenameUsernamesUseCase.ts b/src/domain/usecases/RenameUsernamesUseCase.ts new file mode 100644 index 0000000..6bac262 --- /dev/null +++ b/src/domain/usecases/RenameUsernamesUseCase.ts @@ -0,0 +1,10 @@ +import { UsernameRename } from "domain/entities/UsernameRename"; +import { UsernameRenameRepository } from "domain/repositories/UsernameRenameRepository"; + +export class RenameUsernameUseCase { + constructor(private repository: UsernameRenameRepository) {} + + async execute(mapping: UsernameRename[], options: { dryRun: boolean }): Promise { + return this.repository.run(mapping, options); + } +} diff --git a/src/scripts/commands/users.ts b/src/scripts/commands/users.ts index 16c603c..4ca18e3 100644 --- a/src/scripts/commands/users.ts +++ b/src/scripts/commands/users.ts @@ -5,6 +5,10 @@ import { StringsSeparatedByCommas, getApiUrlOption, getD2Api } from "scripts/com import { MigrateUserNameUseCase } from "domain/usecases/MigrateUserNameUseCase"; import { NotificationsEmailRepository } from "data/NotificationsEmailRepository"; import logger from "utils/log"; +import { RenameUsernameUseCase } from "domain/usecases/RenameUsernamesUseCase"; +import { UsernameRenameSqlRepository } from "data/UsernameRenameSqlRepository"; +import { Maybe } from "utils/ts-utils"; +import { UsernameRename } from "domain/entities/UsernameRename"; export function getCommand() { const migrateUser = command({ @@ -99,8 +103,48 @@ export function getCommand() { }, }); + const renameUsername = command({ + name: "Rename username", + description: "Rename occurences of a username in the DHIS2 database", + args: { + mapping: option({ + type: string, + long: "mapping", + description: "oldusername:newusername,...", + }), + dryRun: flag({ + type: boolean, + long: "dry-run", + description: "The SQL will be executed within a rollback transaction", + }), + sqlFilePath: option({ + type: string, + long: "output", + description: "Path to the output file (SQL)", + }), + }, + handler: async args => { + const mapping = getMappingFromCommaSeparatedKeyValues(args.mapping); + const repository = new UsernameRenameSqlRepository(args.sqlFilePath); + await new RenameUsernameUseCase(repository).execute(mapping, { dryRun: args.dryRun }); + }, + }); + return subcommands({ name: "users", - cmds: { migrate: migrateUser }, + cmds: { + migrate: migrateUser, + "rename-username": renameUsername, + }, }); } + +function getMappingFromCommaSeparatedKeyValues(strMapping: string) { + return _(strMapping.split(",")) + .map((mapping: string): Maybe => { + const [from, to] = mapping.split(":"); + return from && to ? { from, to } : undefined; + }) + .compact() + .value(); +} diff --git a/yarn.lock b/yarn.lock index 8b255e5..cc377b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1217,6 +1217,15 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -2525,7 +2534,7 @@ globals@^13.6.0, globals@^13.9.0: dependencies: type-fest "^0.20.2" -globby@^11.0.4: +globby@^11.0.1, globby@^11.0.4: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -3876,6 +3885,14 @@ psl@^1.1.33: resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== +psqlformat@^1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/psqlformat/-/psqlformat-1.21.0.tgz#59c6422833754248b92b2c57d23f2013aefa59e3" + integrity sha512-A6k6Bvo9WT88xnsS9WDLjIhfGQFwUUkaHRLUUyMOa/6LbEB/8J6GjXHGmXr/n8O0BwKVxMJFSuX4hAc9tJaRDg== + dependencies: + globby "^11.0.1" + yargs "^16.0.3" + pstree.remy@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" @@ -5354,6 +5371,11 @@ yargs-parser@^15.0.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" @@ -5376,6 +5398,19 @@ yargs@^14.0.0: y18n "^4.0.0" yargs-parser "^15.0.1" +yargs@^16.0.3: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^17.5.1: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" From 1a686b0cacbc80ee705e3a27f8ad4b8671f65f08 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Mon, 9 Dec 2024 09:15:46 +0100 Subject: [PATCH 3/6] Add update_usernames_in_nested_json --- src/data/UsernameRenameSqlRepository.ts | 1 + src/data/sql/rename-usernames.sql | 46 +++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/data/UsernameRenameSqlRepository.ts b/src/data/UsernameRenameSqlRepository.ts index 450b07e..9f5dca4 100644 --- a/src/data/UsernameRenameSqlRepository.ts +++ b/src/data/UsernameRenameSqlRepository.ts @@ -16,6 +16,7 @@ export class UsernameRenameSqlRepository implements UsernameRenameRepository { return; } + logger.info(`Mapping: ${JSON.stringify(mapping)}`); const sqlMapping = getSqlForTemporalMappingTable(mapping); const sqlRename = getRenamingSql(); diff --git a/src/data/sql/rename-usernames.sql b/src/data/sql/rename-usernames.sql index 9e25885..b43d3e6 100644 --- a/src/data/sql/rename-usernames.sql +++ b/src/data/sql/rename-usernames.sql @@ -1,3 +1,38 @@ +-- +CREATE OR REPLACE FUNCTION update_usernames_in_nested_json(table_name TEXT) +RETURNS VOID AS $$ +DECLARE mapping RECORD; +updated_count INTEGER; +final_sql TEXT; +BEGIN FOR mapping IN +SELECT old_username, + new_username +FROM username_mapping LOOP -- + final_sql := format( + 'UPDATE %I + SET jbvalue = REPLACE( + jbvalue::TEXT, + %L, + %L + )::JSONB + WHERE jbvalue::TEXT LIKE %L', + table_name, + '\"' || mapping.old_username || '\"', + '\"' || mapping.new_username || '\"', + '%' || '\\"' || mapping.old_username || '\\"' || '%' +); +-- Log the final SQL being executed +RAISE NOTICE 'Executing SQL: %', +final_sql; +EXECUTE final_sql; +GET DIAGNOSTICS updated_count = ROW_COUNT; +RAISE NOTICE '%s.jbvalue[%] = %', +table_name, +mapping.old_username, +updated_count; +END LOOP; +END; +$$ LANGUAGE plpgsql; -- Functions CREATE OR REPLACE FUNCTION update_username_string(table_name TEXT, column_name TEXT) RETURNS VOID AS $$ DECLARE rows_updated INTEGER; @@ -97,10 +132,12 @@ rows_updated; END; $$ LANGUAGE plpgsql; -- Actions +SELECT update_username_string('audit', 'createdby'); SELECT update_username_string('completedatasetregistration', 'lastupdatedby'); SELECT update_username_string('completedatasetregistration', 'storedby'); SELECT update_username_string('datastatisticsevent', 'username'); SELECT update_username_string('datavalue', 'storedby'); +SELECT update_username_string('datavalueaudit', 'modifiedby'); SELECT update_username_string('deletedobject', 'deleted_by'); SELECT update_username_string('externalnotificationlogentry', 'triggerby'); SELECT update_username_string('potentialduplicate', 'createdbyusername'); @@ -127,3 +164,12 @@ SELECT update_username_string('userinfo', 'username'); SELECT update_username_values(); SELECT update_event_datavalues(); SELECT update_tracked_entity_attributes_values(); +--- +SELECT update_usernames_in_nested_json('keyjsonvalue'); +-- +DROP FUNCTION update_usernames_in_nested_json(TEXT); +DROP FUNCTION update_username_string(TEXT, TEXT); +DROP FUNCTION update_username_values(); +DROP FUNCTION update_event_datavalues(); +DROP FUNCTION update_tracked_entity_attributes_values(); +DROP FUNCTION update_username_jsonb(TEXT, TEXT); From 83ac304bc53ebfa757b0f66eca76c2067e7429be Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Mon, 9 Dec 2024 09:59:52 +0100 Subject: [PATCH 4/6] Add _update prefix to delete on end --- src/data/sql/rename-usernames.sql | 134 ++++++++++++------------------ 1 file changed, 51 insertions(+), 83 deletions(-) diff --git a/src/data/sql/rename-usernames.sql b/src/data/sql/rename-usernames.sql index b43d3e6..522e418 100644 --- a/src/data/sql/rename-usernames.sql +++ b/src/data/sql/rename-usernames.sql @@ -1,40 +1,7 @@ --- -CREATE OR REPLACE FUNCTION update_usernames_in_nested_json(table_name TEXT) -RETURNS VOID AS $$ -DECLARE mapping RECORD; -updated_count INTEGER; -final_sql TEXT; -BEGIN FOR mapping IN -SELECT old_username, - new_username -FROM username_mapping LOOP -- - final_sql := format( - 'UPDATE %I - SET jbvalue = REPLACE( - jbvalue::TEXT, - %L, - %L - )::JSONB - WHERE jbvalue::TEXT LIKE %L', - table_name, - '\"' || mapping.old_username || '\"', - '\"' || mapping.new_username || '\"', - '%' || '\\"' || mapping.old_username || '\\"' || '%' -); --- Log the final SQL being executed -RAISE NOTICE 'Executing SQL: %', -final_sql; -EXECUTE final_sql; -GET DIAGNOSTICS updated_count = ROW_COUNT; -RAISE NOTICE '%s.jbvalue[%] = %', -table_name, -mapping.old_username, -updated_count; -END LOOP; -END; -$$ LANGUAGE plpgsql; -- Functions -CREATE OR REPLACE FUNCTION update_username_string(table_name TEXT, column_name TEXT) RETURNS VOID AS $$ +-- +-- Rename plain string username in 'table_name.column_name' +CREATE OR REPLACE FUNCTION _update_username_string(table_name TEXT, column_name TEXT) RETURNS VOID AS $$ DECLARE rows_updated INTEGER; BEGIN EXECUTE format( 'UPDATE %I @@ -54,7 +21,8 @@ rows_updated; END; $$ LANGUAGE plpgsql; -- -CREATE OR REPLACE FUNCTION update_username_values() RETURNS VOID AS $$ +-- Rename plain string username in aggregated data values if dataElement.valuetype is 'USERNAME' +CREATE OR REPLACE FUNCTION _update_username_values() RETURNS VOID AS $$ DECLARE rows_updated INTEGER; BEGIN UPDATE datavalue dv @@ -70,7 +38,8 @@ rows_updated; END; $$ LANGUAGE plpgsql; --- -CREATE OR REPLACE FUNCTION update_event_datavalues() RETURNS VOID AS $$ +-- Update string username in events data values if dataElement.valuetype is 'USERNAME' +CREATE OR REPLACE FUNCTION _update_event_datavalues() RETURNS VOID AS $$ DECLARE rows_updated INTEGER; BEGIN UPDATE programstageinstance @@ -89,7 +58,8 @@ rows_updated; END; $$ LANGUAGE plpgsql; -- -CREATE OR REPLACE FUNCTION update_tracked_entity_attributes_values() RETURNS VOID AS $$ +-- Update tracked entity attributes values if the dataElement valuetype is 'USERNAME' +CREATE OR REPLACE FUNCTION _update_tracked_entity_attributes_values() RETURNS VOID AS $$ DECLARE rows_updated INTEGER; BEGIN WITH updated_values AS ( SELECT teav.trackedentityattributeid, @@ -109,10 +79,8 @@ rows_updated; END; $$ LANGUAGE plpgsql; -- -CREATE OR REPLACE FUNCTION update_username_jsonb( -table_name TEXT, -column_name TEXT -) RETURNS VOID AS $$ +-- Rename username in JSONB table_name.column_name (key: "username") +CREATE OR REPLACE FUNCTION _update_username_jsonb(table_name TEXT, column_name TEXT) RETURNS VOID AS $$ DECLARE rows_updated INTEGER; BEGIN EXECUTE format( 'UPDATE %I @@ -131,45 +99,45 @@ column_name, rows_updated; END; $$ LANGUAGE plpgsql; +-- -- Actions -SELECT update_username_string('audit', 'createdby'); -SELECT update_username_string('completedatasetregistration', 'lastupdatedby'); -SELECT update_username_string('completedatasetregistration', 'storedby'); -SELECT update_username_string('datastatisticsevent', 'username'); -SELECT update_username_string('datavalue', 'storedby'); -SELECT update_username_string('datavalueaudit', 'modifiedby'); -SELECT update_username_string('deletedobject', 'deleted_by'); -SELECT update_username_string('externalnotificationlogentry', 'triggerby'); -SELECT update_username_string('potentialduplicate', 'createdbyusername'); -SELECT update_username_string('potentialduplicate', 'lastupdatebyusername'); -SELECT update_username_string('programinstance', 'completedby'); -SELECT update_username_jsonb('programinstance', 'createdbyuserinfo'); -SELECT update_username_jsonb('programinstance', 'lastupdatedbyuserinfo'); -SELECT update_username_string('programinstance', 'storedby'); -SELECT update_username_string('programownershiphistory', 'createdby'); -SELECT update_username_string('programstageinstance', 'completedby'); -SELECT update_username_jsonb('programstageinstance', 'createdbyuserinfo'); -SELECT update_username_jsonb('programstageinstance', 'lastupdatedbyuserinfo'); -SELECT update_username_string('programstageinstance', 'storedby'); -SELECT update_username_string('programtempownershipaudit', 'accessedby'); -SELECT update_username_string('trackedentityattributevalue', 'storedby'); -SELECT update_username_string('trackedentityattributevalueaudit', 'modifiedby'); -SELECT update_username_string('trackedentitydatavalueaudit', 'modifiedby'); -SELECT update_username_jsonb('trackedentityinstance', 'createdbyuserinfo'); -SELECT update_username_jsonb('trackedentityinstance', 'lastupdatedbyuserinfo'); -SELECT update_username_string('trackedentityinstance', 'storedby'); -SELECT update_username_string('trackedentityinstanceaudit', 'accessedby'); -SELECT update_username_string('trackedentityprogramowner', 'createdby'); -SELECT update_username_string('userinfo', 'username'); -SELECT update_username_values(); -SELECT update_event_datavalues(); -SELECT update_tracked_entity_attributes_values(); ---- -SELECT update_usernames_in_nested_json('keyjsonvalue'); -- -DROP FUNCTION update_usernames_in_nested_json(TEXT); -DROP FUNCTION update_username_string(TEXT, TEXT); -DROP FUNCTION update_username_values(); -DROP FUNCTION update_event_datavalues(); -DROP FUNCTION update_tracked_entity_attributes_values(); -DROP FUNCTION update_username_jsonb(TEXT, TEXT); +SELECT _update_username_string('audit', 'createdby'); +SELECT _update_username_string('completedatasetregistration', 'lastupdatedby'); +SELECT _update_username_string('completedatasetregistration', 'storedby'); +SELECT _update_username_string('datastatisticsevent', 'username'); +SELECT _update_username_string('datavalue', 'storedby'); +SELECT _update_username_string('datavalueaudit', 'modifiedby'); +SELECT _update_username_string('deletedobject', 'deleted_by'); +SELECT _update_username_string('externalnotificationlogentry', 'triggerby'); +SELECT _update_username_string('potentialduplicate', 'createdbyusername'); +SELECT _update_username_string('potentialduplicate', 'lastupdatebyusername'); +SELECT _update_username_string('programinstance', 'completedby'); +SELECT _update_username_jsonb('programinstance', 'createdbyuserinfo'); +SELECT _update_username_jsonb('programinstance', 'lastupdatedbyuserinfo'); +SELECT _update_username_string('programinstance', 'storedby'); +SELECT _update_username_string('programownershiphistory', 'createdby'); +SELECT _update_username_string('programstageinstance', 'completedby'); +SELECT _update_username_jsonb('programstageinstance', 'createdbyuserinfo'); +SELECT _update_username_jsonb('programstageinstance', 'lastupdatedbyuserinfo'); +SELECT _update_username_string('programstageinstance', 'storedby'); +SELECT _update_username_string('programtempownershipaudit', 'accessedby'); +SELECT _update_username_string('trackedentityattributevalue', 'storedby'); +SELECT _update_username_string('trackedentityattributevalueaudit', 'modifiedby'); +SELECT _update_username_string('trackedentitydatavalueaudit', 'modifiedby'); +SELECT _update_username_jsonb('trackedentityinstance', 'createdbyuserinfo'); +SELECT _update_username_jsonb('trackedentityinstance', 'lastupdatedbyuserinfo'); +SELECT _update_username_string('trackedentityinstance', 'storedby'); +SELECT _update_username_string('trackedentityinstanceaudit', 'accessedby'); +SELECT _update_username_string('trackedentityprogramowner', 'createdby'); +SELECT _update_username_string('userinfo', 'username'); +SELECT _update_username_values(); +SELECT _update_event_datavalues(); +SELECT _update_tracked_entity_attributes_values(); +-- Delete all functions +SELECT pg_catalog.pg_get_function_identity_arguments(p.oid) AS arguments, + p.proname AS function_name, + n.nspname AS schema_name +FROM pg_proc p + JOIN pg_namespace n ON p.pronamespace = n.oid +WHERE p.proname LIKE '_update_%'; From 45384120e131af643dd46bf236c105268287f2ca Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Mon, 9 Dec 2024 10:01:18 +0100 Subject: [PATCH 5/6] Remove rows update sufix --- src/data/sql/rename-usernames.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/sql/rename-usernames.sql b/src/data/sql/rename-usernames.sql index 522e418..2bcb309 100644 --- a/src/data/sql/rename-usernames.sql +++ b/src/data/sql/rename-usernames.sql @@ -93,7 +93,7 @@ BEGIN EXECUTE format( column_name ); GET DIAGNOSTICS rows_updated = ROW_COUNT; -RAISE NOTICE '%.%[username]: % rows updated', +RAISE NOTICE '%.%[username]: %', table_name, column_name, rows_updated; From 91c5583fa186984d9cc9dcfc77207a15b1dc4e81 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Mon, 9 Dec 2024 11:35:22 +0100 Subject: [PATCH 6/6] Add rename-username to readme --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 7c1bfac..1224cea 100644 --- a/README.md +++ b/README.md @@ -399,6 +399,26 @@ yarn start users migrate \ } ``` +### Rename username + +DHIS2 does not support renaming usernames directly. While it is possible to update usernames through the API, this only modifies the main table. References in other tables, which rely on the hardcoded username (not `userinfoid` or `userinfo.uid`), will remain unchanged. + +To fully rename a username across all references, you need to execute a SQL script. Start by generating the script using the following command: + +```bash + yarn start users rename-username \ + --mapping=user1old:user1new,user2old:user2new \ + [--dry-run] --output=rename.sql +``` + +And then run the generated SQL script in your database to perform the actual renaming: + +```bash +psql -U dhis dhis2 -f rename.sql +``` + +Replace `dhis` with your database username and `dhis2` with your database name if they differ. Ensure you have a backup of the database before applying the changes. + ## User monitoring ### Users Permissions Fixer and 2FA Reporter