src/analytics/AnalyticsResponse.js
import AnalyticsResponseHeader from './AnalyticsResponseHeader'
const booleanMap = {
0: 'No', // XXX i18n.no || 'No',
1: 'Yes', // i18n.yes || 'Yes',
}
const OUNAME = 'ouname'
const OU = 'ou'
const DEFAULT_COLLECT_IGNORE_HEADERS = [
'psi',
'ps',
'eventdate',
'longitude',
'latitude',
'ouname',
'oucode',
'eventdate',
'eventdate',
]
const DEFAULT_PREFIX_IGNORE_HEADERS = ['dy', ...DEFAULT_COLLECT_IGNORE_HEADERS]
const getParseMiddleware = (type) => {
switch (type) {
case 'STRING':
case 'TEXT':
return (value) => `${value}`
case 'INTEGER':
case 'NUMBER':
return (value) =>
!Number.isNaN(+value) && Number.isFinite(+value)
? parseFloat(+value)
: value
default:
return (value) => value
}
}
const isPrefixHeader = (header, dimensions) => {
if (DEFAULT_PREFIX_IGNORE_HEADERS.includes(header.name)) {
return false
}
return Boolean(Array.isArray(dimensions) && dimensions.length === 0)
}
const isCollectHeader = (header, dimensions) => {
if (DEFAULT_COLLECT_IGNORE_HEADERS.includes(header.name)) {
return false
}
return Boolean(Array.isArray(dimensions) && dimensions.length === 0)
}
const getPrefixedId = (id, prefix) => `${prefix || ''} ${id}`
const getNameByIdsByValueType = (id, valueType) => {
if (valueType === 'BOOLEAN') {
return booleanMap[id]
}
return id
}
class AnalyticsResponse {
constructor(response) {
if (response) {
this.response = response
this.headers = this.extractHeaders()
this.rows = this.extractRows()
this.metaData = this.extractMetadata()
}
}
extractHeaders() {
const { dimensions } = this.response.metaData
const headers = this.response.headers || []
return headers.map(
(header, index) =>
new AnalyticsResponseHeader(header, {
isPrefix: isPrefixHeader(header, dimensions[header.name]),
isCollect: isCollectHeader(header, dimensions[header.name]),
index,
})
)
}
extractRows() {
const headersWithOptionSet = this.headers.filter(
(header) => header.optionSet
)
let { rows } = this.response
if (headersWithOptionSet.length) {
rows = rows.slice()
const optionCodeIdMap = this.optionCodeIdMap()
// replace option code with option uid
headersWithOptionSet.forEach((header) => {
rows.forEach((row, index) => {
const id = optionCodeIdMap[header.name][row[header.index]]
if (id) {
rows[index][header.index] = id
}
})
})
}
return rows
}
extractMetadata() {
const metaData = { ...this.response.metaData }
const { dimensions, items } = metaData
// populate metaData dimensions and items
this.headers
.filter(
(header) =>
!DEFAULT_COLLECT_IGNORE_HEADERS.includes(header.name)
)
.forEach((header) => {
let ids
// collect row values
if (header.isCollect) {
ids = this.getSortedUniqueRowIdStringsByHeader(header)
dimensions[header.name] = ids
} else {
ids = dimensions[header.name]
}
if (header.isPrefix) {
// create prefixed dimensions array
dimensions[header.name] = ids.map((id) =>
getPrefixedId(id, header.name)
)
// create items
dimensions[header.name].forEach((prefixedId, index) => {
const id = ids[index]
const { valueType } = header
const name = getNameByIdsByValueType(id, valueType)
items[prefixedId] = { name }
})
}
})
// for events, add items from 'ouname'
if (this.hasHeader(OUNAME) && this.hasHeader(OU)) {
const ouNameHeaderIndex = this.getHeader(OUNAME).getIndex()
const ouHeaderIndex = this.getHeader(OU).getIndex()
let ouId
let ouName
this.rows.forEach((row) => {
ouId = row[ouHeaderIndex]
if (items[ouId] === undefined) {
ouName = row[ouNameHeaderIndex]
items[ouId] = {
name: ouName,
}
}
})
}
return metaData
}
getHeader(name) {
return this.headers.find((header) => header.name === name)
}
hasHeader(name) {
return this.getHeader(name) !== undefined
}
getSortedUniqueRowIdStringsByHeader(header) {
const parseByType = getParseMiddleware(header.valueType)
const parseString = getParseMiddleware('STRING')
const rowIds = Array.from(
// unique values
new Set(
this.rows.map((responseRow) =>
parseByType(responseRow[header.index])
)
)
// remove empty values
).filter((id) => id !== '')
return rowIds.sort().map((id) => parseString(id))
}
optionCodeIdMap() {
const { dimensions, items } = this.response.metaData
const map = {}
this.headers
.filter((header) => typeof header.optionSet === 'string')
.forEach((header) => {
const optionIds = dimensions[header.name]
map[header.name] = optionIds
.map((id) => ({
[items[id].code]: id,
}))
.reduce((acc, obj) => Object.assign(acc, obj), {})
})
return map
}
sortOrganisationUnitsHierarchy() {
const organisationUnits = this.metaData.dimensions.ou
organisationUnits.forEach((organisationUnit, i) => {
const hierarchyPrefix = this.metaData.ouHierarchy[organisationUnit]
const hierarchyIds = [organisationUnit]
const hierarchyNames = []
hierarchyPrefix
.split('/')
.reverse()
.forEach((ouId) => {
hierarchyIds.unshift(ouId)
})
hierarchyIds.forEach((ouId) => {
if (this.metaData.items[ouId]) {
hierarchyNames.push(this.metaData.items[ouId].name)
}
})
organisationUnits[i] = {
id: organisationUnit,
fullName: hierarchyNames.join(' / '),
}
})
// XXX how does this work with different languages/collations?
organisationUnits.sort((a, b) => {
const aFullName = a.fullName
const bFullName = b.fullName
if (aFullName < bFullName) {
return -1
}
return aFullName > bFullName ? 1 : 0
})
this.metaData.dimensions.ou = organisationUnits.map((ou) => ou.id)
}
}
export default AnalyticsResponse