diff --git a/package.json b/package.json index 32c84ab4..7d8e9f71 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@emotion/react": "11.11.4", "@emotion/styled": "11.11.5", "@eyeseetea/d2-api": "1.16.0-beta.12", + "@eyeseetea/d2-logger": "^1.1.0", "@eyeseetea/d2-ui-components": "v2.9.0-beta.2", "@eyeseetea/feedback-component": "0.0.3", "@material-ui/core": "4.12.4", diff --git a/src/data/entities/Instance.ts b/src/data/entities/Instance.ts new file mode 100644 index 00000000..8ec99106 --- /dev/null +++ b/src/data/entities/Instance.ts @@ -0,0 +1,23 @@ +export interface InstanceData { + url: string; + username?: string; + password?: string; +} + +export class Instance { + public readonly url: string; + private username: string | undefined; + private password: string | undefined; + + constructor(data: InstanceData) { + this.url = data.url; + this.username = data.username; + this.password = data.password; + } + + public get auth(): { username: string; password: string } | undefined { + return this.username && this.password + ? { username: this.username, password: this.password } + : undefined; + } +} diff --git a/src/data/repositories/AlertD2Repository.ts b/src/data/repositories/AlertD2Repository.ts index 7a850837..06ed231d 100644 --- a/src/data/repositories/AlertD2Repository.ts +++ b/src/data/repositories/AlertD2Repository.ts @@ -1,8 +1,6 @@ import { D2Api } from "@eyeseetea/d2-api/2.36"; import { apiToFuture, FutureData } from "../api-futures"; import { - RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, - RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID, RTSL_ZEBRA_ALERTS_NATIONAL_INCIDENT_STATUS_TEA_ID, RTSL_ZEBRA_ALERTS_PROGRAM_ID, @@ -12,32 +10,26 @@ import { AlertOptions, AlertRepository } from "../../domain/repositories/AlertRe import { Id } from "../../domain/entities/Ref"; import _ from "../../domain/entities/generic/Collection"; import { Future } from "../../domain/entities/generic/Future"; -import { - D2TrackerTrackedEntity, - TrackedEntitiesGetResponse, -} from "@eyeseetea/d2-api/api/trackerTrackedEntities"; +import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; import { Maybe } from "../../utils/ts-utils"; import { DataSource } from "../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; import { Alert } from "../../domain/entities/alert/Alert"; - -export type Filter = { - id: Id; - value: Maybe; -}; +import { OutbreakData } from "../../domain/entities/alert/OutbreakAlert"; +import { getAllTrackedEntitiesAsync } from "./utils/getAllTrackedEntities"; +import { outbreakDataSourceMapping, outbreakTEAMapping } from "./utils/AlertOutbreakMapper"; export class AlertD2Repository implements AlertRepository { constructor(private api: D2Api) {} updateAlerts(alertOptions: AlertOptions): FutureData { - const { dataSource, eventId, hazardTypeCode, incidentStatus, suspectedDiseaseCode } = - alertOptions; - const filter = this.getAlertFilter(dataSource, suspectedDiseaseCode, hazardTypeCode); + const { dataSource, eventId, incidentStatus, outbreakValue } = alertOptions; + const outbreakData = this.getAlertOutbreakData(dataSource, outbreakValue); return this.getTrackedEntitiesByTEACode({ program: RTSL_ZEBRA_ALERTS_PROGRAM_ID, orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, ouMode: "DESCENDANTS", - filter: filter, + filter: outbreakData, }).flatMap(alertTrackedEntities => { const alertsToMap: Alert[] = alertTrackedEntities.map(trackedEntity => ({ id: trackedEntity.trackedEntity || "", @@ -79,78 +71,38 @@ export class AlertD2Repository implements AlertRepository { }); } - private async getTrackedEntitiesByTEACodeAsync(options: { + private getTrackedEntitiesByTEACode(options: { program: Id; orgUnit: Id; ouMode: "SELECTED" | "DESCENDANTS"; - filter?: Filter; - }): Promise { + filter: OutbreakData; + }): FutureData { const { program, orgUnit, ouMode, filter } = options; - const d2TrackerTrackedEntities: D2TrackerTrackedEntity[] = []; - - const pageSize = 250; - let page = 1; - let result: TrackedEntitiesGetResponse; - - try { - do { - result = await this.api.tracker.trackedEntities - .get({ - program: program, - orgUnit: orgUnit, - ouMode: ouMode, - totalPages: true, - page: page, - pageSize: pageSize, - fields: { - attributes: true, - orgUnit: true, - trackedEntity: true, - trackedEntityType: true, - enrollments: { - events: { - createdAt: true, - dataValues: { - dataElement: true, - value: true, - }, - event: true, - }, - }, - }, - filter: filter ? `${filter.id}:eq:${filter.value}` : undefined, - }) - .getData(); - d2TrackerTrackedEntities.push(...result.instances); - - page++; - } while (result.page < Math.ceil((result.total as number) / pageSize)); - return d2TrackerTrackedEntities; - } catch { - return []; - } + return Future.fromPromise( + getAllTrackedEntitiesAsync(this.api, { + programId: program, + orgUnitId: orgUnit, + ouMode: ouMode, + filter: { + id: this.getOutbreakFilterId(filter), + value: filter.value, + }, + }) + ); } - getTrackedEntitiesByTEACode(options: { - program: Id; - orgUnit: Id; - ouMode: "SELECTED" | "DESCENDANTS"; - filter?: Filter; - }): FutureData { - return Future.fromPromise(this.getTrackedEntitiesByTEACodeAsync(options)); + private getOutbreakFilterId(filter: OutbreakData): string { + return outbreakTEAMapping[filter.type]; } - private getAlertFilter( + private getAlertOutbreakData( dataSource: DataSource, - suspectedDiseaseCode: Maybe, - hazardTypeCode: Maybe - ): Filter { - switch (dataSource) { - case DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS: - return { id: RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, value: suspectedDiseaseCode }; - case DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS: - return { id: RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, value: hazardTypeCode }; - } + outbreakValue: Maybe + ): OutbreakData { + return { + type: outbreakDataSourceMapping[dataSource], + value: outbreakValue, + }; } } diff --git a/src/data/repositories/AlertSyncDataStoreRepository.ts b/src/data/repositories/AlertSyncDataStoreRepository.ts index cf054678..787d32d8 100644 --- a/src/data/repositories/AlertSyncDataStoreRepository.ts +++ b/src/data/repositories/AlertSyncDataStoreRepository.ts @@ -6,10 +6,13 @@ import { AlertSyncRepository, } from "../../domain/repositories/AlertSyncRepository"; import { apiToFuture, FutureData } from "../api-futures"; -import { getOutbreakKey, getAlertValueFromMap } from "./utils/AlertOutbreakMapper"; +import { getAlertValueFromMap } from "./utils/AlertOutbreakMapper"; import { Maybe } from "../../utils/ts-utils"; import { DataValue } from "@eyeseetea/d2-api/api/trackerEvents"; -import { AlertSynchronizationData } from "../../domain/entities/alert/AlertData"; +import { + AlertSynchronizationData, + getOutbreakKey, +} from "../../domain/entities/alert/AlertSynchronizationData"; import { DataSource } from "../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; import { RTSL_ZEBRA_ALERTS_PROGRAM_ID } from "./consts/DiseaseOutbreakConstants"; import { assertOrError } from "./utils/AssertOrError"; @@ -24,14 +27,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository { } saveAlertSyncData(options: AlertSyncOptions): FutureData { - const { - alert, - dataSource, - hazardTypeCode, - suspectedDiseaseCode, - hazardTypes, - suspectedDiseases, - } = options; + const { alert, outbreakValue, dataSource, hazardTypes, suspectedDiseases } = options; return this.getAlertTrackedEntity(alert).flatMap(alertTrackedEntity => { const verificationStatus = getAlertValueFromMap( @@ -42,7 +38,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository { if (verificationStatus === VerificationStatus.RTSL_ZEB_AL_OS_VERIFICATION_VERIFIED) { const outbreakKey = getOutbreakKey({ dataSource: dataSource, - outbreakValue: suspectedDiseaseCode || hazardTypeCode, + outbreakValue: outbreakValue, hazardTypes: hazardTypes, suspectedDiseases: suspectedDiseases, }); @@ -72,7 +68,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository { }); } - public getAlertTrackedEntity(alert: Alert): FutureData { + private getAlertTrackedEntity(alert: Alert): FutureData { return apiToFuture( this.api.tracker.trackedEntities.get({ program: RTSL_ZEBRA_ALERTS_PROGRAM_ID, @@ -104,7 +100,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository { trackedEntity: D2TrackerTrackedEntity, outbreakKey: string ): AlertSynchronizationData { - const { alert, nationalDiseaseOutbreakEventId, dataSource } = options; + const { alert, dataSource, nationalDiseaseOutbreakEventId } = options; const outbreakType = dataSource === DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS ? "disease" : "hazard"; diff --git a/src/data/repositories/DiseaseOutbreakEventD2Repository.ts b/src/data/repositories/DiseaseOutbreakEventD2Repository.ts index 768b0278..da296776 100644 --- a/src/data/repositories/DiseaseOutbreakEventD2Repository.ts +++ b/src/data/repositories/DiseaseOutbreakEventD2Repository.ts @@ -13,6 +13,7 @@ import { getProgramTEAsMetadata } from "./utils/MetadataHelper"; import { assertOrError } from "./utils/AssertOrError"; import { Future } from "../../domain/entities/generic/Future"; import { getAllTrackedEntitiesAsync } from "./utils/getAllTrackedEntities"; +import { OutbreakData, OutbreakDataType } from "../../domain/entities/alert/OutbreakAlert"; export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRepository { constructor(private api: D2Api) {} @@ -34,7 +35,10 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep getAll(): FutureData { return Future.fromPromise( - getAllTrackedEntitiesAsync(this.api, RTSL_ZEBRA_PROGRAM_ID, RTSL_ZEBRA_ORG_UNIT_ID) + getAllTrackedEntitiesAsync(this.api, { + programId: RTSL_ZEBRA_PROGRAM_ID, + orgUnitId: RTSL_ZEBRA_ORG_UNIT_ID, + }) ).map(trackedEntities => { return trackedEntities .map(trackedEntity => { @@ -44,6 +48,34 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep }); } + getEventByDiseaseOrHazardType( + filter: OutbreakData + ): FutureData { + return Future.fromPromise( + getAllTrackedEntitiesAsync(this.api, { + programId: RTSL_ZEBRA_PROGRAM_ID, + orgUnitId: RTSL_ZEBRA_ORG_UNIT_ID, + filter: { + id: this.getDiseaseOutbreakFilterId(filter), + value: filter.value, + }, + }) + ).map(trackedEntities => { + return trackedEntities.map(trackedEntity => { + return mapTrackedEntityAttributesToDiseaseOutbreak(trackedEntity); + }); + }); + } + + private getDiseaseOutbreakFilterId(filter: OutbreakData): string { + const mapping: Record = { + disease: RTSL_ZEBRA_DISEASE_TEA_ID, + hazard: RTSL_ZEBRA_HAZARD_TEA_ID, + }; + + return mapping[filter.type]; + } + save(diseaseOutbreak: DiseaseOutbreakEventBaseAttrs): FutureData { return getProgramTEAsMetadata(this.api, RTSL_ZEBRA_PROGRAM_ID).flatMap( teasMetadataResponse => { @@ -90,3 +122,8 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep //TO DO : Implement delete/archive after requirement confirmation } + +const RTSL_ZEBRA_DISEASE_TEA_ID = "jLvbkuvPdZ6"; +const RTSL_ZEBRA_HAZARD_TEA_ID = "Dzrw3Tf0ukB"; + +type TrackedEntityAttributeId = Id; diff --git a/src/data/repositories/NotificationD2Repository.ts b/src/data/repositories/NotificationD2Repository.ts index c63d4429..3a76ad8c 100644 --- a/src/data/repositories/NotificationD2Repository.ts +++ b/src/data/repositories/NotificationD2Repository.ts @@ -1,17 +1,45 @@ import { D2Api } from "@eyeseetea/d2-api/2.36"; import { - Notification, + NotificationOptions, NotificationRepository, } from "../../domain/repositories/NotificationRepository"; import { apiToFuture, FutureData } from "../api-futures"; import { Future } from "../../domain/entities/generic/Future"; +import { UserGroup } from "../../domain/entities/UserGroup"; +import { OutbreakAlert } from "../../domain/entities/alert/OutbreakAlert"; +import i18n from "../../utils/i18n"; export class NotificationD2Repository implements NotificationRepository { constructor(private api: D2Api) {} - save(notification: Notification): FutureData { - return apiToFuture(this.api.messageConversations.post(notification)).flatMap(() => - Future.success(undefined) - ); + notifyNationalWatchStaff( + alertData: OutbreakAlert, + outbreakName: string, + userGroups: UserGroup[] + ): FutureData { + const { notificationOptions } = alertData; + + return apiToFuture( + this.api.messageConversations.post({ + subject: `New Outbreak Alert: ${outbreakName} in zm Zambia Ministry of Health`, + text: buildNotificationText(outbreakName, notificationOptions), + userGroups: userGroups, + }) + ).flatMap(() => Future.success(undefined)); } } + +function buildNotificationText(outbreakKey: string, notificationData: NotificationOptions): string { + const { detectionDate, emergenceDate, incidentManager, notificationDate, verificationStatus } = + notificationData; + + return i18n.t(`There has been a new Outbreak detected for ${outbreakKey} in zm Zambia Ministry of Health. + +Please see the details of the outbreak below: + +Emergence date: ${emergenceDate} +Detection Date : ${detectionDate} +Notification Date : ${notificationDate} +Incident Manager : ${incidentManager} +Verification Status : ${verificationStatus}`); +} diff --git a/src/data/repositories/OutbreakAlertD2Repository.ts b/src/data/repositories/OutbreakAlertD2Repository.ts new file mode 100644 index 00000000..5b5ad6c9 --- /dev/null +++ b/src/data/repositories/OutbreakAlertD2Repository.ts @@ -0,0 +1,136 @@ +import { D2Api } from "@eyeseetea/d2-api/2.36"; +import { OutbreakAlert, OutbreakData } from "../../domain/entities/alert/OutbreakAlert"; +import { OutbreakAlertRepository } from "../../domain/repositories/OutbreakAlertRepository"; +import { Attribute, D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; +import { + RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, + RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, + RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID, + RTSL_ZEBRA_ALERTS_PROGRAM_ID, + RTSL_ZEBRA_ORG_UNIT_ID, +} from "./consts/DiseaseOutbreakConstants"; +import { Id } from "../../domain/entities/Ref"; +import { FutureData } from "../api-futures"; +import { Future } from "../../domain/entities/generic/Future"; +import _ from "../../domain/entities/generic/Collection"; +import { getTEAttributeById } from "./utils/MetadataHelper"; +import { DataSource } from "../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; +import { mapTrackedEntityAttributesToNotificationOptions } from "./utils/AlertOutbreakMapper"; +import { getAllTrackedEntitiesAsync } from "./utils/getAllTrackedEntities"; +import { Maybe } from "../../utils/ts-utils"; +import { NotificationOptions } from "../../domain/repositories/NotificationRepository"; + +export class OutbreakAlertD2Repository implements OutbreakAlertRepository { + constructor(private api: D2Api) {} + + get(): FutureData { + return this.getAlertTrackedEntities().map(alertTEIs => { + return this.getOutbreakAlerts(alertTEIs); + }); + } + + private getOutbreakAlerts(alertTrackedEntities: D2TrackerTrackedEntity[]): OutbreakAlert[] { + // these are alerts that have no national event id + const alertsWithNoEventId = _(alertTrackedEntities) + .compactMap(trackedEntity => { + const { diseaseType, hazardType, nationalEventId } = + this.getAlertTEAttributes(trackedEntity); + const notificationOptions = + mapTrackedEntityAttributesToNotificationOptions(trackedEntity); + const outbreakData = this.getOutbreakData(diseaseType, hazardType); + + if (!outbreakData) return undefined; + + const dataSource = this.getAlertDataSource(diseaseType, hazardType); + if (!dataSource) return undefined; + + const alertData: OutbreakAlert = this.buildAlertData( + trackedEntity, + outbreakData, + dataSource, + notificationOptions + ); + + return !nationalEventId && (hazardType || diseaseType) ? alertData : undefined; + }) + .value(); + + return alertsWithNoEventId; + } + + private getAlertDataSource( + diseaseType: Maybe, + hazardType: Maybe + ): Maybe { + if (diseaseType) return DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS; + else if (hazardType) return DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS; + else return undefined; + } + + private buildAlertData( + trackedEntity: D2TrackerTrackedEntity, + outbreakData: OutbreakData, + dataSource: DataSource, + notificationOptions: NotificationOptions + ): OutbreakAlert { + if (!trackedEntity.trackedEntity || !trackedEntity.orgUnit) + throw new Error(`Alert data not found for ${outbreakData.value}`); + + return { + alert: { + id: trackedEntity.trackedEntity, + district: trackedEntity.orgUnit, + }, + outbreakData: outbreakData, + dataSource: dataSource, + notificationOptions: notificationOptions, + }; + } + + private getOutbreakData( + diseaseType: Maybe, + hazardType: Maybe + ): Maybe { + // use a full mapping (record/switch) + return diseaseType + ? { value: diseaseType.value, type: "disease" } + : hazardType + ? { value: hazardType.value, type: "hazard" } + : undefined; + } + + private getAlertTEAttributes(trackedEntity: D2TrackerTrackedEntity) { + const nationalEventId = getTEAttributeById( + trackedEntity, + RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID + ); + const hazardType = getTEAttributeById(trackedEntity, RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID); + const diseaseType = getTEAttributeById(trackedEntity, RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID); + + return { diseaseType, hazardType, nationalEventId }; + } + + private getTrackedEntitiesByTEACode(options: { + program: Id; + orgUnit: Id; + ouMode: "SELECTED" | "DESCENDANTS"; + }): FutureData { + const { program, orgUnit, ouMode } = options; + + return Future.fromPromise( + getAllTrackedEntitiesAsync(this.api, { + programId: program, + orgUnitId: orgUnit, + ouMode: ouMode, + }) + ); + } + + private getAlertTrackedEntities(): FutureData { + return this.getTrackedEntitiesByTEACode({ + program: RTSL_ZEBRA_ALERTS_PROGRAM_ID, + orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, + ouMode: "DESCENDANTS", + }); + } +} diff --git a/src/data/repositories/PerformanceOverviewD2Repository.ts b/src/data/repositories/PerformanceOverviewD2Repository.ts index f2b82472..579fe1ad 100644 --- a/src/data/repositories/PerformanceOverviewD2Repository.ts +++ b/src/data/repositories/PerformanceOverviewD2Repository.ts @@ -24,8 +24,8 @@ import { DiseaseNames, PerformanceMetrics717, } from "../../domain/entities/disease-outbreak-event/PerformanceOverviewMetrics"; -import { AlertSynchronizationData } from "../../domain/entities/alert/AlertData"; import { OrgUnit } from "../../domain/entities/OrgUnit"; +import { AlertSynchronizationData } from "../../domain/entities/alert/AlertSynchronizationData"; import { Id } from "../../domain/entities/Ref"; import { OverviewCard } from "../../domain/entities/PerformanceOverview"; import { assertOrError } from "./utils/AssertOrError"; diff --git a/src/data/repositories/UserGroupD2Repository.ts b/src/data/repositories/UserGroupD2Repository.ts new file mode 100644 index 00000000..4f6caa35 --- /dev/null +++ b/src/data/repositories/UserGroupD2Repository.ts @@ -0,0 +1,26 @@ +import { D2Api } from "@eyeseetea/d2-api/2.36"; +import { UserGroupRepository } from "../../domain/repositories/UserGroupRepository"; +import { apiToFuture, FutureData } from "../api-futures"; +import { assertOrError } from "./utils/AssertOrError"; +import { UserGroup } from "../../domain/entities/UserGroup"; + +export class UserGroupD2Repository implements UserGroupRepository { + constructor(private api: D2Api) {} + + getUserGroupByCode(code: string): FutureData { + return apiToFuture( + this.api.metadata.get({ + userGroups: { + fields: { + id: true, + }, + filter: { + code: { eq: code }, + }, + }, + }) + ) + .flatMap(response => assertOrError(response.userGroups[0], `User group ${code}`)) + .map(userGroup => userGroup); + } +} diff --git a/src/data/repositories/test/DiseaseOutbreakEventTestRepository.ts b/src/data/repositories/test/DiseaseOutbreakEventTestRepository.ts index 821e03ca..220c7780 100644 --- a/src/data/repositories/test/DiseaseOutbreakEventTestRepository.ts +++ b/src/data/repositories/test/DiseaseOutbreakEventTestRepository.ts @@ -1,3 +1,4 @@ +import { OutbreakData } from "../../../domain/entities/alert/OutbreakAlert"; import { DataSource, DiseaseOutbreakEvent, @@ -109,6 +110,74 @@ export class DiseaseOutbreakEventTestRepository implements DiseaseOutbreakEventR }, ]); } + getEventByDiseaseOrHazardType( + _filter: OutbreakData + ): FutureData { + return Future.success([ + { + id: "1", + name: "Disease Outbreak 1", + dataSource: DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS, + created: new Date(), + lastUpdated: new Date(), + createdByName: "createdByName", + hazardType: "Biological:Animal", + mainSyndromeCode: undefined, + suspectedDiseaseCode: undefined, + notificationSourceCode: "1", + areasAffectedDistrictIds: [], + areasAffectedProvinceIds: [], + incidentStatus: NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_WATCH, + emerged: { date: new Date(), narrative: "emerged" }, + detected: { date: new Date(), narrative: "detected" }, + notified: { date: new Date(), narrative: "notified" }, + earlyResponseActions: { + initiateInvestigation: new Date(), + conductEpidemiologicalAnalysis: new Date(), + laboratoryConfirmation: { date: new Date(), na: false }, + appropriateCaseManagement: { date: new Date(), na: false }, + initiatePublicHealthCounterMeasures: { date: new Date(), na: false }, + initiateRiskCommunication: { date: new Date(), na: false }, + establishCoordination: new Date(), + responseNarrative: "responseNarrative", + }, + incidentManagerName: "incidentManager", + notes: undefined, + status: "ACTIVE", + }, + { + id: "2", + name: "Disease Outbreak 2", + dataSource: DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS, + created: new Date(), + lastUpdated: new Date(), + createdByName: "createdByName2", + hazardType: "Biological:Human", + mainSyndromeCode: "2", + suspectedDiseaseCode: undefined, + notificationSourceCode: "2", + areasAffectedDistrictIds: [], + areasAffectedProvinceIds: [], + incidentStatus: NationalIncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_WATCH, + emerged: { date: new Date(), narrative: "emerged" }, + detected: { date: new Date(), narrative: "detected" }, + notified: { date: new Date(), narrative: "notified" }, + earlyResponseActions: { + initiateInvestigation: new Date(), + conductEpidemiologicalAnalysis: new Date(), + laboratoryConfirmation: { date: new Date(), na: false }, + appropriateCaseManagement: { date: new Date(), na: false }, + initiatePublicHealthCounterMeasures: { date: new Date(), na: false }, + initiateRiskCommunication: { date: new Date(), na: false }, + establishCoordination: new Date(), + responseNarrative: "responseNarrative", + }, + incidentManagerName: "incidentManager", + notes: undefined, + status: "COMPLETED", + }, + ]); + } save(_diseaseOutbreak: DiseaseOutbreakEvent): FutureData { return Future.success(""); } diff --git a/src/data/repositories/utils/AlertOutbreakMapper.ts b/src/data/repositories/utils/AlertOutbreakMapper.ts index 0ee08d1b..60097e56 100644 --- a/src/data/repositories/utils/AlertOutbreakMapper.ts +++ b/src/data/repositories/utils/AlertOutbreakMapper.ts @@ -1,47 +1,31 @@ import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; -import { DataSource } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; -import { AlertOptions } from "../../../domain/repositories/AlertRepository"; -import { Maybe } from "../../../utils/ts-utils"; -import { Option } from "../../../domain/entities/Ref"; import { alertOutbreakCodes } from "../consts/AlertConstants"; import { getValueFromMap } from "./DiseaseOutbreakMapper"; +import { NotificationOptions } from "../../../domain/repositories/NotificationRepository"; +import { OutbreakDataType } from "../../../domain/entities/alert/OutbreakAlert"; +import { Id } from "../../../domain/entities/Ref"; import { - dataSourceMap, - diseaseOutbreakCodes, - incidentStatusMap, + RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, + RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, } from "../consts/DiseaseOutbreakConstants"; +import { DataSource } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; -export function mapTrackedEntityAttributesToAlertOptions( - nationalTrackedEntity: D2TrackerTrackedEntity, - alertTrackedEntity: D2TrackerTrackedEntity -): AlertOptions { - if (!nationalTrackedEntity.trackedEntity) throw new Error("Tracked entity not found"); - - const fromDiseaseOutbreakMap = ( - key: keyof typeof diseaseOutbreakCodes, - trackedEntity: D2TrackerTrackedEntity - ) => getValueFromMap(key, trackedEntity); - - const fromAlertOutbreakMap = ( - key: keyof typeof alertOutbreakCodes, - trackedEntity: D2TrackerTrackedEntity - ) => getAlertValueFromMap(key, trackedEntity); - - const dataSource = dataSourceMap[fromDiseaseOutbreakMap("dataSource", nationalTrackedEntity)]; - const incidentStatus = - incidentStatusMap[fromDiseaseOutbreakMap("incidentStatus", nationalTrackedEntity)]; - - if (!dataSource || !incidentStatus) throw new Error("Data source or incident status not valid"); - - const diseaseOutbreak: AlertOptions = { - eventId: nationalTrackedEntity.trackedEntity, - dataSource: dataSource, - hazardTypeCode: fromAlertOutbreakMap("hazardType", alertTrackedEntity), - suspectedDiseaseCode: fromAlertOutbreakMap("suspectedDisease", alertTrackedEntity), - incidentStatus: incidentStatus, +export function mapTrackedEntityAttributesToNotificationOptions( + trackedEntity: D2TrackerTrackedEntity +): NotificationOptions { + const verificationStatus = getAlertValueFromMap("verificationStatus", trackedEntity); + const incidentManager = getAlertValueFromMap("incidentManager", trackedEntity); + const emergenceDate = getValueFromMap("emergedDate", trackedEntity); + const detectionDate = getValueFromMap("detectedDate", trackedEntity); + const notificationDate = getValueFromMap("notifiedDate", trackedEntity); + + return { + detectionDate: detectionDate, + emergenceDate: emergenceDate, + incidentManager: incidentManager, + notificationDate: notificationDate, + verificationStatus: verificationStatus, }; - - return diseaseOutbreak; } export function getAlertValueFromMap( @@ -54,23 +38,14 @@ export function getAlertValueFromMap( ); } -export function getOutbreakKey(options: { - dataSource: DataSource; - outbreakValue: Maybe; - hazardTypes: Option[]; - suspectedDiseases: Option[]; -}): string { - const { dataSource, outbreakValue, hazardTypes, suspectedDiseases } = options; +export const outbreakTEAMapping: Record = { + disease: RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, + hazard: RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, +}; - const diseaseName = suspectedDiseases.find(disease => disease.id === outbreakValue)?.name; - const hazardName = hazardTypes.find(hazardType => hazardType.id === outbreakValue)?.name; +export const outbreakDataSourceMapping: Record = { + [DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS]: "disease", + [DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS]: "hazard", +}; - if (!diseaseName && !hazardName) throw new Error(`Outbreak not found for ${outbreakValue}`); - - switch (dataSource) { - case DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS: - return hazardName ?? ""; - case DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS: - return diseaseName ?? ""; - } -} +type TrackedEntityAttributeId = Id; diff --git a/src/data/repositories/utils/MetadataHelper.ts b/src/data/repositories/utils/MetadataHelper.ts index 7ea8ddb0..a7074a75 100644 --- a/src/data/repositories/utils/MetadataHelper.ts +++ b/src/data/repositories/utils/MetadataHelper.ts @@ -1,8 +1,7 @@ import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; -import { Id, Ref } from "../../../domain/entities/Ref"; +import { Id } from "../../../domain/entities/Ref"; import { D2Api } from "../../../types/d2-api"; -import { apiToFuture, FutureData } from "../../api-futures"; -import { assertOrError } from "./AssertOrError"; +import { apiToFuture } from "../../api-futures"; import { Attribute } from "@eyeseetea/d2-api/api/trackedEntityInstances"; import { Maybe } from "../../../utils/ts-utils"; @@ -26,23 +25,6 @@ export function getProgramTEAsMetadata(api: D2Api, programId: Id) { ); } -export function getUserGroupByCode(api: D2Api, code: string): FutureData { - return apiToFuture( - api.metadata.get({ - userGroups: { - fields: { - id: true, - }, - filter: { - code: { eq: code }, - }, - }, - }) - ) - .flatMap(response => assertOrError(response.userGroups[0], `User group ${code}`)) - .map(userGroup => userGroup); -} - export function getTEAttributeById( trackedEntity: D2TrackerTrackedEntity, attributeId: Id diff --git a/src/data/repositories/utils/NotificationMapper.ts b/src/data/repositories/utils/NotificationMapper.ts deleted file mode 100644 index 54a03bd2..00000000 --- a/src/data/repositories/utils/NotificationMapper.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; -import { getAlertValueFromMap } from "./AlertOutbreakMapper"; -import { NotificationOptions } from "../../../domain/repositories/NotificationRepository"; -import { getValueFromMap } from "./DiseaseOutbreakMapper"; - -export function getNotificationOptionsFromTrackedEntity( - alertTrackedEntity: D2TrackerTrackedEntity -): NotificationOptions { - const verificationStatus = getAlertValueFromMap("verificationStatus", alertTrackedEntity); - const incidentManager = getAlertValueFromMap("incidentManager", alertTrackedEntity); - const emergenceDate = getValueFromMap("emergedDate", alertTrackedEntity); - const detectionDate = getValueFromMap("detectedDate", alertTrackedEntity); - const notificationDate = getValueFromMap("notifiedDate", alertTrackedEntity); - - return { - detectionDate: detectionDate, - emergenceDate: emergenceDate, - incidentManager: incidentManager, - notificationDate: notificationDate, - verificationStatus: verificationStatus, - }; -} diff --git a/src/data/repositories/utils/getAllTrackedEntities.ts b/src/data/repositories/utils/getAllTrackedEntities.ts index 845602b6..add6a7b8 100644 --- a/src/data/repositories/utils/getAllTrackedEntities.ts +++ b/src/data/repositories/utils/getAllTrackedEntities.ts @@ -4,12 +4,18 @@ import { TrackedEntitiesGetResponse, } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; import { Id } from "../../../domain/entities/Ref"; +import { Maybe } from "../../../utils/ts-utils"; export async function getAllTrackedEntitiesAsync( api: D2Api, - programId: Id, - orgUnitId: Id + options: { + programId: Id; + orgUnitId: Id; + ouMode?: "SELECTED" | "DESCENDANTS"; + filter?: { id: string; value: Maybe }; + } ): Promise { + const { programId, orgUnitId, ouMode, filter } = options; const d2TrackerTrackedEntities: D2TrackerTrackedEntity[] = []; const pageSize = 250; @@ -22,18 +28,12 @@ export async function getAllTrackedEntitiesAsync( .get({ program: programId, orgUnit: orgUnitId, + ouMode: ouMode, totalPages: true, page: page, pageSize: pageSize, - fields: { - attributes: true, - orgUnit: true, - trackedEntity: true, - trackedEntityType: true, - enrollments: { - status: true, - }, - }, + fields: fields, + filter: filter ? `${filter.id}:eq:${filter.value}` : undefined, }) .getData(); @@ -46,3 +46,21 @@ export async function getAllTrackedEntitiesAsync( return []; } } + +const fields = { + attributes: true, + orgUnit: true, + trackedEntity: true, + trackedEntityType: true, + enrollments: { + status: true, + events: { + createdAt: true, + dataValues: { + dataElement: true, + value: true, + }, + event: true, + }, + }, +} as const; diff --git a/src/domain/entities/UserGroup.ts b/src/domain/entities/UserGroup.ts new file mode 100644 index 00000000..4049db80 --- /dev/null +++ b/src/domain/entities/UserGroup.ts @@ -0,0 +1,3 @@ +import { Ref } from "./Ref"; + +export type UserGroup = Ref; diff --git a/src/domain/entities/alert/AlertData.ts b/src/domain/entities/alert/AlertData.ts deleted file mode 100644 index dade4a44..00000000 --- a/src/domain/entities/alert/AlertData.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Maybe } from "../../../utils/ts-utils"; -import { DataSource } from "../disease-outbreak-event/DiseaseOutbreakEvent"; -import { Id } from "../Ref"; -import { Alert } from "./Alert"; - -export type AlertData = { - alert: Alert; - dataSource: DataSource; - outbreakData: { - id: string; - value: string; - }; -}; - -export type AlertSynchronizationData = { - lastSyncTime: string; - type: string; - nationalDiseaseOutbreakEventId: Id; - alerts: { - alertId: string; - eventDate: Maybe; - orgUnit: Maybe; - suspectedCases: string; - probableCases: string; - confirmedCases: string; - deaths: string; - }[]; -} & { - [key in "disease" | "hazard"]?: string; -}; diff --git a/src/domain/entities/alert/AlertSynchronizationData.ts b/src/domain/entities/alert/AlertSynchronizationData.ts new file mode 100644 index 00000000..573885d9 --- /dev/null +++ b/src/domain/entities/alert/AlertSynchronizationData.ts @@ -0,0 +1,43 @@ +import { Maybe } from "../../../utils/ts-utils"; +import { OutbreakValueCode } from "../../repositories/AlertRepository"; +import { DataSource } from "../disease-outbreak-event/DiseaseOutbreakEvent"; +import { Id, Option } from "../Ref"; +import { OutbreakDataType } from "./OutbreakAlert"; + +export type AlertSynchronizationData = { + lastSyncTime: string; + type: string; + nationalDiseaseOutbreakEventId: Id; + alerts: { + alertId: string; + eventDate: Maybe; + orgUnit: Maybe; + suspectedCases: string; + probableCases: string; + confirmedCases: string; + deaths: string; + }[]; +} & { + [key in OutbreakDataType]?: string; +}; + +export function getOutbreakKey(options: { + dataSource: DataSource; + outbreakValue: Maybe; + hazardTypes: Option[]; + suspectedDiseases: Option[]; +}): string { + const { dataSource, outbreakValue, hazardTypes, suspectedDiseases } = options; + + const diseaseName = suspectedDiseases.find(disease => disease.id === outbreakValue)?.name; + const hazardName = hazardTypes.find(hazardType => hazardType.id === outbreakValue)?.name; + + if (!diseaseName && !hazardName) throw new Error(`Outbreak not found for ${outbreakValue}`); + + switch (dataSource) { + case DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS: + return hazardName ?? ""; + case DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS: + return diseaseName ?? ""; + } +} diff --git a/src/domain/entities/alert/OutbreakAlert.ts b/src/domain/entities/alert/OutbreakAlert.ts new file mode 100644 index 00000000..4221fbf7 --- /dev/null +++ b/src/domain/entities/alert/OutbreakAlert.ts @@ -0,0 +1,18 @@ +import { Maybe } from "../../../utils/ts-utils"; +import { NotificationOptions } from "../../repositories/NotificationRepository"; +import { DataSource } from "../disease-outbreak-event/DiseaseOutbreakEvent"; +import { Alert } from "./Alert"; + +export type OutbreakDataType = "disease" | "hazard"; + +export type OutbreakData = { + type: OutbreakDataType; + value: Maybe; +}; + +export type OutbreakAlert = { + alert: Alert; + dataSource: DataSource; + outbreakData: OutbreakData; + notificationOptions: NotificationOptions; +}; diff --git a/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts b/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts index b450c739..576889d4 100644 --- a/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts +++ b/src/domain/entities/risk-assessment/RiskAssessmentGrading.ts @@ -1,4 +1,4 @@ -import i18n from "@eyeseetea/feedback-component/locales"; +import i18n from "../../../utils/i18n"; import { Code, Ref } from "../Ref"; import { Struct } from "../generic/Struct"; import { Either } from "../generic/Either"; diff --git a/src/domain/repositories/AlertRepository.ts b/src/domain/repositories/AlertRepository.ts index 3725603f..93fd6685 100644 --- a/src/domain/repositories/AlertRepository.ts +++ b/src/domain/repositories/AlertRepository.ts @@ -11,10 +11,11 @@ export interface AlertRepository { updateAlerts(alertOptions: AlertOptions): FutureData; } +export type OutbreakValueCode = string; + export type AlertOptions = { dataSource: DataSource; eventId: Id; - hazardTypeCode: Maybe; + outbreakValue: Maybe; incidentStatus: NationalIncidentStatus; - suspectedDiseaseCode: Maybe; }; diff --git a/src/domain/repositories/AlertSyncRepository.ts b/src/domain/repositories/AlertSyncRepository.ts index 8148e54c..e2e5d544 100644 --- a/src/domain/repositories/AlertSyncRepository.ts +++ b/src/domain/repositories/AlertSyncRepository.ts @@ -1,8 +1,8 @@ import { FutureData } from "../../data/api-futures"; -import { DataSource } from "../entities/disease-outbreak-event/DiseaseOutbreakEvent"; -import { Id, Option } from "../entities/Ref"; import { Maybe } from "../../utils/ts-utils"; +import { Id, Option } from "../entities/Ref"; import { Alert } from "../entities/alert/Alert"; +import { DataSource } from "../entities/disease-outbreak-event/DiseaseOutbreakEvent"; export interface AlertSyncRepository { saveAlertSyncData(options: AlertSyncOptions): FutureData; @@ -11,9 +11,8 @@ export interface AlertSyncRepository { export type AlertSyncOptions = { alert: Alert; dataSource: DataSource; + outbreakValue: Maybe; nationalDiseaseOutbreakEventId: Id; - hazardTypeCode: Maybe; - suspectedDiseaseCode: Maybe; hazardTypes: Option[]; suspectedDiseases: Option[]; }; diff --git a/src/domain/repositories/DiseaseOutbreakEventRepository.ts b/src/domain/repositories/DiseaseOutbreakEventRepository.ts index 42d3d280..5759095d 100644 --- a/src/domain/repositories/DiseaseOutbreakEventRepository.ts +++ b/src/domain/repositories/DiseaseOutbreakEventRepository.ts @@ -1,10 +1,14 @@ import { FutureData } from "../../data/api-futures"; +import { OutbreakData } from "../entities/alert/OutbreakAlert"; import { DiseaseOutbreakEventBaseAttrs } from "../entities/disease-outbreak-event/DiseaseOutbreakEvent"; import { ConfigLabel, Id } from "../entities/Ref"; export interface DiseaseOutbreakEventRepository { get(id: Id): FutureData; getAll(): FutureData; + getEventByDiseaseOrHazardType( + filter: OutbreakData + ): FutureData; save(diseaseOutbreak: DiseaseOutbreakEventBaseAttrs): FutureData; getConfigStrings(): FutureData; } diff --git a/src/domain/repositories/NotificationRepository.ts b/src/domain/repositories/NotificationRepository.ts index 8f36650f..bcd6b52c 100644 --- a/src/domain/repositories/NotificationRepository.ts +++ b/src/domain/repositories/NotificationRepository.ts @@ -1,16 +1,15 @@ import { FutureData } from "../../data/api-futures"; -import { Ref } from "../entities/Ref"; +import { OutbreakAlert } from "../entities/alert/OutbreakAlert"; +import { UserGroup } from "../entities/UserGroup"; export interface NotificationRepository { - save(notification: Notification): FutureData; + notifyNationalWatchStaff( + alertData: OutbreakAlert, + outbreakName: string, + userGroups: UserGroup[] + ): FutureData; } -export type Notification = { - subject: string; - text: string; - userGroups: Ref[]; -}; - export type NotificationOptions = { detectionDate: string; emergenceDate: string; diff --git a/src/domain/repositories/OutbreakAlertRepository.ts b/src/domain/repositories/OutbreakAlertRepository.ts new file mode 100644 index 00000000..3965fc6d --- /dev/null +++ b/src/domain/repositories/OutbreakAlertRepository.ts @@ -0,0 +1,6 @@ +import { FutureData } from "../../data/api-futures"; +import { OutbreakAlert } from "../entities/alert/OutbreakAlert"; + +export interface OutbreakAlertRepository { + get(): FutureData; +} diff --git a/src/domain/repositories/UserGroupRepository.ts b/src/domain/repositories/UserGroupRepository.ts new file mode 100644 index 00000000..ac92a475 --- /dev/null +++ b/src/domain/repositories/UserGroupRepository.ts @@ -0,0 +1,6 @@ +import { FutureData } from "../../data/api-futures"; +import { UserGroup } from "../entities/UserGroup"; + +export interface UserGroupRepository { + getUserGroupByCode(code: string): FutureData; +} diff --git a/src/domain/usecases/MapAndSaveAlertsUseCase.ts b/src/domain/usecases/MapAndSaveAlertsUseCase.ts new file mode 100644 index 00000000..7b38f221 --- /dev/null +++ b/src/domain/usecases/MapAndSaveAlertsUseCase.ts @@ -0,0 +1,216 @@ +import { Future } from "../entities/generic/Future"; +import { Option } from "../entities/Ref"; +import { AlertOptions, AlertRepository } from "../repositories/AlertRepository"; +import { AlertSyncRepository } from "../repositories/AlertSyncRepository"; +import { OptionsRepository } from "../repositories/OptionsRepository"; +import _ from "../entities/generic/Collection"; +import { + DataSource, + DiseaseOutbreakEventBaseAttrs, +} from "../entities/disease-outbreak-event/DiseaseOutbreakEvent"; +import { logger } from "../../utils/logger"; +import { NotificationRepository } from "../repositories/NotificationRepository"; +import { UserGroupRepository } from "../repositories/UserGroupRepository"; +import { OutbreakAlert, OutbreakData, OutbreakDataType } from "../entities/alert/OutbreakAlert"; +import { OutbreakAlertRepository } from "../repositories/OutbreakAlertRepository"; +import { DiseaseOutbreakEventRepository } from "../repositories/DiseaseOutbreakEventRepository"; +import { getOutbreakKey } from "../entities/alert/AlertSynchronizationData"; +import { promiseMap } from "../../utils/promiseMap"; + +export class MapAndSaveAlertsUseCase { + constructor( + private options: { + alertRepository: AlertRepository; + outbreakAlertRepository: OutbreakAlertRepository; + alertSyncRepository: AlertSyncRepository; + diseaseOutbreakEventRepository: DiseaseOutbreakEventRepository; + notificationRepository: NotificationRepository; + optionsRepository: OptionsRepository; + userGroupRepository: UserGroupRepository; + } + ) {} + + public async execute(): Promise { + const { hazardTypes, suspectedDiseases } = await this.getOptions(); + const alertData = await this.options.outbreakAlertRepository.get().toPromise(); + + logger.info( + `${alertData.length} event(s) found in the Zebra Alerts program with no national event id.` + ); + + const uniqueFiltersWithAlerts = _(alertData) + .groupBy(alert => alert.dataSource) + .values() + .flatMap(alertsByDataSource => { + return getUniqueFilters(alertsByDataSource).map(outbreakData => ({ + outbreakData: outbreakData, + alerts: alertsByDataSource, + })); + }); + + await promiseMap(uniqueFiltersWithAlerts, async uniqueFilterWithAlerts => { + const { outbreakData, alerts } = uniqueFilterWithAlerts; + + return this.mapDiseaseOutbreakToAlertsAndSave( + alerts, + outbreakData, + hazardTypes, + suspectedDiseases + ); + }); + } + + private async mapDiseaseOutbreakToAlertsAndSave( + alertData: OutbreakAlert[], + outbreakData: OutbreakData, + hazardTypes: Option[], + suspectedDiseases: Option[] + ): Promise { + const diseaseOutbreakEvents = await this.getDiseaseOutbreakEvents(outbreakData); + const dataSource = this.getDataSource(outbreakData); + + if (diseaseOutbreakEvents.length > 1) { + const outbreakKey = getOutbreakKey({ + dataSource: dataSource, + outbreakValue: outbreakData.value, + hazardTypes: hazardTypes, + suspectedDiseases: suspectedDiseases, + }); + + return logger.error(`More than 1 National event found for ${outbreakKey} outbreak.`); + } + + const outbreakAlerts = alertData.filter( + alertData => alertData.outbreakData.value === outbreakData.value + ); + + await promiseMap(outbreakAlerts, alertData => { + return this.mapAndSaveAlertData( + alertData, + diseaseOutbreakEvents, + hazardTypes, + suspectedDiseases + ); + }); + } + + private getDataSource(outbreakData: OutbreakData): DataSource { + return mapping[outbreakData.type]; + } + + private getDiseaseOutbreakEvents( + outbreakData: OutbreakData + ): Promise { + return this.options.diseaseOutbreakEventRepository + .getEventByDiseaseOrHazardType(outbreakData) + .toPromise(); + } + + private getOptions(): Promise<{ hazardTypes: Option[]; suspectedDiseases: Option[] }> { + const { optionsRepository } = this.options; + + return Future.joinObj({ + hazardTypes: optionsRepository.getHazardTypesByCode(), + suspectedDiseases: optionsRepository.getSuspectedDiseases(), + }).toPromise(); + } + + private mapAndSaveAlertData( + alertData: OutbreakAlert, + diseaseOutbreakEvents: DiseaseOutbreakEventBaseAttrs[], + hazardTypes: Option[], + suspectedDiseases: Option[] + ): Promise { + const { dataSource, outbreakData } = alertData; + + const outbreakType = outbreakData.type; + const outbreakName = getOutbreakKey({ + dataSource: dataSource, + outbreakValue: outbreakData.value, + hazardTypes: hazardTypes, + suspectedDiseases: suspectedDiseases, + }); + + if (diseaseOutbreakEvents.length === 0) { + return this.notifyNationalWatchStaff(alertData, outbreakType, outbreakName); + } + + const diseaseOutbreakEvent = diseaseOutbreakEvents[0]; + if (!diseaseOutbreakEvent) + return logger.error( + `No disease outbreak event found for ${outbreakType} type ${outbreakName}.` + ); + + return this.updateAlertData({ + alertData: alertData, + diseaseOutbreakEvent: diseaseOutbreakEvent, + hazardTypes: hazardTypes, + suspectedDiseases: suspectedDiseases, + }); + } + + private async notifyNationalWatchStaff( + alertData: OutbreakAlert, + alertOutbreakType: string, + outbreakName: string + ): Promise { + const { notificationRepository, userGroupRepository } = this.options; + logger.debug(`There is no national event with ${outbreakName} ${alertOutbreakType} type.`); + + const userGroup = await userGroupRepository + .getUserGroupByCode(RTSL_ZEBRA_NATIONAL_WATCH_STAFF_USER_GROUP_CODE) + .toPromise(); + + return notificationRepository + .notifyNationalWatchStaff(alertData, outbreakName, [userGroup]) + .toPromise() + .then(() => logger.success("Successfully notified all national watch staff.")); + } + + private async updateAlertData(options: { + alertData: OutbreakAlert; + diseaseOutbreakEvent: DiseaseOutbreakEventBaseAttrs; + hazardTypes: Option[]; + suspectedDiseases: Option[]; + }): Promise { + const { alertData, diseaseOutbreakEvent, hazardTypes, suspectedDiseases } = options; + + const alertOptions: AlertOptions = { + eventId: diseaseOutbreakEvent.id, + dataSource: diseaseOutbreakEvent.dataSource, + outbreakValue: alertData.outbreakData.value, + incidentStatus: diseaseOutbreakEvent.incidentStatus, + }; + + await this.options.alertRepository + .updateAlerts(alertOptions) + .toPromise() + .then(() => logger.success("Successfully updated alert.")); + + return this.options.alertSyncRepository + .saveAlertSyncData({ + alert: alertData.alert, + dataSource: alertOptions.dataSource, + outbreakValue: alertOptions.outbreakValue, + nationalDiseaseOutbreakEventId: alertOptions.eventId, + hazardTypes: hazardTypes, + suspectedDiseases: suspectedDiseases, + }) + .toPromise() + .then(() => logger.success("Successfully saved alert sync data.")); + } +} + +function getUniqueFilters(alerts: OutbreakAlert[]): OutbreakData[] { + return _(alerts) + .uniqBy(alertData => alertData.outbreakData.value) + .map(alertData => alertData.outbreakData) + .value(); +} + +const mapping: Record = { + disease: DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS, + hazard: DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS, +}; + +const RTSL_ZEBRA_NATIONAL_WATCH_STAFF_USER_GROUP_CODE = "RTSL_ZEBRA_NATONAL_WATCH_STAFF"; diff --git a/src/domain/usecases/MapDiseaseOutbreakToAlertsUseCase.ts b/src/domain/usecases/MapDiseaseOutbreakToAlertsUseCase.ts index b1aa5b3b..aca61c98 100644 --- a/src/domain/usecases/MapDiseaseOutbreakToAlertsUseCase.ts +++ b/src/domain/usecases/MapDiseaseOutbreakToAlertsUseCase.ts @@ -33,13 +33,14 @@ export class MapDiseaseOutbreakToAlertsUseCase { if (!diseaseOutbreakEventId) return Future.error(new Error("Disease Outbreak Event Id is required")); + const outbreakValue = hazardTypeCode ?? suspectedDiseaseCode; + return this.alertRepository .updateAlerts({ dataSource: dataSource, eventId: diseaseOutbreakEventId, - hazardTypeCode: hazardTypeCode, incidentStatus: incidentStatus, - suspectedDiseaseCode: suspectedDiseaseCode, + outbreakValue: outbreakValue, }) .flatMap((alerts: Alert[]) => Future.joinObj({ @@ -53,8 +54,7 @@ export class MapDiseaseOutbreakToAlertsUseCase { alert: alert, nationalDiseaseOutbreakEventId: diseaseOutbreakEventId, dataSource: dataSource, - hazardTypeCode: hazardTypeCode, - suspectedDiseaseCode: suspectedDiseaseCode, + outbreakValue: outbreakValue, hazardTypes: hazardTypes, suspectedDiseases: suspectedDiseases, }) diff --git a/src/domain/usecases/NotifyWatchStaffUseCase.ts b/src/domain/usecases/NotifyWatchStaffUseCase.ts deleted file mode 100644 index 0eb47c9b..00000000 --- a/src/domain/usecases/NotifyWatchStaffUseCase.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Ref } from "../entities/Ref"; -import { - NotificationOptions, - NotificationRepository, -} from "../repositories/NotificationRepository"; -import { FutureData } from "../../data/api-futures"; - -export class NotifyWatchStaffUseCase { - constructor(private notificationRepository: NotificationRepository) {} - - public execute( - outbreakKey: string, - notificationData: NotificationOptions, - userGroups: Ref[] - ): FutureData { - return this.notificationRepository.save({ - subject: `New Outbreak Alert: ${outbreakKey} in zm Zambia Ministry of Health`, - text: buildNotificationText(outbreakKey, notificationData), - userGroups: userGroups, - }); - } -} - -function buildNotificationText(outbreakKey: string, notificationData: NotificationOptions): string { - const { detectionDate, emergenceDate, incidentManager, notificationDate, verificationStatus } = - notificationData; - - return `There has been a new Outbreak detected for ${outbreakKey} in zm Zambia Ministry of Health. - -Please see the details of the outbreak below: - -Emergence date: ${emergenceDate} -Detection Date : ${detectionDate} -Notification Date : ${notificationDate} -Incident Manager : ${incidentManager} -Verification Status : ${verificationStatus}`; -} diff --git a/src/scripts/common.ts b/src/scripts/common.ts index bcada9fb..80b82111 100644 --- a/src/scripts/common.ts +++ b/src/scripts/common.ts @@ -1,4 +1,5 @@ import { D2Api } from "@eyeseetea/d2-api/2.36"; +import { Instance } from "../data/entities/Instance"; type Auth = { username: string; @@ -15,3 +16,35 @@ export function getD2ApiFromArgs(args: D2ApiArgs): D2Api { return new D2Api({ baseUrl: url, auth }); } + +export function getInstance(args: D2ApiArgs): Instance { + const instance = new Instance({ url: args.url, ...args.auth }); + return instance; +} + +export function getApiInstanceFromEnvVariables() { + if (!process.env.VITE_DHIS2_BASE_URL) + throw new Error("VITE_DHIS2_BASE_URL must be set in the .env file"); + + if (!process.env.VITE_DHIS2_AUTH) + throw new Error("VITE_DHIS2_AUTH must be set in the .env file"); + + const [username, password] = process.env.VITE_DHIS2_AUTH.split(":"); + + if (!username || !password) { + throw new Error("VITE_DHIS2_AUTH must be in the format 'username:password'"); + } + + const envVars = { + url: process.env.VITE_DHIS2_BASE_URL, + auth: { + username: username, + password: password, + }, + }; + + const api = getD2ApiFromArgs(envVars); + const instance = getInstance(envVars); + + return { api: api, instance: instance }; +} diff --git a/src/scripts/mapDiseaseOutbreakToAlerts.ts b/src/scripts/mapDiseaseOutbreakToAlerts.ts index a46097e6..1606e01f 100644 --- a/src/scripts/mapDiseaseOutbreakToAlerts.ts +++ b/src/scripts/mapDiseaseOutbreakToAlerts.ts @@ -1,335 +1,57 @@ -import { command, run } from "cmd-ts"; +import { boolean, command, flag, run } from "cmd-ts"; +import { setupLogger } from "../utils/logger"; import path from "path"; -import { getD2ApiFromArgs } from "./common"; -import { - RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, - RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, - RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID, - RTSL_ZEBRA_ALERTS_PROGRAM_ID, - RTSL_ZEBRA_ORG_UNIT_ID, - RTSL_ZEBRA_PROGRAM_ID, -} from "../data/repositories/consts/DiseaseOutbreakConstants"; +import { getApiInstanceFromEnvVariables } from "./common"; import _ from "../domain/entities/generic/Collection"; import { AlertD2Repository } from "../data/repositories/AlertD2Repository"; import { NotificationD2Repository } from "../data/repositories/NotificationD2Repository"; import { OptionsD2Repository } from "../data/repositories/OptionsD2Repository"; -import { Future } from "../domain/entities/generic/Future"; -import { getTEAttributeById, getUserGroupByCode } from "../data/repositories/utils/MetadataHelper"; -import { NotifyWatchStaffUseCase } from "../domain/usecases/NotifyWatchStaffUseCase"; -import { - getOutbreakKey, - mapTrackedEntityAttributesToAlertOptions, -} from "../data/repositories/utils/AlertOutbreakMapper"; import { AlertSyncDataStoreRepository } from "../data/repositories/AlertSyncDataStoreRepository"; -import { getNotificationOptionsFromTrackedEntity } from "../data/repositories/utils/NotificationMapper"; -import { AlertData } from "../domain/entities/alert/AlertData"; -import { DataSource } from "../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; -import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; -import { FutureData } from "../data/api-futures"; -import { Alert } from "../domain/entities/alert/Alert"; -import { AlertOptions } from "../domain/repositories/AlertRepository"; -import { Option } from "../domain/entities/Ref"; - -//TO DO : Fetch from metadata on app load -const RTSL_ZEBRA_DISEASE_TEA_ID = "jLvbkuvPdZ6"; -const RTSL_ZEBRA_HAZARD_TEA_ID = "Dzrw3Tf0ukB"; -const RTSL_ZEBRA_NATIONAL_WATCH_STAFF_USER_GROUP_CODE = "RTSL_ZEBRA_NATONAL_WATCH_STAFF"; +import { UserGroupD2Repository } from "../data/repositories/UserGroupD2Repository"; +import { MapAndSaveAlertsUseCase } from "../domain/usecases/MapAndSaveAlertsUseCase"; +import { OutbreakAlertD2Repository } from "../data/repositories/OutbreakAlertD2Repository"; +import { DiseaseOutbreakEventD2Repository } from "../data/repositories/DiseaseOutbreakEventD2Repository"; function main() { const cmd = command({ name: path.basename(__filename), - description: "Map national event ID to Zebra Alert Events with no event ID", - args: {}, - handler: async () => { - if (!process.env.VITE_DHIS2_BASE_URL) - throw new Error("VITE_DHIS2_BASE_URL must be set in the .env file"); - - if (!process.env.VITE_DHIS2_AUTH) - throw new Error("VITE_DHIS2_AUTH must be set in the .env file"); - - const username = process.env.VITE_DHIS2_AUTH.split(":")[0] ?? ""; - const password = process.env.VITE_DHIS2_AUTH.split(":")[1] ?? ""; - - if (username === "" || password === "") { - throw new Error("VITE_DHIS2_AUTH must be in the format 'username:password'"); - } - - const envVars = { - url: process.env.VITE_DHIS2_BASE_URL, - auth: { - username: username, - password: password, - }, - }; + description: + "Map national event ID to Zebra Alert Events with no event ID, and save alert data to datastore", + args: { + debug: flag({ + type: boolean, + defaultValue: () => true, + long: "debug", + description: "Option to print also logs in console", + }), + }, + handler: async args => { + const { api, instance } = getApiInstanceFromEnvVariables(); + await setupLogger(instance, { isDebug: args.debug }); - const api = getD2ApiFromArgs(envVars); const alertRepository = new AlertD2Repository(api); const alertSyncRepository = new AlertSyncDataStoreRepository(api); + const diseaseOutbreakEventRepository = new DiseaseOutbreakEventD2Repository(api); const notificationRepository = new NotificationD2Repository(api); const optionsRepository = new OptionsD2Repository(api); - - const notifyWatchStaffUseCase = new NotifyWatchStaffUseCase(notificationRepository); - - return Future.joinObj({ - alertTrackedEntities: getAlertTrackedEntities(), - hazardTypes: optionsRepository.getHazardTypesByCode(), - suspectedDiseases: optionsRepository.getSuspectedDiseases(), - }).run( - ({ alertTrackedEntities, hazardTypes, suspectedDiseases }) => { - const alertsWithNoEventId = - getAlertsWithNoNationalEventId(alertTrackedEntities); - - console.debug( - `${alertsWithNoEventId.length} event(s) found in the Zebra Alerts program with no national event id.` - ); - - return _(alertsWithNoEventId) - .groupBy(alert => alert.dataSource) - .values() - .forEach(alertsByDataSource => { - const uniqueFilters = getUniqueFilters(alertsByDataSource); - - return uniqueFilters.forEach(filter => - getNationalTrackedEntities({ - id: filter.filterId, - value: filter.filterValue, - }).run( - nationalTrackedEntities => { - if (nationalTrackedEntities.length > 1) { - const outbreakKey = getOutbreakKey({ - dataSource: filter.dataSource, - outbreakValue: filter.filterValue, - hazardTypes: hazardTypes, - suspectedDiseases: suspectedDiseases, - }); - - console.error( - `More than 1 National event found for ${outbreakKey} outbreak.` - ); - - return undefined; - } - - return alertsByDataSource - .filter( - alertData => - alertData.outbreakData.value === - filter.filterValue - ) - .forEach(alertData => { - const { alert, dataSource, outbreakData } = - alertData; - - const outbreakType = - dataSource === - DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS - ? "disease" - : "hazard"; - const outbreakName = getOutbreakKey({ - dataSource: dataSource, - outbreakValue: outbreakData.value, - hazardTypes: hazardTypes, - suspectedDiseases: suspectedDiseases, - }); - - alertSyncRepository - .getAlertTrackedEntity(alert) - .run( - alertTrackedEntity => { - if ( - nationalTrackedEntities.length === 0 - ) { - return notifyNationalWatchStaff( - alertTrackedEntity, - outbreakType, - outbreakName - ).run( - () => - console.debug( - "Successfully notified all national watch staff." - ), - error => console.error(error) - ); - } - - const nationalTrackedEntity = - nationalTrackedEntities[0]; - if (!nationalTrackedEntity) - throw new Error( - `No tracked entity found.` - ); - - const alertOptions = - mapTrackedEntityAttributesToAlertOptions( - nationalTrackedEntity, - alertTrackedEntity - ); - - alertRepository - .updateAlerts(alertOptions) - .run( - () => - console.debug( - "Successfully updated alert." - ), - error => console.error(error) - ); - - saveAlertSyncData({ - alertOptions: alertOptions, - alert: alert, - hazardTypes: hazardTypes, - suspectedDiseases: - suspectedDiseases, - }).run( - () => - console.debug( - `Saved alert data for ${outbreakName} ${outbreakType}.` - ), - error => console.error(error) - ); - }, - error => console.error(error) - ); - }); - }, - error => console.error(error) - ) - ); - }); - }, - error => console.error(error) - ); - - function getAlertTrackedEntities(): FutureData { - return alertRepository.getTrackedEntitiesByTEACode({ - program: RTSL_ZEBRA_ALERTS_PROGRAM_ID, - orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, - ouMode: "DESCENDANTS", - }); - } - - function getAlertsWithNoNationalEventId( - alertTrackedEntities: D2TrackerTrackedEntity[] - ): AlertData[] { - return _(alertTrackedEntities) - .compactMap(trackedEntity => { - const nationalEventId = getTEAttributeById( - trackedEntity, - RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID - ); - const hazardType = getTEAttributeById( - trackedEntity, - RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID - ); - const diseaseType = getTEAttributeById( - trackedEntity, - RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID - ); - - const outbreakData = diseaseType - ? { id: diseaseType.attribute, value: diseaseType.value } - : hazardType - ? { id: hazardType.value, value: hazardType.value } - : undefined; - - if (!outbreakData) return undefined; - if (!trackedEntity.trackedEntity || !trackedEntity.orgUnit) - throw new Error("Tracked entity not found"); - - const alertData: AlertData = { - alert: { - id: trackedEntity.trackedEntity, - district: trackedEntity.orgUnit, - }, - outbreakData: outbreakData, - dataSource: diseaseType - ? DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS - : DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS, - }; - - return !nationalEventId && (hazardType || diseaseType) - ? alertData - : undefined; - }) - .value(); - } - - function getNationalTrackedEntities(filter: { - id: string; - value: string; - }): FutureData { - return alertRepository.getTrackedEntitiesByTEACode({ - program: RTSL_ZEBRA_PROGRAM_ID, - orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, - ouMode: "SELECTED", - filter: filter, - }); - } - - function notifyNationalWatchStaff( - alertTrackedEntity: D2TrackerTrackedEntity, - alertOutbreakType: string, - outbreakName: string - ): FutureData { - console.debug( - `There is no national event with ${outbreakName} ${alertOutbreakType} type.` - ); - - return getUserGroupByCode( - api, - RTSL_ZEBRA_NATIONAL_WATCH_STAFF_USER_GROUP_CODE - ).flatMap(userGroup => { - const notificationOptions = - getNotificationOptionsFromTrackedEntity(alertTrackedEntity); - - return notifyWatchStaffUseCase.execute(outbreakName, notificationOptions, [ - userGroup, - ]); - }); - } - - function saveAlertSyncData(options: { - alertOptions: AlertOptions; - alert: Alert; - hazardTypes: Option[]; - suspectedDiseases: Option[]; - }) { - const { alert, alertOptions, hazardTypes, suspectedDiseases } = options; - const { dataSource, eventId, hazardTypeCode, suspectedDiseaseCode } = alertOptions; - - return alertSyncRepository.saveAlertSyncData({ - dataSource: dataSource, - hazardTypeCode: hazardTypeCode, - suspectedDiseaseCode: suspectedDiseaseCode, - nationalDiseaseOutbreakEventId: eventId, - alert: alert, - hazardTypes: hazardTypes, - suspectedDiseases: suspectedDiseases, - }); - } + const outbreakAlertRepository = new OutbreakAlertD2Repository(api); + const userGroupRepository = new UserGroupD2Repository(api); + + const mapAndSaveAlertsUseCase = new MapAndSaveAlertsUseCase({ + alertRepository: alertRepository, + outbreakAlertRepository: outbreakAlertRepository, + alertSyncRepository: alertSyncRepository, + diseaseOutbreakEventRepository: diseaseOutbreakEventRepository, + notificationRepository: notificationRepository, + optionsRepository: optionsRepository, + userGroupRepository: userGroupRepository, + }); + + return await mapAndSaveAlertsUseCase.execute(); }, }); run(cmd, process.argv.slice(2)); } -function getUniqueFilters(alerts: AlertData[]): { - filterId: string; - filterValue: string; - dataSource: DataSource; -}[] { - return _(alerts) - .uniqBy(filter => filter.outbreakData.value) - .map(filter => ({ - filterId: - filter.dataSource === DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS - ? RTSL_ZEBRA_DISEASE_TEA_ID - : RTSL_ZEBRA_HAZARD_TEA_ID, - filterValue: filter.outbreakData.value ?? "", - dataSource: filter.dataSource, - })) - .value(); -} - main(); diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 00000000..0c681813 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,37 @@ +import { ConsoleLogger, ProgramLogger, initLogger, BatchLogContent } from "@eyeseetea/d2-logger"; +import { Instance } from "../data/entities/Instance"; +import { Id } from "../domain/entities/Ref"; +import { RTSL_ZEBRA_ORG_UNIT_ID } from "../data/repositories/consts/DiseaseOutbreakConstants"; + +const LOGS_PROGRAM = "usU7YBzuhaE"; +const MESSAGE_DATA_ELEMENT = "OCXD513wyZU"; +const MESSAGE_TYPE_DATA_ELEMENT = "UF08oi330lh"; + +export let logger: ProgramLogger | ConsoleLogger; +export type { BatchLogContent }; + +export async function setupLogger( + instance: Instance, + options?: { isDebug?: boolean; orgUnitId?: Id } +): Promise { + const { isDebug = false, orgUnitId } = options ?? {}; + + logger = await initLogger({ + type: "program", + debug: isDebug, + baseUrl: instance.url, + auth: instance.auth, + programId: LOGS_PROGRAM, + organisationUnitId: orgUnitId || RTSL_ZEBRA_ORG_UNIT_ID, + dataElements: { + messageId: MESSAGE_DATA_ELEMENT, + messageTypeId: MESSAGE_TYPE_DATA_ELEMENT, + }, + }); +} + +export async function setupLoggerForTesting(): Promise { + logger = await initLogger({ + type: "console", + }); +} diff --git a/src/utils/promiseMap.ts b/src/utils/promiseMap.ts new file mode 100644 index 00000000..517f4153 --- /dev/null +++ b/src/utils/promiseMap.ts @@ -0,0 +1,10 @@ +export function promiseMap(inputValues: T[], mapper: (value: T) => Promise): Promise { + const reducer = (acc$: Promise, inputValue: T): Promise => + acc$.then((acc: S[]) => + mapper(inputValue).then(result => { + acc.push(result); + return acc; + }) + ); + return inputValues.reduce(reducer, Promise.resolve([])); +} diff --git a/yarn.lock b/yarn.lock index 89e95e32..cc8c269d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3419,6 +3419,36 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eyeseetea/d2-api@1.14.0": + version "1.14.0" + resolved "https://registry.yarnpkg.com/@eyeseetea/d2-api/-/d2-api-1.14.0.tgz#546398b00b9f01b60a72ecba648a35f57b6f7559" + integrity sha512-gVNXfK8sk1STuM8QDed0JY8DM63SwI3UJXeKsyzJjHtPIqi76ukdCYAo8XwtfqhGdZIwIMrrWPEOmAJ3dbvCKQ== + dependencies: + "@babel/runtime" "^7.5.4" + "@dhis2/d2-i18n" "^1.0.5" + "@types/prettier" "^1.18.3" + "@types/qs" "^6.5.3" + abort-controller "3.0.0" + argparse "^2.0.1" + axios "0.19.2" + axios-debug-log "^0.6.2" + axios-mock-adapter "1.18.2" + btoa "^1.2.1" + cronstrue "^1.81.0" + cryptr "^4.0.2" + d2 "^31.8.1" + dotenv "^8.0.0" + express "^4.17.1" + form-data "^4.0.0" + iconv-lite "0.6.2" + isomorphic-fetch "3.0.0" + lodash "^4.17.15" + log4js "^4.5.1" + node-schedule "^1.3.2" + qs "^6.9.0" + react "^16.12.0" + yargs "^14.0.0" + "@eyeseetea/d2-api@1.16.0-beta.12": version "1.16.0-beta.12" resolved "https://registry.yarnpkg.com/@eyeseetea/d2-api/-/d2-api-1.16.0-beta.12.tgz#02fd26e7a28f2debf7d890364a077020e4eab7b7" @@ -3450,6 +3480,17 @@ side-channel "^1.0.4" yargs "^14.0.0" +"@eyeseetea/d2-logger@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@eyeseetea/d2-logger/-/d2-logger-1.1.0.tgz#9a78b5e5c624bfc7550c7cab019ab9321d17ded0" + integrity sha512-2abLQDoT0A9wAVt2h2vr8UcRqb5ygZrUrU51lT9Ljs/X1wgFuKM/fqghOe714xvieFmPc/6K3o3FfabQ0rSCCg== + dependencies: + "@babel/runtime" "^7.5.4" + "@eyeseetea/d2-api" "1.14.0" + cmd-ts "0.13.0" + real-cancellable-promise "^1.1.2" + typed-immutable-map "0.2.0" + "@eyeseetea/d2-ui-components@v2.9.0-beta.2": version "2.9.0-beta.2" resolved "https://registry.yarnpkg.com/@eyeseetea/d2-ui-components/-/d2-ui-components-2.9.0-beta.2.tgz#7df5ea659ed1d487d78301f8e0c1f735dcb9f6e0" @@ -5533,6 +5574,16 @@ clsx@^2.1.0, clsx@^2.1.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== +cmd-ts@0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/cmd-ts/-/cmd-ts-0.13.0.tgz#57bdbc5dc95eb5a3503ab3ac9591c91427a79fa1" + integrity sha512-nsnxf6wNIM/JAS7T/x/1JmbEsjH0a8tezXqqpaL0O6+eV0/aDEnRxwjxpu0VzDdRcaC1ixGSbRlUuf/IU59I4g== + dependencies: + chalk "^4.0.0" + debug "^4.3.4" + didyoumean "^1.2.2" + strip-ansi "^6.0.0" + cmd-ts@^0.12.1: version "0.12.1" resolved "https://registry.yarnpkg.com/cmd-ts/-/cmd-ts-0.12.1.tgz#5ddf69f27887e7380ce6d50a07a3850cb82ea3f7" @@ -11060,6 +11111,11 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typed-immutable-map@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/typed-immutable-map/-/typed-immutable-map-0.2.0.tgz#42c16f261fc0a75da0358298a2aae248a0c0b7d5" + integrity sha512-eraiB5BugF8ZjfpcQWcBArZUPYBUQ5ziy8w/BDvdb7uKBTb5aUyd+jWkiNn+ADvH/Wtl7w2ppVNDSoIGaMWWeg== + typed-immutable-map@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/typed-immutable-map/-/typed-immutable-map-0.1.1.tgz#4f7d67c6afa3daa2eaa09c0afa1b9d8ef35a6045"