Skip to content

Commit

Permalink
Stats + adatpt OutsideEnrollment for new tracker (clean arch)
Browse files Browse the repository at this point in the history
  • Loading branch information
tokland committed Sep 27, 2024
1 parent a283909 commit 9afaa06
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 231 deletions.
10 changes: 6 additions & 4 deletions src/data/CategoryOptionD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,26 +74,28 @@ export class CategoryOptionD2Repository implements CategoryOptionRepository {
{
recordsSkipped: saveResponse.status === "ERROR" ? catOptionsToSave.map(co => co.id) : [],
errorMessage,
created: saveResponse.response.stats.created,
ignored: saveResponse.response.stats.ignored,
updated: saveResponse.response.stats.updated,
...saveResponse.response.stats,
},
];
});

return stats.reduce(
(acum, stat) => {
(acum, stat): Stats => {
return {
recordsSkipped: [...acum.recordsSkipped, ...stat.recordsSkipped],
errorMessage: `${acum.errorMessage}${stat.errorMessage}`,
created: acum.created + stat.created,
ignored: acum.ignored + stat.ignored,
updated: acum.updated + stat.updated,
deleted: acum.deleted + stat.deleted,
total: acum.total + stat.total,
};
},
{
recordsSkipped: [],
errorMessage: "",
deleted: 0,
total: 0,
created: 0,
ignored: 0,
updated: 0,
Expand Down
84 changes: 55 additions & 29 deletions src/data/D2Tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ import { Async } from "domain/entities/Async";
import { Stats } from "domain/entities/Stats";
import { D2Api } from "types/d2-api";
import log from "utils/log";
import { TrackerPostRequest } from "@eyeseetea/d2-api/api/tracker";
import { TrackedEntitiesGetResponse } from "@eyeseetea/d2-api/api/trackerTrackedEntities";
import { TrackerEnrollmentsResponse } from "@eyeseetea/d2-api/api/trackerEnrollments";
import { TrackerEventsResponse } from "@eyeseetea/d2-api/api/trackerEvents";

export class D2Tracker {
constructor(private api: D2Api) {}

async postTracker(key: TrackerDataKey, objects: object[]): Async<TrackerResponse[]> {
async postTracker<Key extends TrackerDataKey>(
key: Key,
objects: Array<NonNullable<TrackerPostRequest[Key]>[number]>
): Async<TrackerResponse[]> {
const total = objects.length;
log.info(`Import data: ${key} - Total: ${total}`);
let page = 1;
Expand All @@ -29,17 +36,21 @@ export class D2Tracker {
return result;
}

private async postTrackerData(data: object, options: { payloadId: string }): Async<TrackerResponse> {
const response: TrackerResponse = await this.api
.post<TrackerResponse>("/tracker", { async: false }, data)
private async postTrackerData(
data: TrackerPostRequest,
options: { payloadId: string }
): Async<TrackerResponse> {
const response: TrackerResponse = await this.api.tracker
.post({ async: false }, data)
.getData()
.catch(err => {
if (err?.response?.data) {
return err.response.data as TrackerResponse;
.then(res => ({ ...res, stats: { ...Stats.empty(), ...res.stats } }))
.catch((err): TrackerResponse => {
const data = err?.response?.data;
if (data) {
return data;
} else {
return {
status: "ERROR",
typeReports: [],
stats: Stats.empty(),
};
}
Expand All @@ -56,37 +67,47 @@ export class D2Tracker {
}
}

async getFromTracker<T>(
apiPath: string,
async getFromTracker<Key extends TrackerDataKey>(
model: Key,
options: {
programIds: string[];
orgUnitIds: string[] | undefined;
fields?: string;
trackedEntity?: string | undefined;
}
): Promise<T[]> {
const output = [];
const { programIds, orgUnitIds, fields = "*", trackedEntity } = options;
): Promise<Array<Mapping[Key][number]>> {
type Output = Array<Mapping[Key][number]>;

const output: Output = [];
const { programIds, orgUnitIds, trackedEntity } = options;

for (const programId of programIds) {
let page = 1;
let dataRemaining = true;

while (dataRemaining) {
const pageSize = 1000;
log.debug(`GET ${apiPath} (pageSize=${pageSize}, page=${page})`);

const { instances } = await this.api
.get<{ instances: T[] }>(`/tracker/${apiPath}`, {
page,
pageSize: pageSize,
ouMode: orgUnitIds ? "SELECTED" : "ALL",
orgUnit: orgUnitIds?.join(";"),
fields: fields,
program: programId,
trackedEntity,
})
.getData();
log.debug(`GET ${model} (pageSize=${pageSize}, page=${page})`);

const apiOptions = {
page,
pageSize: pageSize,
ouMode: orgUnitIds ? ("SELECTED" as const) : ("ALL" as const),
orgUnit: orgUnitIds?.join(";"),
fields: { $all: true as const },
program: programId,
trackedEntity,
};

const { tracker } = this.api;

const endpoint = {
trackedEntities: () => tracker.trackedEntities.get(apiOptions),

Check failure on line 104 in src/data/D2Tracker.ts

View workflow job for this annotation

GitHub Actions / Unit tests

Argument of type '{ page: number; pageSize: number; ouMode: "ALL" | "SELECTED"; orgUnit: string | undefined; fields: { $all: true; }; program: string; trackedEntity: string | undefined; }' is not assignable to parameter of type 'TrackerTrackedEntitiesParams<{ $all: true; }>'.
enrollments: () => tracker.enrollments.get(apiOptions),

Check failure on line 105 in src/data/D2Tracker.ts

View workflow job for this annotation

GitHub Actions / Unit tests

Argument of type '{ page: number; pageSize: number; ouMode: "ALL" | "SELECTED"; orgUnit: string | undefined; fields: { $all: true; }; program: string; trackedEntity: string | undefined; }' is not assignable to parameter of type 'TrackerEnrollmentsParams<{ $all: true; }>'.
events: () => tracker.events.get(apiOptions),

Check failure on line 106 in src/data/D2Tracker.ts

View workflow job for this annotation

GitHub Actions / Unit tests

Argument of type '{ page: number; pageSize: number; ouMode: "ALL" | "SELECTED"; orgUnit: string | undefined; fields: { $all: true; }; program: string; trackedEntity: string | undefined; }' is not assignable to parameter of type 'EventsParams<{ $all: true; }>'.
};

const res = await endpoint[model]().getData();
const instances: Output = res.instances as Output;

if (instances.length === 0) {
dataRemaining = false;
Expand All @@ -96,16 +117,21 @@ export class D2Tracker {
}
}
}
log.info(`GET ${apiPath} -> Total: ${output.length}`);
log.info(`GET ${model} -> Total: ${output.length}`);

return output;
}
}

type TrackerResponse = {
status: string;
typeReports: object[];
stats: Stats;
};

type TrackerDataKey = "events" | "enrollments" | "trackedEntities";

type Mapping = {
trackedEntities: TrackedEntitiesGetResponse<{ $all: true }>["instances"];

Check failure on line 134 in src/data/D2Tracker.ts

View workflow job for this annotation

GitHub Actions / Unit tests

Type 'TrackedEntitiesGetResponse' is not generic.
enrollments: TrackerEnrollmentsResponse<{ $all: true }>["instances"];

Check failure on line 135 in src/data/D2Tracker.ts

View workflow job for this annotation

GitHub Actions / Unit tests

Type 'TrackerEnrollmentsResponse' is not generic.
events: TrackerEventsResponse<{ $all: true }>["instances"];

Check failure on line 136 in src/data/D2Tracker.ts

View workflow job for this annotation

GitHub Actions / Unit tests

Type 'TrackerEventsResponse' is not generic.
};
110 changes: 68 additions & 42 deletions src/data/ProgramEventsD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { GetOptions, ProgramEventsRepository } from "domain/repositories/Program
import { D2Api, Ref } from "types/d2-api";
import { cartesianProduct } from "utils/array";
import logger from "utils/log";
import { getId, Id } from "domain/entities/Base";
import { getId, Id, NamedRef } from "domain/entities/Base";
import { Result } from "domain/entities/Result";
import { getInChunks } from "./dhis2-utils";
import { promiseMap } from "./dhis2-utils";
Expand Down Expand Up @@ -41,48 +41,10 @@ export class ProgramEventsD2Repository implements ProgramEventsRepository {
constructor(private api: D2Api) {}

async get(options: GetOptions): Async<ProgramEvent[]> {
const d2EventsMapper = await D2EventsMapper.build(this.api);
const d2Events = await this.getD2Events(options);

const { programs } = await this.api.metadata
.get({
programs: {
fields: {
id: true,
name: true,
programStages: { id: true, name: true },
},
},
})
.getData();

const programsById = _.keyBy(programs, getId);

const programStagesById = _(programs)
.flatMap(program => program.programStages)
.uniqBy(getId)
.keyBy(getId)
.value();

return d2Events.map(
(event): ProgramEvent => ({
created: event.createdAt,
id: event.event,
program: programsById[event.program] || { id: event.program, name: "" },
programStage: programStagesById[event.programStage] || { id: event.programStage, name: "" },
orgUnit: { id: event.orgUnit, name: event.orgUnitName },
trackedEntityInstanceId: event.trackedEntity,
status: event.status,
date: event.occurredAt,
dueDate: event.scheduledAt,
dataValues: event.dataValues.map(dv => ({
dataElementId: dv.dataElement,
value: dv.value,
storedBy: dv.storedBy,
providedElsewhere: dv.providedElsewhere,
lastUpdated: dv.updatedAt,
})),
})
);
return d2Events.map(d2Event => d2EventsMapper.getEventEntityFromD2Object(d2Event));
}

async delete(events: Ref[]): Async<Result> {
Expand Down Expand Up @@ -205,13 +167,77 @@ export class ProgramEventsD2Repository implements ProgramEventsRepository {
}
}

export class D2EventsMapper {
constructor(
private programsById: Record<Id, NamedRef>,
private programStagesById: Record<Id, NamedRef>
) {}

static async build(api: D2Api) {
const { programs } = await api.metadata
.get({
programs: {
fields: {
id: true,
name: true,
programStages: { id: true, name: true },
},
},
})
.getData();

const programsById = _.keyBy(programs, getId);

const programStagesById = _(programs)
.flatMap(program => program.programStages)
.uniqBy(getId)
.keyBy(getId)
.value();

return new D2EventsMapper(programsById, programStagesById);
}

getEventEntityFromD2Object(event: Event): ProgramEvent {
return {
id: event.event,
created: event.createdAt,
program: this.programsById[event.program] || { id: event.program, name: "" },
programStage: this.programStagesById[event.programStage] || { id: event.programStage, name: "" },
orgUnit: { id: event.orgUnit, name: event.orgUnitName },
trackedEntityInstanceId: event.trackedEntity,
status: event.status,
date: event.occurredAt,
dueDate: event.scheduledAt,
dataValues: event.dataValues.map(dv => ({

Check failure on line 211 in src/data/ProgramEventsD2Repository.ts

View workflow job for this annotation

GitHub Actions / Unit tests

Parameter 'dv' implicitly has an 'any' type.
dataElementId: dv.dataElement,
value: dv.value,
storedBy: dv.storedBy,
providedElsewhere: dv.providedElsewhere,
lastUpdated: dv.updatedAt,
})),
};
}
}

type EventToPost = NonNullable<TrackerPostRequest["events"]>[number];

async function importEvents(api: D2Api, events: EventToPost[], params?: TrackerPostParams): Async<Result> {
if (_.isEmpty(events)) return { type: "success", message: "No events to post" };

const resList = await promiseMap(_.chunk(events, 100), async eventsGroup => {
const res = await api.tracker.post(params || {}, { events: eventsGroup }).getData();
const res = await api.tracker
.post(
{
async: false,
skipPatternValidation: true,
skipSideEffects: true,
skipRuleEngine: true,
importMode: "COMMIT",
...params,
},
{ events: eventsGroup }
)
.getData();
if (res.status === "OK") {
const message = JSON.stringify(
_.pick(res, ["status", "imported", "updated", "deleted", "ignored"])
Expand Down
27 changes: 11 additions & 16 deletions src/data/ProgramsD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { promiseMap, runMetadata } from "./dhis2-utils";
import { D2ProgramRules } from "./d2-program-rules/D2ProgramRules";
import { D2Tracker } from "./D2Tracker";
import { Program, ProgramType } from "domain/entities/Program";
import { D2TrackerEnrollmentToPost } from "@eyeseetea/d2-api/api/trackerEnrollments";

Check failure on line 12 in src/data/ProgramsD2Repository.ts

View workflow job for this annotation

GitHub Actions / Unit tests

'"@eyeseetea/d2-api/api/trackerEnrollments"' has no exported member named 'D2TrackerEnrollmentToPost'. Did you mean 'D2TrackerEnrollmentSchema'?
import { D2TrackerEventToPost } from "@eyeseetea/d2-api/api/trackerEvents";

Check failure on line 13 in src/data/ProgramsD2Repository.ts

View workflow job for this annotation

GitHub Actions / Unit tests

Module '"@eyeseetea/d2-api/api/trackerEvents"' has no exported member 'D2TrackerEventToPost'.
import { D2TrackedEntityInstanceToPost } from "@eyeseetea/d2-api/api/trackerTrackedEntities";

type MetadataRes = { date: string } & { [k: string]: Array<{ id: string }> };

Expand Down Expand Up @@ -43,12 +46,9 @@ export class ProgramsD2Repository implements ProgramsRepository {
const metadata = await this.getMetadata(programIds);

const getOptions = { programIds, orgUnitIds };
const events = await this.d2Tracker.getFromTracker<object>("events", getOptions);
const enrollments = await this.d2Tracker.getFromTracker<D2Enrollment>("enrollments", getOptions);
const trackedEntities = await this.d2Tracker.getFromTracker<D2TrackedEntity>(
"trackedEntities",
getOptions
);
const events = await this.d2Tracker.getFromTracker("events", getOptions);
const enrollments = await this.d2Tracker.getFromTracker("enrollments", getOptions);
const trackedEntities = await this.d2Tracker.getFromTracker("trackedEntities", getOptions);

/* Remove redundant enrollments info from TEIs */
const trackedEntitiesWithoutEnrollments = trackedEntities.map(trackedEntity => ({
Expand All @@ -59,7 +59,7 @@ export class ProgramsD2Repository implements ProgramsRepository {
return {
metadata,
data: {
events,
events: events,
enrollments: enrollments,
trackedEntities: trackedEntitiesWithoutEnrollments,
},
Expand Down Expand Up @@ -98,7 +98,7 @@ export class ProgramsD2Repository implements ProgramsRepository {
// DHIS2 exports enrollments without attributes, but requires it on import, add from TEI
const enrollmentsWithAttributes = enrollments.map(enrollment => ({
...enrollment,
attributes: teisById[enrollment.trackedEntity]?.attributes || [],
attributes: (enrollment.trackedEntity && teisById[enrollment.trackedEntity]?.attributes) || [],
}));

log.info(`Import data`);
Expand All @@ -119,16 +119,11 @@ interface D2ProgramExport {
}

type D2ProgramData = {
events: object[];
enrollments: D2Enrollment[];
trackedEntities: D2TrackedEntity[];
events: D2TrackerEventToPost[];
enrollments: D2TrackerEnrollmentToPost[];
trackedEntities: D2TrackedEntityInstanceToPost[];
};

interface D2Enrollment {
enrollment: string;
trackedEntity: string;
}

export interface D2TrackedEntity {
trackedEntity: Id;
orgUnit: Id;
Expand Down
Loading

0 comments on commit 9afaa06

Please sign in to comment.