From 0ec18ac1f75b630dd6ab8a6ed03c97d20e0050a1 Mon Sep 17 00:00:00 2001 From: Chris Pollard Date: Tue, 10 Jan 2023 15:15:21 +1100 Subject: [PATCH 1/9] MAUI-1446: Delete Sol Islands Covid project (#4295) --- ...IslandsDeleteCovidProject-modifies-data.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/database/src/migrations/20230110023215-SolIslandsDeleteCovidProject-modifies-data.js diff --git a/packages/database/src/migrations/20230110023215-SolIslandsDeleteCovidProject-modifies-data.js b/packages/database/src/migrations/20230110023215-SolIslandsDeleteCovidProject-modifies-data.js new file mode 100644 index 0000000000..ebf4977d55 --- /dev/null +++ b/packages/database/src/migrations/20230110023215-SolIslandsDeleteCovidProject-modifies-data.js @@ -0,0 +1,38 @@ +'use strict'; + +import { codeToId, deleteObject, nameToId } from '../utilities'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +const PROJECT_CODE = 'covid_solomon'; + +exports.up = async function (db) { + const projectId = await codeToId(db, 'project', PROJECT_CODE); + await deleteObject(db, 'access_request', { project_id: projectId }); + await deleteObject(db, 'project', { id: projectId }); + await deleteObject(db, 'dashboard', { root_entity_code: PROJECT_CODE }); + const entityHierarchyId = await nameToId(db, 'entity_hierarchy', PROJECT_CODE); + await deleteObject(db, 'entity_relation', { entity_hierarchy_id: entityHierarchyId }); + await deleteObject(db, 'entity_hierarchy', { name: PROJECT_CODE }); + await deleteObject(db, 'entity', { code: PROJECT_CODE, type: 'project' }); +}; + +exports.down = function (db) { + return null; +}; + +exports._meta = { + version: 1, +}; From 301e3b9c1343ffa9b7a98d79f39aebed5759741c Mon Sep 17 00:00:00 2001 From: Rohan Port <59544282+rohan-bes@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:05:28 +1100 Subject: [PATCH 2/9] RN-702: Re-instate excluding columns when using the 'exclude' merge function (#4248) --- .../reportBuilder/transform/mergeRows.test.ts | 9 ++-- .../transform/parser/parser.test.ts | 25 ----------- .../functions/mergeRows/mergeRows.ts | 41 ++++++++----------- 3 files changed, 23 insertions(+), 52 deletions(-) diff --git a/packages/report-server/src/__tests__/reportBuilder/transform/mergeRows.test.ts b/packages/report-server/src/__tests__/reportBuilder/transform/mergeRows.test.ts index 407bc95249..b6d5b11c02 100644 --- a/packages/report-server/src/__tests__/reportBuilder/transform/mergeRows.test.ts +++ b/packages/report-server/src/__tests__/reportBuilder/transform/mergeRows.test.ts @@ -283,10 +283,11 @@ describe('mergeRows', () => { }, ]); expect(transform(TransformTable.fromRows(MERGEABLE_ANALYTICS))).toStrictEqual( - TransformTable.fromRows( - [{ period: '20200101' }, { period: '20200102' }, { period: '20200103' }], - ['period', 'organisationUnit', 'BCD1', 'BCD2'], // excludes values, but keeps columns - ), + TransformTable.fromRows([ + { period: '20200101' }, + { period: '20200102' }, + { period: '20200103' }, + ]), ); }); diff --git a/packages/report-server/src/__tests__/reportBuilder/transform/parser/parser.test.ts b/packages/report-server/src/__tests__/reportBuilder/transform/parser/parser.test.ts index 3e7f9dfacc..0cec28b46c 100644 --- a/packages/report-server/src/__tests__/reportBuilder/transform/parser/parser.test.ts +++ b/packages/report-server/src/__tests__/reportBuilder/transform/parser/parser.test.ts @@ -121,31 +121,6 @@ describe('parser', () => { }); describe('in transforms', () => { - it('mergeRows supports parser lookups on where', () => { - const transform = buildTransform([ - { - transform: 'mergeRows', - using: { - organisationUnit: 'exclude', - period: 'exclude', - '*': 'sum', - }, - where: "=eq($organisationUnit, 'TO')", - }, - ]); - expect(transform(TransformTable.fromRows(PARSABLE_ANALYTICS))).toStrictEqual( - TransformTable.fromRows( - [ - { BCD1: 11 }, - { period: '20200101', organisationUnit: 'PG', BCD1: 7 }, - { period: '20200102', organisationUnit: 'PG', BCD1: 8 }, - { period: '20200103', organisationUnit: 'PG', BCD1: 2 }, - ], - ['period', 'organisationUnit', 'BCD1'], - ), - ); - }); - it('excludeRows supports parser lookups on where', () => { const transform = buildTransform([ { diff --git a/packages/report-server/src/reportBuilder/transform/functions/mergeRows/mergeRows.ts b/packages/report-server/src/reportBuilder/transform/functions/mergeRows/mergeRows.ts index 1cde67feda..e549365085 100644 --- a/packages/report-server/src/reportBuilder/transform/functions/mergeRows/mergeRows.ts +++ b/packages/report-server/src/reportBuilder/transform/functions/mergeRows/mergeRows.ts @@ -6,10 +6,7 @@ import { yup } from '@tupaia/utils'; import { yupTsUtils } from '@tupaia/tsutils'; -import { Context } from '../../../context'; -import { TransformParser } from '../../parser'; import { mergeStrategies } from './mergeStrategies'; -import { buildWhere } from '../where'; import { FieldValue, Row } from '../../../types'; import { buildCreateGroupKey } from './createGroupKey'; import { buildGetMergeStrategy } from './getMergeStrategy'; @@ -19,7 +16,6 @@ import { TransformTable } from '../../table'; type MergeRowsParams = { createGroupKey: (row: Row) => string; getMergeStrategy: (field: string) => keyof typeof mergeStrategies; - where: (parser: TransformParser) => boolean; }; const optionalMergeStrategyNameValidator = yup @@ -65,23 +61,15 @@ type Group = { [columnName: string]: FieldValue[]; }; -const groupRows = (table: TransformTable, params: MergeRowsParams, context: Context) => { +const groupRows = (table: TransformTable, params: MergeRowsParams) => { const groupsByKey: Record = {}; - const parser = new TransformParser(table, context); - const ungroupedRows: Row[] = []; // Rows that don't match the 'where' clause are left ungrouped table.getRows().forEach((row: Row) => { - if (!params.where(parser)) { - ungroupedRows.push(row); - parser.next(); - return; - } const groupKey = params.createGroupKey(row); addRowToGroup(groupsByKey, groupKey, row); // mutates groupsByKey - parser.next(); }); - return { groups: Object.values(groupsByKey), ungroupedRows }; + return Object.values(groupsByKey); }; const addRowToGroup = (groupsByKey: Record, groupKey: string, row: Row) => { @@ -102,10 +90,17 @@ const addRowToGroup = (groupsByKey: Record, groupKey: string, row }; const mergeGroups = (groups: Group[], params: MergeRowsParams) => { - return groups.map(group => { + const excludedColumns = new Set(); + const mergedRows = groups.map(group => { const mergedRow: Row = {}; Object.entries(group).forEach(([columnName, groupValues]) => { const mergeStrategy = params.getMergeStrategy(columnName); + + // We track the excluded columns so that we can remove them from the TransformTable later + if (mergeStrategy === 'exclude') { + excludedColumns.add(columnName); + } + const mergedValue = mergeStrategies[mergeStrategy](groupValues); if (mergedValue !== undefined) { mergedRow[columnName] = mergedValue; @@ -113,13 +108,14 @@ const mergeGroups = (groups: Group[], params: MergeRowsParams) => { }); return mergedRow; }); + return { mergedRows, excludedColumns: Array.from(excludedColumns) }; }; -const mergeRows = (table: TransformTable, params: MergeRowsParams, context: Context) => { - const { groups, ungroupedRows } = groupRows(table, params, context); - const mergedRows = mergeGroups(groups, params); - const newRowData = mergedRows.concat(ungroupedRows); - return new TransformTable(table.getColumns(), newRowData); +const mergeRows = (table: TransformTable, params: MergeRowsParams) => { + const groups = groupRows(table, params); + const { mergedRows, excludedColumns } = mergeGroups(groups, params); + const columns = table.getColumns().filter(columnName => !excludedColumns.includes(columnName)); + return new TransformTable(columns, mergedRows); }; const buildParams = (params: unknown): MergeRowsParams => { @@ -130,11 +126,10 @@ const buildParams = (params: unknown): MergeRowsParams => { return { createGroupKey: buildCreateGroupKey(groupBy), getMergeStrategy: buildGetMergeStrategy(groupBy, using), - where: buildWhere(params), }; }; -export const buildMergeRows = (params: unknown, context: Context) => { +export const buildMergeRows = (params: unknown) => { const builtParams = buildParams(params); - return (table: TransformTable) => mergeRows(table, builtParams, context); + return (table: TransformTable) => mergeRows(table, builtParams); }; From 344badd4bfcf987365cd99a34394adb564181bd9 Mon Sep 17 00:00:00 2001 From: Rohan Port <59544282+rohan-bes@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:46:10 +1100 Subject: [PATCH 3/9] RN-685: Add MockTupaiaApiClient for use in unit tests (#4213) --- packages/api-client/package.json | 2 + .../api-client/src/MockTupaiaApiClient.ts | 32 +++ packages/api-client/src/TupaiaApiClient.ts | 20 +- .../api-client/src/connections/AuthApi.ts | 3 + .../api-client/src/connections/CentralApi.ts | 3 + .../api-client/src/connections/EntityApi.ts | 3 + .../api-client/src/connections/ReportApi.ts | 3 + packages/api-client/src/connections/index.ts | 8 +- .../src/connections/mocks/MockAuthApi.ts | 37 +++ .../src/connections/mocks/MockCentralApi.ts | 58 +++++ .../src/connections/mocks/MockEntityApi.ts | 246 ++++++++++++++++++ .../src/connections/mocks/MockReportApi.ts | 21 ++ .../api-client/src/connections/mocks/index.ts | 9 + packages/api-client/src/connections/types.ts | 9 + packages/api-client/src/index.ts | 4 + .../context/buildContext.test.ts | 11 +- .../customReport/testCustomReport.test.ts | 9 +- .../customReport/tongaCovidRawData.test.ts | 12 +- .../reportBuilder/query/QueryBuilder.test.ts | 9 +- .../reportBuilder/testUtils/entityApiMock.ts | 176 ------------- .../reportBuilder/testUtils/index.ts | 6 - yarn.lock | 2 + 22 files changed, 464 insertions(+), 219 deletions(-) create mode 100644 packages/api-client/src/MockTupaiaApiClient.ts create mode 100644 packages/api-client/src/connections/mocks/MockAuthApi.ts create mode 100644 packages/api-client/src/connections/mocks/MockCentralApi.ts create mode 100644 packages/api-client/src/connections/mocks/MockEntityApi.ts create mode 100644 packages/api-client/src/connections/mocks/MockReportApi.ts create mode 100644 packages/api-client/src/connections/mocks/index.ts create mode 100644 packages/api-client/src/connections/types.ts delete mode 100644 packages/report-server/src/__tests__/reportBuilder/testUtils/entityApiMock.ts delete mode 100644 packages/report-server/src/__tests__/reportBuilder/testUtils/index.ts diff --git a/packages/api-client/package.json b/packages/api-client/package.json index b6b045fa00..7183caea8f 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -20,6 +20,8 @@ "directory": "packages/api-client" }, "dependencies": { + "@tupaia/tsutils": "1.0.0", + "lodash.pick": "^4.4.0", "node-fetch": "^1.7.3", "qs": "^6.10.1" } diff --git a/packages/api-client/src/MockTupaiaApiClient.ts b/packages/api-client/src/MockTupaiaApiClient.ts new file mode 100644 index 0000000000..a817a2c8f5 --- /dev/null +++ b/packages/api-client/src/MockTupaiaApiClient.ts @@ -0,0 +1,32 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + */ + +import { + AuthApiInterface, + CentralApiInterface, + EntityApiInterface, + ReportApiInterface, +} from './connections'; + +import { MockAuthApi, MockCentralApi, MockEntityApi, MockReportApi } from './connections/mocks'; + +export class MockTupaiaApiClient { + public readonly entity: EntityApiInterface; + public readonly central: CentralApiInterface; + public readonly auth: AuthApiInterface; + public readonly report: ReportApiInterface; + + public constructor({ + auth = new MockAuthApi(), + central = new MockCentralApi(), + entity = new MockEntityApi(), + report = new MockReportApi(), + }) { + this.auth = auth; + this.central = central; + this.entity = entity; + this.report = report; + } +} diff --git a/packages/api-client/src/TupaiaApiClient.ts b/packages/api-client/src/TupaiaApiClient.ts index 123bd7aa5e..f984d7e5c7 100644 --- a/packages/api-client/src/TupaiaApiClient.ts +++ b/packages/api-client/src/TupaiaApiClient.ts @@ -5,14 +5,24 @@ */ import { AuthHandler } from './types'; -import { ApiConnection, AuthApi, EntityApi, CentralApi, ReportApi } from './connections'; +import { + ApiConnection, + AuthApi, + EntityApi, + CentralApi, + ReportApi, + EntityApiInterface, + CentralApiInterface, + AuthApiInterface, + ReportApiInterface, +} from './connections'; import { PRODUCTION_BASE_URLS, ServiceBaseUrlSet } from './constants'; export class TupaiaApiClient { - public readonly entity: EntityApi; - public readonly central: CentralApi; - public readonly auth: AuthApi; - public readonly report: ReportApi; + public readonly entity: EntityApiInterface; + public readonly central: CentralApiInterface; + public readonly auth: AuthApiInterface; + public readonly report: ReportApiInterface; public constructor(authHandler: AuthHandler, baseUrls: ServiceBaseUrlSet = PRODUCTION_BASE_URLS) { this.auth = new AuthApi(new ApiConnection(authHandler, baseUrls.auth)); diff --git a/packages/api-client/src/connections/AuthApi.ts b/packages/api-client/src/connections/AuthApi.ts index 51537d62da..db05143551 100644 --- a/packages/api-client/src/connections/AuthApi.ts +++ b/packages/api-client/src/connections/AuthApi.ts @@ -6,6 +6,7 @@ import { AccessPolicyObject } from '../types'; import { BaseApi } from './BaseApi'; +import { PublicInterface } from './types'; type ServerAuthResponse = { accessToken?: string; @@ -52,3 +53,5 @@ export class AuthApi extends BaseApi { return { accessToken, refreshToken, accessPolicy, email, user }; } } + +export interface AuthApiInterface extends PublicInterface {} diff --git a/packages/api-client/src/connections/CentralApi.ts b/packages/api-client/src/connections/CentralApi.ts index 763ed1b709..e3695797da 100644 --- a/packages/api-client/src/connections/CentralApi.ts +++ b/packages/api-client/src/connections/CentralApi.ts @@ -6,6 +6,7 @@ import { QueryParameters } from '../types'; import { RequestBody } from './ApiConnection'; import { BaseApi } from './BaseApi'; +import { PublicInterface } from './types'; export type SurveyResponse = { surveyId: string; @@ -97,3 +98,5 @@ export class CentralApi extends BaseApi { return resource; } } + +export interface CentralApiInterface extends PublicInterface {} diff --git a/packages/api-client/src/connections/EntityApi.ts b/packages/api-client/src/connections/EntityApi.ts index 0f2416505f..8cf114a215 100644 --- a/packages/api-client/src/connections/EntityApi.ts +++ b/packages/api-client/src/connections/EntityApi.ts @@ -5,6 +5,7 @@ */ import { BaseApi } from './BaseApi'; +import { PublicInterface } from './types'; const CLAUSE_DELIMITER = ';'; const NESTED_FIELD_DELIMITER = '_'; @@ -254,3 +255,5 @@ export class EntityApi extends BaseApi { ); } } + +export interface EntityApiInterface extends PublicInterface {} diff --git a/packages/api-client/src/connections/ReportApi.ts b/packages/api-client/src/connections/ReportApi.ts index 565538b01e..b525866c61 100644 --- a/packages/api-client/src/connections/ReportApi.ts +++ b/packages/api-client/src/connections/ReportApi.ts @@ -6,6 +6,7 @@ import { QueryParameters } from '../types'; import { RequestBody } from './ApiConnection'; import { BaseApi } from './BaseApi'; +import { PublicInterface } from './types'; export class ReportApi extends BaseApi { public async testReport(query: QueryParameters, body: RequestBody) { @@ -20,3 +21,5 @@ export class ReportApi extends BaseApi { return this.connection.get('fetchTransformSchemas'); } } + +export interface ReportApiInterface extends PublicInterface {} diff --git a/packages/api-client/src/connections/index.ts b/packages/api-client/src/connections/index.ts index 872b8e6889..505e8b850c 100644 --- a/packages/api-client/src/connections/index.ts +++ b/packages/api-client/src/connections/index.ts @@ -5,7 +5,7 @@ */ export { ApiConnection } from './ApiConnection'; -export { AuthApi } from './AuthApi'; -export { EntityApi } from './EntityApi'; -export { CentralApi } from './CentralApi'; -export { ReportApi } from './ReportApi'; +export { AuthApi, AuthApiInterface } from './AuthApi'; +export { EntityApi, EntityApiInterface } from './EntityApi'; +export { CentralApi, CentralApiInterface } from './CentralApi'; +export { ReportApi, ReportApiInterface } from './ReportApi'; diff --git a/packages/api-client/src/connections/mocks/MockAuthApi.ts b/packages/api-client/src/connections/mocks/MockAuthApi.ts new file mode 100644 index 0000000000..23fa8409d0 --- /dev/null +++ b/packages/api-client/src/connections/mocks/MockAuthApi.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Tupaia + * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + */ + +import { AuthApiInterface } from '..'; +import { AccessPolicyObject } from '../../types'; + +export class MockAuthApi implements AuthApiInterface { + public login(authDetails: { + emailAddress: string; + password: string; + deviceName: string; + devicePlatform?: string | undefined; + installId?: string | undefined; + }): Promise<{ + accessToken: string; + refreshToken: string; + accessPolicy: AccessPolicyObject; + email: string; + user: { email: string; accessPolicy: AccessPolicyObject }; + }> { + throw new Error('Method not implemented.'); + } + public refreshAccessToken( + refreshToken: string, + ): Promise<{ + accessToken: string; + refreshToken: string; + accessPolicy: AccessPolicyObject; + email: string; + user: { email: string; accessPolicy: AccessPolicyObject }; + }> { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/api-client/src/connections/mocks/MockCentralApi.ts b/packages/api-client/src/connections/mocks/MockCentralApi.ts new file mode 100644 index 0000000000..862af63c2f --- /dev/null +++ b/packages/api-client/src/connections/mocks/MockCentralApi.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Tupaia + * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + */ + +import { CentralApiInterface } from '..'; +import { RequestBody } from '../ApiConnection'; +import { SurveyResponse } from '../CentralApi'; + +export class MockCentralApi implements CentralApiInterface { + public getUser(): Promise { + throw new Error('Method not implemented.'); + } + public registerUserAccount( + userFields: Record, + ): Promise<{ userId: string; message: string }> { + throw new Error('Method not implemented.'); + } + public changeUserPassword( + passwordChangeFields: Record, + ): Promise<{ message: string }> { + throw new Error('Method not implemented.'); + } + public createSurveyResponses(responses: SurveyResponse[]): Promise { + throw new Error('Method not implemented.'); + } + public fetchResources( + endpoint: string, + params?: Record | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + public createResource( + endpoint: string, + params: Record, + body: RequestBody, + ): Promise { + throw new Error('Method not implemented.'); + } + public updateResource( + endpoint: string, + params: Record, + body: RequestBody, + ): Promise { + throw new Error('Method not implemented.'); + } + public deleteResource(endpoint: string): Promise { + throw new Error('Method not implemented.'); + } + public upsertResource( + endpoint: string, + params: Record, + body: RequestBody, + ): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/api-client/src/connections/mocks/MockEntityApi.ts b/packages/api-client/src/connections/mocks/MockEntityApi.ts new file mode 100644 index 0000000000..0189296929 --- /dev/null +++ b/packages/api-client/src/connections/mocks/MockEntityApi.ts @@ -0,0 +1,246 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Tupaia + * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + */ + +import pick from 'lodash.pick'; +import { isDefined } from '@tupaia/tsutils'; +import { EntityApiInterface } from '..'; + +export class MockEntityApi implements EntityApiInterface { + private readonly entities: Record[]>; + private readonly relations: Record; + + private getEntitiesStub( + hierarchyName: string, + entityCodes: string[], + queryOptions: { field?: string; fields?: string[]; filter?: Record } = {}, + ) { + const entitiesInHierarchy = this.entities[hierarchyName] || []; + const foundEntities = entitiesInHierarchy.filter(e => entityCodes.includes(e.code)); + const { field, fields, filter } = queryOptions; + + let filteredEntities = foundEntities; + if (filter) { + filteredEntities = filteredEntities.filter(e => + Object.entries(filter).every(([key, value]) => e[key] === value), + ); + } + + if (field) { + return filteredEntities.map(e => e[field]); + } + + if (fields) { + return filteredEntities.map(e => pick(e, fields)); + } + + return filteredEntities; + } + + private getDescendants( + hierarchyName: string, + entityCodes: string[], + queryOptions: { fields?: string[]; filter?: { type?: string } } = {}, + ) { + const entitiesInHierarchy = this.entities[hierarchyName] || []; + const relationsInHierarchy = this.relations[hierarchyName] || []; + const ancestorEntities = entitiesInHierarchy.filter(e => entityCodes.includes(e.code)); + const ancestorEntityQueue = [...ancestorEntities]; + const descendantEntityCodes = []; + while (ancestorEntityQueue.length > 0) { + const parent = ancestorEntityQueue.shift(); + if (parent === undefined) { + continue; + } + + const children = relationsInHierarchy + .filter(({ parent: parentCode }) => parent.code === parentCode) + .map(({ child }) => entitiesInHierarchy.find(entity => entity.code === child)) + .filter(isDefined); + + descendantEntityCodes.push(...children.map(e => e.code)); + ancestorEntityQueue.push(...children); + } + + return this.getEntitiesStub(hierarchyName, descendantEntityCodes, queryOptions); + } + + private getAncestors( + hierarchyName: string, + entityCodes: string[], + queryOptions: { fields?: string[]; filter?: { type?: string } } = {}, + ) { + const entitiesInHierarchy = this.entities[hierarchyName] || []; + const relationsInHierarchy = this.relations[hierarchyName] || []; + const descendantEntities = entitiesInHierarchy.filter(e => entityCodes.includes(e.code)); + const descendantEntityQueue = [...descendantEntities]; + const ancestorCodes = []; + while (descendantEntityQueue.length > 0) { + const child = descendantEntityQueue.shift(); + if (child === undefined) { + continue; + } + + const parents = relationsInHierarchy + .filter(({ child: childCode }) => child.code === childCode) + .map(({ parent }) => entitiesInHierarchy.find(entity => entity.code === parent)) + .filter(isDefined); + + ancestorCodes.push(...parents.map(e => e.code)); + descendantEntityQueue.push(...parents); + } + + return this.getEntitiesStub(hierarchyName, ancestorCodes, queryOptions); + } + + private getRelationsOfEntities( + hierarchyName: string, + entityCodes: string[], + queryOptions: { fields?: string[]; filter?: { type?: string } } = {}, + ) { + return [ + ...this.getAncestors(hierarchyName, entityCodes, queryOptions), + ...this.getEntitiesStub(hierarchyName, entityCodes, queryOptions), + ...this.getDescendants(hierarchyName, entityCodes, queryOptions), + ]; + } + + public constructor( + entities: Record[]> = {}, + relations: Record = {}, + ) { + this.entities = entities; + this.relations = relations; + } + + public async getEntity( + hierarchyName: string, + entityCode: string, + queryOptions?: + | { field?: string | undefined; fields?: string[] | undefined; filter?: any } + | undefined, + ) { + return this.getEntitiesStub(hierarchyName, [entityCode], queryOptions); + } + + public async getEntities( + hierarchyName: string, + entityCodes: string[], + queryOptions: { field?: string; fields?: string[]; filter?: Record } = {}, + ) { + return this.getEntitiesStub(hierarchyName, entityCodes, queryOptions); + } + + public getDescendantsOfEntity( + hierarchyName: string, + entityCode: string, + queryOptions?: + | { field?: string | undefined; fields?: string[] | undefined; filter?: any } + | undefined, + includeRootEntity?: boolean, + ): Promise { + return this.getDescendantsOfEntities(hierarchyName, [entityCode], queryOptions); + } + + public async getDescendantsOfEntities( + hierarchyName: string, + entityCodes: string[], + queryOptions: { fields?: string[]; filter?: { type: string } } = {}, + ) { + return this.getDescendants(hierarchyName, entityCodes, queryOptions); + } + + public getRelativesOfEntity( + hierarchyName: string, + entityCode: string, + queryOptions?: + | { field?: string | undefined; fields?: string[] | undefined; filter?: any } + | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + public getRelativesOfEntities( + hierarchyName: string, + entityCodes: string[], + queryOptions?: + | { field?: string | undefined; fields?: string[] | undefined; filter?: any } + | undefined, + ): Promise { + throw new Error('Method not implemented.'); + } + public async getRelationshipsOfEntity( + hierarchyName: string, + entityCode: string, + groupBy: 'ancestor' | 'descendant', + queryOptions?: any, + ancestorQueryOptions?: any, + descendantQueryOptions?: any, + ) { + return this.getRelationshipsOfEntities( + hierarchyName, + [entityCode], + groupBy, + queryOptions, + ancestorQueryOptions, + descendantQueryOptions, + ); + } + + public async getRelationshipsOfEntities( + hierarchyName: string, + entityCodes: string[], + groupBy: 'ancestor' | 'descendant', + queryOptions: { field?: string } = {}, + ancestorQueryOptions: { field?: string; filter?: { type: string } } = {}, + descendantQueryOptions: { field?: string; filter?: { type: string } } = {}, + ) { + const { field: queryField, ...restOfQueryOptions } = queryOptions; + const { + field: ancestorField = queryField || 'code', + ...restOfAncestorQueryOptions + } = ancestorQueryOptions; + const ancestorOptions = { + ...restOfQueryOptions, + ...restOfAncestorQueryOptions, + fields: [ancestorField, 'code'], + }; + const descendantOptions = { + field: 'code', + ...queryOptions, + ...descendantQueryOptions, + }; + const ancestorEntities = ancestorOptions.filter?.type + ? this.getRelationsOfEntities(hierarchyName, entityCodes, ancestorOptions) + : this.getEntitiesStub(hierarchyName, entityCodes, ancestorOptions); + const descendantsGroupedByAncestor: Record = ancestorEntities.reduce( + (obj, ancestor) => { + return { + ...obj, + [ancestor[ancestorField]]: this.getDescendants( + hierarchyName, + [ancestor.code], + descendantOptions, + ), + }; + }, + {} as Record, + ); + + if (groupBy === 'ancestor') { + return descendantsGroupedByAncestor; + } + const ancestorsGroupedByDescendant = Object.entries(descendantsGroupedByAncestor).reduce( + (obj, [ancestor, descendants]) => { + descendants.forEach(descendant => { + // eslint-disable-next-line no-param-reassign + obj[descendant] = ancestor; + }); + return obj; + }, + {} as Record, + ); + return ancestorsGroupedByDescendant; + } +} diff --git a/packages/api-client/src/connections/mocks/MockReportApi.ts b/packages/api-client/src/connections/mocks/MockReportApi.ts new file mode 100644 index 0000000000..dad42d0c44 --- /dev/null +++ b/packages/api-client/src/connections/mocks/MockReportApi.ts @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/** + * Tupaia + * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + */ + +import { ReportApiInterface } from '..'; +import { RequestBody } from '../ApiConnection'; +import { QueryParameters } from '../../types'; + +export class MockReportApi implements ReportApiInterface { + public testReport(query: QueryParameters, body: RequestBody): Promise { + throw new Error('Method not implemented.'); + } + public fetchAggregationOptions(): Promise { + throw new Error('Method not implemented.'); + } + public fetchTransformSchemas(): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages/api-client/src/connections/mocks/index.ts b/packages/api-client/src/connections/mocks/index.ts new file mode 100644 index 0000000000..75e87fb00c --- /dev/null +++ b/packages/api-client/src/connections/mocks/index.ts @@ -0,0 +1,9 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + */ + +export { MockAuthApi } from './MockAuthApi'; +export { MockCentralApi } from './MockCentralApi'; +export { MockEntityApi } from './MockEntityApi'; +export { MockReportApi } from './MockReportApi'; diff --git a/packages/api-client/src/connections/types.ts b/packages/api-client/src/connections/types.ts new file mode 100644 index 0000000000..851090dc7f --- /dev/null +++ b/packages/api-client/src/connections/types.ts @@ -0,0 +1,9 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + */ + +// Using a mapped type will strip out private properties of a class +export type PublicInterface = { + [Prop in keyof T]: T[Prop]; +}; diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 8baabbb364..c84f35bbf6 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -13,3 +13,7 @@ export { AuthHandler } from './types'; export * from './auth'; export * from './constants'; + +export { MockTupaiaApiClient } from './MockTupaiaApiClient'; + +export * from './connections/mocks'; diff --git a/packages/report-server/src/__tests__/reportBuilder/context/buildContext.test.ts b/packages/report-server/src/__tests__/reportBuilder/context/buildContext.test.ts index 8c7873c9a7..e65c353cf0 100644 --- a/packages/report-server/src/__tests__/reportBuilder/context/buildContext.test.ts +++ b/packages/report-server/src/__tests__/reportBuilder/context/buildContext.test.ts @@ -4,12 +4,11 @@ */ import { AccessPolicy } from '@tupaia/access-policy'; +import { MockTupaiaApiClient, MockEntityApi } from '@tupaia/api-client'; import { Row } from '../../../reportBuilder'; import { buildContext, ReqContext } from '../../../reportBuilder/context/buildContext'; -import { entityApiMock } from '../testUtils'; - describe('buildContext', () => { const HIERARCHY = 'test_hierarchy'; const ENTITIES = { @@ -44,14 +43,12 @@ describe('buildContext', () => { ], }; - const apiMock = entityApiMock(ENTITIES, RELATIONS); - const reqContext: ReqContext = { hierarchy: HIERARCHY, permissionGroup: 'Public', - services: { - entity: apiMock, - } as ReqContext['services'], + services: new MockTupaiaApiClient({ + entity: new MockEntityApi(ENTITIES, RELATIONS), + }), accessPolicy: new AccessPolicy({ AU: ['Public'] }), }; diff --git a/packages/report-server/src/__tests__/reportBuilder/customReport/testCustomReport.test.ts b/packages/report-server/src/__tests__/reportBuilder/customReport/testCustomReport.test.ts index 8a0d42725c..b3277f3d6b 100644 --- a/packages/report-server/src/__tests__/reportBuilder/customReport/testCustomReport.test.ts +++ b/packages/report-server/src/__tests__/reportBuilder/customReport/testCustomReport.test.ts @@ -4,12 +4,11 @@ */ import { AccessPolicy } from '@tupaia/access-policy'; +import { MockEntityApi, MockTupaiaApiClient } from '@tupaia/api-client'; import { ReqContext } from '../../../reportBuilder/context'; import { testCustomReport } from '../../../reportBuilder/customReports/testCustomReport'; -import { entityApiMock } from '../testUtils'; - describe('testCustomReport', () => { const HIERARCHY = 'test_hierarchy'; const ENTITIES = { @@ -44,14 +43,10 @@ describe('testCustomReport', () => { ], }; - const apiMock = entityApiMock(ENTITIES, RELATIONS); - const reqContext: ReqContext = { hierarchy: HIERARCHY, permissionGroup: 'Public', - services: { - entity: apiMock, - } as ReqContext['services'], + services: new MockTupaiaApiClient({ entity: new MockEntityApi(ENTITIES, RELATIONS) }), accessPolicy: new AccessPolicy({ AU: ['Public'] }), }; diff --git a/packages/report-server/src/__tests__/reportBuilder/customReport/tongaCovidRawData.test.ts b/packages/report-server/src/__tests__/reportBuilder/customReport/tongaCovidRawData.test.ts index a45f26778e..7e98078538 100644 --- a/packages/report-server/src/__tests__/reportBuilder/customReport/tongaCovidRawData.test.ts +++ b/packages/report-server/src/__tests__/reportBuilder/customReport/tongaCovidRawData.test.ts @@ -5,11 +5,11 @@ import MockDate from 'mockdate'; import { AccessPolicy } from '@tupaia/access-policy'; -import { ReqContext } from '../../../reportBuilder/context'; +import { MockTupaiaApiClient, MockEntityApi } from '@tupaia/api-client'; +import { ReqContext } from '../../../reportBuilder/context'; import { tongaCovidRawData } from '../../../reportBuilder/customReports/tongaCovidRawData'; -import { entityApiMock } from '../testUtils'; import { ENTITIES, EVENTS, HIERARCHY, RELATIONS } from './tongaCovidRawData.fixtures'; const CURRENT_DATE_STUB = '2020-12-15T00:00:00Z'; @@ -40,14 +40,12 @@ jest.mock('@tupaia/aggregator', () => ({ })); describe('tongaCovidRawData', () => { - const apiMock = entityApiMock(ENTITIES, RELATIONS); - const reqContext: ReqContext = { hierarchy: HIERARCHY, permissionGroup: 'Public', - services: { - entity: apiMock, - } as ReqContext['services'], + services: new MockTupaiaApiClient({ + entity: new MockEntityApi(ENTITIES, RELATIONS), + }), accessPolicy: new AccessPolicy({ AU: ['Public'] }), }; diff --git a/packages/report-server/src/__tests__/reportBuilder/query/QueryBuilder.test.ts b/packages/report-server/src/__tests__/reportBuilder/query/QueryBuilder.test.ts index 465514a851..873ca3ccb2 100644 --- a/packages/report-server/src/__tests__/reportBuilder/query/QueryBuilder.test.ts +++ b/packages/report-server/src/__tests__/reportBuilder/query/QueryBuilder.test.ts @@ -8,13 +8,12 @@ import MockDate from 'mockdate'; import { AccessPolicy } from '@tupaia/access-policy'; +import { MockEntityApi, MockTupaiaApiClient } from '@tupaia/api-client'; import { QueryBuilder } from '../../../reportBuilder/query/QueryBuilder'; import { FetchReportQuery } from '../../../types'; import { ReqContext } from '../../../reportBuilder/context'; -import { entityApiMock } from '../testUtils'; - describe('QueryBuilder', () => { const CURRENT_DATE_STUB = '2020-12-15T00:00:00Z'; @@ -48,14 +47,10 @@ describe('QueryBuilder', () => { underwater_world: [{ parent: 'underwater_world', child: 'AQUA_LAND' }], }; - const apiMock = entityApiMock(ENTITIES, RELATIONS); - const reqContext: ReqContext = { hierarchy: HIERARCHY, permissionGroup: 'Admin', - services: { - entity: apiMock, - } as ReqContext['services'], + services: new MockTupaiaApiClient({ entity: new MockEntityApi(ENTITIES, RELATIONS) }), accessPolicy: new AccessPolicy({ PG: ['Admin'], TO: ['Admin'], diff --git a/packages/report-server/src/__tests__/reportBuilder/testUtils/entityApiMock.ts b/packages/report-server/src/__tests__/reportBuilder/testUtils/entityApiMock.ts deleted file mode 100644 index 6a87d93744..0000000000 --- a/packages/report-server/src/__tests__/reportBuilder/testUtils/entityApiMock.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Tupaia - * Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd - */ - -import { isDefined } from '@tupaia/tsutils'; -import pick from 'lodash.pick'; - -export const entityApiMock = ( - entities: Record[]>, - relations: Record = {}, -) => { - const getEntities = ( - hierarchyName: string, - entityCodes: string[], - queryOptions: { field?: string; fields?: string[]; filter?: Record } = {}, - ) => { - const entitiesInHierarchy = entities[hierarchyName] || []; - const foundEntities = entitiesInHierarchy.filter(e => entityCodes.includes(e.code)); - const { field, fields, filter } = queryOptions; - - let filteredEntities = foundEntities; - if (filter) { - filteredEntities = filteredEntities.filter(e => - Object.entries(filter).every(([key, value]) => e[key] === value), - ); - } - - if (field) { - return filteredEntities.map(e => e[field]); - } - - if (fields) { - return filteredEntities.map(e => pick(e, fields)); - } - - return filteredEntities; - }; - - const getDescendantsOfEntities = ( - hierarchyName: string, - entityCodes: string[], - queryOptions: { fields?: string[]; filter?: { type?: string } } = {}, - ) => { - const entitiesInHierarchy = entities[hierarchyName] || []; - const relationsInHierarchy = relations[hierarchyName] || []; - const ancestorEntities = entitiesInHierarchy.filter(e => entityCodes.includes(e.code)); - const ancestorEntityQueue = [...ancestorEntities]; - const descendantEntityCodes = []; - while (ancestorEntityQueue.length > 0) { - const parent = ancestorEntityQueue.shift(); - if (parent === undefined) { - continue; - } - - const children = relationsInHierarchy - .filter(({ parent: parentCode }) => parent.code === parentCode) - .map(({ child }) => entitiesInHierarchy.find(entity => entity.code === child)) - .filter(isDefined); - - descendantEntityCodes.push(...children.map(e => e.code)); - ancestorEntityQueue.push(...children); - } - - return getEntities(hierarchyName, descendantEntityCodes, queryOptions); - }; - - const getAncestorsOfEntities = ( - hierarchyName: string, - entityCodes: string[], - queryOptions: { fields?: string[]; filter?: { type?: string } } = {}, - ) => { - const entitiesInHierarchy = entities[hierarchyName] || []; - const relationsInHierarchy = relations[hierarchyName] || []; - const descendantEntities = entitiesInHierarchy.filter(e => entityCodes.includes(e.code)); - const descendantEntityQueue = [...descendantEntities]; - const ancestorCodes = []; - while (descendantEntityQueue.length > 0) { - const child = descendantEntityQueue.shift(); - if (child === undefined) { - continue; - } - - const isDefined = (val: T): val is Exclude => val !== undefined; - const parents = relationsInHierarchy - .filter(({ child: childCode }) => child.code === childCode) - .map(({ parent }) => entitiesInHierarchy.find(entity => entity.code === parent)) - .filter(isDefined); - - ancestorCodes.push(...parents.map(e => e.code)); - descendantEntityQueue.push(...parents); - } - - return getEntities(hierarchyName, ancestorCodes, queryOptions); - }; - - const getRelationsOfEntities = ( - hierarchyName: string, - entityCodes: string[], - queryOptions: { fields?: string[]; filter?: { type?: string } } = {}, - ) => { - return [ - ...getAncestorsOfEntities(hierarchyName, entityCodes, queryOptions), - ...getEntities(hierarchyName, entityCodes, queryOptions), - ...getDescendantsOfEntities(hierarchyName, entityCodes, queryOptions), - ]; - }; - - return { - getEntities: async ( - hierarchyName: string, - entityCodes: string[], - queryOptions: { fields?: string[] } = {}, - ) => getEntities(hierarchyName, entityCodes, queryOptions), - getDescendantsOfEntities: async ( - hierarchyName: string, - entityCodes: string[], - queryOptions: { fields?: string[]; filter?: { type: string } } = {}, - ) => getDescendantsOfEntities(hierarchyName, entityCodes, queryOptions), - getRelationshipsOfEntities: async ( - hierarchyName: string, - entityCodes: string[], - groupBy: 'ancestor' | 'descendant', - queryOptions: { field?: string } = {}, - ancestorQueryOptions: { field?: string; filter?: { type: string } } = {}, - descendantQueryOptions: { field?: string; filter?: { type: string } } = {}, - ) => { - const { field: queryField, ...restOfQueryOptions } = queryOptions; - const { - field: ancestorField = queryField || 'code', - ...restOfAncestorQueryOptions - } = ancestorQueryOptions; - const ancestorOptions = { - ...restOfQueryOptions, - ...restOfAncestorQueryOptions, - fields: [ancestorField, 'code'], - }; - const descendantOptions = { - field: 'code', - ...queryOptions, - ...descendantQueryOptions, - }; - const ancestorEntities = ancestorOptions.filter?.type - ? getRelationsOfEntities(hierarchyName, entityCodes, ancestorOptions) - : getEntities(hierarchyName, entityCodes, ancestorOptions); - const descendantsGroupedByAncestor: Record = ancestorEntities.reduce( - (obj, ancestor) => { - return { - ...obj, - [ancestor[ancestorField]]: getDescendantsOfEntities( - hierarchyName, - [ancestor.code], - descendantOptions, - ), - }; - }, - {} as Record, - ); - - if (groupBy === 'ancestor') { - return descendantsGroupedByAncestor; - } - const ancestorsGroupedByDescendant = Object.entries(descendantsGroupedByAncestor).reduce( - (obj, [ancestor, descendants]) => { - descendants.forEach(descendant => { - // eslint-disable-next-line no-param-reassign - obj[descendant] = ancestor; - }); - return obj; - }, - {} as Record, - ); - return ancestorsGroupedByDescendant; - }, - }; -}; diff --git a/packages/report-server/src/__tests__/reportBuilder/testUtils/index.ts b/packages/report-server/src/__tests__/reportBuilder/testUtils/index.ts deleted file mode 100644 index 20be60280f..0000000000 --- a/packages/report-server/src/__tests__/reportBuilder/testUtils/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Tupaia - * Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd - */ - -export { entityApiMock } from './entityApiMock'; diff --git a/yarn.lock b/yarn.lock index c1b8e43ac2..5e4d626f21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5132,6 +5132,8 @@ __metadata: version: 0.0.0-use.local resolution: "@tupaia/api-client@workspace:packages/api-client" dependencies: + "@tupaia/tsutils": 1.0.0 + lodash.pick: ^4.4.0 node-fetch: ^1.7.3 qs: ^6.10.1 languageName: unknown From 8049be782744c4e52936f9b82bccbc8107767719 Mon Sep 17 00:00:00 2001 From: Igor Date: Mon, 16 Jan 2023 13:23:34 +1100 Subject: [PATCH 4/9] Superset filter by org unit (#4299) --- .../superset/SupersetService.stubs.js | 7 +++ .../services/superset/SupersetService.test.js | 59 ++++++++++++++++--- .../src/services/superset/SupersetService.js | 22 +++++-- 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/packages/data-broker/src/__tests__/services/superset/SupersetService.stubs.js b/packages/data-broker/src/__tests__/services/superset/SupersetService.stubs.js index 8cdf7ae8c2..8f87394f47 100644 --- a/packages/data-broker/src/__tests__/services/superset/SupersetService.stubs.js +++ b/packages/data-broker/src/__tests__/services/superset/SupersetService.stubs.js @@ -89,6 +89,13 @@ export const DEFAULT_DATA_SERVICE_MAPPING = new DataServiceMapping( [], ); +export const DEFAULT_ORG_UNIT_CODES = ['STORE_1', 'STORE_2']; + +export const DEFAULT_PULL_OPTIONS = { + dataServiceMapping: DEFAULT_DATA_SERVICE_MAPPING, + organisationUnitCodes: DEFAULT_ORG_UNIT_CODES, +}; + export const createModelsStub = () => { return baseCreateModelsStub({ dataElement: { diff --git a/packages/data-broker/src/__tests__/services/superset/SupersetService.test.js b/packages/data-broker/src/__tests__/services/superset/SupersetService.test.js index d187c04748..0fa0b5f417 100644 --- a/packages/data-broker/src/__tests__/services/superset/SupersetService.test.js +++ b/packages/data-broker/src/__tests__/services/superset/SupersetService.test.js @@ -8,6 +8,8 @@ import { createModelsStub, DATA_ELEMENTS, DEFAULT_DATA_SERVICE_MAPPING, + DEFAULT_ORG_UNIT_CODES, + DEFAULT_PULL_OPTIONS, stubGetSupersetApi, SUPERSET_CHART_DATA_RESPONSE, } from './SupersetService.stubs'; @@ -43,9 +45,7 @@ describe('SupersetService', () => { describe('pullAnalytics()', () => { it('pulls', () => expect( - supersetService.pull([DATA_ELEMENTS.ITEM_1], 'dataElement', { - dataServiceMapping: DEFAULT_DATA_SERVICE_MAPPING, - }), + supersetService.pull([DATA_ELEMENTS.ITEM_1], 'dataElement', DEFAULT_PULL_OPTIONS), ).resolves.toEqual({ metadata: { dataElementCodeToName: {}, @@ -75,6 +75,7 @@ describe('SupersetService', () => { config: { supersetInstanceCode: 'SUPERSET_INSTANCE_B', supersetChartId: 456 }, // different }, ]), + organisationUnitCodes: DEFAULT_ORG_UNIT_CODES, }); return expect(getSupersetApiInstance).toHaveBeenCalledWith( @@ -87,9 +88,11 @@ describe('SupersetService', () => { it('uses supersetItemCode as a fallback', () => expect( - supersetService.pull([DATA_ELEMENTS.ITEM_2_CUSTOM_CODE], 'dataElement', { - dataServiceMapping: DEFAULT_DATA_SERVICE_MAPPING, - }), + supersetService.pull( + [DATA_ELEMENTS.ITEM_2_CUSTOM_CODE], + 'dataElement', + DEFAULT_PULL_OPTIONS, + ), ).resolves.toEqual({ metadata: expect.anything(), results: [ @@ -110,10 +113,50 @@ describe('SupersetService', () => { it('throws if supersetChartId not set', () => expect( - supersetService.pull([DATA_ELEMENTS.DE_NO_CHART_ID], 'dataElement', { + supersetService.pull([DATA_ELEMENTS.DE_NO_CHART_ID], 'dataElement', DEFAULT_PULL_OPTIONS), + ).toBeRejectedWith('Data Element DE_NO_CHART_ID missing supersetChartId')); + + it('filters by the org units you specify', () => + expect( + supersetService.pull([DATA_ELEMENTS.ITEM_1], 'dataElement', { dataServiceMapping: DEFAULT_DATA_SERVICE_MAPPING, + organisationUnitCode: 'STORE_1', }), - ).toBeRejectedWith('Data Element DE_NO_CHART_ID missing supersetChartId')); + ).resolves.toEqual({ + metadata: expect.anything(), + results: [ + { + dataElement: 'ITEM_1', + organisationUnit: 'STORE_1', + period: '20200101', + value: 1, + }, + ], + })); + + it('returns data for all org units if no org units specified', () => + expect( + supersetService.pull([DATA_ELEMENTS.ITEM_1], 'dataElement', { + dataServiceMapping: DEFAULT_DATA_SERVICE_MAPPING, + organisationUnitCodes: [], + }), + ).resolves.toEqual({ + metadata: expect.anything(), + results: [ + { + dataElement: 'ITEM_1', + organisationUnit: 'STORE_1', + period: '20200101', + value: 1, + }, + { + dataElement: 'ITEM_1', + organisationUnit: 'STORE_2', + period: '20200101', + value: 2, + }, + ], + })); }); describe('pullEvents()', () => { diff --git a/packages/data-broker/src/services/superset/SupersetService.js b/packages/data-broker/src/services/superset/SupersetService.js index 71453cce59..c66b04993f 100644 --- a/packages/data-broker/src/services/superset/SupersetService.js +++ b/packages/data-broker/src/services/superset/SupersetService.js @@ -42,7 +42,17 @@ export class SupersetService extends Service { * @private */ async pullAnalytics(dataSources, options) { - const { dataServiceMapping, startDate, endDate } = options; + const { + dataServiceMapping, + startDate, + endDate, + organisationUnitCode, + organisationUnitCodes: inputOrganisationUnitCodes, + } = options; + const organisationUnitCodes = organisationUnitCode + ? [organisationUnitCode] + : inputOrganisationUnitCodes; + let mergedResults = []; for (const [supersetInstanceCode, instanceDataSources] of Object.entries( this.groupBySupersetInstanceCode(dataSources, dataServiceMapping), @@ -61,7 +71,7 @@ export class SupersetService extends Service { chartId, chartDataSources, dataServiceMapping, - { startDate, endDate }, + { startDate, endDate, organisationUnitCodes }, ); mergedResults = mergedResults.concat(results); } @@ -79,12 +89,12 @@ export class SupersetService extends Service { * @param {string} chartId * @param {DataElement[]} dataElements * @param {DataServiceMapping} dataServiceMapping - * @param {{ startDate?: string; endDate?: string }} options + * @param {{ startDate?: string; endDate?: string, organisationUnitCodes?: string[] }} options * @return {Promise} analytic results * @private */ async pullForApiForChart(api, chartId, dataElements, dataServiceMapping, options) { - const { startDate, endDate } = options; + const { startDate, endDate, organisationUnitCodes } = options; const startDateMoment = startDate ? moment(startDate).startOf('day') : undefined; const endDateMoment = endDate ? moment(endDate).endOf('day') : undefined; @@ -96,6 +106,10 @@ export class SupersetService extends Service { for (const datum of data) { const { item_code: itemCode, store_code: storeCode, value, date } = datum; + if (organisationUnitCodes && organisationUnitCodes.length > 0) { + if (!organisationUnitCodes.includes(storeCode)) continue; // unneeded data + } + let dataElement = dataElements.find(de => de.code === itemCode); if (!dataElement) { // Check by supersetItemCode From 2802e478014aa230d6a95c766f6ca542a7e7171f Mon Sep 17 00:00:00 2001 From: Biao Li <31789355+billli0@users.noreply.github.com> Date: Tue, 17 Jan 2023 14:48:25 +1100 Subject: [PATCH 5/9] Add ajv validator and fix build err (#4306) --- packages/tsutils/package.json | 3 + .../validation/ajv/customFormats/IdFormat.ts | 20 + .../src/validation/ajv/customFormats/index.ts | 6 + packages/tsutils/src/validation/ajv/getAjv.ts | 35 + packages/tsutils/src/validation/ajv/index.ts | 6 + packages/tsutils/src/validation/index.ts | 1 + .../validation/{ => yup}/DescribableLazy.ts | 0 .../src/validation/{yup.ts => yup/index.ts} | 0 packages/types/README.md | 2 +- .../types/config/{ => models}/config.json | 4 +- .../config/{ => models}/template.handlebars | 11 +- packages/types/config/schemas/config.json | 1 + .../types/{generate.ts => generate-models.ts} | 2 +- packages/types/generate-schemas.ts | 78 + packages/types/package.json | 11 +- packages/types/src/index.ts | 3 +- packages/types/src/models-extra/report.ts | 69 - packages/types/src/schemas/index.ts | 6 + packages/types/src/schemas/schemas.ts | 3030 +++++++++++++++++ packages/types/src/types/index.ts | 8 + .../src/{ => types}/models-extra/index.ts | 0 .../types/src/types/models-extra/report.ts | 38 + packages/types/src/{ => types}/models.ts | 11 +- .../requests/MeditrakSurveyResponseRequest.ts | 74 + packages/types/src/types/requests/index.ts | 7 + packages/types/tsconfig-build.json | 3 +- scripts/bash/getInternalDependencies.sh | 2 +- tupaia-packages.code-workspace | 4 + yarn.lock | 121 +- 29 files changed, 3470 insertions(+), 86 deletions(-) create mode 100644 packages/tsutils/src/validation/ajv/customFormats/IdFormat.ts create mode 100644 packages/tsutils/src/validation/ajv/customFormats/index.ts create mode 100644 packages/tsutils/src/validation/ajv/getAjv.ts create mode 100644 packages/tsutils/src/validation/ajv/index.ts rename packages/tsutils/src/validation/{ => yup}/DescribableLazy.ts (100%) rename packages/tsutils/src/validation/{yup.ts => yup/index.ts} (100%) rename packages/types/config/{ => models}/config.json (87%) rename packages/types/config/{ => models}/template.handlebars (84%) create mode 100644 packages/types/config/schemas/config.json rename packages/types/{generate.ts => generate-models.ts} (94%) create mode 100644 packages/types/generate-schemas.ts delete mode 100644 packages/types/src/models-extra/report.ts create mode 100644 packages/types/src/schemas/index.ts create mode 100644 packages/types/src/schemas/schemas.ts create mode 100644 packages/types/src/types/index.ts rename packages/types/src/{ => types}/models-extra/index.ts (100%) create mode 100644 packages/types/src/types/models-extra/report.ts rename packages/types/src/{ => types}/models.ts (98%) create mode 100644 packages/types/src/types/requests/MeditrakSurveyResponseRequest.ts create mode 100644 packages/types/src/types/requests/index.ts diff --git a/packages/tsutils/package.json b/packages/tsutils/package.json index 06b4cb066a..79a9fef482 100644 --- a/packages/tsutils/package.json +++ b/packages/tsutils/package.json @@ -21,6 +21,8 @@ }, "dependencies": { "@tupaia/utils": "1.0.0", + "ajv": "^8.12.0", + "ajv-formats": "v3.0.0-rc.0", "cookie": "^0.5.0", "moment": "^2.24.0", "moment-timezone": "^0.5.25", @@ -28,6 +30,7 @@ "yup": "^0.32.9" }, "devDependencies": { + "@tupaia/types": "1.0.0", "@types/moment-timezone": "0.5.13" } } diff --git a/packages/tsutils/src/validation/ajv/customFormats/IdFormat.ts b/packages/tsutils/src/validation/ajv/customFormats/IdFormat.ts new file mode 100644 index 0000000000..5b016854ef --- /dev/null +++ b/packages/tsutils/src/validation/ajv/customFormats/IdFormat.ts @@ -0,0 +1,20 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import { takesIdForm } from '@tupaia/utils'; + +export const IdFormat = { + name: 'id', + config: { + validate: (data: string) => { + try { + takesIdForm(data); + return true; + } catch (e) { + return false; + } + }, + }, +}; diff --git a/packages/tsutils/src/validation/ajv/customFormats/index.ts b/packages/tsutils/src/validation/ajv/customFormats/index.ts new file mode 100644 index 0000000000..cf0183965a --- /dev/null +++ b/packages/tsutils/src/validation/ajv/customFormats/index.ts @@ -0,0 +1,6 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +export { IdFormat } from './IdFormat'; diff --git a/packages/tsutils/src/validation/ajv/getAjv.ts b/packages/tsutils/src/validation/ajv/getAjv.ts new file mode 100644 index 0000000000..130b6a6c64 --- /dev/null +++ b/packages/tsutils/src/validation/ajv/getAjv.ts @@ -0,0 +1,35 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2022 Beyond Essential Systems Pty Ltd + */ + +import { constructRecordExistsWithId } from '@tupaia/utils'; +import Ajv from 'ajv'; +import addFormats from 'ajv-formats'; + +import { IdFormat } from './customFormats'; + +type DatabaseModels = { entity: any; question: any; optionSet: any }; + +export const getAjv = (models: DatabaseModels) => { + const ajv = new Ajv(); + addFormats(ajv); + + ajv.addFormat(IdFormat.name, IdFormat.config); + + ajv.addKeyword({ + keyword: 'checkIdExists', + async: true, + validate: async (schema: { table: keyof DatabaseModels }, data: string) => { + try { + const validateFunc = constructRecordExistsWithId(models[schema.table], schema.table); + await validateFunc(data); + } catch (e) { + return false; + } + return true; + }, + }); + + return ajv; +}; diff --git a/packages/tsutils/src/validation/ajv/index.ts b/packages/tsutils/src/validation/ajv/index.ts new file mode 100644 index 0000000000..7f8fd28a13 --- /dev/null +++ b/packages/tsutils/src/validation/ajv/index.ts @@ -0,0 +1,6 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +export { getAjv } from './getAjv'; diff --git a/packages/tsutils/src/validation/index.ts b/packages/tsutils/src/validation/index.ts index fad2d9683a..71673a7108 100644 --- a/packages/tsutils/src/validation/index.ts +++ b/packages/tsutils/src/validation/index.ts @@ -4,3 +4,4 @@ */ export * from './yup'; +export * from './ajv'; diff --git a/packages/tsutils/src/validation/DescribableLazy.ts b/packages/tsutils/src/validation/yup/DescribableLazy.ts similarity index 100% rename from packages/tsutils/src/validation/DescribableLazy.ts rename to packages/tsutils/src/validation/yup/DescribableLazy.ts diff --git a/packages/tsutils/src/validation/yup.ts b/packages/tsutils/src/validation/yup/index.ts similarity index 100% rename from packages/tsutils/src/validation/yup.ts rename to packages/tsutils/src/validation/yup/index.ts diff --git a/packages/types/README.md b/packages/types/README.md index 2b4c3ed167..c12cf6ac92 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -1,6 +1,6 @@ ## Type definitions for Tupaia objects -Models are generated from the db schema, run `yarn generate` to update this after changes to the db schema. +Models are generated from the db schema, run `yarn generate:models` to update this after changes to the db schema. ### Configuring diff --git a/packages/types/config/config.json b/packages/types/config/models/config.json similarity index 87% rename from packages/types/config/config.json rename to packages/types/config/models/config.json index 40df0d109c..cea1ce466e 100644 --- a/packages/types/config/config.json +++ b/packages/types/config/models/config.json @@ -1,5 +1,5 @@ { - "filename": "src/models.ts", + "filename": "src/types/models.ts", "tableNameCasing": "pascal", "interfaceNameFormat": "${table}", "excludedTables": [ @@ -20,7 +20,7 @@ "typeOverrides": { "public.report.config": "ReportConfig" }, - "template": "config/template.handlebars", + "template": "config/models/template.handlebars", "custom": { "imports": { "ReportConfig": "./models-extra" diff --git a/packages/types/config/template.handlebars b/packages/types/config/models/template.handlebars similarity index 84% rename from packages/types/config/template.handlebars rename to packages/types/config/models/template.handlebars index ab45bf3548..ef73585137 100644 --- a/packages/types/config/template.handlebars +++ b/packages/types/config/models/template.handlebars @@ -26,9 +26,14 @@ export enum {{convertedName}} { {{/each}} {{/inline}} /* -* This file was generated by a tool.x -* Rerun sql-ts to regenerate this file. -*/ + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + * + */ +/* + * This file was generated by a tool. + * Rerun generate:models to recreate this file. + */ {{#each custom.imports as |import|}} import { {{@key}} } from '{{this}}'; {{/each}} diff --git a/packages/types/config/schemas/config.json b/packages/types/config/schemas/config.json new file mode 100644 index 0000000000..18aa71031b --- /dev/null +++ b/packages/types/config/schemas/config.json @@ -0,0 +1 @@ +{ "filename": "src/schemas/schemas.ts", "typesPath": "src/types/index.ts" } \ No newline at end of file diff --git a/packages/types/generate.ts b/packages/types/generate-models.ts similarity index 94% rename from packages/types/generate.ts rename to packages/types/generate-models.ts index 930b98fc31..4a10841146 100644 --- a/packages/types/generate.ts +++ b/packages/types/generate-models.ts @@ -9,7 +9,7 @@ import sqlts from '@rmp135/sql-ts'; import Knex from 'knex'; // @ts-ignore -import config from './config/config.json'; +import config from './config/models/config.json'; import * as dotenv from 'dotenv'; import * as fs from 'fs'; diff --git a/packages/types/generate-schemas.ts b/packages/types/generate-schemas.ts new file mode 100644 index 0000000000..92f0806276 --- /dev/null +++ b/packages/types/generate-schemas.ts @@ -0,0 +1,78 @@ +import { resolve } from 'path'; +import * as fs from 'fs'; +import * as TJS from 'typescript-json-schema'; + +// @ts-ignore +import config from './config/schemas/config.json'; + +const customAsyncValidationKeys = ['checkIdExists']; + +const settings: TJS.PartialArgs = { + ref: false, + required: true, + ignoreErrors: true, + validationKeywords: customAsyncValidationKeys, +}; + +interface SchemaWithProperties { + $async?: boolean; + properties: { $async?: boolean; [key: string]: any }; +} + +const addAsyncKey = (schema: TJS.Definition) => { + const { properties } = schema; + + if (properties) { + const schemaWithAsyncKey: SchemaWithProperties = { properties: {}, ...schema }; + + for (const [propertyKey, value] of Object.entries(properties)) { + if (typeof value === 'object') { + for (const asyncKey of customAsyncValidationKeys) { + if (JSON.stringify(value).includes(asyncKey)) { + // https://ajv.js.org/guide/async-validation.html + schemaWithAsyncKey.properties[propertyKey] = { $async: true, ...value }; + schemaWithAsyncKey.$async = true; + break; + } + } + } + } + return schemaWithAsyncKey; + } + + return schema; +}; + +const HEADER = `/* + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + * + */ + +/* + * This file was generated by a tool. + * Rerun generate:schemas to regenerate this file. + */ +`; + +const { filename, typesPath } = config as any; + +const program = TJS.getProgramFromFiles([resolve(typesPath)]); +const schemas = TJS.generateSchema(program, '*', settings); + +if (schemas?.definitions) { + fs.writeFileSync(filename, HEADER); + console.log(`Created file: ${filename}`); + + Object.entries(schemas.definitions || {}).forEach(([typeName, schema]) => { + if (typeof schema !== 'boolean') { + const finalisedSchema = `export const ${typeName}Schema = ${JSON.stringify( + addAsyncKey(schema), + null, + '\t', + )} \n\n`; + fs.appendFileSync(filename, finalisedSchema); + console.log(`Added schema: ${typeName}`); + } + }); +} diff --git a/packages/types/package.json b/packages/types/package.json index 1bd8c00bc8..74566609fc 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -9,19 +9,26 @@ "directory": "packages/types" }, "author": "Beyond Essential Systems (https://beyondessential.com.au)", + "main": "dist/index.js", "types": "dist/index.d.ts", + "installConfig": { + "hoistingLimits": "workspaces" + }, "scripts": { "build": "rm -rf dist && npm run --prefix ../../ package:build:ts", "build-dev": "npm run build", "lint": "yarn package:lint:ts", "lint:fix": "yarn lint --fix", "test": "echo \"No tests specified\" && exit 0", - "generate": "ts-node generate.ts && yarn build" + "generate": "yarn generate:models && yarn generate:schemas && yarn build", + "generate:models": "ts-node generate-models.ts", + "generate:schemas": "ts-node generate-schemas.ts" }, "devDependencies": { "@rmp135/sql-ts": "^1.15.1", "dotenv": "^16.0.3", "knex": "^2.3.0", - "ts-node": "^10.9.1" + "ts-node": "^10.9.1", + "typescript-json-schema": "^0.55.0" } } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b9e2a2c7b1..fa13a395ef 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -4,4 +4,5 @@ * */ -export * from './models'; +export * from './types'; +export * from './schemas'; diff --git a/packages/types/src/models-extra/report.ts b/packages/types/src/models-extra/report.ts deleted file mode 100644 index dc1f4b501a..0000000000 --- a/packages/types/src/models-extra/report.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Tupaia - * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd - */ - -type PeriodType = 'day' | 'week' | 'month' | 'quarter' | 'year'; - -export type DateOffset = { - unit: PeriodType; - offset?: number; - modifier?: 'start_of' | 'end_of'; - modifierUnit?: PeriodType; - from?: string; -}; - -export type DateSpecs = string | DateOffset; - -export type PeriodParams = { - period?: string; - startDate?: string; - endDate?: string; -}; - -export type FetchReportQuery = PeriodParams & { - organisationUnitCodes: string[]; - hierarchy: string; -}; - -export type AggregationObject = { - type: string; - config?: Record; -}; - -export type Aggregation = string | AggregationObject; - -type Transform = string | Record; - -type CustomReportConfig = { - customReport: string; -}; - -export type ReportConfig = { - fetch: { - dataElements?: string[]; - dataGroups?: string[]; - aggregations?: Aggregation[]; - startDate?: DateSpecs; - endDate?: DateSpecs; - organisationUnits?: string[]; - }; - transform: Transform[]; - output?: Record; -}; - -export type StandardOrCustomReportConfig = ReportConfig | CustomReportConfig; - -export interface Event { - event: string; - eventDate: string; - orgUnitName: string; - orgUnit: string; - dataValues?: Record; -} - -export interface TransformSchema { - code: string; - alias?: boolean; - string?: Record | null; -} diff --git a/packages/types/src/schemas/index.ts b/packages/types/src/schemas/index.ts new file mode 100644 index 0000000000..1033083483 --- /dev/null +++ b/packages/types/src/schemas/index.ts @@ -0,0 +1,6 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + */ + +export * from './schemas'; diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts new file mode 100644 index 0000000000..fd19d707bd --- /dev/null +++ b/packages/types/src/schemas/schemas.ts @@ -0,0 +1,3030 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + * + */ + +/* + * This file was generated by a tool. + * Rerun generate:schemas to regenerate this file. + */ +export const PeriodTypeSchema = { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" +} + +export const DateOffsetSchema = { + "properties": { + "unit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "offset": { + "type": "number" + }, + "modifier": { + "enum": [ + "end_of", + "start_of" + ], + "type": "string" + }, + "modifierUnit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "from": { + "type": "string" + } + }, + "type": "object", + "required": [ + "unit" + ] +} + +export const DateSpecsSchema = { + "anyOf": [ + { + "type": "object", + "properties": { + "unit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "offset": { + "type": "number" + }, + "modifier": { + "enum": [ + "end_of", + "start_of" + ], + "type": "string" + }, + "modifierUnit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "from": { + "type": "string" + } + }, + "required": [ + "unit" + ] + }, + { + "type": "string" + } + ] +} + +export const AggregationObjectSchema = { + "properties": { + "type": { + "type": "string" + }, + "config": { + "type": "object" + } + }, + "type": "object", + "required": [ + "type" + ] +} + +export const AggregationSchema = { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "config": { + "type": "object" + } + }, + "required": [ + "type" + ] + }, + { + "type": "string" + } + ] +} + +export const TransformSchema = { + "type": [ + "string", + "object" + ] +} + +export const ReportConfigSchema = { + "properties": { + "fetch": { + "type": "object", + "properties": { + "dataElements": { + "type": "array", + "items": { + "type": "string" + } + }, + "dataGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "aggregations": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "config": { + "type": "object" + } + }, + "required": [ + "type" + ] + }, + { + "type": "string" + } + ] + } + }, + "startDate": { + "anyOf": [ + { + "type": "object", + "properties": { + "unit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "offset": { + "type": "number" + }, + "modifier": { + "enum": [ + "end_of", + "start_of" + ], + "type": "string" + }, + "modifierUnit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "from": { + "type": "string" + } + }, + "required": [ + "unit" + ] + }, + { + "type": "string" + } + ] + }, + "endDate": { + "anyOf": [ + { + "type": "object", + "properties": { + "unit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "offset": { + "type": "number" + }, + "modifier": { + "enum": [ + "end_of", + "start_of" + ], + "type": "string" + }, + "modifierUnit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "from": { + "type": "string" + } + }, + "required": [ + "unit" + ] + }, + { + "type": "string" + } + ] + }, + "organisationUnits": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "transform": { + "type": "array", + "items": { + "type": [ + "string", + "object" + ] + } + }, + "output": { + "type": "object" + } + }, + "type": "object", + "required": [ + "fetch", + "transform" + ] +} + +export const AccessRequestSchema = { + "properties": { + "approved": { + "type": "boolean" + }, + "createdTime": { + "type": "string", + "format": "date-time" + }, + "entityId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "note": { + "type": "string" + }, + "permissionGroupId": { + "type": "string" + }, + "processedBy": { + "type": "string" + }, + "processedDate": { + "type": "string", + "format": "date-time" + }, + "projectId": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id" + ] +} + +export const AdminPanelSessionSchema = { + "properties": { + "accessPolicy": {}, + "accessToken": { + "type": "string" + }, + "accessTokenExpiry": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "refreshToken": { + "type": "string" + } + }, + "type": "object", + "required": [ + "accessPolicy", + "accessToken", + "accessTokenExpiry", + "email", + "id", + "refreshToken" + ] +} + +export const AnalyticsSchema = { + "properties": { + "answerEntityMRow": { + "type": "string" + }, + "answerMRow": { + "type": "string" + }, + "dataElementCode": { + "type": "string" + }, + "dataElementMRow": { + "type": "string" + }, + "dataGroupCode": { + "type": "string" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "dayPeriod": { + "type": "string" + }, + "entityCode": { + "type": "string" + }, + "entityMRow": { + "type": "string" + }, + "entityName": { + "type": "string" + }, + "eventId": { + "type": "string" + }, + "monthPeriod": { + "type": "string" + }, + "questionMRow": { + "type": "string" + }, + "surveyMRow": { + "type": "string" + }, + "surveyResponseMRow": { + "type": "string" + }, + "type": { + "type": "string" + }, + "value": { + "type": "string" + }, + "weekPeriod": { + "type": "string" + }, + "yearPeriod": { + "type": "string" + } + }, + "type": "object" +} + +export const AncestorDescendantRelationSchema = { + "properties": { + "ancestorId": { + "type": "string" + }, + "descendantId": { + "type": "string" + }, + "entityHierarchyId": { + "type": "string" + }, + "generationalDistance": { + "type": "number" + }, + "id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "ancestorId", + "descendantId", + "entityHierarchyId", + "generationalDistance", + "id" + ] +} + +export const AnswerSchema = { + "properties": { + "id": { + "type": "string" + }, + "mRow": { + "type": "string" + }, + "questionId": { + "type": "string" + }, + "surveyResponseId": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "questionId", + "surveyResponseId", + "type" + ] +} + +export const ApiClientSchema = { + "properties": { + "id": { + "type": "string" + }, + "secretKeyHash": { + "type": "string" + }, + "userAccountId": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "secretKeyHash", + "username" + ] +} + +export const ApiRequestLogSchema = { + "properties": { + "api": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "id": { + "type": "string" + }, + "metadata": {}, + "method": { + "type": "string" + }, + "query": {}, + "refreshToken": { + "type": "string" + }, + "requestTime": { + "type": "string", + "format": "date-time" + }, + "userId": { + "type": "string" + }, + "version": { + "type": "number" + } + }, + "type": "object", + "required": [ + "api", + "endpoint", + "id", + "version" + ] +} + +export const ClinicSchema = { + "properties": { + "categoryCode": { + "type": "string" + }, + "code": { + "type": "string" + }, + "countryId": { + "type": "string" + }, + "geographicalAreaId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "typeName": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "countryId", + "geographicalAreaId", + "id", + "name" + ] +} + +export const CountrySchema = { + "properties": { + "code": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "id", + "name" + ] +} + +export const DashboardSchema = { + "properties": { + "code": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rootEntityCode": { + "type": "string" + }, + "sortOrder": { + "type": "number" + } + }, + "type": "object", + "required": [ + "code", + "id", + "name", + "rootEntityCode" + ] +} + +export const DashboardItemSchema = { + "properties": { + "code": { + "type": "string" + }, + "config": {}, + "id": { + "type": "string" + }, + "legacy": { + "type": "boolean" + }, + "reportCode": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "id" + ] +} + +export const DashboardRelationSchema = { + "properties": { + "childId": { + "type": "string" + }, + "dashboardId": { + "type": "string" + }, + "entityTypes": {}, + "id": { + "type": "string" + }, + "permissionGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "projectCodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "sortOrder": { + "type": "number" + } + }, + "type": "object", + "required": [ + "childId", + "dashboardId", + "entityTypes", + "id", + "permissionGroups", + "projectCodes" + ] +} + +export const DataElementSchema = { + "properties": { + "code": { + "type": "string" + }, + "config": {}, + "id": { + "type": "string" + }, + "mRow": { + "type": "string" + }, + "permissionGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "serviceType": { + "enum": [ + "data-lake", + "dhis", + "indicator", + "kobo", + "superset", + "tupaia", + "weather" + ], + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "id", + "serviceType" + ] +} + +export const DataElementDataGroupSchema = { + "properties": { + "dataElementId": { + "type": "string" + }, + "dataGroupId": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "dataElementId", + "dataGroupId", + "id" + ] +} + +export const DataElementDataServiceSchema = { + "properties": { + "countryCode": { + "type": "string" + }, + "dataElementCode": { + "type": "string" + }, + "id": { + "type": "string" + }, + "serviceConfig": {}, + "serviceType": { + "enum": [ + "data-lake", + "dhis", + "indicator", + "kobo", + "superset", + "tupaia", + "weather" + ], + "type": "string" + } + }, + "type": "object", + "required": [ + "countryCode", + "dataElementCode", + "id", + "serviceType" + ] +} + +export const DataGroupSchema = { + "properties": { + "code": { + "type": "string" + }, + "config": {}, + "id": { + "type": "string" + }, + "serviceType": { + "enum": [ + "data-lake", + "dhis", + "indicator", + "kobo", + "superset", + "tupaia", + "weather" + ], + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "id", + "serviceType" + ] +} + +export const DataServiceEntitySchema = { + "properties": { + "config": {}, + "entityCode": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "config", + "entityCode", + "id" + ] +} + +export const DataServiceSyncGroupSchema = { + "properties": { + "code": { + "type": "string" + }, + "config": {}, + "dataGroupCode": { + "type": "string" + }, + "id": { + "type": "string" + }, + "serviceType": { + "enum": [ + "data-lake", + "dhis", + "indicator", + "kobo", + "superset", + "tupaia", + "weather" + ], + "type": "string" + }, + "syncCursor": { + "type": "string" + }, + "syncStatus": { + "enum": [ + "ERROR", + "IDLE", + "SYNCING" + ], + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "config", + "dataGroupCode", + "id", + "serviceType" + ] +} + +export const DataTableSchema = { + "properties": { + "code": { + "type": "string" + }, + "config": {}, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "permissionGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string", + "enum": [ + "internal" + ] + } + }, + "type": "object", + "required": [ + "code", + "id", + "type" + ] +} + +export const DhisInstanceSchema = { + "properties": { + "code": { + "type": "string" + }, + "config": {}, + "id": { + "type": "string" + }, + "readonly": { + "type": "boolean" + } + }, + "type": "object", + "required": [ + "code", + "config", + "id", + "readonly" + ] +} + +export const DhisSyncLogSchema = { + "properties": { + "data": { + "type": "string" + }, + "deleted": { + "type": "number" + }, + "dhisReference": { + "type": "string" + }, + "errorList": { + "type": "string" + }, + "id": { + "type": "string" + }, + "ignored": { + "type": "number" + }, + "imported": { + "type": "number" + }, + "recordId": { + "type": "string" + }, + "recordType": { + "type": "string" + }, + "updated": { + "type": "number" + } + }, + "type": "object", + "required": [ + "id", + "recordId", + "recordType" + ] +} + +export const DhisSyncQueueSchema = { + "properties": { + "badRequestCount": { + "type": "number" + }, + "changeTime": { + "type": "number" + }, + "details": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isDeadLetter": { + "type": "boolean" + }, + "isDeleted": { + "type": "boolean" + }, + "priority": { + "type": "number" + }, + "recordId": { + "type": "string" + }, + "recordType": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "recordId", + "recordType", + "type" + ] +} + +export const DisasterSchema = { + "properties": { + "countryCode": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "enum": [ + "cyclone", + "earthquake", + "eruption", + "flood", + "tsunami" + ], + "type": "string" + } + }, + "type": "object", + "required": [ + "countryCode", + "id", + "name", + "type" + ] +} + +export const DisasterEventSchema = { + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "disasterId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "organisationUnitCode": { + "type": "string" + }, + "type": { + "enum": [ + "end", + "resolve", + "start" + ], + "type": "string" + } + }, + "type": "object", + "required": [ + "date", + "disasterId", + "id", + "organisationUnitCode", + "type" + ] +} + +export const EntitySchema = { + "properties": { + "attributes": {}, + "bounds": {}, + "code": { + "type": "string" + }, + "countryCode": { + "type": "string" + }, + "id": { + "type": "string" + }, + "imageUrl": { + "type": "string" + }, + "mRow": { + "type": "string" + }, + "metadata": {}, + "name": { + "type": "string" + }, + "parentId": { + "type": "string" + }, + "point": {}, + "region": {}, + "type": { + "enum": [ + "case", + "case_contact", + "catchment", + "city", + "country", + "disaster", + "district", + "facility", + "fetp_graduate", + "field_station", + "fiji_aspen_facility", + "household", + "incident", + "incident_reported", + "individual", + "larval_habitat", + "local_government", + "medical_area", + "nursing_zone", + "postcode", + "project", + "school", + "sub_catchment", + "sub_district", + "sub_facility", + "village", + "wish_sub_district", + "world" + ], + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "id", + "name" + ] +} + +export const EntityHierarchySchema = { + "properties": { + "canonicalTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "name" + ] +} + +export const EntityRelationSchema = { + "properties": { + "childId": { + "type": "string" + }, + "entityHierarchyId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "parentId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "childId", + "entityHierarchyId", + "id", + "parentId" + ] +} + +export const ErrorLogSchema = { + "properties": { + "apiRequestLogId": { + "type": "string" + }, + "errorTime": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id" + ] +} + +export const ExternalDatabaseConnectionSchema = { + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "permissionGroups": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "type": "object", + "required": [ + "code", + "id", + "name" + ] +} + +export const FeedItemSchema = { + "properties": { + "countryId": { + "type": "string" + }, + "creationDate": { + "type": "string", + "format": "date-time" + }, + "geographicalAreaId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "permissionGroupId": { + "type": "string" + }, + "recordId": { + "type": "string" + }, + "templateVariables": { + "type": "object", + "properties": { + "constructor": { + "$ref": "#/definitions/Function" + } + }, + "required": [ + "constructor" + ] + }, + "type": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id" + ] +} + +export const GeographicalAreaSchema = { + "properties": { + "code": { + "type": "string" + }, + "countryId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "levelCode": { + "type": "string" + }, + "levelName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "countryId", + "id", + "levelCode", + "levelName", + "name" + ] +} + +export const IndicatorSchema = { + "properties": { + "builder": { + "type": "string" + }, + "code": { + "type": "string" + }, + "config": {}, + "id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "builder", + "code", + "id" + ] +} + +export const LegacyReportSchema = { + "properties": { + "code": { + "type": "string" + }, + "dataBuilder": { + "type": "string" + }, + "dataBuilderConfig": {}, + "dataServices": {}, + "id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "id" + ] +} + +export const LesmisSessionSchema = { + "properties": { + "accessPolicy": {}, + "accessToken": { + "type": "string" + }, + "accessTokenExpiry": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "refreshToken": { + "type": "string" + } + }, + "type": "object", + "required": [ + "accessPolicy", + "accessToken", + "accessTokenExpiry", + "email", + "id", + "refreshToken" + ] +} + +export const MapOverlaySchema = { + "properties": { + "code": { + "type": "string" + }, + "config": {}, + "countryCodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "dataServices": {}, + "id": { + "type": "string" + }, + "legacy": { + "type": "boolean" + }, + "linkedMeasures": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "permissionGroup": { + "type": "string" + }, + "projectCodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "reportCode": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "name", + "permissionGroup" + ] +} + +export const MapOverlayGroupSchema = { + "properties": { + "code": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "id", + "name" + ] +} + +export const MapOverlayGroupRelationSchema = { + "properties": { + "childId": { + "type": "string" + }, + "childType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "mapOverlayGroupId": { + "type": "string" + }, + "sortOrder": { + "type": "number" + } + }, + "type": "object", + "required": [ + "childId", + "childType", + "id", + "mapOverlayGroupId" + ] +} + +export const MeditrakDeviceSchema = { + "properties": { + "appVersion": { + "type": "string" + }, + "config": {}, + "id": { + "type": "string" + }, + "installId": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "installId", + "userId" + ] +} + +export const MeditrakSyncQueueSchema = { + "properties": { + "changeTime": { + "type": "number" + }, + "id": { + "type": "string" + }, + "recordId": { + "type": "string" + }, + "recordType": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "recordId", + "recordType", + "type" + ] +} + +export const Ms1SyncLogSchema = { + "properties": { + "count": { + "type": "number" + }, + "data": { + "type": "string" + }, + "endpoint": { + "type": "string" + }, + "errorList": { + "type": "string" + }, + "id": { + "type": "string" + }, + "recordId": { + "type": "string" + }, + "recordType": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "recordId", + "recordType" + ] +} + +export const Ms1SyncQueueSchema = { + "properties": { + "badRequestCount": { + "type": "number" + }, + "changeTime": { + "type": "number" + }, + "details": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isDeadLetter": { + "type": "boolean" + }, + "isDeleted": { + "type": "boolean" + }, + "priority": { + "type": "number" + }, + "recordId": { + "type": "string" + }, + "recordType": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "recordId", + "recordType", + "type" + ] +} + +export const OneTimeLoginSchema = { + "properties": { + "creationDate": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "string" + }, + "token": { + "type": "string" + }, + "useDate": { + "type": "string", + "format": "date-time" + }, + "userId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "token", + "userId" + ] +} + +export const OptionSchema = { + "properties": { + "attributes": {}, + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "optionSetId": { + "type": "string" + }, + "sortOrder": { + "type": "number" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "optionSetId", + "value" + ] +} + +export const OptionSetSchema = { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "name" + ] +} + +export const PermissionGroupSchema = { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parentId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "name" + ] +} + +export const PermissionsBasedMeditrakSyncQueueSchema = { + "properties": { + "changeTime": { + "type": "number" + }, + "countryIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "entityType": { + "enum": [ + "case", + "case_contact", + "catchment", + "city", + "country", + "disaster", + "district", + "facility", + "fetp_graduate", + "field_station", + "fiji_aspen_facility", + "household", + "incident", + "incident_reported", + "individual", + "larval_habitat", + "local_government", + "medical_area", + "nursing_zone", + "postcode", + "project", + "school", + "sub_catchment", + "sub_district", + "sub_facility", + "village", + "wish_sub_district", + "world" + ], + "type": "string" + }, + "id": { + "type": "string" + }, + "permissionGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "recordId": { + "type": "string" + }, + "recordType": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object" +} + +export const ProjectSchema = { + "properties": { + "code": { + "type": "string" + }, + "config": {}, + "dashboardGroupName": { + "type": "string" + }, + "defaultMeasure": { + "type": "string" + }, + "description": { + "type": "string" + }, + "entityHierarchyId": { + "type": "string" + }, + "entityId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "imageUrl": { + "type": "string" + }, + "logoUrl": { + "type": "string" + }, + "permissionGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "sortOrder": { + "type": "number" + } + }, + "type": "object", + "required": [ + "code", + "id" + ] +} + +export const PsssSessionSchema = { + "properties": { + "accessPolicy": {}, + "accessToken": { + "type": "string" + }, + "accessTokenExpiry": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "refreshToken": { + "type": "string" + } + }, + "type": "object", + "required": [ + "accessPolicy", + "accessToken", + "accessTokenExpiry", + "email", + "id", + "refreshToken" + ] +} + +export const QuestionSchema = { + "properties": { + "code": { + "type": "string" + }, + "dataElementId": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "hook": { + "type": "string" + }, + "id": { + "type": "string" + }, + "mRow": { + "type": "string" + }, + "name": { + "type": "string" + }, + "optionSetId": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "string" + } + }, + "text": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "text", + "type" + ] +} + +export const RefreshTokenSchema = { + "properties": { + "device": { + "type": "string" + }, + "expiry": { + "type": "number" + }, + "id": { + "type": "string" + }, + "meditrakDeviceId": { + "type": "string" + }, + "token": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "token", + "userId" + ] +} + +export const ReportSchema = { + "properties": { + "code": { + "type": "string" + }, + "config": { + "type": "object", + "properties": { + "fetch": { + "type": "object", + "properties": { + "dataElements": { + "type": "array", + "items": { + "type": "string" + } + }, + "dataGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "aggregations": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "config": { + "type": "object" + } + }, + "required": [ + "type" + ] + }, + { + "type": "string" + } + ] + } + }, + "startDate": { + "anyOf": [ + { + "type": "object", + "properties": { + "unit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "offset": { + "type": "number" + }, + "modifier": { + "enum": [ + "end_of", + "start_of" + ], + "type": "string" + }, + "modifierUnit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "from": { + "type": "string" + } + }, + "required": [ + "unit" + ] + }, + { + "type": "string" + } + ] + }, + "endDate": { + "anyOf": [ + { + "type": "object", + "properties": { + "unit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "offset": { + "type": "number" + }, + "modifier": { + "enum": [ + "end_of", + "start_of" + ], + "type": "string" + }, + "modifierUnit": { + "description": "Tupaia\nCopyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd", + "enum": [ + "day", + "month", + "quarter", + "week", + "year" + ], + "type": "string" + }, + "from": { + "type": "string" + } + }, + "required": [ + "unit" + ] + }, + { + "type": "string" + } + ] + }, + "organisationUnits": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "transform": { + "type": "array", + "items": { + "type": [ + "string", + "object" + ] + } + }, + "output": { + "type": "object" + } + }, + "required": [ + "fetch", + "transform" + ] + }, + "id": { + "type": "string" + }, + "permissionGroupId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "config", + "id", + "permissionGroupId" + ] +} + +export const SettingSchema = { + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "key" + ] +} + +export const SupersetInstanceSchema = { + "properties": { + "code": { + "type": "string" + }, + "config": {}, + "id": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "config", + "id" + ] +} + +export const SurveySchema = { + "properties": { + "canRepeat": { + "type": "boolean" + }, + "code": { + "type": "string" + }, + "countryIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "dataGroupId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "integrationMetadata": {}, + "mRow": { + "type": "string" + }, + "name": { + "type": "string" + }, + "periodGranularity": { + "enum": [ + "daily", + "monthly", + "quarterly", + "weekly", + "yearly" + ], + "type": "string" + }, + "permissionGroupId": { + "type": "string" + }, + "requiresApproval": { + "type": "boolean" + }, + "surveyGroupId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "id", + "name" + ] +} + +export const SurveyGroupSchema = { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "name" + ] +} + +export const SurveyResponseSchema = { + "properties": { + "approvalStatus": { + "enum": [ + "approved", + "not_required", + "pending", + "rejected" + ], + "type": "string" + }, + "assessorName": { + "type": "string" + }, + "dataTime": { + "type": "string", + "format": "date-time" + }, + "endTime": { + "type": "string", + "format": "date-time" + }, + "entityId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "mRow": { + "type": "string" + }, + "metadata": { + "type": "string" + }, + "outdated": { + "type": "boolean" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "surveyId": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "assessorName", + "endTime", + "entityId", + "id", + "startTime", + "surveyId", + "userId" + ] +} + +export const SurveyResponseCommentSchema = { + "properties": { + "commentId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "surveyResponseId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "commentId", + "id", + "surveyResponseId" + ] +} + +export const SurveyScreenSchema = { + "properties": { + "id": { + "type": "string" + }, + "screenNumber": { + "type": "number" + }, + "surveyId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "screenNumber", + "surveyId" + ] +} + +export const SurveyScreenComponentSchema = { + "properties": { + "answersEnablingFollowUp": { + "type": "array", + "items": { + "type": "string" + } + }, + "componentNumber": { + "type": "number" + }, + "config": { + "type": "string" + }, + "detailLabel": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isFollowUp": { + "type": "boolean" + }, + "questionId": { + "type": "string" + }, + "questionLabel": { + "type": "string" + }, + "screenId": { + "type": "string" + }, + "validationCriteria": { + "type": "string" + }, + "visibilityCriteria": { + "type": "string" + } + }, + "type": "object", + "required": [ + "componentNumber", + "id", + "questionId", + "screenId" + ] +} + +export const SyncGroupLogSchema = { + "properties": { + "id": { + "type": "string" + }, + "logMessage": { + "type": "string" + }, + "serviceType": { + "enum": [ + "data-lake", + "dhis", + "indicator", + "kobo", + "superset", + "tupaia", + "weather" + ], + "type": "string" + }, + "syncGroupCode": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + "type": "object", + "required": [ + "id", + "logMessage", + "serviceType", + "syncGroupCode" + ] +} + +export const UserAccountSchema = { + "properties": { + "creationDate": { + "type": "string", + "format": "date-time" + }, + "email": { + "type": "string" + }, + "employer": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "mobileNumber": { + "type": "string" + }, + "passwordHash": { + "type": "string" + }, + "passwordSalt": { + "type": "string" + }, + "position": { + "type": "string" + }, + "primaryPlatform": { + "enum": [ + "lesmis", + "tupaia" + ], + "type": "string" + }, + "profileImage": { + "type": "string" + }, + "verifiedEmail": { + "enum": [ + "new_user", + "unverified", + "verified" + ], + "type": "string" + } + }, + "type": "object", + "required": [ + "email", + "id", + "passwordHash", + "passwordSalt" + ] +} + +export const UserEntityPermissionSchema = { + "properties": { + "entityId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "permissionGroupId": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id" + ] +} + +export const UserFavouriteDashboardItemSchema = { + "properties": { + "dashboardItemId": { + "type": "string" + }, + "id": { + "type": "string" + }, + "userId": { + "type": "string" + } + }, + "type": "object", + "required": [ + "dashboardItemId", + "id", + "userId" + ] +} + +export const UserSessionSchema = { + "properties": { + "accessTokenExpiry": { + "type": "string" + }, + "accessPolicy": {}, + "accessToken": { + "type": "string" + }, + "id": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "userName": { + "type": "string" + } + }, + "type": "object", + "required": [ + "id", + "refreshToken", + "userName" + ] +} + +export const VerifiedEmailSchema = { + "enum": [ + "new_user", + "unverified", + "verified" + ], + "type": "string" +} + +export const SyncGroupSyncStatusSchema = { + "enum": [ + "ERROR", + "IDLE", + "SYNCING" + ], + "type": "string" +} + +export const ServiceTypeSchema = { + "enum": [ + "data-lake", + "dhis", + "indicator", + "kobo", + "superset", + "tupaia", + "weather" + ], + "type": "string" +} + +export const PrimaryPlatformSchema = { + "enum": [ + "lesmis", + "tupaia" + ], + "type": "string" +} + +export const PeriodGranularitySchema = { + "enum": [ + "daily", + "monthly", + "quarterly", + "weekly", + "yearly" + ], + "type": "string" +} + +export const EntityTypeSchema = { + "enum": [ + "case", + "case_contact", + "catchment", + "city", + "country", + "disaster", + "district", + "facility", + "fetp_graduate", + "field_station", + "fiji_aspen_facility", + "household", + "incident", + "incident_reported", + "individual", + "larval_habitat", + "local_government", + "medical_area", + "nursing_zone", + "postcode", + "project", + "school", + "sub_catchment", + "sub_district", + "sub_facility", + "village", + "wish_sub_district", + "world" + ], + "type": "string" +} + +export const DisasterTypeSchema = { + "enum": [ + "cyclone", + "earthquake", + "eruption", + "flood", + "tsunami" + ], + "type": "string" +} + +export const DisasterEventTypeSchema = { + "enum": [ + "end", + "resolve", + "start" + ], + "type": "string" +} + +export const DataTableTypeSchema = { + "type": "string", + "enum": [ + "internal" + ] +} + +export const DataSourceTypeSchema = { + "enum": [ + "dataElement", + "dataGroup" + ], + "type": "string" +} + +export const ApprovalStatusSchema = { + "enum": [ + "approved", + "not_required", + "pending", + "rejected" + ], + "type": "string" +} + +export const IdSchema = { + "type": "string" +} + +export const AnswerTypeSchema = { + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "body": { + "type": "string" + }, + "question_id": { + "$async": true, + "checkIdExists": { + "table": "question" + }, + "type": "string" + } + }, + "type": "object", + "required": [ + "body", + "id", + "question_id", + "type" + ], + "$async": true +} + +export const EntityCreatedSchema = { + "properties": { + "id": { + "type": "string" + }, + "code": { + "type": "string" + }, + "parent_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "enum": [ + "case", + "case_contact", + "catchment", + "city", + "country", + "disaster", + "district", + "facility", + "fetp_graduate", + "field_station", + "fiji_aspen_facility", + "household", + "incident", + "incident_reported", + "individual", + "larval_habitat", + "local_government", + "medical_area", + "nursing_zone", + "postcode", + "project", + "school", + "sub_catchment", + "sub_district", + "sub_facility", + "village", + "wish_sub_district", + "world" + ], + "type": "string" + }, + "country_code": { + "type": "string" + } + }, + "type": "object", + "required": [ + "code", + "country_code", + "id", + "name", + "parent_id", + "type" + ] +} + +export const OptionCreatedSchema = { + "properties": { + "id": { + "type": "string" + }, + "value": { + "type": "number" + }, + "option_set_id": { + "$async": true, + "checkIdExists": { + "table": "optionSet" + }, + "type": "string" + }, + "sort_order": { + "type": "number" + } + }, + "type": "object", + "required": [ + "id", + "option_set_id", + "sort_order", + "value" + ], + "$async": true +} + +export const MeditrakSurveyResponseRequestSchema = { + "properties": { + "id": { + "type": "string" + }, + "timestamp": { + "format": "iso-date-time", + "type": "string" + }, + "survey_id": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "answers": { + "$async": true, + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "body": { + "type": "string" + }, + "question_id": { + "checkIdExists": { + "table": "question" + }, + "type": "string" + } + }, + "required": [ + "body", + "id", + "question_id", + "type" + ] + } + }, + "clinic_id": { + "type": "string" + }, + "entity_id": { + "type": "string" + }, + "start_time": { + "format": "iso-date-time", + "type": "string" + }, + "end_time": { + "format": "iso-date-time", + "type": "string" + }, + "data_time": { + "format": "iso-date-time", + "type": "string" + }, + "approval_status": { + "type": "string" + }, + "entities_created": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "code": { + "type": "string" + }, + "parent_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "enum": [ + "case", + "case_contact", + "catchment", + "city", + "country", + "disaster", + "district", + "facility", + "fetp_graduate", + "field_station", + "fiji_aspen_facility", + "household", + "incident", + "incident_reported", + "individual", + "larval_habitat", + "local_government", + "medical_area", + "nursing_zone", + "postcode", + "project", + "school", + "sub_catchment", + "sub_district", + "sub_facility", + "village", + "wish_sub_district", + "world" + ], + "type": "string" + }, + "country_code": { + "type": "string" + } + }, + "required": [ + "code", + "country_code", + "id", + "name", + "parent_id", + "type" + ] + } + }, + "options_created": { + "$async": true, + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "value": { + "type": "number" + }, + "option_set_id": { + "checkIdExists": { + "table": "optionSet" + }, + "type": "string" + }, + "sort_order": { + "type": "number" + } + }, + "required": [ + "id", + "option_set_id", + "sort_order", + "value" + ] + } + }, + "submission_time": { + "description": "only used in meditrak-app-server, v1.7.87 to v1.9.110 (inclusive) uses submission_time", + "type": "string" + }, + "timezone": { + "type": "string" + } + }, + "type": "object", + "required": [ + "answers", + "id", + "survey_id", + "timestamp", + "user_id" + ], + "$async": true +} + diff --git a/packages/types/src/types/index.ts b/packages/types/src/types/index.ts new file mode 100644 index 0000000000..2f5c3f5053 --- /dev/null +++ b/packages/types/src/types/index.ts @@ -0,0 +1,8 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + * + */ + +export * from './models'; +export * from './requests'; diff --git a/packages/types/src/models-extra/index.ts b/packages/types/src/types/models-extra/index.ts similarity index 100% rename from packages/types/src/models-extra/index.ts rename to packages/types/src/types/models-extra/index.ts diff --git a/packages/types/src/types/models-extra/report.ts b/packages/types/src/types/models-extra/report.ts new file mode 100644 index 0000000000..29ea142da5 --- /dev/null +++ b/packages/types/src/types/models-extra/report.ts @@ -0,0 +1,38 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + */ + +type PeriodType = 'day' | 'week' | 'month' | 'quarter' | 'year'; + +type DateOffset = { + unit: PeriodType; + offset?: number; + modifier?: 'start_of' | 'end_of'; + modifierUnit?: PeriodType; + from?: string; +}; + +type DateSpecs = string | DateOffset; + +type AggregationObject = { + type: string; + config?: Record; +}; + +type Aggregation = string | AggregationObject; + +type Transform = string | Record; + +export type ReportConfig = { + fetch: { + dataElements?: string[]; + dataGroups?: string[]; + aggregations?: Aggregation[]; + startDate?: DateSpecs; + endDate?: DateSpecs; + organisationUnits?: string[]; + }; + transform: Transform[]; + output?: Record; +}; diff --git a/packages/types/src/models.ts b/packages/types/src/types/models.ts similarity index 98% rename from packages/types/src/models.ts rename to packages/types/src/types/models.ts index 6e3aaf7240..294a9862ab 100644 --- a/packages/types/src/models.ts +++ b/packages/types/src/types/models.ts @@ -1,7 +1,12 @@ /* -* This file was generated by a tool.x -* Rerun sql-ts to regenerate this file. -*/ + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + * + */ +/* + * This file was generated by a tool. + * Rerun generate:models to recreate this file. + */ import { ReportConfig } from './models-extra'; export interface AccessRequest { diff --git a/packages/types/src/types/requests/MeditrakSurveyResponseRequest.ts b/packages/types/src/types/requests/MeditrakSurveyResponseRequest.ts new file mode 100644 index 0000000000..92f7adf58d --- /dev/null +++ b/packages/types/src/types/requests/MeditrakSurveyResponseRequest.ts @@ -0,0 +1,74 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + * + */ + +/** + * @format id + */ +import { EntityType } from '../models'; + +type Id = string; + +type AnswerType = { + id: Id; + type: string; + body: string; + /** + * @checkIdExists { "table": "question" } + */ + question_id: string; +}; + +type EntityCreated = { + id: Id; + code: string; + parent_id: Id; + name: string; + type: EntityType; + country_code: string; +}; + +type OptionCreated = { + id: Id; + value: number; + /** + * @checkIdExists { "table": "optionSet" } + */ + option_set_id: string; + sort_order: number; +}; + +export interface MeditrakSurveyResponseRequest { + id: Id; + /** + * @format iso-date-time + */ + timestamp: string; + survey_id: Id; + user_id: Id; + answers: AnswerType[]; + clinic_id?: Id; + entity_id?: Id; + /** + * @format iso-date-time + */ + start_time?: string; + /** + * @format iso-date-time + */ + end_time?: string; + /** + * @format iso-date-time + */ + data_time?: string; + approval_status?: string; + entities_created?: EntityCreated[]; + options_created?: OptionCreated[]; + /** + * @description only used in meditrak-app-server, v1.7.87 to v1.9.110 (inclusive) uses submission_time + */ + submission_time?: string; + timezone?: string; +} diff --git a/packages/types/src/types/requests/index.ts b/packages/types/src/types/requests/index.ts new file mode 100644 index 0000000000..9ff6479fc7 --- /dev/null +++ b/packages/types/src/types/requests/index.ts @@ -0,0 +1,7 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd + * + */ + +export { MeditrakSurveyResponseRequest } from './MeditrakSurveyResponseRequest'; diff --git a/packages/types/tsconfig-build.json b/packages/types/tsconfig-build.json index 6e9d456fed..f10fad37d9 100644 --- a/packages/types/tsconfig-build.json +++ b/packages/types/tsconfig-build.json @@ -4,8 +4,7 @@ "compilerOptions": { "outDir": "dist", "declaration": true, - "noEmit": false, - "emitDeclarationOnly": true + "noEmit": false }, "include": ["src/**/*"], diff --git a/scripts/bash/getInternalDependencies.sh b/scripts/bash/getInternalDependencies.sh index 6a9b9a1a7d..96307dfff9 100755 --- a/scripts/bash/getInternalDependencies.sh +++ b/scripts/bash/getInternalDependencies.sh @@ -12,7 +12,7 @@ dependencies_already_visited=($@) # if no package.json entrypoint is specified, just return all internal dependencies if [ -z ${package_path} ]; then - echo "types ui-components" "tsutils" "utils" "access-policy" "admin-panel" "aggregator" "api-client" "auth" "database" "data-api" "data-broker" "data-lake-api" "dhis-api" "expression-parser" "indicators" "weather-api" "server-boilerplate" "kobo-api" "superset-api" + echo "types ui-components" "utils" "tsutils" "access-policy" "admin-panel" "aggregator" "api-client" "auth" "database" "data-api" "data-broker" "data-lake-api" "dhis-api" "expression-parser" "indicators" "weather-api" "server-boilerplate" "kobo-api" "superset-api" exit 0 fi diff --git a/tupaia-packages.code-workspace b/tupaia-packages.code-workspace index 264829125c..ff808846e3 100644 --- a/tupaia-packages.code-workspace +++ b/tupaia-packages.code-workspace @@ -124,6 +124,10 @@ "name": "tsutils", "path": "packages/tsutils" }, + { + "name": "types", + "path": "packages/types" + }, { "name": "weather-api", "path": "packages/weather-api" diff --git a/yarn.lock b/yarn.lock index 5e4d626f21..3d81460a3d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5699,8 +5699,11 @@ __metadata: version: 0.0.0-use.local resolution: "@tupaia/tsutils@workspace:packages/tsutils" dependencies: + "@tupaia/types": 1.0.0 "@tupaia/utils": 1.0.0 "@types/moment-timezone": 0.5.13 + ajv: ^8.12.0 + ajv-formats: v3.0.0-rc.0 cookie: ^0.5.0 moment: ^2.24.0 moment-timezone: ^0.5.25 @@ -5717,6 +5720,7 @@ __metadata: dotenv: ^16.0.3 knex: ^2.3.0 ts-node: ^10.9.1 + typescript-json-schema: ^0.55.0 languageName: unknown linkType: soft @@ -6304,6 +6308,13 @@ __metadata: languageName: node linkType: hard +"@types/json-schema@npm:^7.0.9": + version: 7.0.11 + resolution: "@types/json-schema@npm:7.0.11" + checksum: 527bddfe62db9012fccd7627794bd4c71beb77601861055d87e3ee464f2217c85fca7a4b56ae677478367bbd248dbde13553312b7d4dbc702a2f2bbf60c4018d + languageName: node + linkType: hard + "@types/json5@npm:^0.0.29": version: 0.0.29 resolution: "@types/json5@npm:0.0.29" @@ -6490,6 +6501,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^16.9.2": + version: 16.18.11 + resolution: "@types/node@npm:16.18.11" + checksum: 2a3b1da13063debe6e26f732defb5f03ef4ef732c3e08daba838d8850433bd00e537ce1a97ce9bcfc4b15db5218d701d1265fae94e0d6926906bec157e6b46e0 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.0 resolution: "@types/normalize-package-data@npm:2.4.0" @@ -7739,6 +7757,20 @@ __metadata: languageName: node linkType: hard +"ajv-formats@npm:v3.0.0-rc.0": + version: 3.0.0-rc.0 + resolution: "ajv-formats@npm:3.0.0-rc.0" + dependencies: + ajv: ^8.0.0 + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + checksum: a07d8c094bb4e9f1752173fc9c919a1e0b2e910fe8c1fc79c8903013f192f23dec334b3f2d76a90327edca2f1f300bf49adaf1b93fdafb8fd11d2a61a0ab556c + languageName: node + linkType: hard + "ajv-keywords@npm:^3.1.0, ajv-keywords@npm:^3.4.1": version: 3.4.1 resolution: "ajv-keywords@npm:3.4.1" @@ -7817,6 +7849,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^8.0.0, ajv@npm:^8.12.0": + version: 8.12.0 + resolution: "ajv@npm:8.12.0" + dependencies: + fast-deep-equal: ^3.1.1 + json-schema-traverse: ^1.0.0 + require-from-string: ^2.0.2 + uri-js: ^4.2.2 + checksum: 4dc13714e316e67537c8b31bc063f99a1d9d9a497eb4bbd55191ac0dcd5e4985bbb71570352ad6f1e76684fb6d790928f96ba3b2d4fd6e10024be9612fe3f001 + languageName: node + linkType: hard + "ajv@npm:^8.0.1": version: 8.6.3 resolution: "ajv@npm:8.6.3" @@ -16983,6 +17027,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:^7.1.7": + version: 7.2.3 + resolution: "glob@npm:7.2.3" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^3.1.1 + once: ^1.3.0 + path-is-absolute: ^1.0.0 + checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 + languageName: node + linkType: hard + "glob@npm:^8.0.1": version: 8.0.3 resolution: "glob@npm:8.0.3" @@ -24148,6 +24206,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^3.1.1": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: ^1.1.7 + checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a + languageName: node + linkType: hard + "minimatch@npm:^5.0.1": version: 5.1.0 resolution: "minimatch@npm:5.1.0" @@ -26431,6 +26498,13 @@ __metadata: languageName: node linkType: hard +"path-equal@npm:^1.1.2": + version: 1.2.5 + resolution: "path-equal@npm:1.2.5" + checksum: 2bef7bcb98c7ae371c52c1562b2fc515bfd03bc1a5571df9a8591038db8d742ba2d1ff39aa5130853e6afb69e773ccba5095f54d2e6d17422ca03ef9047992d7 + languageName: node + linkType: hard + "path-exists@npm:^2.0.0": version: 2.1.0 resolution: "path-exists@npm:2.1.0" @@ -31285,6 +31359,13 @@ __metadata: languageName: node linkType: hard +"safe-stable-stringify@npm:^2.2.0": + version: 2.4.2 + resolution: "safe-stable-stringify@npm:2.4.2" + checksum: 0324ba2e40f78cae63e31a02b1c9bdf1b786621f9e8760845608eb9e81aef401944ac2078e5c9c1533cf516aea34d08fa8052ca853637ced84b791caaf1e394e + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.0.2, safer-buffer@npm:^2.1.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -34433,6 +34514,24 @@ __metadata: languageName: node linkType: hard +"typescript-json-schema@npm:^0.55.0": + version: 0.55.0 + resolution: "typescript-json-schema@npm:0.55.0" + dependencies: + "@types/json-schema": ^7.0.9 + "@types/node": ^16.9.2 + glob: ^7.1.7 + path-equal: ^1.1.2 + safe-stable-stringify: ^2.2.0 + ts-node: ^10.9.1 + typescript: ~4.8.2 + yargs: ^17.1.1 + bin: + typescript-json-schema: bin/typescript-json-schema + checksum: 4188e9d4cc1f1fc3201e2aa06b8d87d35695b0eaf049ff9d2cd3027d87406efc6978c4f90c2bf06f4fcd8d6dd40ceb65e744dc3cf9b412e1f74b8eb5bae96a05 + languageName: node + linkType: hard + "typescript-logic@npm:^0.0.0": version: 0.0.0 resolution: "typescript-logic@npm:0.0.0" @@ -34459,6 +34558,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:~4.8.2": + version: 4.8.4 + resolution: "typescript@npm:4.8.4" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 + languageName: node + linkType: hard + "typescript@patch:typescript@^4.2.0#~builtin": version: 4.2.3 resolution: "typescript@patch:typescript@npm%3A4.2.3#~builtin::version=4.2.3&hash=7ad353" @@ -34469,6 +34578,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@~4.8.2#~builtin": + version: 4.8.4 + resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=7ad353" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 563a0ef47abae6df27a9a3ab38f75fc681f633ccf1a3502b1108e252e187787893de689220f4544aaf95a371a4eb3141e4a337deb9895de5ac3c1ca76430e5f0 + languageName: node + linkType: hard + "typical@npm:^2.6.0, typical@npm:^2.6.1": version: 2.6.1 resolution: "typical@npm:2.6.1" @@ -36636,7 +36755,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1": +"yargs@npm:^17.1.1, yargs@npm:^17.3.1": version: 17.6.2 resolution: "yargs@npm:17.6.2" dependencies: From 1f682f57c33c78bfa9e6a8685edac5ef93c0aa6a Mon Sep 17 00:00:00 2001 From: Chris Pollard Date: Wed, 18 Jan 2023 14:45:54 +1100 Subject: [PATCH 6/9] RN-205: Build With Test Data w/out Orgunit or Hierarchy (#4278) * RN-205: Build With Test Data w/out Orgunit or Hierarchy * Fix merge issue Co-authored-by: Andrew --- .../src/__tests__/reportBuilder.test.ts | 96 +++++++++++++++++++ .../src/reportBuilder/reportBuilder.ts | 20 +++- 2 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 packages/report-server/src/__tests__/reportBuilder.test.ts diff --git a/packages/report-server/src/__tests__/reportBuilder.test.ts b/packages/report-server/src/__tests__/reportBuilder.test.ts new file mode 100644 index 0000000000..bf003779ee --- /dev/null +++ b/packages/report-server/src/__tests__/reportBuilder.test.ts @@ -0,0 +1,96 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2021 Beyond Essential Systems Pty Ltd + */ + +import { Aggregator } from '@tupaia/aggregator'; +import { AccessPolicy } from '@tupaia/access-policy'; +import { MockTupaiaApiClient, MockEntityApi } from '@tupaia/api-client'; +import { ReportBuilder } from '../reportBuilder'; +import { ReqContext } from '../reportBuilder/context/buildContext'; +import { ReportServerAggregator } from '../aggregator'; + +describe('ReportBuilder', () => { + const dataBroker = { context: {} }; + const aggregator = new Aggregator(dataBroker); + const HIERARCHY = 'test_hierarchy'; + const ENTITIES = { + test_hierarchy: [ + { id: 'ouId1', code: 'AU', name: 'Australia', type: 'country', attributes: {} }, + { id: 'ouId2', code: 'FJ', name: 'Fiji', type: 'country', attributes: {} }, + { id: 'ouId3', code: 'KI', name: 'Kiribati', type: 'country', attributes: {} }, + { id: 'ouId4', code: 'TO', name: 'Tonga', type: 'country', attributes: {} }, + { + id: 'ouId5', + code: 'TO_Facility1', + name: 'Tonga Facility 1', + type: 'facility', + attributes: { x: 1 }, + }, + { + id: 'ouId6', + code: 'TO_Facility2', + name: 'Tonga Facility 2', + type: 'facility', + attributes: {}, + }, + { id: 'ouId7', code: 'FJ_Facility', name: 'Fiji Facility', type: 'facility', attributes: {} }, + ], + }; + + const RELATIONS = { + test_hierarchy: [ + { parent: 'TO', child: 'TO_Facility1' }, + { parent: 'TO', child: 'TO_Facility2' }, + { parent: 'FJ', child: 'FJ_Facility' }, + ], + }; + + const reqContext: ReqContext = { + hierarchy: HIERARCHY, + permissionGroup: 'Public', + services: new MockTupaiaApiClient({ + entity: new MockEntityApi(ENTITIES, RELATIONS), + }), + accessPolicy: new AccessPolicy({ AU: ['Public'] }), + }; + + it('creates an instance', () => { + expect(new ReportBuilder(reqContext)).toBeInstanceOf(ReportBuilder); + }); + + it('returns an error without a config being set', async () => { + const reportQuery = { + organisationUnitCodes: ['TO'], + hierarchy: 'explore', + period: '2020', + startDate: '2020-01-01', + endDate: '2020-12-31', + }; + const reportServerAggregator = new ReportServerAggregator(aggregator); + const reportBuilder = new ReportBuilder(reqContext); + await expect(reportBuilder.build(reportServerAggregator, reportQuery)).rejects.toThrow( + 'Report requires a config be set', + ); + }); + + it('returns test data if test data is provided without orgunit or hierarchy', async () => { + const testConfig = { fetch: { dataElements: ['DE'] }, transform: [] }; + const testData = [ + { dataElement: 'TEST001', value: 4, organisationUnit: 'TO', period: '20220101' }, + ]; + + const reportQuery = { + organisationUnitCodes: [], + hierarchy: '', + period: '2020', + startDate: '2020-01-01', + endDate: '2020-12-31', + }; + + const reportServerAggregator = new ReportServerAggregator(aggregator); + const reportBuilder = new ReportBuilder(reqContext).setConfig(testConfig).setTestData(testData); + const results = await reportBuilder.build(reportServerAggregator, reportQuery); + expect(results).toStrictEqual({ results: testData }); + }); +}); diff --git a/packages/report-server/src/reportBuilder/reportBuilder.ts b/packages/report-server/src/reportBuilder/reportBuilder.ts index b294a76d17..a1cbaef2e2 100644 --- a/packages/report-server/src/reportBuilder/reportBuilder.ts +++ b/packages/report-server/src/reportBuilder/reportBuilder.ts @@ -3,6 +3,7 @@ * Copyright (c) 2017 - 2020 Beyond Essential Systems Pty Ltd */ +import { Report } from '@tupaia/types'; import { ReportServerAggregator } from '../aggregator'; import { FetchReportQuery, StandardOrCustomReportConfig } from '../types'; import { configValidator } from './configValidator'; @@ -38,6 +39,19 @@ export class ReportBuilder { return this; }; + public fetch = async ( + aggregator: ReportServerAggregator, + query: FetchReportQuery, + config: Report['config'], + ) => { + if (this.testData) { + return { results: this.testData } as FetchResponse; + } + const builtQuery = await new QueryBuilder(this.reqContext, config, query).build(); + const fetchFromStandardQuery = buildFetch(config.fetch); + return fetchFromStandardQuery(aggregator, builtQuery); + }; + public build = async ( aggregator: ReportServerAggregator, query: FetchReportQuery, @@ -56,11 +70,7 @@ export class ReportBuilder { return { results: customReportData }; } - const fetch = this.testData - ? () => ({ results: this.testData } as FetchResponse) - : buildFetch(this.config?.fetch); - const builtQuery = await new QueryBuilder(this.reqContext, this.config, query).build(); - const data = await fetch(aggregator, builtQuery); + const data = await this.fetch(aggregator, query, this.config); const context = await buildContext(this.config.transform, this.reqContext, data, query); const transform = buildTransform(this.config.transform, context); From f663a19e8dcf06c60446615f7f01b7c68c442186 Mon Sep 17 00:00:00 2001 From: Chris Pollard Date: Thu, 19 Jan 2023 09:43:53 +1100 Subject: [PATCH 7/9] MDEV-2: Revert Search Term Filter in Admin Panel (#4308) --- packages/admin-panel/src/utilities/convertSearchTermToFilter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin-panel/src/utilities/convertSearchTermToFilter.js b/packages/admin-panel/src/utilities/convertSearchTermToFilter.js index 09eb41f438..eda1d73e65 100644 --- a/packages/admin-panel/src/utilities/convertSearchTermToFilter.js +++ b/packages/admin-panel/src/utilities/convertSearchTermToFilter.js @@ -13,7 +13,7 @@ export const convertSearchTermToFilter = (unprocessedFilterObject = {}) => { filterObject[key] = { comparator: `ilike`, - comparisonValue: `%${value}%`, + comparisonValue: `${value}%`, castAs: 'text', }; }); From f2b475dadfcdffa06e047ada5ebb4d56620bb1ae Mon Sep 17 00:00:00 2001 From: Chris Pollard Date: Thu, 19 Jan 2023 15:55:18 +1100 Subject: [PATCH 8/9] MDEV-24: Update Superset Country Code to FJ (#4310) --- ...geSupersetCountryCodeToFj-modifies-data.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/database/src/migrations/20230118223340-ChangeSupersetCountryCodeToFj-modifies-data.js diff --git a/packages/database/src/migrations/20230118223340-ChangeSupersetCountryCodeToFj-modifies-data.js b/packages/database/src/migrations/20230118223340-ChangeSupersetCountryCodeToFj-modifies-data.js new file mode 100644 index 0000000000..2d31a82b66 --- /dev/null +++ b/packages/database/src/migrations/20230118223340-ChangeSupersetCountryCodeToFj-modifies-data.js @@ -0,0 +1,43 @@ +'use strict'; + +import { updateValues } from '../utilities'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +const TABLE_NAME = 'data_element_data_service'; +const CURRENT_COUNTRY_CODE = 'FW'; +const NEW_COUNTRY_CODE = 'FJ'; + +exports.up = async function (db) { + return updateValues( + db, + TABLE_NAME, + { country_code: NEW_COUNTRY_CODE }, + { country_code: CURRENT_COUNTRY_CODE }, + ); +}; + +exports.down = async function (db) { + return updateValues( + db, + TABLE_NAME, + { country_code: CURRENT_COUNTRY_CODE }, + { country_code: NEW_COUNTRY_CODE }, + ); +}; + +exports._meta = { + version: 1, +}; From 3f5cbdcc8482e29fb070a5d7a7f91ff1a10db6a7 Mon Sep 17 00:00:00 2001 From: Chris Pollard Date: Fri, 20 Jan 2023 09:42:00 +1100 Subject: [PATCH 9/9] MAUI-1444: Detangle Palau Olangch Project Permission Groups (#4294) * MAUI-1444: Detangle Palau Olangch Project Permission Groups * Delete one p-group Co-authored-by: Andrew --- ...uEditAndCreatePermissions-modifies-data.js | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/database/src/migrations/20230109035514-PalauEditAndCreatePermissions-modifies-data.js diff --git a/packages/database/src/migrations/20230109035514-PalauEditAndCreatePermissions-modifies-data.js b/packages/database/src/migrations/20230109035514-PalauEditAndCreatePermissions-modifies-data.js new file mode 100644 index 0000000000..cbf6d95da1 --- /dev/null +++ b/packages/database/src/migrations/20230109035514-PalauEditAndCreatePermissions-modifies-data.js @@ -0,0 +1,106 @@ +'use strict'; + +import { deleteObject, updateValues } from '../utilities'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +const PERMISSION_GROUPS = { + nameAndParentChange: [ + { + id: '60e66e7361f76a6e8803101c', + name: 'Palau Behavioural Health', + parent_id: '62e1f49fdb5a0076ef0169bb', + }, + + { + id: '61f775cfd6e5f034fd1527e6', + name: 'Palau COVID-19', + parent_id: '62e1f49fdb5a0076ef0169bb', + }, + { + id: '6296bcb652e7897cf500273c', + name: 'Palau Public Health Nursing Unit', + parent_id: '62e1f49fdb5a0076ef0169bb', + }, + ], + parentChangeOnly: [ + { + id: '62cd0196a1110f2666003e82', + parent_id: '639aba23dbde622fb60dad76', + }, + { + id: '639abb94dbde622fb60dae49', + parent_id: '639aba23dbde622fb60dad76', + }, + { + id: '6221754ff4bae84c690f80a9', + parent_id: '62e1f49fdb5a0076ef0169bb', + }, + { + id: '6283037f717cba22280062b6', + parent_id: '62e1f49fdb5a0076ef0169bb', + }, + { + id: '627b5c0d052c6c2181002d97', + parent_id: '62e1f49fdb5a0076ef0169bb', + }, + { + id: '6243ab627c6c003b3906a2b5', + parent_id: '62e1f49fdb5a0076ef0169bb', + }, + { + id: '630fb5ff268b8f423f01a0f0', + parent_id: '62e1f49fdb5a0076ef0169bb', + }, + { + id: '62830b2e717cba2228007685', + parent_id: '62e1f49fdb5a0076ef0169bb', + }, + ], +}; + +const DELETE_PERMISSION_GROUP_ID = '627b5c30052c6c2181002dcb'; + +const TABLE_NAME = 'permission_group'; + +exports.up = async function (db) { + for (const permissionGroup of PERMISSION_GROUPS.nameAndParentChange) { + await updateValues( + db, + TABLE_NAME, + { name: permissionGroup.name, parent_id: permissionGroup.parent_id }, + { id: permissionGroup.id }, + ); + } + + for (const permissionGroup of PERMISSION_GROUPS.parentChangeOnly) { + await updateValues( + db, + TABLE_NAME, + { parent_id: permissionGroup.parent_id }, + { id: permissionGroup.id }, + ); + } + + return deleteObject(db, TABLE_NAME, { id: DELETE_PERMISSION_GROUP_ID }); +}; + +exports.down = function (db) { + return null; +}; + +exports._meta = { + version: 1, +};