Skip to content

Commit

Permalink
feat: add support for trackedEntities analytics requests (DHIS2-16023) (
Browse files Browse the repository at this point in the history
#1594)

implement Analytics class for tracked entities request
add support for the new trackedEntity/query endpoint
adjust dimension id formatting
  • Loading branch information
edoardo authored Mar 13, 2024
1 parent da7469a commit 885af10
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 36 deletions.
15 changes: 14 additions & 1 deletion src/api/analytics/Analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import AnalyticsEnrollments from './AnalyticsEnrollments.js'
import AnalyticsEvents from './AnalyticsEvents.js'
import AnalyticsRequest from './AnalyticsRequest.js'
import AnalyticsResponse from './AnalyticsResponse.js'
import AnalyticsTrackedEntities from './AnalyticsTrackedEntities.js'

/**
* @description
* Analytics class used to request analytics data from Web API.
*
* @requires analytics.AnalyticsAggregate
* @requires analytics.AnalyticsTrackedEntities
* @requires analytics.AnalyticsEnrollments
* @requires analytics.AnalyticsEvents
* @requires analytics.AnalyticsRequest
* @requires analytics.AnalyticsResponse
Expand All @@ -31,13 +34,22 @@ import AnalyticsResponse from './AnalyticsResponse.js'
class Analytics {
/**
* @param {!module:analytics.AnalyticsAggregate} analyticsAggregate The AnalyticsAggregate instance
* @param {!module:analytics.AnalyticsTrackedEntities} analyticsTrackedEntities The AnalyticsTrackedEntities instance
* @param {!module:analytics.AnalyticsEnrollments} analyticsEnrollments The AnalyticsEnrollments instance
* @param {!module:analytics.AnalyticsEvents} analyticsEvents The AnalyticsEvents instance
* @param {!module:analytics.AnalyticsRequest} analyticsRequest The AnalyticsRequest class
* @param {!module:analytics.AnalyticsResponse} analyticsResponse The AnalyticsResponse class
*/
constructor({ aggregate, enrollments, events, request, response }) {
constructor({
aggregate,
trackedEntities,
enrollments,
events,
request,
response,
}) {
this.aggregate = aggregate
this.trackedEntities = trackedEntities
this.enrollments = enrollments
this.events = events
this.request = request
Expand All @@ -60,6 +72,7 @@ class Analytics {
if (!Analytics.getAnalytics.analytics) {
Analytics.getAnalytics.analytics = new Analytics({
aggregate: new AnalyticsAggregate(dataEngine),
trackedEntities: new AnalyticsTrackedEntities(dataEngine),
enrollments: new AnalyticsEnrollments(dataEngine),
events: new AnalyticsEvents(dataEngine),
request: AnalyticsRequest,
Expand Down
30 changes: 21 additions & 9 deletions src/api/analytics/AnalyticsBase.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import sortBy from 'lodash/sortBy'
import AnalyticsRequest from './AnalyticsRequest.js'
import { formatRequestPath } from './utils.js'

const analyticsQuery = {
resource: 'analytics',
id: ({ path, program }) => {
return [path, program].filter(Boolean).join('/')
},
id: ({ path, program, trackedEntityType }) =>
formatRequestPath({
path,
program,
trackedEntityType,
}),
params: ({ dimensions, filters, parameters }) => ({
dimension: dimensions.length ? dimensions : undefined,
filter: filters.length ? filters : undefined,
Expand All @@ -15,9 +19,12 @@ const analyticsQuery = {

const analyticsDataQuery = {
resource: 'analytics',
id: ({ path, program }) => {
return [path, program].filter(Boolean).join('/')
},
id: ({ path, program, trackedEntityType }) =>
formatRequestPath({
path,
program,
trackedEntityType,
}),
params: ({ dimensions, filters, parameters }) => {
return {
dimension: dimensions.length ? dimensions : undefined,
Expand All @@ -31,9 +38,12 @@ const analyticsDataQuery = {

const analyticsMetaDataQuery = {
resource: 'analytics',
id: ({ path, program }) => {
return [path, program].filter(Boolean).join('/')
},
id: ({ path, program, trackedEntityType }) =>
formatRequestPath({
path,
program,
trackedEntityType,
}),
params: ({ dimensions, filters, parameters }) => ({
dimension: dimensions.length ? dimensions : undefined,
filter: filters.length ? filters : undefined,
Expand Down Expand Up @@ -120,6 +130,7 @@ class AnalyticsBase {
variables: {
path: req.path,
program: req.program,
trackedEntityType: req.trackedEntityType,
dimensions: generateDimensionStrings(req.dimensions),
filters: generateDimensionStrings(req.filters),
parameters: req.parameters,
Expand Down Expand Up @@ -164,6 +175,7 @@ class AnalyticsBase {
variables: {
path: req.path,
program: req.program,
trackedEntityType: req.trackedEntityType,
dimensions: generateDimensionStrings(
req.dimensions,
options
Expand Down
43 changes: 32 additions & 11 deletions src/api/analytics/AnalyticsRequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AnalyticsRequestBase from './AnalyticsRequestBase.js'
import AnalyticsRequestDimensionsMixin from './AnalyticsRequestDimensionsMixin.js'
import AnalyticsRequestFiltersMixin from './AnalyticsRequestFiltersMixin.js'
import AnalyticsRequestPropertiesMixin from './AnalyticsRequestPropertiesMixin.js'
import { formatDimension } from './utils.js'

/**
* @description
Expand Down Expand Up @@ -45,6 +46,8 @@ class AnalyticsRequest extends AnalyticsRequestDimensionsMixin(
fromVisualization(visualization, passFilterAsDimension = false) {
let request = this

const outputType = visualization.outputType

// extract dimensions from visualization
const columns = visualization.columns || []
const rows = visualization.rows || []
Expand All @@ -56,23 +59,31 @@ class AnalyticsRequest extends AnalyticsRequestDimensionsMixin(
dimension += `-${d.legendSet.id}`
}

if (d.programStage?.id) {
dimension = `${d.programStage.id}.${dimension}`
}

if (d.filter) {
dimension += `:${d.filter}`
}

const programStageId = d.programStage?.id

if (d.repetition?.indexes?.length) {
d.repetition.indexes.forEach((index) => {
request = request.addDimension(
dimension.replace(/\./, `[${index}].`)
formatDimension({
programId: d.program?.id,
programStageId: `${programStageId}[${index}]`,
dimension,
outputType,
})
)
})
} else {
request = request.addDimension(
dimension,
formatDimension({
programId: d.program?.id,
programStageId,
dimension,
outputType,
}),
d.items?.map((item) => item.id)
)
}
Expand All @@ -91,23 +102,33 @@ class AnalyticsRequest extends AnalyticsRequestDimensionsMixin(
f.items?.map((item) => item.id)
)
} else {
let filterString = f.programStage?.id
? `${f.programStage.id}.${f.dimension}`
: f.dimension
let filterString = f.dimension

if (f.filter) {
filterString += `:${f.filter}`
}

const programStageId = f.programStage?.id

if (f.repetition?.indexes?.length) {
f.repetition.indexes.forEach((index) => {
request = request.addFilter(
filterString.replace(/\./, `[${index}].`)
formatDimension({
programId: f.program?.id,
programStageId: `${programStageId}[${index}]`,
dimension: filterString,
outputType,
})
)
})
} else {
request = request.addFilter(
filterString,
formatDimension({
programId: f.program?.id,
programStageId,
dimension: filterString,
outputType,
}),
f.items?.map((item) => item.id)
)
}
Expand Down
11 changes: 9 additions & 2 deletions src/api/analytics/AnalyticsRequestBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class AnalyticsRequestBase {
format = 'json',
path,
program,
trackedEntityType,
dimensions = [],
filters = [],
parameters = {},
Expand All @@ -25,6 +26,7 @@ class AnalyticsRequestBase {
this.format = format.toLowerCase()
this.path = path
this.program = program
this.trackedEntityType = trackedEntityType

this.dimensions = dimensions
this.filters = filters
Expand Down Expand Up @@ -63,8 +65,13 @@ class AnalyticsRequestBase {
return dimension
})

const endPoint = [this.endPoint, this.path, this.program]
.filter((e) => !!e)
const endPoint = [
this.endPoint,
this.path,
this.program,
this.trackedEntityType,
]
.filter(Boolean)
.join('/')

let url = `${endPoint}.${this.format}`
Expand Down
20 changes: 20 additions & 0 deletions src/api/analytics/AnalyticsRequestPropertiesMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,26 @@ const AnalyticsRequestPropertiesMixin = (base) =>
return new AnalyticsRequest(this)
}

/**
* Sets the tracked entity type for the request.
* It appends the tracked entity type id to the request's path.
*
* @param {!String} trackedEntityType The tracked entity type id
*
* @returns {AnalyticsRequest} A new instance of the class for chaining purposes
*
* @example
* const req = new analytics.request()
* .withTrackedEntityType('nEenWmSyUEp');
*/
withTrackedEntityType(trackedEntityType) {
if (trackedEntityType) {
this.trackedEntityType = trackedEntityType
}

return new AnalyticsRequest(this)
}

/**
* Sets the program for the request.
* It appends the program id to the request's path.
Expand Down
25 changes: 25 additions & 0 deletions src/api/analytics/AnalyticsTrackedEntities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import AnalyticsBase from './AnalyticsBase.js'

/**
* @extends module:analytics.AnalyticsBase
*
* @description
* Analytics tracked entities class used to request analytics tracked entities data from Web API.
*
* @memberof module:analytics
*/
class AnalyticsTrackedEntities extends AnalyticsBase {
/**
* @param {!AnalyticsRequest} req Request object
*
* @returns {Promise} Promise that resolves with the analytics query data from the api.
*
* @example
// TODO: provide working example
*/
getQuery(req) {
return this.fetch(req.withPath('trackedEntities/query'))
}
}

export default AnalyticsTrackedEntities
17 changes: 17 additions & 0 deletions src/api/analytics/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,20 @@ const whitelistRegExp = new RegExp(`(?:${whitelistURICodes.join('|')})`, 'g')

export const customEncodeURIComponent = (uri) =>
encodeURIComponent(uri).replace(whitelistRegExp, decodeURIComponent)

export const formatRequestPath = ({ path, program, trackedEntityType }) =>
[path, program, trackedEntityType].filter(Boolean).join('/')

export const formatDimension = ({
outputType,
programId,
programStageId,
dimension,
}) =>
[
outputType === 'TRACKED_ENTITY_INSTANCE' ? programId : undefined,
programStageId,
dimension,
]
.filter(Boolean)
.join('.')
4 changes: 2 additions & 2 deletions src/modules/layout/axisGetDimensionIds.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AXIS } from './axis.js'
import { dimensionGetId } from './dimensionGetId.js'

export const axisGetDimensionIds = (axis) =>
export const axisGetDimensionIds = (axis, outputType) =>
AXIS.isValid(axis)
? axis.map((dimension) => dimensionGetId(dimension))
? axis.map((dimension) => dimensionGetId(dimension, outputType))
: AXIS.defaultValue
8 changes: 8 additions & 0 deletions src/modules/layout/dimension.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ export const DIMENSION_PROP_LEGEND_SET = {
isValid: (prop) => isString(prop),
}

export const DIMENSION_PROP_PROGRAM = {
name: 'program',
defaultValue: {},
required: false,
isValid: (prop) => isObject(prop),
}

export const DIMENSION_PROP_PROGRAM_STAGE = {
name: 'programStage',
defaultValue: {},
Expand All @@ -56,6 +63,7 @@ export const DIMENSION_PROPS = [
DIMENSION_PROP_ITEMS,
DIMENSION_PROP_FILTER,
DIMENSION_PROP_LEGEND_SET,
DIMENSION_PROP_PROGRAM,
DIMENSION_PROP_PROGRAM_STAGE,
DIMENSION_PROP_REPETITION,
]
4 changes: 4 additions & 0 deletions src/modules/layout/dimensionCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
DIMENSION_PROP_ITEMS,
DIMENSION_PROP_FILTER,
DIMENSION_PROP_LEGEND_SET,
DIMENSION_PROP_PROGRAM,
DIMENSION_PROP_PROGRAM_STAGE,
DIMENSION_PROP_REPETITION,
} from './dimension.js'
Expand All @@ -17,6 +18,9 @@ export const dimensionCreate = (dimensionId, itemIds = [], args = {}) => {
...(args.legendSet && {
[DIMENSION_PROP_LEGEND_SET.name]: args.legendSet,
}),
...(args.program && {
[DIMENSION_PROP_PROGRAM.name]: args.program,
}),
...(args.programStage && {
[DIMENSION_PROP_PROGRAM_STAGE.name]: args.programStage,
}),
Expand Down
20 changes: 13 additions & 7 deletions src/modules/layout/dimensionGetId.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { DIMENSION_PROP_ID, DIMENSION_PROP_PROGRAM_STAGE } from './dimension.js'
import { formatDimension } from '../../api/analytics/utils.js'
import {
DIMENSION_PROP_ID,
DIMENSION_PROP_PROGRAM_STAGE,
DIMENSION_PROP_PROGRAM,
} from './dimension.js'

export const dimensionGetId = (dimension) =>
dimension[DIMENSION_PROP_PROGRAM_STAGE.name]?.id
? `${dimension[DIMENSION_PROP_PROGRAM_STAGE.name].id}.${
dimension[DIMENSION_PROP_ID.name]
}`
: dimension[DIMENSION_PROP_ID.name]
export const dimensionGetId = (dimension, outputType) =>
formatDimension({
dimension: dimension[DIMENSION_PROP_ID.name],
programId: dimension[DIMENSION_PROP_PROGRAM.name]?.id,
programStageId: dimension[DIMENSION_PROP_PROGRAM_STAGE.name]?.id,
outputType,
})
5 changes: 4 additions & 1 deletion src/modules/layout/layoutFilterDimensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ export const layoutFilterDimensions = (layout, dimensionIds) => {
DEFAULT_AXIS_IDS.forEach((axisId) => {
if (AXIS.isValid(filteredLayout[axisId])) {
filteredLayout[axisId] = filteredLayout[axisId].filter(
(dimension) => !idArray.includes(dimensionGetId(dimension))
(dimension) =>
!idArray.includes(
dimensionGetId(dimension, layout.outputType)
)
)
}
})
Expand Down
Loading

0 comments on commit 885af10

Please sign in to comment.