/**
 * Copyright 2017 - 2019  The Hyve B.V.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */ import { PedigreeConstraint } from '../../models/constraint-models/pedigree-constraint' ; import { DateOperatorState } from '../../models/constraint-models/date-operator-state' ; import { TrueConstraint } from '../../models/constraint-models/true-constraint' ; import { ValueConstraint } from '../../models/constraint-models/value-constraint' ; import { NegationConstraint } from '../../models/constraint-models/negation-constraint' ; import { CombinationState } from '../../models/constraint-models/combination-state' ; import { ConceptConstraint } from '../../models/constraint-models/concept-constraint' ; import { TimeConstraint } from '../../models/constraint-models/time-constraint' ; import { SubjectSetConstraint } from '../../models/constraint-models/subject-set-constraint' ; import { TrialVisitConstraint } from '../../models/constraint-models/trial-visit-constraint' ; import { CombinationConstraint } from '../../models/constraint-models/combination-constraint' ; import { StudyConstraint } from '../../models/constraint-models/study-constraint' ; import { AbstractConstraintVisitor } from '../constraint-utilities/abstract-constraint-visitor' ; import { ExtendedConstraint , TransmartCombinationConstraint , TransmartConceptConstraint , TransmartConstraint , TransmartFieldConstraint , TransmartNegationConstraint , TransmartPatientSetConstraint , TransmartRelationConstraint , TransmartSubSelectionConstraint , TransmartTimeConstraint , TransmartTrueConstraint , TransmartValueConstraint } from '../../models/transmart-models/transmart-constraint' ; import { Constraint } from '../../models/constraint-models/constraint' ; import { TransmartConstraintRewriter } from './transmart-constraint-rewriter' ; import { Operator , OperatorValues } from '../../models/constraint-models/operator' ; import { ValueType } from '../../models/constraint-models/value-type' ; import { ConceptType } from '../../models/constraint-models/concept-type' ; /**
 * Serialisation class for serialising constraint objects for use in the TranSMART API.
 * There are two modes:
 * - default: generating objects with only the fields allowed by the API;
 *   (Create with <code>let serialiser = new TransmartConstraintSerialiser(false)</code>)
 * - full: decorating concept constraints with extra information, to be used when saving
 *   queries to be later restored.
 *   (Create with <code>let serialiser = new TransmartConstraintSerialiser(true)</code>)
 *
 * To serialise a constraint, use <code>serialiser.visit(constraint)</code>.
 */ export class TransmartConstraintSerialiser extends AbstractConstraintVisitor < TransmartConstraint > { private readonly _full : boolean ; param { TransmartConstraint } constraint returns { TransmartConstraint } static unWrapNestedQueryObject ( constraint ) { if ( ( constraint . type === 'and' || constraint . type === 'or' ) && constraint [ 'args' ] . length === 1 ) { return this . unWrapNestedQueryObject ( constraint [ 'args' ] [ 0 ] ) ; } return constraint ; } param { string } dimension param { TransmartConstraint } constraint returns { TransmartConstraint } static wrapWithSubselection ( dimension , constraint ) { const queryConstraint = this . unWrapNestedQueryObject ( constraint ) ; if ( queryConstraint . type === 'true' && dimension === 'patient' ) { return { 'type' : 'true' } ; } if ( queryConstraint . type === 'negation' ) { const arg = this . wrapWithSubselection ( dimension , ( < TransmartNegationConstraint > queryConstraint ) . arg ) ; if ( arg . type === 'subselection' ) { return { type : 'subselection' , dimension : dimension , constraint : new TransmartNegationConstraint ( arg ) } as TransmartSubSelectionConstraint ; } else { return new TransmartNegationConstraint ( arg ) ; } } else { return { type : 'subselection' , dimension : dimension , constraint : queryConstraint } as TransmartSubSelectionConstraint ; } } static convertOperator ( operator ) { if ( ( OperatorValues as string [ ] ) . includes ( operator ) ) { return < Operator > operator ; } throw new Error ( `Unknown operator: ${ operator }` ) ; } constructor ( full ) { super ( ) ; this . _full = full ; } visitValueConstraint ( constraint ) { return { type : 'value' , valueType : constraint . valueType , operator : TransmartConstraintSerialiser . convertOperator ( constraint . operator ) , value : constraint . value } ; } visitTrueConstraint ( constraint ) { return { type : 'true' } ; } visitTrialVisitConstraint ( constraint ) { const trialVisitIds = constraint . trialVisits . map ( trialVisit => Number ( trialVisit . id ) ) ; return { type : 'field' , field : { dimension : 'trial visit' , fieldName : 'id' , type : ValueType . numeric } , operator : Operator . in , value : trialVisitIds } ; } visitTimeConstraint ( constraint ) { const operator = { [ DateOperatorState . BETWEEN ] : Operator . between , [ DateOperatorState . NOT_BETWEEN ] : Operator . between , [ DateOperatorState . BEFORE ] : Operator . before , [ DateOperatorState . AFTER ] : Operator . after } [ constraint . dateOperator ] ; let values = [ constraint . date1 . getTime ( ) ] ; if ( constraint . dateOperator === DateOperatorState . BETWEEN || constraint . dateOperator === DateOperatorState . NOT_BETWEEN ) { values . push ( constraint . date2 . getTime ( ) ) ; } let dimension = constraint . isObservationDate ? 'start time' : 'value' ; let fieldName = constraint . isObservationDate ? 'startDate' : 'numberValue' ; const result = { type : 'time' , field : { dimension : dimension , fieldName : fieldName , type : ValueType . date } , operator : operator , values : values } ; if ( constraint . dateOperator === DateOperatorState . NOT_BETWEEN ) { return new TransmartNegationConstraint ( result ) ; } return result ; } visitSubjectSetConstraint ( constraint ) { let result = null ; const type = 'patient_set' ; if ( constraint . subjectIds . length > 0 ) { result = { type : type , subjectIds : constraint . subjectIds } ; } else if ( constraint . patientIds . length > 0 ) { result = { type : type , patientIds : constraint . patientIds } ; } else if ( constraint . id ) { result = { type : type , patientSetId : constraint . id } ; } return result ; } visitStudyConstraint ( constraint ) { let result = null ; if ( constraint . studies . length !== 0 ) { let childQueryObjects = [ ] ; for ( let study of constraint . studies ) { childQueryObjects . push ( { type : 'study_name' , studyId : study . id } ) ; } if ( childQueryObjects . length === 1 ) { result = childQueryObjects [ 0 ] ; } else { result = { type : 'or' , args : childQueryObjects } ; } } return result ; } visitPedigreeConstraint ( constraint ) { const result = { type : 'relation' , relatedSubjectsConstraint : this . visit ( constraint . rightHandSideConstraint ) , relationTypeLabel : constraint . label } ; if ( constraint . biological !== undefined ) { result . biological = constraint . biological ; } if ( constraint . shareHousehold !== undefined ) { result . shareHousehold = constraint . shareHousehold ; } return result ; } visitNegationConstraint ( constraint ) { return { type : 'negation' , arg : this . visit ( constraint . constraint ) } ; } param { ConceptConstraint } constraint returns { object } visitConceptConstraint ( constraint ) { let result = null ; if ( ! constraint . concept ) { throw new Error ( 'Cannot serialise concept constraint without concept' ) ; } const args = [ ] ; const conceptConstraint = { type : 'concept' , conceptCode : constraint . concept . code , } ; args . push ( conceptConstraint ) ; if ( constraint . valueConstraints . length > 0 ) { if ( constraint . concept . type === ConceptType . NUMERICAL ) { for ( let val of constraint . valueConstraints ) { args . push ( this . visit ( val ) ) ; } } else if ( constraint . concept . type === ConceptType . CATEGORICAL ) { let categorical = { type : 'or' , args : constraint . valueConstraints . map ( ( val ) => this . visit ( val ) ) } ; if ( categorical . args . length === 1 ) { args . push ( categorical . args [ 0 ] ) ; } else { args . push ( categorical ) ; } } else { throw new Error ( `Concept type not supported: ${ constraint . concept . type }` ) ; } } if ( constraint . applyValDateConstraint ) { args . push ( this . visit ( constraint . valDateConstraint ) ) ; } if ( constraint . applyObsDateConstraint ) { args . push ( this . visit ( constraint . obsDateConstraint ) ) ; } if ( constraint . applyTrialVisitConstraint && constraint . trialVisitConstraint . trialVisits . length > 0 ) { args . push ( this . visit ( constraint . trialVisitConstraint ) ) ; } if ( constraint . applyStudyConstraint && constraint . studyConstraint . studies . length > 0 ) { args . push ( this . visit ( constraint . studyConstraint ) ) ; } if ( args . length === 1 ) { result = args [ 0 ] ; } else { result = { type : 'and' , args : args } ; } if ( this . _full ) { const fullConstraint = < ExtendedConstraint > result ; fullConstraint . name = constraint . concept . name ; fullConstraint . fullName = constraint . concept . fullName ; fullConstraint . valueType = constraint . concept . type ; result = fullConstraint ; } return result ; } param { CombinationConstraint } constraint returns { TransmartConstraint [ ] } private getNonEmptyChildObjectsWithSubselection ( combination ) { const nonEmptyChildren = combination . children . reduce ( ( result , child ) => { const queryObject = this . visit ( child ) ; if ( ! queryObject ) { return result ; } if ( combination . combinationState === CombinationState . And && queryObject . type === 'true' ) { return result ; } if ( combination . combinationState === CombinationState . Or && queryObject . type === 'negation' ) { const negatedConstraint = ( < TransmartNegationConstraint > queryObject ) . arg ; if ( negatedConstraint . type === 'true' ) { return result ; } } result . push ( this . getChildObjectsWithSubselection ( queryObject , combination , child ) ) ; return result ; } , [ ] ) ; if ( nonEmptyChildren . length === 1 && nonEmptyChildren [ 0 ] . type === 'subselection' ) { const childConstraint = ( < TransmartSubSelectionConstraint > nonEmptyChildren [ 0 ] ) . constraint ; if ( combination . dimension === 'patient' && childConstraint . type !== 'subselection' && ( combination . isRoot || ( combination . parentConstraint && combination . parentConstraint . className === 'PedigreeConstraint' ) ) ) { return [ childConstraint ] ; } } else if ( nonEmptyChildren . length === 0 && combination . dimension !== 'patient' ) { return [ TransmartConstraintSerialiser . wrapWithSubselection ( combination . dimension , { 'type' : 'true' } as TransmartTrueConstraint ) ] ; } return nonEmptyChildren ; } param { TransmartConstraint } queryObject param { CombinationConstraint } combination param { Constraint } child returns { TransmartConstraint } private getChildObjectsWithSubselection ( queryObject , combination , child ) { if ( combination . dimension === 'patient' && ( child . className === 'SubjectSetConstraint' || child . className === 'PedigreeConstraint' ) ) { return queryObject ; } if ( queryObject . type === 'subselection' && combination . parentDimension === combination . dimension ) { return queryObject ; } else if ( combination . dimension === 'observation' || ( child . className === 'CombinationConstraint' && ( < CombinationConstraint > child ) . dimension === 'observation' ) ) { return queryObject ; } else { return TransmartConstraintSerialiser . wrapWithSubselection ( combination . dimension , queryObject ) ; } } visitCombinationConstraint ( constraint ) { const children = this . getNonEmptyChildObjectsWithSubselection ( constraint ) ; if ( children . length === 1 ) { return children [ 0 ] ; } const result = { type : constraint . combinationState === CombinationState . And ? 'and' : 'or' , args : children } as TransmartCombinationConstraint ; return new TransmartConstraintRewriter ( ) . visit ( result ) ; } }