Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PHX-97: Add date features in report server #3341

Merged
merged 14 commits into from
Oct 10, 2021
Prev Previous commit
Next Next commit
Add QueryBuilder
  • Loading branch information
kael89 committed Oct 7, 2021
commit 0dc7692b100286203b5ce15ab4dd7ff09183e18e
45 changes: 3 additions & 42 deletions packages/report-server/src/aggregator/Aggregator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,8 @@
*/

import { Aggregator as BaseAggregator } from '@tupaia/aggregator';
import {
getDefaultPeriod,
convertPeriodStringToDateRange,
convertDateRangeToPeriodString,
} from '@tupaia/utils';
import { Aggregation, Event } from '../types';

type PeriodParams = {
period?: string;
startDate?: string;
endDate?: string;
};
import { Aggregation, Event, PeriodParams } from '../types';

export class Aggregator extends BaseAggregator {
aggregationToAggregationConfig = (aggregation: Aggregation) =>
Expand All @@ -32,7 +22,7 @@ export class Aggregator extends BaseAggregator {
hierarchy: string | undefined,
periodParams: PeriodParams,
) {
const { period, startDate, endDate } = buildPeriodQueryParams(periodParams);
const { period, startDate, endDate } = periodParams;
const aggregations = aggregationList
? aggregationList.map(this.aggregationToAggregationConfig)
: [{ type: 'RAW' }];
Expand All @@ -59,7 +49,7 @@ export class Aggregator extends BaseAggregator {
periodParams: PeriodParams,
dataElementCodes?: string[],
): Promise<Event[]> {
const { period, startDate, endDate } = buildPeriodQueryParams(periodParams);
const { period, startDate, endDate } = periodParams;
const aggregations = aggregationList
? aggregationList.map(this.aggregationToAggregationConfig)
: [{ type: 'RAW' }];
Expand All @@ -78,32 +68,3 @@ export class Aggregator extends BaseAggregator {
);
}
}

/**
* Returns { startDate, endDate } format if either startDate, or endDate are passed in
* Otherwise returns { period, startDate, endDate } format
*/
const buildPeriodQueryParams = ({ period, startDate, endDate }: PeriodParams) => {
let builtPeriod: string | undefined;
let builtStartDate: string | undefined;
let builtEndDate: string | undefined;
if (startDate && endDate) {
builtStartDate = startDate;
builtEndDate = endDate;
builtPeriod = convertDateRangeToPeriodString(startDate, endDate);
} else if (!period && !startDate && !endDate) {
builtPeriod = getDefaultPeriod();
[builtStartDate, builtEndDate] = convertPeriodStringToDateRange(builtPeriod);
} else if (!startDate && !endDate) {
builtPeriod = period;
[builtStartDate, builtEndDate] = convertPeriodStringToDateRange(builtPeriod);
} else if (startDate) {
builtStartDate = startDate;
[, builtEndDate] = convertPeriodStringToDateRange(period || getDefaultPeriod());
} else if (endDate) {
[builtStartDate] = convertPeriodStringToDateRange(period || getDefaultPeriod());
builtEndDate = endDate;
}

return { period: builtPeriod, startDate: builtStartDate, endDate: builtEndDate };
};
88 changes: 88 additions & 0 deletions packages/report-server/src/reportBuilder/QueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Tupaia
* Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd
*/

import {
convertDateRangeToPeriodString,
convertPeriodStringToDateRange,
getDefaultPeriod,
getOffsetMoment,
momentToPeriod,
periodToDateString,
} from '@tupaia/utils';

import { DateOffset, FetchReportQuery, PeriodParams, ReportConfig } from '../types';

const buildDateUsingSpecs = (
date: string | undefined,
dateOffset: DateOffset,
{ isEndDate = false },
) => {
const moment = getOffsetMoment(date, dateOffset);
const periodType = dateOffset.modifierUnit || dateOffset.unit;
const period = momentToPeriod(moment, periodType);
return periodToDateString(period, isEndDate);
};

export class QueryBuilder {
private readonly config: ReportConfig;

private readonly query: FetchReportQuery;

constructor(config: ReportConfig, query: FetchReportQuery) {
this.config = config;
this.query = query;
}

public build() {
const { period, startDate, endDate } = this.buildPeriodParams();
return { ...this.query, period, startDate, endDate };
}

private buildPeriodParams(): Required<PeriodParams> {
let { period = getDefaultPeriod(), startDate, endDate } = this.query;
const { startDate: startDateSpecs, endDate: endDateSpecs } = this.config.fetch;

// Use specific date if date specs is string
if (typeof startDateSpecs === 'string') {
startDate = startDateSpecs;
}
if (typeof endDateSpecs === 'string') {
endDate = endDateSpecs;
}

// Calculate missing period params using other existing params/default values
if (startDate && endDate) {
// Force period to be consistent with start and end dates
period = convertDateRangeToPeriodString(startDate, endDate);
} else if (startDate) {
[, endDate] = convertPeriodStringToDateRange(period);
} else if (endDate) {
[startDate] = convertPeriodStringToDateRange(period);
} else {
[startDate, endDate] = convertPeriodStringToDateRange(period);
}

// Apply date offset if date specs is object
if (typeof startDateSpecs === 'object') {
startDate = buildDateUsingSpecs(startDate, startDateSpecs, { isEndDate: false });
}
if (typeof endDateSpecs === 'object') {
endDate = buildDateUsingSpecs(endDate, endDateSpecs, { isEndDate: true });
}

if (!this.matchesOriginalQuery({ startDate, endDate })) {
// Re-adjust period to the new date range
period = convertDateRangeToPeriodString(startDate, endDate);
}

return { period, startDate, endDate };
}

private matchesOriginalQuery(subQuery: Record<string, unknown>) {
return Object.entries(subQuery).every(
([key, value]) => key in this.query && this.query[key as keyof FetchReportQuery] === value,
);
}
}
50 changes: 50 additions & 0 deletions packages/report-server/src/reportBuilder/configValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Tupaia
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { yup, yupUtils } from '@tupaia/utils';

const periodTypeValidator = yup.mixed().oneOf(['day', 'week', 'month', 'quarter', 'year']);

const createDataSourceValidator = (sourceType: 'dataElement' | 'dataGroup') => {
const otherSourceKey = sourceType === 'dataElement' ? 'dataGroups' : 'dataElements';

return yup
.array()
.of(yup.string())
.when(['$testData', otherSourceKey], {
is: ($testData: unknown, otherDataSource: string[]) =>
!$testData && (!otherDataSource || otherDataSource.length === 0),
then: yup.array().of(yup.string()).required('Requires "dataGroups" or "dataElements"').min(1),
otherwise: yup.array().of(yup.string()),
});
};

const dataElementValidator = createDataSourceValidator('dataElement');
const dataGroupValidator = createDataSourceValidator('dataGroup');

const dateSpecsValidator = yupUtils.polymorphic({
object: yup.object().shape({
unit: periodTypeValidator,
offset: yup.number(),
modifier: yup.mixed().oneOf(['start_of', 'end_of']),
modifierUnit: periodTypeValidator,
}),
string: yup.string().min(4),
});

export const configValidator = yup.object().shape({
fetch: yup.object().shape(
{
dataElements: dataElementValidator,
dataGroups: dataGroupValidator,
aggregations: yup.array().required(),
startDate: dateSpecsValidator,
endDate: dateSpecsValidator,
},
[['dataElements', 'dataGroups']],
),
transform: yup.array().required(),
output: yup.object(),
});
26 changes: 5 additions & 21 deletions packages/report-server/src/reportBuilder/fetch/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
* Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd
*/

import { FetchReportQuery } from '../../types';
import { FetchReportQuery, ReportConfig } from '../../types';
import { Aggregator } from '../../aggregator';
import { FetchResponse } from './types';
import { fetchBuilders } from './functions';

const FETCH_PARAM_KEYS = ['dataGroups', 'dataElements', 'aggregations'];
type FetchConfig = ReportConfig['fetch'];

type FetchParams = {
call: (aggregator: Aggregator, query: FetchReportQuery) => Promise<FetchResponse>;
Expand All @@ -22,31 +22,15 @@ const fetch = (
return fetcher.call(aggregator, query);
};

const buildParams = (params: unknown): FetchParams => {
if (typeof params !== 'object' || params === null) {
throw new Error(`Expected object but got ${params}`);
}

Object.keys(params).forEach(p => {
if (!FETCH_PARAM_KEYS.includes(p)) {
throw new Error(`Invalid fetch param key ${p}, must be one of ${FETCH_PARAM_KEYS}`);
}
});

const buildParams = (params: FetchConfig): FetchParams => {
const fetchFunction = 'dataGroups' in params ? 'dataGroups' : 'dataElements';

if (!(fetchFunction in fetchBuilders)) {
throw new Error(
`Expected fetch to be one of ${Object.keys(fetchBuilders)} but got ${fetchFunction}`,
);
}

return {
call: fetchBuilders[fetchFunction as keyof typeof fetchBuilders](params),
call: fetchBuilders[fetchFunction](params),
};
};

export const buildFetch = (params: unknown) => {
export const buildFetch = (params: FetchConfig) => {
const builtParams = buildParams(params);
return (aggregator: Aggregator, query: FetchReportQuery) => fetch(aggregator, query, builtParams);
};
11 changes: 7 additions & 4 deletions packages/report-server/src/reportBuilder/reportBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
*/

import { Aggregator } from '../aggregator';
import { FetchReportQuery } from '../types';
import { FetchReportQuery, ReportConfig } from '../types';
import { configValidator } from './configValidator';
import { buildContext, ReqContext } from './context';
import { buildFetch, FetchResponse } from './fetch';
import { buildTransform } from './transform';
import { buildOutput } from './output';
import { Row } from './types';
import { OutputType } from './output/functions/outputBuilders';
import { QueryBuilder } from './QueryBuilder';

export interface BuiltReport {
results: OutputType;
Expand All @@ -19,7 +21,7 @@ export interface BuiltReport {
export class ReportBuilder {
reqContext: ReqContext;

config?: Record<string, unknown>;
config?: ReportConfig;

testData?: Row[];

Expand All @@ -28,7 +30,7 @@ export class ReportBuilder {
}

public setConfig = (config: Record<string, unknown>) => {
this.config = config;
this.config = configValidator.validateSync(config);
return this;
};

Expand All @@ -45,7 +47,8 @@ export class ReportBuilder {
const fetch = this.testData
? () => ({ results: this.testData } as FetchResponse)
: buildFetch(this.config?.fetch);
const data = await fetch(aggregator, query);
const builtQuery = new QueryBuilder(this.config, query).build();
const data = await fetch(aggregator, builtQuery);

const context = await buildContext(this.config.transform, this.reqContext, data);
const transform = buildTransform(this.config.transform, context);
Expand Down
15 changes: 9 additions & 6 deletions packages/report-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ export interface ReportServerModelRegistry extends ModelRegistry {
readonly report: ReportModel;
}

export interface FetchReportQuery {
organisationUnitCodes: string[];
hierarchy?: string;
export type PeriodParams = {
period?: string;
startDate?: string;
endDate?: string;
}
};

export type FetchReportQuery = PeriodParams & {
organisationUnitCodes: string[];
hierarchy?: string;
};

export type AggregationObject = {
type: string;
Expand All @@ -27,14 +30,14 @@ export type Aggregation = string | AggregationObject;

type PeriodType = 'day' | 'week' | 'month' | 'quarter' | 'year';

type DateSpecsObject = {
export type DateOffset = {
unit: PeriodType;
offset?: number;
modifier?: 'start_of' | 'end_of';
modifierUnit?: PeriodType;
};

type DateSpecs = string | DateSpecsObject;
export type DateSpecs = string | DateOffset;

type Transform = string | Record<string, unknown>;

Expand Down
2 changes: 1 addition & 1 deletion tupaia-packages.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
"packages/": true
},
"search.exclude": {
"**/index.esm.js": true,
"**/lib": true,
"**/logfile*.log": true,
"**/yarn-1.18.0.js": true
}
Expand Down