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 93eee53f6db901e4ac0d8dd5fac6b87627a4563a..d09e8f8dfbfa9995ad94384b79b9248bf17f0263 100644 --- a/api/src/engine/connectors/datashield/datashield.connector.ts +++ b/api/src/engine/connectors/datashield/datashield.connector.ts @@ -6,7 +6,9 @@ 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'; @@ -240,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( ';', @@ -300,7 +302,10 @@ export default class DataShieldConnector implements Connector { } } - return expResult.results; + return { + results: expResult.results, + status: expResult.status, + }; } private async runAlgorithm( 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 index af31a2ad169be180c79d58f7765657c3287c8795..5332b195c2e3b18b9be2cf3ded1198de4f85db00 100644 --- a/api/src/engine/connectors/datashield/handlers/algorithms/error-algorithm.handler.ts +++ b/api/src/engine/connectors/datashield/handlers/algorithms/error-algorithm.handler.ts @@ -1,7 +1,10 @@ import { Variable } from '../../../../models/variable.model'; import { AlertLevel } from '../../../../models/result/alert-result.model'; -import { Experiment } from '../../../../models/experiment/experiment.model'; +import { + Experiment, + ExperimentStatus, +} from '../../../../models/experiment/experiment.model'; import BaseHandler from '../base.handler'; export default class ErrorAlgorithmHandler extends BaseHandler { @@ -25,5 +28,7 @@ export default class ErrorAlgorithmHandler extends BaseHandler { 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.ts b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts index dcb3bc4b0c57b9ee39e27beb098d9ff512f5f497..623ca2190b5db5cf08c78e98f51ba76dcac0761e 100644 --- a/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts +++ b/api/src/engine/connectors/datashield/handlers/algorithms/linear-regression.handler.ts @@ -1,6 +1,6 @@ -import { Variable } from '../../../../models/variable.model'; 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 = { 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 index 14060f2ca105c87ac4045b893442ffed6734a1a9..17df4bf0962e99598383ed1403a8b99e77618338 100644 --- a/api/src/engine/connectors/datashield/handlers/algorithms/terminal-algorithm.handler.ts +++ b/api/src/engine/connectors/datashield/handlers/algorithms/terminal-algorithm.handler.ts @@ -2,7 +2,10 @@ import { AlertLevel, AlertResult, } from '../../../../models/result/alert-result.model'; -import { Experiment } from '../../../../models/experiment/experiment.model'; +import { + Experiment, + ExperimentStatus, +} from '../../../../models/experiment/experiment.model'; import BaseHandler from '../base.handler'; export default class TerminalAlgorithmHandler extends BaseHandler { @@ -14,5 +17,6 @@ export default class TerminalAlgorithmHandler extends BaseHandler { }; experiment.results.push(alertResult); + experiment.status = ExperimentStatus.WARN; } } 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 a3227f202b29d703854476ed89b6afdc84ea7da8..c96416a187c23e2a1e304bc681db99fb8ce8fa4f 100644 --- a/api/src/experiments/experiments.resolver.ts +++ b/api/src/experiments/experiments.resolver.ts @@ -1,7 +1,6 @@ import { Logger, UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Request } from 'express'; -import { AlertLevel } from '../engine/models/result/alert-result.model'; import { GlobalAuthGuard } from '../auth/guards/global-auth.guard'; import { GQLRequest } from '../common/decorators/gql-request.decoractor'; import { CurrentUser } from '../common/decorators/user.decorator'; @@ -78,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, @@ -93,35 +92,17 @@ export class ExperimentsResolver { ); //create an async query that will update the result when it's done - this.engineService - .runExperiment(data, req) - .then((results) => { - this.experimentService.update( - experiment.id, - { - results, - finishedAt: new Date().toISOString(), - status: ExperimentStatus.SUCCESS, - }, - user, - ); - }) - .catch((err) => { - this.experimentService.update( - experiment.id, - { - finishedAt: new Date().toISOString(), - results: [ - { - level: AlertLevel.ERROR, - message: `Error while running experiment, details '${err}'`, - }, - ], - status: ExperimentStatus.ERROR, - }, - user, - ); - }); + this.engineService.runExperiment(data, req).then((runResult) => { + this.experimentService.update( + experiment.id, + { + status: ExperimentStatus.SUCCESS, // default status + ...runResult, + finishedAt: new Date().toISOString(), + }, + user, + ); + }); //we return the experiment before finishing the runExperiment return experiment; 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