Skip to content
Merged
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
45 changes: 36 additions & 9 deletions src/data/Dhis2RelationshipTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,30 @@ import { promiseMap } from "../utils/promises";
import { getUid } from "./dhis2-uid";
import { getTrackedEntities, TrackedEntityGetRequest } from "./Dhis2TrackedEntityInstances";
import { TrackerRelationship, RelationshipItem, TrackedEntitiesApiRequest } from "../domain/entities/TrackedEntity";
import { buildOrgUnitsParameter } from "../domain/entities/OrgUnit";
import { EventsAPIResponse } from "../domain/entities/DhisDataPackage";
import { getVersion, getMajorVersion } from "../utils/d2-api";

type RelationshipTypesById = Record<Id, Pick<D2RelationshipType, "id" | "toConstraint" | "fromConstraint">>;

export type RelationshipOrgUnitFilter = TrackedEntityOURequestApi["ouMode"];

export function buildOrgUnitMode(ouMode: RelationshipOrgUnitFilter, orgUnits?: Ref[]) {
function buildOrgUnitsParameter(apiVersion: number, orgUnitsIds: Ref[]): string {
const separator = apiVersion >= 42 ? "," : ";";
return orgUnitsIds.map(({ id }) => id).join(separator);
}

export function buildOrgUnitMode(apiVersion: string, ouMode: RelationshipOrgUnitFilter, orgUnits?: Ref[]) {
const majorVersion = getMajorVersion(apiVersion);
const isOuReq = ouMode === "SELECTED" || ouMode === "CHILDREN" || ouMode === "DESCENDANTS";
//issue: v41 - orgUnitMode/ouMode; v38-40 ouMode; ouMode to be deprecated
//issue: v42 - orgUnitMode and orgUnits; v41 - orgUnitMode/ouMode and orgUnit; v38-40 ouMode; ouMode to be deprecated
//can't use both orgUnitMode and ouMode in v41
if (!isOuReq) {
return { ouMode };
if (majorVersion >= 42) return { orgUnitMode: ouMode };
else return { ouMode };
} else if (orgUnits && orgUnits.length > 0) {
return { ouMode, orgUnit: buildOrgUnitsParameter(orgUnits) };
const orgUnitsParam = buildOrgUnitsParameter(majorVersion, orgUnits);
if (majorVersion >= 42) return { orgUnitMode: ouMode, orgUnits: orgUnitsParam };
else return { ouMode, orgUnit: orgUnitsParam };
} else {
throw new Error(`No orgUnits selected for ouMode ${ouMode}`);
}
Expand Down Expand Up @@ -169,6 +178,8 @@ export async function getRelationshipMetadata(
api: D2Api,
filters?: ProgramFilters
): Promise<RelationshipMetadata> {
const apiVersion = await getVersion(api);

const {
trackedEntityTypes,
relationshipTypes: allRelationshipTypes,
Expand Down Expand Up @@ -201,8 +212,22 @@ export async function getRelationshipMetadata(

if (!isProgramAssociatedWithSomeConstraint) return;

const fromConstraint = await getConstraint(api, trackedEntityTypes, programs, type.fromConstraint, filters);
const toConstraint = await getConstraint(api, trackedEntityTypes, programs, type.toConstraint, filters);
const fromConstraint = await getConstraint(
api,
apiVersion,
trackedEntityTypes,
programs,
type.fromConstraint,
filters
);
const toConstraint = await getConstraint(
api,
apiVersion,
trackedEntityTypes,
programs,
type.toConstraint,
filters
);

return fromConstraint && toConstraint
? { id: relType.id, name: relType.name, constraints: { from: fromConstraint, to: toConstraint } }
Expand All @@ -217,6 +242,7 @@ type ProgramInfo = NamedRef & { programStages: NamedRef[] };
const getConstraint = memoizeAsync(
async (
api: D2Api,
apiVersion: string,
trackedEntityTypes: NamedRef[],
programs: ProgramInfo[],
constraint: D2RelationshipConstraint,
Expand All @@ -235,7 +261,7 @@ const getConstraint = memoizeAsync(

switch (constraint.relationshipEntity) {
case "TRACKED_ENTITY_INSTANCE":
return getConstraintForTypeTei(api, filters, trackedEntityTypes, constraint);
return getConstraintForTypeTei(api, apiVersion, filters, trackedEntityTypes, constraint);
case "PROGRAM_STAGE_INSTANCE": {
if (constraint.program) {
const program = programsById[constraint.program.id];
Expand All @@ -251,14 +277,15 @@ const getConstraint = memoizeAsync(

async function getConstraintForTypeTei(
api: D2Api,
apiVersion: string,
filters: ProgramFilters | undefined,
trackedEntityTypes: NamedRef[],
constraint: D2RelationshipConstraint
): Promise<RelationshipConstraint> {
const { ouMode = "CAPTURE", organisationUnits = [] } = filters || {};
const trackedEntityTypesById = _.keyBy(trackedEntityTypes, obj => obj.id);

const ouModeQuery = buildOrgUnitMode(ouMode, organisationUnits);
const ouModeQuery = buildOrgUnitMode(apiVersion, ouMode, organisationUnits);

const query = {
...ouModeQuery,
Expand Down
19 changes: 14 additions & 5 deletions src/data/Dhis2TrackedEntityInstances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,17 @@ import { TrackedEntitiesApiRequest, TrackedEntitiesResponse, TrackedEntity } fro
import { Params } from "@eyeseetea/d2-api/api/common";
import { ImportDataPackageOptions } from "../domain/repositories/InstanceRepository";
import { MULTI_TEXT_OPTION_DELIMITER } from "../domain/helpers/ExcelBuilder";
import { getVersion } from "../utils/d2-api";

export interface GetOptions {
type GetOptions = {
api: D2Api;
program: Ref;
orgUnits: Ref[];
pageSize?: number;
enrollmentStartDate?: Moment;
enrollmentEndDate?: Moment;
relationshipsOuFilter?: RelationshipOrgUnitFilter;
}
};

type TrackerParams = Params & Omit<TeiGetRequest, "ou" | "ouMode">;

Expand All @@ -60,6 +61,8 @@ export async function getTrackedEntityInstances(options: GetOptions): Promise<Tr
} = options;
if (_.isEmpty(orgUnits)) return [];

const apiVersion = await getVersion(api);

const program = await getProgram(api, options.program.id);
if (!program) return [];

Expand All @@ -79,6 +82,7 @@ export async function getTrackedEntityInstances(options: GetOptions): Promise<Tr
for (let page = 1; ; page++) {
const { pageCount, instances } = await getTeisFromApi({
api,
apiVersion,
program,
orgUnits,
page,
Expand Down Expand Up @@ -462,8 +466,11 @@ function getMultiTextValue(options: {
}

async function getExistingTeis(api: D2Api): Promise<Ref[]> {
const apiVersion = await getVersion(api);
const ouModeParam = buildOrgUnitMode(apiVersion, "CAPTURE");

const query = {
ouMode: "CAPTURE",
...ouModeParam,
pageSize: 1000,
totalPages: true,
fields: "trackedEntity",
Expand Down Expand Up @@ -504,6 +511,7 @@ type TeiKey = KeysOfUnion<TrackedEntitiesApiRequest>;

async function getTeisFromApi(options: {
api: D2Api;
apiVersion: string;
program: Program;
orgUnits: Ref[];
page: number;
Expand All @@ -512,7 +520,8 @@ async function getTeisFromApi(options: {
enrollmentEndDate?: Moment;
ouMode: RelationshipOrgUnitFilter;
}): Promise<TrackedEntitiesResponse> {
const { api, program, orgUnits, page, pageSize, enrollmentStartDate, enrollmentEndDate, ouMode } = options;
const { api, apiVersion, program, orgUnits, page, pageSize, enrollmentStartDate, enrollmentEndDate, ouMode } =
options;

const fields: TeiKey[] = [
"trackedEntity",
Expand All @@ -524,7 +533,7 @@ async function getTeisFromApi(options: {
"geometry",
];

const ouModeQuery = buildOrgUnitMode(ouMode, orgUnits);
const ouModeQuery = buildOrgUnitMode(apiVersion, ouMode, orgUnits);

const filters: TrackedEntityGetRequest = {
...ouModeQuery,
Expand Down
42 changes: 42 additions & 0 deletions src/data/__tests__/Dhis2RelationshipTypes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { buildOrgUnitMode } from "../Dhis2RelationshipTypes";

const orgUnits = [{ id: "ou1" }, { id: "ou2" }];

describe("buildOrgUnitMode", () => {
describe("on versions before v42", () => {
it("uses ouMode for non org-unit-requiring modes", () => {
expect(buildOrgUnitMode("2.41.6", "CAPTURE")).toEqual({ ouMode: "CAPTURE" });
});

it("uses ouMode + orgUnit (singular, semicolon-separated) for selected org units", () => {
expect(buildOrgUnitMode("2.41.6", "SELECTED", orgUnits)).toEqual({
ouMode: "SELECTED",
orgUnit: "ou1;ou2",
});
});
});

describe("on v42 and later", () => {
it("uses orgUnitMode for non org-unit-requiring modes", () => {
expect(buildOrgUnitMode("2.42.0", "CAPTURE")).toEqual({ orgUnitMode: "CAPTURE" });
});

it("uses orgUnitMode + orgUnits (plural, comma-separated) for selected org units", () => {
expect(buildOrgUnitMode("2.42.0", "SELECTED", orgUnits)).toEqual({
orgUnitMode: "SELECTED",
orgUnits: "ou1,ou2",
});
});

it("treats pre-release builds as their major version (regression for NaN parsing)", () => {
expect(buildOrgUnitMode("2.44-SNAPSHOT", "SELECTED", orgUnits)).toEqual({
orgUnitMode: "SELECTED",
orgUnits: "ou1,ou2",
});
});
});

it("throws when an org-unit-requiring mode has no org units", () => {
expect(() => buildOrgUnitMode("2.42.0", "SELECTED", [])).toThrow();
});
});
6 changes: 1 addition & 5 deletions src/domain/entities/OrgUnit.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { Id, Ref } from "./ReferenceObject";
import { Id } from "./ReferenceObject";

export interface OrgUnit {
id: Id;
path: string;
name: string;
level: number;
}

export function buildOrgUnitsParameter(orgUnitsIds: Ref[]): string {
return orgUnitsIds.map(({ id }) => id).join(";");
}
27 changes: 27 additions & 0 deletions src/utils/__tests__/d2-api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getMajorVersion } from "../d2-api";

describe("getMajorVersion", () => {
it("parses a stable release version", () => {
expect(getMajorVersion("2.42.0")).toEqual(42);
});

it("parses a fork build (suffix on a later component)", () => {
expect(getMajorVersion("2.41.6.1-eyeseetea-fork-1")).toEqual(41);
});

it("parses a pre-release build (suffix on the minor component)", () => {
expect(getMajorVersion("2.44-SNAPSHOT")).toEqual(44);
});

it("parses a version with only major.minor", () => {
expect(getMajorVersion("2.40")).toEqual(40);
});

it("throws on a version without a minor component", () => {
expect(() => getMajorVersion("2")).toThrow();
});

it("throws on a non-numeric minor component", () => {
expect(() => getMajorVersion("2.x")).toThrow();
});
});
12 changes: 11 additions & 1 deletion src/utils/d2-api.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import _ from "lodash";
import { DhisInstance } from "../domain/entities/DhisInstance";
import { D2Api } from "../types/d2-api";
import { memoizeAsync } from "./cache";

export function getMajorVersion(version: string): number {
const apiVersion = _.get(version.split("."), 1);
if (!apiVersion) throw new Error(`Invalid version: ${version}`);
return Number(apiVersion);
// Use parseInt so pre-release suffixes on the minor component are tolerated
// (e.g. "2.44-SNAPSHOT" -> 44 instead of NaN).
const majorVersion = parseInt(apiVersion, 10);
if (Number.isNaN(majorVersion)) throw new Error(`Invalid version: ${version}`);
return majorVersion;
}

export function getD2APiFromInstance(instance: DhisInstance) {
Expand All @@ -15,3 +20,8 @@ export function getD2APiFromInstance(instance: DhisInstance) {
backend: "fetch",
});
}

export const getVersion = memoizeAsync(async (api: D2Api): Promise<string> => {
const { version } = await api.system.info.getData();
return version;
});
Loading