diff --git a/api/src/engine/connectors/csv/csv.connector.ts b/api/src/engine/connectors/csv/csv.connector.ts index 9d1e2e7a4697ec6faf9edefe4372c8f41fe78b04..9397ddbc7ddc31fbc3b2d9d8d2fdd7358850405b 100644 --- a/api/src/engine/connectors/csv/csv.connector.ts +++ b/api/src/engine/connectors/csv/csv.connector.ts @@ -1,11 +1,10 @@ import { HttpService } from '@nestjs/axios'; import { NotImplementedException } from '@nestjs/common'; import { firstValueFrom } from 'rxjs'; -import { - Dictionary, - ExperimentResult, -} from '../../../common/interfaces/utilities.interface'; -import Connector from '../../../engine/interfaces/connector.interface'; +import { Dictionary } from '../../../common/interfaces/utilities.interface'; +import Connector, { + RunResult, +} from '../../../engine/interfaces/connector.interface'; import EngineOptions from '../../../engine/interfaces/engine-options.interface'; import { Domain } from '../../../engine/models/domain.model'; import { Algorithm } from '../../../engine/models/experiment/algorithm.model'; @@ -26,7 +25,7 @@ export default class CSVConnector implements Connector { throw new NotImplementedException(); } - async runExperiment(): Promise<ExperimentResult[]> { + async runExperiment(): Promise<RunResult> { throw new NotImplementedException(); } diff --git a/api/src/engine/connectors/datashield/datashield.connector.ts b/api/src/engine/connectors/datashield/datashield.connector.ts index 135d72682a0b5b35c6fce8b8406d218226c50749..d09e8f8dfbfa9995ad94384b79b9248bf17f0263 100644 --- a/api/src/engine/connectors/datashield/datashield.connector.ts +++ b/api/src/engine/connectors/datashield/datashield.connector.ts @@ -2,20 +2,19 @@ import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException, Logger } from '@nestjs/common'; import { Request } from 'express'; import { catchError, firstValueFrom } from 'rxjs'; -import { - ExperimentResult, - MIME_TYPES, -} from '../../../common/interfaces/utilities.interface'; +import { ExperimentResult } from '../../../common/interfaces/utilities.interface'; import { errorAxiosHandler } from '../../../common/utils/shared.utils'; import EngineService from '../../../engine/engine.service'; import ConnectorConfiguration from '../../../engine/interfaces/connector-configuration.interface'; -import Connector from '../../../engine/interfaces/connector.interface'; +import Connector, { + RunResult, +} from '../../../engine/interfaces/connector.interface'; import EngineOptions from '../../../engine/interfaces/engine-options.interface'; import { Domain } from '../../../engine/models/domain.model'; import { Algorithm } from '../../../engine/models/experiment/algorithm.model'; import { AllowedLink } from '../../../engine/models/experiment/algorithm/nominal-parameter.model'; import { Experiment } from '../../../engine/models/experiment/experiment.model'; -import { RawResult } from '../../../engine/models/result/raw-result.model'; +import { AlertLevel } from '../../../engine/models/result/alert-result.model'; import { TableResult, TableStyle, @@ -23,6 +22,7 @@ import { import { Variable } from '../../../engine/models/variable.model'; import { ExperimentCreateInput } from '../../../experiments/models/input/experiment-create.input'; import { User } from '../../../users/models/user.model'; +import handlers from './handlers'; import { dataToGroups, dsGroup, @@ -120,12 +120,7 @@ export default class DataShieldConnector implements Connector { { name: 'pos-level', label: 'Positive level', - linkedTo: AllowedLink.VARIABLE, - isRequired: true, - }, - { - name: 'neg-level', - label: 'Negative level', + hint: 'All other categories will be considered negative', linkedTo: AllowedLink.VARIABLE, isRequired: true, }, @@ -138,7 +133,7 @@ export default class DataShieldConnector implements Connector { variable: Variable, datasets: string[], cookie?: string, - ): Promise<RawResult> { + ): Promise<ExperimentResult> { const url = new URL(this.options.baseurl + `histogram`); url.searchParams.append('var', variable.id); @@ -159,12 +154,9 @@ export default class DataShieldConnector implements Connector { DataShieldConnector.logger.warn('Cannot parse histogram result'); DataShieldConnector.logger.verbose(path); return { - rawdata: { - data: - 'Engine error when processing the request. Reason: ' + - response.data, - type: MIME_TYPES.ERROR, - }, + level: AlertLevel.ERROR, + message: + 'Engine error when processing the request. Reason: ' + response.data, }; } @@ -250,7 +242,7 @@ export default class DataShieldConnector implements Connector { async runExperiment( data: ExperimentCreateInput, request: Request, - ): Promise<ExperimentResult[]> { + ): Promise<RunResult> { const user = request.user as User; const cookie = [`sid=${user.extraFields['sid']}`, `user=${user.id}`].join( ';', @@ -281,7 +273,7 @@ export default class DataShieldConnector implements Connector { switch (data.algorithm.id) { case 'MULTIPLE_HISTOGRAMS': { - expResult.results = await Promise.all<RawResult>( + expResult.results = await Promise.all<ExperimentResult>( allVariables.map((variable) => this.getHistogram(variable, expResult.datasets, cookie), ), @@ -304,9 +296,47 @@ export default class DataShieldConnector implements Connector { expResult.results = results; break; } + default: { + expResult.results = []; + await this.runAlgorithm(expResult, allVariables, cookie); + } } - return expResult.results; + return { + results: expResult.results, + status: expResult.status, + }; + } + + private async runAlgorithm( + experiment: Experiment, + vars: Variable[], + cookie?: string, + ) { + const path = new URL('/runAlgorithm', this.options.baseurl); + + // Covariable and variable are inversed in Datashield API + const coVariable = + experiment.variables.length > 0 ? experiment.variables[0] : undefined; + + const result = await firstValueFrom( + this.httpService.post( + path.href, + { + coVariable, + variables: experiment.coVariables, + algorithm: { + id: experiment.algorithm.name, + }, + datasets: experiment.datasets, + }, + { + headers: { cookie, 'Content-Type': 'application/json' }, + }, + ), + ); + + handlers(experiment, result.data, vars); } async logout(request: Request): Promise<void> { diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/error-algorithm.handler.spec.ts b/api/src/engine/connectors/datashield/handlers/algorithms/error-algorithm.handler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c98aac3c296578a453816679b11688ec9da6aa98 --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/algorithms/error-algorithm.handler.spec.ts @@ -0,0 +1,44 @@ +import { AlertResult } from '../../../../models/result/alert-result.model'; +import { Experiment } from '../../../../models/experiment/experiment.model'; +import ErrorAlgorithmHandler from './error-algorithm.handler'; + +const vars = []; + +const createExperiment = (): Experiment => ({ + id: 'dummy-id', + name: 'Testing purpose', + algorithm: { + name: 'LINEAR_REGRESSION', + }, + datasets: ['desd-synthdata'], + domain: 'dementia', + variables: ['lefthippocampus'], + coVariables: ['righthippocampus', 'leftamygdala'], + results: [], +}); + +const data = { + errorMessage: [['There is an error']], +}; + +describe('Error handler', () => { + let errorHandler: ErrorAlgorithmHandler; + let exp: Experiment; + + beforeEach(() => { + errorHandler = new ErrorAlgorithmHandler(); + exp = createExperiment(); + }); + + describe('Handle', () => { + it('should output an error message', () => { + errorHandler.handle(exp, data, vars); + + expect(exp.results).toHaveLength(1); + + const result = exp.results[0] as AlertResult; + + expect(result.message).toBe(data.errorMessage[0][0]); + }); + }); +}); diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/error-algorithm.handler.ts b/api/src/engine/connectors/datashield/handlers/algorithms/error-algorithm.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..5332b195c2e3b18b9be2cf3ded1198de4f85db00 --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/algorithms/error-algorithm.handler.ts @@ -0,0 +1,34 @@ +import { Variable } from '../../../../models/variable.model'; + +import { AlertLevel } from '../../../../models/result/alert-result.model'; +import { + Experiment, + ExperimentStatus, +} from '../../../../models/experiment/experiment.model'; +import BaseHandler from '../base.handler'; + +export default class ErrorAlgorithmHandler extends BaseHandler { + canHandle(data: any) { + const errors = data.errorMessage; + + return errors && errors[0] && errors[0][0] && errors[0][0] !== 'No errors'; + } + + handle(experiment: Experiment, data: any, vars: Variable[]): void { + if (!this.canHandle(data)) { + return this.next?.handle(experiment, data, vars); + } + + const errors = data.errorMessage as string[][]; + + errors + .filter((err) => err) + .map((error) => ({ + level: AlertLevel.ERROR, + message: error.join(' '), + })) + .forEach((error) => experiment.results.push(error)); + + experiment.status = ExperimentStatus.ERROR; + } +} diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.spec.ts b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca1f91e8b2e2d6de8af4ed0357420993d97531ea --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.spec.ts @@ -0,0 +1,108 @@ +import { Experiment } from '../../../../models/experiment/experiment.model'; +import { TableResult } from '../../../../models/result/table-result.model'; +import { Variable } from '../../../../models/variable.model'; +import LinearRegressionHandler from './linear-regression.handler'; + +const data = { + Nvalid: 107, + Nmissing: 506, + Ntotal: 613, + 'disclosure.risk': [[0]], + errorMessage: [['No errors']], + nsubs: 107, + iter: 3, + formula: + 'Alanine.aminotransferase..Enzymatic.activity.volume..in.Serum.or.Plasma ~ Urea.nitrogen..Mass.volume..in.Serum.or.Plasma + Albumin..Mass.volume..in.Serum.or.Plasma', + coefficients: [ + { + Estimate: -0.2119, + 'Std. Error': 30.8718, + 'z-value': -0.0069, + 'p-value': 0.9945, + 'low0.95CI': -60.7195, + 'high0.95CI': 60.2956, + _row: '(Intercept)', + }, + { + Estimate: 0.1606, + 'Std. Error': 0.4521, + 'z-value': 0.3553, + 'p-value': 0.7224, + 'low0.95CI': -0.7255, + 'high0.95CI': 1.0468, + _row: 'Urea.nitrogen..Mass.volume..in.Serum.or.Plasma', + }, + { + Estimate: 3.7592, + 'Std. Error': 6.8887, + 'z-value': 0.5457, + 'p-value': 0.5853, + 'low0.95CI': -9.7423, + 'high0.95CI': 17.2608, + _row: 'Albumin..Mass.volume..in.Serum.or.Plasma', + }, + ], + dev: 15817.4616, + df: 104, + 'output.information': + 'SEE TOP OF OUTPUT FOR INFORMATION ON MISSING DATA AND ERROR MESSAGES', +}; + +const createExperiment = (): Experiment => ({ + id: 'dummy-id', + name: 'Testing purpose', + algorithm: { + name: 'linear-regression', + }, + datasets: ['sophia.db'], + domain: 'dementia', + variables: ['lefthippocampus'], + coVariables: ['righthippocampus', 'leftamygdala'], + results: [], +}); + +const vars: Variable[] = [ + { + id: 'Alanine.aminotransferase..Enzymatic.activity.volume..in.Serum.or.Plasma', + label: + 'Alanine aminotransferase Enzymatic activity volume in Serum or Plasma', + }, + { + id: 'Urea.nitrogen..Mass.volume..in.Serum.or.Plasma', + label: 'Urea nitrogen Mass volume in Serum or Plasma', + }, + { + id: 'Albumin..Mass.volume..in.Serum.or.Plasma', + label: 'Albumin Mass volume in Serum or Plasma', + }, +]; + +describe('linear regression result handler', () => { + let linearHandler: LinearRegressionHandler; + let experiment: Experiment; + + beforeEach(() => { + linearHandler = new LinearRegressionHandler(); + experiment = createExperiment(); + }); + + describe('Handle', () => { + it('should output a tableResult', () => { + linearHandler.handle(experiment, data, vars); + + expect(experiment.results).toHaveLength(1); + + const result = experiment.results[0] as TableResult; + + expect(result.headers).toHaveLength(7); + data.coefficients.forEach((coef, index) => { + expect(result.data[index][1]).toBe(coef.Estimate); + expect(result.data[index][2]).toBe(coef['Std. Error']); + expect(result.data[index][3]).toBe(coef['z-value']); + expect(result.data[index][4]).toBe(coef['p-value']); + expect(result.data[index][5]).toBe(coef['low0.95CI']); + expect(result.data[index][6]).toBe(coef['high0.95CI']); + }); + }); + }); +}); diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..623ca2190b5db5cf08c78e98f51ba76dcac0761e --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts @@ -0,0 +1,58 @@ +import { Experiment } from '../../../../models/experiment/experiment.model'; +import { TableResult } from '../../../../models/result/table-result.model'; +import { Variable } from '../../../../models/variable.model'; +import BaseHandler from '../base.handler'; + +const lookupDict = { + Estimate: 'Estimate', + 'Std. Error': 'Std. Error', + 'z-value': 'z-value', + 'p-value': 'p-value', + 'low0.95CI': 'Low 95% CI', + 'high0.95CI': 'High 95% CI', + _row: '', +}; + +const properties = [ + '_row', + 'Estimate', + 'Std. Error', + 'z-value', + 'p-value', + 'low0.95CI', + 'high0.95CI', +]; + +export default class LinearRegressionHandler extends BaseHandler { + canHandle(algorithm: string, data: any): boolean { + return ( + algorithm.toLowerCase() === 'linear-regression' && data['coefficients'] + ); + } + + private getTableResult(data: any, vars: Variable[]): TableResult { + return { + name: 'Results', + headers: properties.map((name) => ({ + name: lookupDict[name], + type: 'string', + })), + data: data['coefficients'].map((row: any) => { + const variable = vars.find((v) => v.id === row['_row']); + if (variable) row['_row'] = variable.label ?? variable.id; + return properties.map((name) => row[name]); + }), + }; + } + + handle(experiment: Experiment, data: unknown, vars: Variable[]): void { + if (!this.canHandle(experiment.algorithm.name, data)) + return this.next?.handle(experiment, data, vars); + + const tableResult = this.getTableResult(data, vars); + + if (tableResult) { + experiment.results.push(tableResult); + } + } +} diff --git a/api/src/engine/connectors/datashield/handlers/algorithms/terminal-algorithm.handler.ts b/api/src/engine/connectors/datashield/handlers/algorithms/terminal-algorithm.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..17df4bf0962e99598383ed1403a8b99e77618338 --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/algorithms/terminal-algorithm.handler.ts @@ -0,0 +1,22 @@ +import { + AlertLevel, + AlertResult, +} from '../../../../models/result/alert-result.model'; +import { + Experiment, + ExperimentStatus, +} from '../../../../models/experiment/experiment.model'; +import BaseHandler from '../base.handler'; + +export default class TerminalAlgorithmHandler extends BaseHandler { + handle(experiment: Experiment): void { + const alertResult: AlertResult = { + level: AlertLevel.ERROR, + message: + "The algorithm's result cannot be processed by the engine. Either the algorithm is not supported or the result is not in the expected format.", + }; + + experiment.results.push(alertResult); + experiment.status = ExperimentStatus.WARN; + } +} diff --git a/api/src/engine/connectors/datashield/handlers/base.handler.ts b/api/src/engine/connectors/datashield/handlers/base.handler.ts new file mode 100644 index 0000000000000000000000000000000000000000..f90c575e1f2f06935046608e6fb60d161d1e4f86 --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/base.handler.ts @@ -0,0 +1,19 @@ +import { Logger } from '@nestjs/common'; +import { Variable } from '../../../models/variable.model'; +import { Experiment } from '../../../models/experiment/experiment.model'; +import ResultHandler from './result-handler.interface'; + +export default abstract class BaseHandler implements ResultHandler { + protected static readonly logger = new Logger(this.name); + + next: ResultHandler = null; + + setNext(h: ResultHandler): ResultHandler { + this.next = h; + return h; + } + + handle(experiment: Experiment, data: unknown, vars: Variable[]): void { + this.next?.handle(experiment, data, vars); + } +} diff --git a/api/src/engine/connectors/datashield/handlers/index.ts b/api/src/engine/connectors/datashield/handlers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3275a8ea79c49c11a5cf32e8dc41da0f703b0115 --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/index.ts @@ -0,0 +1,20 @@ +import { Variable } from 'src/engine/models/variable.model'; +import { Experiment } from '../../../../engine/models/experiment/experiment.model'; +import ErrorAlgorithmHandler from './algorithms/error-algorithm.handler'; +import LinearRegressionHandler from './algorithms/linear-regression.handler'; +import TerminalAlgorithmHandler from './algorithms/terminal-algorithm.handler'; + +const start = new ErrorAlgorithmHandler(); + +start + .setNext(new LinearRegressionHandler()) + .setNext(new TerminalAlgorithmHandler()); + +export default ( + exp: Experiment, + data: unknown, + vars: Variable[], +): Experiment => { + start.handle(exp, data, vars); + return exp; +}; diff --git a/api/src/engine/connectors/datashield/handlers/result-handler.interface.ts b/api/src/engine/connectors/datashield/handlers/result-handler.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..94f16bf04cd8a66822e63a91d56faf9d4ee66eb7 --- /dev/null +++ b/api/src/engine/connectors/datashield/handlers/result-handler.interface.ts @@ -0,0 +1,8 @@ +import { Experiment } from '../../../models/experiment/experiment.model'; +import { Variable } from '../../../models/variable.model'; + +// produce algo handler +export default interface ResultHandler { + setNext(h: ResultHandler): ResultHandler; + handle(partialExperiment: Experiment, data: unknown, vars: Variable[]): void; +} diff --git a/api/src/engine/connectors/local/local.connector.ts b/api/src/engine/connectors/local/local.connector.ts index bfc517a33c9d32bcf27a1db48ab50daefdd14a27..5c0d749734a9d6f9f559f98e3cc72210ae23f1e8 100644 --- a/api/src/engine/connectors/local/local.connector.ts +++ b/api/src/engine/connectors/local/local.connector.ts @@ -1,8 +1,7 @@ -import Connector from '../../interfaces/connector.interface'; +import { User } from '../../../users/models/user.model'; +import Connector, { RunResult } from '../../interfaces/connector.interface'; import { Domain } from '../../models/domain.model'; import { Algorithm } from '../../models/experiment/algorithm.model'; -import { ResultUnion } from '../../models/result/common/result-union.model'; -import { User } from '../../../users/models/user.model'; export default class LocalConnector implements Connector { async login(): Promise<User> { @@ -16,7 +15,7 @@ export default class LocalConnector implements Connector { throw new Error('Method not implemented.'); } - async runExperiment(): Promise<Array<typeof ResultUnion>> { + async runExperiment(): Promise<RunResult> { throw new Error('Method not implemented.'); } diff --git a/api/src/engine/engine.service.ts b/api/src/engine/engine.service.ts index c9661520f1cb929da165e584eced8f43bb1b3ebb..b4b1824e94b1a2b53c1caf13430b92c3a8a57bfa 100644 --- a/api/src/engine/engine.service.ts +++ b/api/src/engine/engine.service.ts @@ -10,7 +10,6 @@ import { ConfigType } from '@nestjs/config'; import { Cache } from 'cache-manager'; import { Request } from 'express'; import { Observable } from 'rxjs'; -import { ExperimentResult } from '../common/interfaces/utilities.interface'; import cacheConfig from '../config/cache.config'; import { ExperimentCreateInput } from '../experiments/models/input/experiment-create.input'; import { ExperimentEditInput } from '../experiments/models/input/experiment-edit.input'; @@ -18,17 +17,19 @@ import { UpdateUserInput } from '../users/inputs/update-user.input'; import { User } from '../users/models/user.model'; import { ENGINE_MODULE_OPTIONS } from './engine.constants'; import ConnectorConfiguration from './interfaces/connector-configuration.interface'; -import Connector from './interfaces/connector.interface'; +import Connector, { RunResult } from './interfaces/connector.interface'; import EngineOptions from './interfaces/engine-options.interface'; import { Domain } from './models/domain.model'; import { Algorithm } from './models/experiment/algorithm.model'; import { Experiment, + ExperimentStatus, PartialExperiment, } from './models/experiment/experiment.model'; import { ListExperiments } from './models/experiment/list-experiments.model'; import { FilterConfiguration } from './models/filter/filter-configuration'; import { FormulaOperation } from './models/formula/formula-operation.model'; +import { AlertLevel } from './models/result/alert-result.model'; import { Variable } from './models/variable.model'; const DOMAINS_CACHE_KEY = 'domains'; @@ -161,9 +162,17 @@ export default class EngineService implements Connector { async runExperiment( data: ExperimentCreateInput, req?: Request, - ): Promise<ExperimentResult[]> { + ): Promise<RunResult> { if (!this.connector.runExperiment) throw new NotImplementedException(); - return this.connector.runExperiment(data, req); + return this.connector.runExperiment(data, req).catch((err) => ({ + results: [ + { + level: AlertLevel.ERROR, + message: `Error while running experiment, details '${err}'`, + }, + ], + status: ExperimentStatus.ERROR, + })); } async listExperiments?( diff --git a/api/src/engine/interfaces/connector.interface.ts b/api/src/engine/interfaces/connector.interface.ts index f1cd94914674d242cd7967861f9355fb2ab3809f..bca83dfda48fa27a7cef0538ea9f581e6e33c78e 100644 --- a/api/src/engine/interfaces/connector.interface.ts +++ b/api/src/engine/interfaces/connector.interface.ts @@ -9,6 +9,7 @@ import { Domain } from '../models/domain.model'; import { Algorithm } from '../models/experiment/algorithm.model'; import { Experiment, + ExperimentStatus, PartialExperiment, } from '../models/experiment/experiment.model'; import { ListExperiments } from '../models/experiment/list-experiments.model'; @@ -16,6 +17,10 @@ import { FilterConfiguration } from '../models/filter/filter-configuration'; import { FormulaOperation } from '../models/formula/formula-operation.model'; import ConnectorConfiguration from './connector-configuration.interface'; +export type RunResult = { + results: ExperimentResult[]; + status?: ExperimentStatus; +}; export default interface Connector { /** * Allow specific configuration for the engine @@ -51,7 +56,7 @@ export default interface Connector { runExperiment?( data: ExperimentCreateInput, req?: Request, - ): Promise<ExperimentResult[]>; + ): Promise<RunResult>; /** * Get a list of experiment (limited to 10 per page) diff --git a/api/src/experiments/experiments.resolver.ts b/api/src/experiments/experiments.resolver.ts index 3739933dfa2c2b736f4851375afc0f272e456b3c..c96416a187c23e2a1e304bc681db99fb8ce8fa4f 100644 --- a/api/src/experiments/experiments.resolver.ts +++ b/api/src/experiments/experiments.resolver.ts @@ -1,10 +1,10 @@ import { Logger, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Request } from 'express'; -import EngineService from '../engine/engine.service'; import { GlobalAuthGuard } from '../auth/guards/global-auth.guard'; import { GQLRequest } from '../common/decorators/gql-request.decoractor'; import { CurrentUser } from '../common/decorators/user.decorator'; +import EngineService from '../engine/engine.service'; import { Experiment, ExperimentStatus, @@ -77,14 +77,14 @@ export class ExperimentsResolver { return this.engineService.createExperiment(data, isTransient, req); } - //if the experiment is transient we wait a response before returning a response + //if the experiment is transient we wait a connector's response before returning a client's response if (isTransient) { const results = await this.engineService.runExperiment(data, req); const expTransient = this.experimentService.dataToExperiment(data, user); return { ...expTransient, results, status: ExperimentStatus.SUCCESS }; } - //if not we will create an experiment in local db + //if not transient we will create an experiment in local db const experiment = await this.experimentService.create( data, user, @@ -92,13 +92,13 @@ export class ExperimentsResolver { ); //create an async query that will update the result when it's done - this.engineService.runExperiment(data, req).then((results) => { + this.engineService.runExperiment(data, req).then((runResult) => { this.experimentService.update( experiment.id, { - results, + status: ExperimentStatus.SUCCESS, // default status + ...runResult, finishedAt: new Date().toISOString(), - status: ExperimentStatus.SUCCESS, }, user, ); diff --git a/api/src/schema.gql b/api/src/schema.gql index 88e2456ea1e31782c278e0e2b1bebb63c59502bd..9ac6694a8a9c43a82c3770c9f4aa56cb1106dd3e 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -21,157 +21,6 @@ type User { agreeNDA: Boolean } -type AuthenticationOutput { - accessToken: String! - refreshToken: String! -} - -type Configuration { - connectorId: String! - hasGalaxy: Boolean @deprecated(reason: "Only used for legacy reason should be avoided") - - """Indicates if histograms can handle grouping""" - hasGrouping: Boolean - - """Indicates if filters and formula are enabled""" - hasFilters: Boolean - version: String! - skipAuth: Boolean - skipTos: Boolean - enableSSO: Boolean -} - -type Dataset { - id: String! - label: String - isLongitudinal: Boolean -} - -type Group { - id: String! - label: String - description: String - groups: [String!] - - """List of variable's ids""" - variables: [String!] - - """List of datasets avalaible, set null if all datasets allowed""" - datasets: [String!] -} - -type Category { - value: String! - label: String -} - -type Variable { - id: String! - label: String - type: String - description: String - enumerations: [Category!] - groups: [Group!] - - """List of datasets avalaible, set null if all datasets allowed""" - datasets: [String!] -} - -type Domain { - id: String! - label: String - description: String - version: String - groups: [Group!]! - variables: [Variable!]! - datasets: [Dataset!]! - rootGroup: Group! -} - -type OptionValue { - value: String! - label: String! -} - -type NominalParameter implements BaseParameter { - name: String! - label: String - - """Small hint (description) for the end user""" - hint: String - isRequired: Boolean - hasMultiple: Boolean - defaultValue: String - - """Id of the parameter""" - linkedTo: AllowedLink - allowedValues: [OptionValue!] -} - -"""The supported links.""" -enum AllowedLink { - VARIABLE - COVARIABLE -} - -type NumberParameter implements BaseParameter { - name: String! - label: String - - """Small hint (description) for the end user""" - hint: String - isRequired: Boolean - hasMultiple: Boolean - defaultValue: String - min: Float - max: Float - isReal: Boolean -} - -type StringParameter implements BaseParameter { - name: String! - label: String - - """Small hint (description) for the end user""" - hint: String - isRequired: Boolean - hasMultiple: Boolean - defaultValue: String -} - -type VariableParameter { - hint: String - isRequired: Boolean - hasMultiple: Boolean - - """If undefined, all types are allowed""" - allowedTypes: [String!] -} - -type Algorithm { - id: String! - parameters: [BaseParameter!] - variable: VariableParameter! - coVariable: VariableParameter - hasFormula: Boolean - type: String - label: String - description: String -} - -type FilterConfiguration { - """List of types that can considered as number""" - numberTypes: [String!] -} - -type FormulaOperation { - """Type name of the variable""" - variableType: String! - - """List of operation available for this type""" - operationTypes: [String!]! -} - type ChartAxis { """label of the Axis""" label: String @@ -374,6 +223,157 @@ type PartialExperiment { algorithm: AlgorithmResult } +type AuthenticationOutput { + accessToken: String! + refreshToken: String! +} + +type Configuration { + connectorId: String! + hasGalaxy: Boolean @deprecated(reason: "Only used for legacy reason should be avoided") + + """Indicates if histograms can handle grouping""" + hasGrouping: Boolean + + """Indicates if filters and formula are enabled""" + hasFilters: Boolean + version: String! + skipAuth: Boolean + skipTos: Boolean + enableSSO: Boolean +} + +type Dataset { + id: String! + label: String + isLongitudinal: Boolean +} + +type Group { + id: String! + label: String + description: String + groups: [String!] + + """List of variable's ids""" + variables: [String!] + + """List of datasets avalaible, set null if all datasets allowed""" + datasets: [String!] +} + +type Category { + value: String! + label: String +} + +type Variable { + id: String! + label: String + type: String + description: String + enumerations: [Category!] + groups: [Group!] + + """List of datasets avalaible, set null if all datasets allowed""" + datasets: [String!] +} + +type Domain { + id: String! + label: String + description: String + version: String + groups: [Group!]! + variables: [Variable!]! + datasets: [Dataset!]! + rootGroup: Group! +} + +type OptionValue { + value: String! + label: String! +} + +type NominalParameter implements BaseParameter { + name: String! + label: String + + """Small hint (description) for the end user""" + hint: String + isRequired: Boolean + hasMultiple: Boolean + defaultValue: String + + """Id of the parameter""" + linkedTo: AllowedLink + allowedValues: [OptionValue!] +} + +"""The supported links.""" +enum AllowedLink { + VARIABLE + COVARIABLE +} + +type NumberParameter implements BaseParameter { + name: String! + label: String + + """Small hint (description) for the end user""" + hint: String + isRequired: Boolean + hasMultiple: Boolean + defaultValue: String + min: Float + max: Float + isReal: Boolean +} + +type StringParameter implements BaseParameter { + name: String! + label: String + + """Small hint (description) for the end user""" + hint: String + isRequired: Boolean + hasMultiple: Boolean + defaultValue: String +} + +type VariableParameter { + hint: String + isRequired: Boolean + hasMultiple: Boolean + + """If undefined, all types are allowed""" + allowedTypes: [String!] +} + +type Algorithm { + id: String! + parameters: [BaseParameter!] + variable: VariableParameter! + coVariable: VariableParameter + hasFormula: Boolean + type: String + label: String + description: String +} + +type FilterConfiguration { + """List of types that can considered as number""" + numberTypes: [String!] +} + +type FormulaOperation { + """Type name of the variable""" + variableType: String! + + """List of operation available for this type""" + operationTypes: [String!]! +} + type ListExperiments { currentPage: Float totalPages: Float