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
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,37 @@ const analyticsData = await api.analytics

```ts
const analyticsData = await api.analytics
.getEnrollmentsQuery("IpHINAT79UW", {
dimension: ["GxdhnY5wmHq", "ou:ImspTQPwCqd"],
enrollmentDate: "LAST_12_MONTHS,THIS_MONTH",
.getEnrollmentsQuery({
programId: "IpHINAT79UW",
dimension: ["cejWyOfXge6", "lZGmxYbs97q", "ou:USER_ORGUNIT", "w75KJ2mc4zz", "zDhUuAYrxNC"],
programStatus: "ACTIVE",
})
.getData();
```

Disable pagination:

```ts
const analyticsData = await api.analytics
.getEnrollmentsQuery({
programId: "IpHINAT79UW",
dimension: ["cejWyOfXge6", "lZGmxYbs97q", "ou:USER_ORGUNIT", "w75KJ2mc4zz", "zDhUuAYrxNC"],
programStatus: "ACTIVE",
paging: false,
})
.getData();
```

Get pagination and total pages:

```ts
const analyticsData = await api.analytics
.getEnrollmentsQuery({
programId: "IpHINAT79UW",
dimension: ["cejWyOfXge6", "lZGmxYbs97q", "ou:USER_ORGUNIT", "w75KJ2mc4zz", "zDhUuAYrxNC"],
programStatus: "ACTIVE",
paging: true,
totalPages: true,
})
.getData();
```
Expand Down
151 changes: 134 additions & 17 deletions src/api/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,23 @@ import { Id } from "../schemas";
import { EmptyObject } from "../utils/types";
import { D2ApiResponse, HttpResponse } from "./common";
import { D2ApiGeneric } from "./d2Api";
import { Pager } from "./model";

type Operator = "EQ" | "GT" | "GE" | "LT" | "LE";

type ColumnsSeparatedBySemicolon = string;

type RowsSeparatedBySemicolon = string;

type UserOrgUnitSeparatedBySemicolon = string;

export type AnalyticsOptions = {
/** Dimensions and dimension items to be retrieved, repeated for each */
dimension: string[];

/** Filters and filter items to apply to the query, repeated for each. */
filter?: string[];

/** Aggregation type to use in the aggregation process. */
aggregationType?:
| "SUM"
| "AVERAGE"
Expand All @@ -20,44 +30,136 @@ export type AnalyticsOptions = {
| "VARIANCE"
| "MIN"
| "MAX";

/** Filters for the data/measures. */
measureCriteria?: Operator;

/** Filters for the data/measure, applied before aggregation is performed. */
preAggregationMeasureCriteria?: Operator;

/** Start date for a date range. Will be applied as a filter. Can not be used together with a period dimension or filter. (yyyy-MM-dd) */
startDate?: string;

/** End date for date range. Will be applied as a filter. Can not be used together with a period dimension or filter. (yyyy-MM-dd) */
endDate?: string;

/** Exclude the metadata part of the response (improves performance). */
skipMeta?: boolean;

/** Exclude the data part of the response. */
skipData?: boolean;

/** Skip rounding of data values, i.e. provide full precision. */
skipRounding?: boolean;

/** Include names of organisation unit ancestors and hierarchy paths of organisation units in the metadata. */
hierarchyMeta?: boolean;

/** Ignore limit on max 50 000 records in response - use with care. */
ignoreLimit?: boolean;

/** Use plain data source or table layout for the response. */
tableLayout?: boolean;

/** Hides empty rows in response, applicable when table layout is true. */
hideEmptyRows?: boolean;

/** Hides empty columns in response, applicable when table layout is true. */
hideEmptyColumns?: boolean;

/** Display full org unit hierarchy path together with org unit name. */
showHierarchy?: boolean;

/** Include the numerator and denominator used to calculate the value in the response. */
includeNumDen?: boolean;

/** Include metadata details to raw data response. */
includeMetadataDetails?: boolean;

/** Property to display for metadata. */
displayProperty?: "NAME" | "SHORTNAME";

/** Identifier scheme used for metadata items in the query response. It accepts identifier, code or attributes. */
outputIdScheme?: string;

/** Identifier scheme to use for metadata items in the query request, can be an identifier, code or attributes. */
inputIdScheme?: string;

/** Include data which has been approved at least up to the given approval level, refers to identifier of approval level. */
approvalLevel?: string;

/** Date used as basis for relative periods. */
relativePeriodDate?: string;
userOrgUnit?: string;
columns?: string;
rows?: string;

/** Explicitly define the user org units to utilize, overrides organisation units associated with the current user, multiple identifiers can be separated by semicolon. */
userOrgUnit?: UserOrgUnitSeparatedBySemicolon;

/** Dimensions to use as columns for table layout. */
columns?: ColumnsSeparatedBySemicolon;

/** Dimensions to use as rows for table layout. */
rows?: RowsSeparatedBySemicolon;

/** Specify the ordering of rows based on value. */
order?: "ASC" | "DESC";

/** The time field to base event aggregation on. Applies to event data items only. Can be a predefined option or the ID of an attribute or data element with a time-based value type. */
timeField?: string;

/** The organisation unit field to base event aggregation on. Applies to event data items only. Can be the ID of an attribute or data element with the Organisation unit value type. The default option is specified as omitting the query parameter. */
orgUnitField?: string;

/** Custom period on enrollmentDate */
enrollmentDate?: string;
};

type KnownAscDescValues =
| "ouname"
| "programstatus"
| "createdbydisplayname"
| "lastupdatedbydisplayname"
| "enrollmentdate"
| "incidentdate"
| "lastupdated";

type AscDescParameter = KnownAscDescValues | Id;

type PageOptions = {
totalPages: boolean;
page: number;
pageSize: number;
paging: boolean;
};
Copy link
Contributor

@tokland tokland Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, so now we get .metaData.pager object, but we don't have types for the response, right? Is that something we want to implement at this point? (we would use conditional types, let me know if you have no experience with them)


type HeadersSeparatedByCommas = string;

export type GetEnrollmentsQueryOptions = {
programId: Id;
} & AnalyticsOptions;
programStatus?: "ACTIVE" | "COMPLETED" | "CANCELLED";
ouMode?: "DESCENDANTS" | "CHILDREN" | "SELECTED";
asc?: AscDescParameter;
desc?: AscDescParameter;
coordinatesOnly?: boolean;
headers?: HeadersSeparatedByCommas;
} & AnalyticsOptions &
Partial<PageOptions>;

type PaginationOptions = Pick<GetEnrollmentsQueryOptions, "paging" | "totalPages" | "skipMeta">;

type PagerWithoutTotals = { page: number; pageSize: number; isLastPage: boolean };

type PagerWithTotals = { page: number; pageCount: number; pageSize: number; total: number };

type MetadataPager<Options extends PaginationOptions> = Options["paging"] extends false
? undefined
: (Options["totalPages"] extends true ? PagerWithTotals : PagerWithoutTotals);

export type AnalyticsResponse = {
headers: Array<{
name: "dx" | "dy";
column: "Data";
valueType: "TEXT" | "NUMBER";
type: "java.lang.String" | "java.lang.Double";
name: string;
column: string;
valueType: string;
type: string;
hidden: boolean;
meta: boolean;
}>;
Expand All @@ -66,13 +168,17 @@ export type AnalyticsResponse = {
| {
dimensions: Record<string, string[]>;
items: Record<string, { name: string; uid?: Id; code?: string; options: any[] }>;
pager?: Pager;
};

rows: Array<string[]>;
width: number;
height: number;
};

type AnalyticsResponseWithPager<Options extends GetEnrollmentsQueryOptions> = AnalyticsResponse & {
metaData: { pager: MetadataPager<Options> };
};

export type RunAnalyticsResponse = HttpResponse<{
id: string;
created: string;
Expand All @@ -86,10 +192,22 @@ export type RunAnalyticsResponse = HttpResponse<{
}>;

export type RunAnalyticsOptions = {
/** Skip generation of resource tables */
skipResourceTables?: boolean;

/** Skip generation of aggregate data and completeness data */
skipAggregate?: boolean;

/** Skip generation of event data */
skipEvents?: boolean;

/** Skip generation of enrollment data */
skipEnrollment?: boolean;

/** Skip generation of organization unit ownership data */
skipOrgUnitOwnership?: boolean;

/** Number of last years of data to include */
lastYears?: number;
};

Expand All @@ -101,13 +219,12 @@ export class Analytics {
}

// https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-240/analytics.html#webapi_enrollment_analytics
getEnrollmentsQuery({
programId,
...options
}: GetEnrollmentsQueryOptions): D2ApiResponse<AnalyticsResponse> {
return this.d2Api.get<AnalyticsResponse>(
`/analytics/enrollments/query/${programId}`,
options as AnalyticsOptions
getEnrollmentsQuery<Options extends GetEnrollmentsQueryOptions>(
options: Options
): D2ApiResponse<AnalyticsResponseWithPager<Options>> {
return this.d2Api.get<AnalyticsResponseWithPager<Options>>(
`/analytics/enrollments/query/${options.programId}`,
options
);
}

Expand Down