Skip to content
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Available levels: 'debug' | 'info' | 'warn' | 'error'
- [Revert data values](#revert-data-values)
- [Delete duplicated event data values](#delete-duplicated-event-data-values)
- [Email notification for data values](#email-notification-for-data-values)
- [Move data value set to a new org unit](#move-data-value-set-to-a-new-org-unit)
- [Notifications](#notifications)
- [Send user info email](#send-user-info-email)
- [Load testing](#load-testing)
Expand Down Expand Up @@ -438,6 +439,38 @@ FLAGS:
--help, -h - show help
```


### Move data value set to a new org unit

Move a data set values from a source org unit to a target org unit.
The values can be limited to a date range via the start/end date option. If no start date is provided the script uses _1970-01-01_. If no end date is provided the script uses either the last day of the current year or the last day of the year of start date if its in the future.
Its possible to delete the values in the target org unit before moving the values and to delete the values in the source org unit after moving the values.

Usage example:
```shell
$ yarn start datavalues change-orgunit \
--url "http://localhost:8080" --auth "USER:PASSWORD" \
--dataset-id='Tu81BTLUuCT' --source-orgunit-id='XKKI1hhyFxk' --target-orgunit-id='XKKI1hhyFxK' --start-date='2026-01-01' --end-date='2026-01-31' --delete-target --delete-source --dry-run
```

#### Options
```shell
OPTIONS:
--url <str> - http[s]://[USERNAME:PASSWORD@]HOST:PORT
--auth <value> - USERNAME:PASSWORD [optional]
--dataset-id <str> - Data set ID
--source-orgunit-id <str> - Source org unit ID
--target-orgunit-id <str> - Target org unit ID
--start-date <str> - Start date (YYYY-MM-DD) [optional]
--end-date <str> - End date (YYYY-MM-DD) [optional]

FLAGS:
--delete-source - Delete the source data values after changing the org unit
--delete-target - Delete the target data values before changing the org unit
--dry-run - Perform the operation in dry run mode
--help, -h - show help
```

## Notifications

### Send user info email
Expand Down
3 changes: 2 additions & 1 deletion src/data/DataValuesD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ export class DataValuesD2Repository implements DataValuesRepository {
}
}

async post(options: { dataValues: DataValueToPost[] }): Async<void> {
async post(options: { dataValues: DataValueToPost[]; dryRun?: boolean }): Async<void> {
return this.postDataValueSet({
dataValues: options.dataValues,
postParams: { dryRun: options.dryRun },
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/domain/repositories/DataValuesRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DataValueAudit } from "domain/entities/DataValueAudit";

export interface DataValuesRepository {
get(options: DataValuesSelector): Async<DataValue[]>;
post(options: { dataValues: DataValueToPost[] }): Async<void>;
post(options: { dataValues: DataValueToPost[]; dryRun?: boolean }): Async<void>;
delete(options: { dataValues: DataValueToPost[]; dryRun: boolean }): Async<void>;
getMetadata(options: { dataValues: DataValue[] }): Async<DataValuesMetadata>;
getAudits(options: DataValueAuditsSelector): Async<DataValueAudit[]>;
Expand Down
138 changes: 138 additions & 0 deletions src/domain/usecases/ChangeDataValuesOrgUnitUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { Logger } from "domain/logger/Logger";

import { OrgUnitRepository } from "domain/repositories/OrgUnitRepository";
import { DataSetsRepository } from "domain/repositories/DataSetsRepository";
import { DataValuesRepository, DataValuesSelector } from "domain/repositories/DataValuesRepository";

interface ChangeDataValuesOrgUnitOptions {
dataSetId: string;
sourceOrgUnitId: string;
targetOrgUnitId: string;
startDate?: string;
endDate?: string;
deleteSourceDataValues: boolean;
deleteTargetDataValues: boolean;
dryRun: boolean;
}

export class ChangeDataValuesOrgUnitUseCase {
constructor(
private logger: Logger,
private orgUnitRepository: OrgUnitRepository,
private dataSetsRepository: DataSetsRepository,
private dataValuesRepository: DataValuesRepository
) {}

async execute(options: ChangeDataValuesOrgUnitOptions) {
const {
dataSetId,
sourceOrgUnitId,
targetOrgUnitId,
startDate,
endDate,
deleteSourceDataValues,
deleteTargetDataValues,
dryRun,
} = options;

const dataSetsResults = await this.dataSetsRepository.get([dataSetId]);
const dataSet = dataSetsResults[dataSetId];

if (!dataSet) {
throw new Error(`Data set with ID ${dataSetId} not found.`);
}

const orgUnits = await this.orgUnitRepository.getByIdentifiables([sourceOrgUnitId, targetOrgUnitId]);
if (!orgUnits.some(orgUnit => orgUnit.id === sourceOrgUnitId)) {
throw new Error(`Source org unit with ID ${sourceOrgUnitId} not found.`);
}
if (!orgUnits.some(orgUnit => orgUnit.id === targetOrgUnitId)) {
throw new Error(`Target org unit with ID ${targetOrgUnitId} not found.`);
}

this.logger.info(
`Changing org unit from ${sourceOrgUnitId} to ${targetOrgUnitId} for data set: ${dataSetId}`
);

const dataValuesSelector: DataValuesSelector = {
dataSetIds: [dataSetId],
orgUnitIds: [sourceOrgUnitId],
startDate: this.getDefaultDate(startDate, endDate, "start"),
endDate: this.getDefaultDate(startDate, endDate, "end"),
};

this.logger.info(`Data values selector: ${JSON.stringify(dataValuesSelector, null, 4)}`);
if (dryRun) {
this.logger.info("Dry run mode enabled. No data values will be posted or deleted.");
}

const dataValues = await this.dataValuesRepository.get(dataValuesSelector);

if (dataValues.length === 0) {
this.logger.info("No data values found.");
return;
}

this.logger.info(`Found ${dataValues.length} data values to change org unit.`);
this.logger.debug(`Data values: ${JSON.stringify(dataValues, null, 4)}`);

if (deleteTargetDataValues) {
this.logger.info("Deleting target org unit data values...");
const targetDataValuesSelector: DataValuesSelector = {
dataSetIds: [dataSetId],
orgUnitIds: [targetOrgUnitId],
startDate: dataValuesSelector.startDate,
endDate: dataValuesSelector.endDate,
};
const targetDataValues = await this.dataValuesRepository.get(targetDataValuesSelector);
if (targetDataValues.length > 0) {
this.logger.info(`Deleting ${targetDataValues.length} target org unit data values.`);
await this.dataValuesRepository.delete({
dataValues: targetDataValues,
dryRun: dryRun,
});
} else {
this.logger.info("No target org unit data values found to delete.");
}
}

this.logger.info("Posting data values to target org unit.");
const updatedDataValues = dataValues.map(dataValue => ({
...dataValue,
orgUnit: targetOrgUnitId,
}));

await this.dataValuesRepository.post({
dataValues: updatedDataValues,
dryRun: dryRun,
});

if (deleteSourceDataValues) {
this.logger.info("Deleting source org unit data values...");
await this.dataValuesRepository.delete({
dataValues: dataValues,
dryRun: dryRun,
});
}
}

private getDefaultDate(
startDate: string | undefined,
endDate: string | undefined,
type: "start" | "end"
): string {
if (type === "start") {
return startDate || "1970-01-01";
} else {
const now = new Date();
const endOfCurrentYear = now.getFullYear();
if (!startDate) {
return `${endOfCurrentYear}-12-31`;
} else {
const startYear = new Date(startDate).getFullYear();
const defaultEndYear = endOfCurrentYear < startYear ? startYear : endOfCurrentYear;
return endDate || `${defaultEndYear}-12-31`;
}
}
}
}
69 changes: 69 additions & 0 deletions src/scripts/commands/dataValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ExecutionJsonRepository } from "data/DataSetExecutionJsonRepository";
import { TimeZoneD2Repository } from "data/TimeZoneD2Repository";
import { BulkDeleteDataValuesUseCase } from "domain/usecases/BulkDeleteDataValuesUseCase";
import { Ref } from "domain/entities/Base";
import { ChangeDataValuesOrgUnitUseCase } from "domain/usecases/ChangeDataValuesOrgUnitUseCase";

const SEND_EMAIL_AFTER_MINUTES = 5;
const BULK_DELETE_DEFAULT_BATCH_SIZE = 30000;
Expand All @@ -41,6 +42,7 @@ export function getCommand() {
"post-dangling-values": postDanglingValuesCmd,
"monitoring-values": monitoringDataValues,
"bulk-delete": bulkDeleteDataValuesCmd,
"change-orgunit": changeDataValueSetOrgUnitCmd,
},
});
}
Expand Down Expand Up @@ -320,6 +322,73 @@ const bulkDeleteDataValuesCmd = command({
},
});

const changeDataValueSetOrgUnitCmd = command({
name: "change-orgunit",
description: "Change the org unit of a data value set",
args: {
...getApiUrlOptions(),
dataSetId: option({
type: string,
long: "dataset-id",
description: "Data set ID",
}),
sourceOrgUnitId: option({
type: string,
long: "source-orgunit-id",
description: "Source org unit ID",
}),
targetOrgUnitId: option({
type: string,
long: "target-orgunit-id",
description: "Target org unit ID",
}),
startDate: option({
type: optional(string),
long: "start-date",
description: "Start date (YYYY-MM-DD)",
}),
endDate: option({
type: optional(string),
long: "end-date",
description: "End date (YYYY-MM-DD)",
}),
deleteSourceDataValues: flag({
long: "delete-source",
description: "Delete the source data values after changing the org unit",
}),
deleteTargetDataValues: flag({
long: "delete-target",
description: "Delete the target data values before changing the org unit",
}),
dryRun: flag({
long: "dry-run",
description: "Perform the operation in dry run mode",
}),
},
handler: async args => {
try {
if (args.sourceOrgUnitId === args.targetOrgUnitId) {
throw new Error("Source and target org unit IDs are the same.");
}

const api = getD2ApiFromArgs(args);
const orgUnitRepository = new OrgUnitD2Repository(api);
const dataSetsRepository = new DataSetsD2Repository(api);
const dataValuesRepository = new DataValuesD2Repository(api);

await new ChangeDataValuesOrgUnitUseCase(
new TerminalLogger(),
orgUnitRepository,
dataSetsRepository,
dataValuesRepository
).execute(args);
} catch (error) {
console.error((error as Error).message);
process.exit(1);
}
},
});

async function readDataElementsFile(csvPath: string): Promise<string[]> {
if (!fs.existsSync(csvPath) || !fs.statSync(csvPath).isFile()) {
throw new Error(`Can't find file: ${csvPath}`);
Expand Down
Loading