diff --git a/api/package.json b/api/package.json index 481ac4aeefe16a08e350d7df20fbce7e2996b9f9..7e11e6620c4fd9c8cafe1ea3c5d93e90ee7d091d 100644 --- a/api/package.json +++ b/api/package.json @@ -14,7 +14,7 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", + "test": "jest --verbose", "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", diff --git a/api/src/engine/connectors/csv/main.connector.ts b/api/src/engine/connectors/csv/main.connector.ts index d725496356e3133f54a364df1fa2158e13b14616..79c592b3f882ebf19e12df8e687990a72431ef28 100644 --- a/api/src/engine/connectors/csv/main.connector.ts +++ b/api/src/engine/connectors/csv/main.connector.ts @@ -37,8 +37,7 @@ export default class CSVService implements IEngineService { domain: '', datasets: [], algorithm: { - id: '', - description: '', + name: '', }, name: 'test', variables: [], diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts index 0055daa83b082ef08267e055ce79a36a91dbb8b9..f796f3c5a0427b895bdb56ab848eea74a2076983 100644 --- a/api/src/engine/connectors/datashield/main.connector.ts +++ b/api/src/engine/connectors/datashield/main.connector.ts @@ -159,7 +159,7 @@ export default class DataShieldService implements IEngineService { domain: data.domain, datasets: data.datasets, algorithm: { - id: data.algorithm.id, + name: data.algorithm.id, }, }; diff --git a/api/src/engine/connectors/exareme/converters.ts b/api/src/engine/connectors/exareme/converters.ts index 56f35ddcd2876a5cd6435cd3454c6fb414034923..4ba0bc25f7405d3a4a9031cf07c1d43f6aa4fe9b 100644 --- a/api/src/engine/connectors/exareme/converters.ts +++ b/api/src/engine/connectors/exareme/converters.ts @@ -1,7 +1,6 @@ import { MIME_TYPES } from 'src/common/interfaces/utilities.interface'; import { Category } from 'src/engine/models/category.model'; import { Dataset } from 'src/engine/models/dataset.model'; -import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; import { Experiment } from 'src/engine/models/experiment/experiment.model'; import { AlgorithmParamInput } from 'src/engine/models/experiment/input/algorithm-parameter.input'; import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; @@ -26,7 +25,6 @@ import { dataToHeatmap, descriptiveModelToTables, descriptiveSingleToTables, - transformToAlgorithms, transformToExperiment, } from './transformations'; @@ -45,7 +43,7 @@ export const dataToGroup = (data: Hierarchy): Group => { export const dataToCategory = (data: Entity): Category => { return { - id: data.code, + value: data.code, label: data.label, }; }; @@ -215,7 +213,8 @@ export const dataToExperiment = ( exp.results = data.result ? data.result - .map((result) => dataToResult(result, exp.algorithm.id)) + .map((result) => dataToResult(result, exp.algorithm.name)) + .filter((r) => r.length > 0) .flat() : []; @@ -237,16 +236,12 @@ export const dataToExperiment = ( ], datasets: [], algorithm: { - id: 'unknown', + name: 'unknown', }, }; } }; -export const dataToAlgorithms = (data: string): Algorithm[] => { - return transformToAlgorithms.evaluate(data); -}; - export const dataToRaw = ( algo: string, result: ResultExperiment, @@ -284,9 +279,11 @@ export const dataJSONtoResult = ( ): Array<typeof ResultUnion> => { switch (algo.toLowerCase()) { case 'descriptive_stats': + case 'cart': + case 'id3': return descriptiveDataToTableResult(result); default: - return dataToRaw(algo, result); + return []; } }; diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.spec.ts b/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.spec.ts index a7af56efcc46540e8565e6130e5d7c70e420ee8e..7b414bda9c4ae870c272c0c1fce525327fb613fa 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.spec.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.spec.ts @@ -7,7 +7,7 @@ const createExperiment = (): Experiment => ({ id: 'dummy-id', name: 'Testing purpose', algorithm: { - id: 'PCA', + name: 'PCA', }, datasets: ['desd-synthdata'], domain: 'dementia', diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.ts index 64ff970285259fd69147fe2ff8fc0b3ca9eab668..61f82be6593f6165326387e228971236679e1d84 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/PCA.handler.ts @@ -9,7 +9,8 @@ export default class PCAHandler extends BaseHandler { } handle(exp: Experiment, data: unknown): void { - if (!this.canHandle(exp.algorithm.id)) return this.next?.handle(exp, data); + if (!this.canHandle(exp.algorithm.name)) + return this.next?.handle(exp, data); const barChar: BarChartResult = { name: 'Eigen values', diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.spec.ts b/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.spec.ts index c4cd6dcf6a9f3cd8c1d837b8a46b4d310842b874..bc55d0dc7b83b6c92f2ac6ce5ffa4d7831736d07 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.spec.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.spec.ts @@ -6,7 +6,7 @@ const createExperiment = (): Experiment => ({ id: 'dummy-id', name: 'Testing purpose', algorithm: { - id: 'Anova_OnEway', + name: 'Anova_OnEway', }, datasets: ['desd-synthdata'], domain: 'dementia', diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.ts index c8d9a6750785d57df2f2637572d306cd313935ee..5b25340ea67578d4367e003807f19a76f190fc59 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/anova-one-way.handler.ts @@ -98,7 +98,7 @@ export default class AnovaOneWayHandler extends BaseHandler { } handle(exp: Experiment, data: unknown): void { - if (!this.canHandle(exp.algorithm.id)) return super.handle(exp, data); + if (!this.canHandle(exp.algorithm.name)) return super.handle(exp, data); const summaryTable = this.getSummaryTable(data, exp.coVariables[0]); if (summaryTable) exp.results.push(summaryTable); diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts index d9fbc65f432ac29931d69428078d55e72eecd2a7..56396522d7a5504f218f586962d44cf81ac461b3 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/descriptive.handler.ts @@ -113,7 +113,7 @@ $fn := function($o, $prefix) { handle(exp: Experiment, data: unknown): void { let req = data; - if (exp.algorithm.id.toLowerCase() === 'descriptive_stats') { + if (exp.algorithm.name.toLowerCase() === 'descriptive_stats') { const inputs = data as ResultExperiment[]; if (inputs && Array.isArray(inputs)) { diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.spec.ts b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.spec.ts index f3ca9a5a31aae0db39f78813c63013071b840ce0..36a2759dff39197636dad6efbbaee39d37d2e7ce 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.spec.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.spec.ts @@ -6,7 +6,7 @@ const createExperiment = (): Experiment => ({ id: 'dummy-id', name: 'Testing purpose', algorithm: { - id: 'pearson', + name: 'pearson', }, datasets: ['desd-synthdata'], domain: 'dementia', diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts index 0832d960e4ceb8bcf29551e6aaf98ebf9ffff2a5..40248088dcbb5aeff0a6b9b822d093743ed0607c 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/pearson.handler.ts @@ -41,7 +41,7 @@ export default class PearsonHandler extends BaseHandler { * @returns */ handle(exp: Experiment, data: unknown): void { - if (this.canHandle(exp.algorithm.id)) { + if (this.canHandle(exp.algorithm.name)) { try { const results = PearsonHandler.transform.evaluate( data, diff --git a/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts b/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts index 2e0dd77d9f86138dca531e1a0680286ba03fb40c..08cdac953300a1a06dee98568e37268a1999298c 100644 --- a/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts +++ b/api/src/engine/connectors/exareme/handlers/algorithms/raw.handler.ts @@ -21,7 +21,7 @@ export default class RawHandler extends BaseHandler { if (inputs && Array.isArray(inputs)) inputs .filter((input) => !!input.data && !!input.type) - .map((input) => this.dataToRaw(exp.algorithm.id, input)) + .map((input) => this.dataToRaw(exp.algorithm.name, input)) .forEach((input) => exp.results.push(input)); this.next?.handle(exp, data); diff --git a/api/src/engine/connectors/exareme/main.connector.ts b/api/src/engine/connectors/exareme/main.connector.ts index a87e303490c3338cf2b776a725cf28f74f2a8679..8a9d84b638329ee3d79c0a4d0267300127774860 100644 --- a/api/src/engine/connectors/exareme/main.connector.ts +++ b/api/src/engine/connectors/exareme/main.connector.ts @@ -31,7 +31,6 @@ import { UpdateUserInput } from 'src/users/inputs/update-user.input'; import { User } from 'src/users/models/user.model'; import { transformToUser } from '../datashield/transformations'; import { - dataToAlgorithms, dataToDataset, dataToExperiment, dataToGroup, @@ -42,6 +41,7 @@ import { ExperimentData } from './interfaces/experiment/experiment.interface'; import { ExperimentsData } from './interfaces/experiment/experiments.interface'; import { Hierarchy } from './interfaces/hierarchy.interface'; import { Pathology } from './interfaces/pathology.interface'; +import transformToAlgorithms from './transformations/algorithms'; type Headers = Record<string, string>; @@ -106,9 +106,8 @@ export default class ExaremeService implements IEngineService { const resultAPI = await firstValueFrom(this.get<string>(request, path)); - return dataToAlgorithms(resultAPI.data); + return transformToAlgorithms.evaluate(resultAPI.data); } - async getExperiment(id: string, request: Request): Promise<Experiment> { const path = this.options.baseurl + `experiments/${id}`; diff --git a/api/src/engine/connectors/exareme/transformations.ts b/api/src/engine/connectors/exareme/transformations.ts index 07e59d977343342a6686f1f71ce936ee350cd7cd..19e33b3a5f40cc832e87b368b08ba68bda893962 100644 --- a/api/src/engine/connectors/exareme/transformations.ts +++ b/api/src/engine/connectors/exareme/transformations.ts @@ -3,31 +3,6 @@ import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' -export const transformToAlgorithms = jsonata(` -( - $params := ["y", "pathology", "dataset", "filter", "x"]; - - $toArray := function($x) { $type($x) = 'array' ? $x : [$x]}; - - *.{ - 'id': name, - 'label': label, - 'description': desc, - 'parameters': $toArray(parameters[$not(name in $params)].{ - 'id': name, - 'description': desc, - 'label': label, - 'type': valueType, - 'defaultValue': defaultValue, - 'isMultiple': $boolean(valueMultiple), - 'isRequired': $boolean(valueNotBlank), - 'min': valueMin, - 'max': valueMax - }) -} -) -`); - export const transformToExperiment = jsonata(` ( $params := ["y", "pathology", "dataset", "filter", "x", "formula"]; @@ -61,10 +36,10 @@ export const transformToExperiment = jsonata(` "interactions" : $formula.interactions.[var1, var2][] }, "algorithm": { - "id": algorithm.name, + "name": algorithm.name, "parameters" : $toArray( algorithm.parameters[$not(name in $params)].({ - "id": name, + "name": name, "label": label, "value": value }) diff --git a/api/src/engine/connectors/exareme/transformations/algorithms/algorithms.spec.ts b/api/src/engine/connectors/exareme/transformations/algorithms/algorithms.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ed945272518d566d82debaff425b3b0dc148f20 --- /dev/null +++ b/api/src/engine/connectors/exareme/transformations/algorithms/algorithms.spec.ts @@ -0,0 +1,289 @@ +import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; +import { NominalParameter } from 'src/engine/models/experiment/algorithm/nominal-parameter.model'; +import transformToAlgorithms from '.'; + +describe('Algorithms', () => { + describe('when data is correct (Dummy Kaplan)', () => { + const data = [ + { + name: 'KAPLAN_MEIER', + desc: 'Kaplan-Meier Estimator for the Survival Function', + label: 'Kaplan-Meier Estimator', + type: 'python_local_global', + parameters: [ + { + name: 'y', + desc: 'A single categorical variable whose values describe a binary event.', + label: 'y', + type: 'column', + columnValuesSQLType: 'integer, text', + columnValuesIsCategorical: 'true', + value: 'alzheimerbroadcategory', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'x', + desc: 'A single categorical variable based on which patients are grouped.', + label: 'x', + type: 'column', + columnValuesSQLType: 'integer, text', + columnValuesIsCategorical: 'false', + value: 'apoe4', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'pathology', + desc: 'The name of the pathology that the dataset belongs to.', + label: 'pathology', + type: 'pathology', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: 'dementia_longitudinal', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'dataset', + desc: '', + label: 'dataset', + type: 'dataset', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: 'alzheimer_fake_cohort', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'true', + valueMultiple: 'true', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'filter', + desc: '', + label: 'filter', + type: 'filter', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: '', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'true', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'outcome_pos', + desc: '', + label: 'Positive outcome', + type: 'other', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: 'AD', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'outcome_neg', + desc: '', + label: 'Negative outcome', + type: 'other', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: 'MCI', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'test', + desc: '', + label: 'Total duration of experiment in days', + type: 'other', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: '1100', + defaultValue: null, + valueType: 'real', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: ['test', 'test2'], + }, + { + name: 'total_duration', + desc: '', + label: 'Total duration of experiment in days', + type: 'other', + columnValuesSQLType: null, + columnValuesIsCategorical: null, + value: '1100', + defaultValue: null, + valueType: 'real', + valueNotBlank: 'true', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + ], + }, + ]; + + const algorithms: Algorithm[] = transformToAlgorithms.evaluate(data); + const kaplan = algorithms[0]; + + it('should produce one algorithm', () => { + expect(algorithms.length).toEqual(1); + }); + + it('should produce two algorithms', () => { + const algorithms: Algorithm[] = transformToAlgorithms.evaluate([ + ...data, + ...data, + ]); + expect(algorithms.length).toEqual(2); + }); + + it('variable should be nominal', () => { + expect(kaplan.variable).not.toBeUndefined(); + expect(kaplan.variable?.allowedTypes).toStrictEqual(['nominal']); + }); + + it('covariable should be integer or text', () => { + expect(kaplan.coVariable).not.toBeUndefined(); + expect(kaplan.coVariable?.allowedTypes).toStrictEqual([ + 'integer', + 'text', + ]); + }); + + it('should have 4 parameters', () => { + expect(kaplan.parameters.length).toBe(4); + }); + + it('nominal parameters should have values allowed', () => { + const nominalParam = kaplan.parameters.find( + (p) => p.name === 'test', + ) as NominalParameter; + + expect(JSON.stringify(nominalParam.allowedValues)).toEqual( + JSON.stringify([ + { + value: 'test', + label: 'test', + }, + { + value: 'test2', + label: 'test2', + }, + ]), + ); + }); + + it('should have at least one linkedTo parameter', () => { + expect(kaplan.parameters.some((param) => param['linkedTo'])); + }); + }); + + describe('when data does not contains any parameters', () => { + const data = [ + { + name: 'KAPLAN_MEIER', + desc: 'Kaplan-Meier Estimator for the Survival Function', + label: 'Kaplan-Meier Estimator', + type: 'python_local_global', + parameters: [ + { + name: 'y', + desc: 'A single categorical variable whose values describe a binary event.', + label: 'y', + type: 'column', + columnValuesSQLType: 'integer, text', + columnValuesIsCategorical: 'true', + value: 'alzheimerbroadcategory', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + { + name: 'x', + desc: 'A single categorical variable based on which patients are grouped.', + label: 'x', + type: 'column', + columnValuesSQLType: 'integer, text', + columnValuesIsCategorical: 'false', + value: 'apoe4', + defaultValue: null, + valueType: 'string', + valueNotBlank: 'false', + valueMultiple: 'false', + valueMin: null, + valueMax: null, + valueEnumerations: null, + }, + ], + }, + ]; + + const algorithms: Algorithm[] = transformToAlgorithms.evaluate(data); + const kaplan = algorithms[0]; + + it('Algo parameters should be undefined', () => { + expect(kaplan.parameters).toBeUndefined; + }); + }); + + describe('when data is empty or undefined', () => { + it('should be undefined when data is undefined', () => { + const data = undefined; + const algorithms: Algorithm[] = transformToAlgorithms.evaluate(data); + + expect(algorithms).toBeUndefined(); + }); + + it('should be undefined when data is empty', () => { + const data = ` + { + dummy: + }`; + + const algorithms: Algorithm[] = transformToAlgorithms.evaluate(data); + + expect(algorithms).toBeUndefined(); + }); + }); +}); diff --git a/api/src/engine/connectors/exareme/transformations/algorithms/index.ts b/api/src/engine/connectors/exareme/transformations/algorithms/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f177cbfdee60baf64000ad170a520fb097560b0f --- /dev/null +++ b/api/src/engine/connectors/exareme/transformations/algorithms/index.ts @@ -0,0 +1,60 @@ +import * as jsonata from 'jsonata'; + +const transformToAlgorithms = jsonata(` +( + $dict:={ + 'integer': 'NumberParameter', + 'real': 'NumberParameter' + }; + $checkVal:= function($val) { $val ? $val : undefined}; + $excludedParams:= ['centers', 'formula']; + $includes:= ['ANOVA_ONEWAY','ANOVA','LINEAR_REGRESSION', + 'LOGISTIC_REGRESSION','TTEST_INDEPENDENT','TTEST_PAIRED', + 'PEARSON_CORRELATION','ID3','KMEANS','NAIVE_BAYES', + 'TTEST_ONESAMPLE','PCA','CALIBRATION_BELT','CART', + 'KAPLAN_MEIER','THREE_C']; + $linkedVars:= ['positive_level', 'negative_level', 'outcome_neg', 'outcome_pos']; + $linkedCoVars:= ['referencevalues', 'xlevels']; + $truthy:= function($val) {( + $v:= $lowercase($val); + $v='true' ? true : ($v='false'? false : undefined) + )}; + $extract:= function($v) { + $v? + { + "hint": $v.desc, + "isRequired": $truthy($checkVal($v.valueNotBlank)), + "hasMultiple": $truthy($checkVal($v.valueMultiple)), + "allowedTypes": $v.columnValuesIsCategorical = '' and $v.columnValuesSQLType = '' ? + undefined : $append($v.columnValuesIsCategorical = '' ? + ['nominal'] : [], $truthy($v.columnValuesIsCategorical) ? 'nominal' : $map(($checkVal($v.columnValuesSQLType) ~> $split(',')), $trim)) + } : undefined + }; + + $[name in $includes].{ + "id": name, + "label": $checkVal(label), + "type": type, + "description": $checkVal(desc), + "variable": parameters[(type='column' or type='formula') and name='y'] ~> $extract, + "coVariable": parameters[(type='column' or type='formula') and name='x'] ~> $extract, + "hasFormula": $boolean(parameters[(type='formula_description')]), + "parameters": parameters[type='other' and $not(name in $excludedParams)].{ + "__typename": $lookup($dict, valueType), + "name": name, + "label": label, + "hint": $checkVal(desc), + "defaultValue": (name in $linkedCoVars) ? "[]" : (defaultValue ? defaultValue : value), + "isRequired": $truthy(valueNotBlank), + "hasMultiple": $truthy(valueMultiple), + "isReal": valueType = 'real' ? true : undefined, + "min": $checkVal(valueMin), + "max": $checkVal(valueMax), + "allowedValues": $checkVal(valueEnumerations).{'value':$, 'label': $}[], + "linkedTo": (name in $linkedVars) ? "VARIABLE" : ((name in $linkedCoVars) ? "COVARIABLE" : undefined) + }[] + }[] +) +`); + +export default transformToAlgorithms; diff --git a/api/src/engine/engine.constants.ts b/api/src/engine/engine.constants.ts index 3c9ae327491d37456964f9ce2cba8a7f04ecccf5..641ceed91bb4a29953fc558479f5b667fd112808 100644 --- a/api/src/engine/engine.constants.ts +++ b/api/src/engine/engine.constants.ts @@ -1,3 +1,4 @@ export const ENGINE_MODULE_OPTIONS = 'EngineModuleOption'; export const ENGINE_SERVICE = 'EngineService'; export const ENGINE_SKIP_TOS = 'TOS_SKIP'; +export const ENGINE_ONTOLOGY_URL = 'ONTOLOGY_URL'; diff --git a/api/src/engine/engine.controller.ts b/api/src/engine/engine.controller.ts index f00a8e74803cc955fae75214d99bf561713e63cb..fc656aabb1d5d1c9f21e4a14af9cba01a5ee7440 100644 --- a/api/src/engine/engine.controller.ts +++ b/api/src/engine/engine.controller.ts @@ -12,11 +12,6 @@ export class EngineController { @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService, ) {} - @Get('/algorithms') - getAlgorithms(@Req() request: Request): Observable<string> | string { - return this.engineService.getAlgorithmsREST(request); - } - @Get('galaxy') galaxy(@Req() request: Request): Observable<string> | string { return this.engineService.getPassthrough?.('galaxy', request); diff --git a/api/src/engine/engine.interfaces.ts b/api/src/engine/engine.interfaces.ts index 3df6762aba25a3cb3cb5aedc6b8db07478f5f99e..8ca07651bcdc9883190d8ba7365546002c08ba1a 100644 --- a/api/src/engine/engine.interfaces.ts +++ b/api/src/engine/engine.interfaces.ts @@ -21,8 +21,6 @@ export interface IEngineOptions { export type IConfiguration = Pick<Configuration, 'contactLink' | 'hasGalaxy'>; export interface IEngineService { - //GraphQL - /** * Allow specific configuration for the engine * @@ -56,9 +54,6 @@ export interface IEngineService { getAlgorithms(req?: Request): Promise<Algorithm[]>; - // Standard REST API call - getAlgorithmsREST(req?: Request): Observable<string> | string; - getActiveUser?(req?: Request): Promise<User>; updateUser?( diff --git a/api/src/engine/engine.resolver.ts b/api/src/engine/engine.resolver.ts index 6260111702f3268881409f80f5db91cc197c1cba..d5acde6a200cda20f7105e610713c3cf407fac79 100644 --- a/api/src/engine/engine.resolver.ts +++ b/api/src/engine/engine.resolver.ts @@ -6,6 +6,7 @@ import { GQLRequest } from '../common/decorators/gql-request.decoractor'; import { Md5 } from 'ts-md5'; import { ENGINE_MODULE_OPTIONS, + ENGINE_ONTOLOGY_URL, ENGINE_SERVICE, ENGINE_SKIP_TOS, } from './engine.constants'; @@ -55,6 +56,7 @@ export class EngineResolver { true, ), matomo, + ontologyUrl: this.configSerivce.get(ENGINE_ONTOLOGY_URL), }; const version = Md5.hashStr(JSON.stringify(data)); diff --git a/api/src/engine/models/category.model.ts b/api/src/engine/models/category.model.ts index 2000d8f655368b17f99c6c7eee5540b32d94b9a0..4cb4460ffb878b00883419e1b34190b107882fbe 100644 --- a/api/src/engine/models/category.model.ts +++ b/api/src/engine/models/category.model.ts @@ -1,5 +1,10 @@ -import { ObjectType } from '@nestjs/graphql'; -import { BaseModel } from './entity.model'; +import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() -export class Category extends BaseModel {} +export class Category { + @Field() + value: string; + + @Field({ nullable: true }) + label?: string; +} diff --git a/api/src/engine/models/configuration.model.ts b/api/src/engine/models/configuration.model.ts index 6ffe779cc55962c0dc0c20fd0e3d47c9e4d73348..9471881a191b9bdbc33a8d181487f6888d453fc3 100644 --- a/api/src/engine/models/configuration.model.ts +++ b/api/src/engine/models/configuration.model.ts @@ -25,4 +25,7 @@ export class Configuration { @Field(() => Matomo, { nullable: true }) matomo?: Matomo; + + @Field({ nullable: true }) + ontologyUrl?: string; } diff --git a/api/src/engine/models/experiment/algorithm.model.ts b/api/src/engine/models/experiment/algorithm.model.ts index 818b3c9d5c131a58625b3d724185cb6d87bc7827..761a0986d17aee99b192e939c9385637e0251f86 100644 --- a/api/src/engine/models/experiment/algorithm.model.ts +++ b/api/src/engine/models/experiment/algorithm.model.ts @@ -1,20 +1,30 @@ import { Field, ObjectType } from '@nestjs/graphql'; -import { AlgorithmParameter } from './algorithm-parameter.model'; +import { BaseParameter } from './algorithm/base-parameter.model'; +import { VariableParameter } from './algorithm/variable-parameter.model'; @ObjectType() export class Algorithm { @Field() id: string; - @Field(() => [AlgorithmParameter], { nullable: true, defaultValue: [] }) - parameters?: AlgorithmParameter[]; + @Field(() => [BaseParameter], { nullable: true, defaultValue: [] }) + parameters?: BaseParameter[]; - @Field({ nullable: true }) - label?: string; + @Field(() => VariableParameter) + variable?: VariableParameter; + + @Field(() => VariableParameter, { nullable: true }) + coVariable?: VariableParameter; + + @Field({ nullable: true, defaultValue: false }) + hasFormula?: boolean; @Field({ nullable: true }) type?: string; + @Field({ nullable: true }) + label?: string; + @Field({ nullable: true }) description?: string; } diff --git a/api/src/engine/models/experiment/algorithm/base-parameter.model.ts b/api/src/engine/models/experiment/algorithm/base-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b60e0806b187b1c5e37ee089dc9ce1e5f6f7713 --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/base-parameter.model.ts @@ -0,0 +1,47 @@ +import { Field, InterfaceType } from '@nestjs/graphql'; +import { NominalParameter } from './nominal-parameter.model'; +import { NumberParameter } from './number-parameter.model'; +import { StringParameter } from './string-parameter.model'; + +@InterfaceType({ + resolveType(param) { + if ( + param.min || + param.max || + param.isReal || + param.__typename === 'NumberParameter' + ) + return NumberParameter; + + if ( + param.allowedValues || + param.linkedTo || + param.__typename === 'NominalParameter' + ) + return NominalParameter; + + return StringParameter; + }, +}) +export abstract class BaseParameter { + @Field() + name: string; + + @Field({ nullable: true }) + label?: string; + + @Field({ + nullable: true, + description: 'Small hint (description) for the end user', + }) + hint?: string; + + @Field({ nullable: true, defaultValue: false }) + isRequired?: boolean; + + @Field({ nullable: true, defaultValue: false }) + hasMultiple?: boolean; + + @Field({ nullable: true }) + defaultValue?: string; +} diff --git a/api/src/engine/models/experiment/algorithm/nominal-parameter.model.ts b/api/src/engine/models/experiment/algorithm/nominal-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..2008b60e545a5e91820295b5f285478df28e80fa --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/nominal-parameter.model.ts @@ -0,0 +1,40 @@ +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { BaseParameter } from './base-parameter.model'; + +enum AllowedLink { + VARIABLE = 'VARIABLE', + COVARIABLE = 'COVARIABLE', +} + +registerEnumType(AllowedLink, { + name: 'AllowedLink', + description: 'The supported links.', +}); + +@ObjectType() +export class OptionValue { + @Field() + value: string; + + @Field() + label: string; +} + +@ObjectType({ implements: () => [BaseParameter] }) +export class NominalParameter implements BaseParameter { + name: string; + label?: string; + hint?: string; + isRequired?: boolean; + hasMultiple?: boolean; + defaultValue?: string; + + @Field(() => AllowedLink, { + nullable: true, + description: 'Id of the parameter', + }) + linkedTo?: AllowedLink; + + @Field(() => [OptionValue], { defaultValue: [], nullable: true }) + allowedValues?: OptionValue[]; +} diff --git a/api/src/engine/models/experiment/algorithm/number-parameter.model.ts b/api/src/engine/models/experiment/algorithm/number-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..88b3604dcd4f8d2ac88bd2c422c5f67cf3d62fce --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/number-parameter.model.ts @@ -0,0 +1,20 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { BaseParameter } from './base-parameter.model'; +@ObjectType({ implements: () => [BaseParameter] }) +export class NumberParameter implements BaseParameter { + name: string; + label?: string; + hint?: string; + isRequired?: boolean; + hasMultiple?: boolean; + defaultValue?: string; + + @Field({ nullable: true }) + min?: number; + + @Field({ nullable: true }) + max?: number; + + @Field({ nullable: true, defaultValue: false }) + isReal?: boolean; +} diff --git a/api/src/engine/models/experiment/algorithm/string-parameter.model.ts b/api/src/engine/models/experiment/algorithm/string-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..d7c2e40b16f64dc36d24c1c5d7283888a2d8cee8 --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/string-parameter.model.ts @@ -0,0 +1,12 @@ +import { ObjectType } from '@nestjs/graphql'; +import { BaseParameter } from './base-parameter.model'; + +@ObjectType({ implements: () => [BaseParameter] }) +export class StringParameter implements BaseParameter { + name: string; + label?: string; + hint?: string; + isRequired?: boolean; + hasMultiple?: boolean; + defaultValue?: string; +} diff --git a/api/src/engine/models/experiment/algorithm/variable-parameter.model.ts b/api/src/engine/models/experiment/algorithm/variable-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..0f9e55f7d4831de18538b3070b31bd8e390650e3 --- /dev/null +++ b/api/src/engine/models/experiment/algorithm/variable-parameter.model.ts @@ -0,0 +1,16 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class VariableParameter { + @Field({ nullable: true }) + hint?: string; + + @Field({ nullable: true, defaultValue: false }) + isRequired: boolean; + + @Field({ nullable: true, defaultValue: false }) + hasMultiple: boolean; + + @Field(() => [String], { nullable: true }) + allowedTypes: string[]; +} diff --git a/api/src/engine/models/experiment/experiment.model.ts b/api/src/engine/models/experiment/experiment.model.ts index eb757d64d9444f4c505705d3d6faa2b7e32c4d72..b0fed2aa4bd3649d7f2c6f7bba4ce8601364b1c6 100644 --- a/api/src/engine/models/experiment/experiment.model.ts +++ b/api/src/engine/models/experiment/experiment.model.ts @@ -1,6 +1,5 @@ import { Field, ObjectType, PartialType } from '@nestjs/graphql'; import { ResultUnion } from '../result/common/result-union.model'; -import { Algorithm } from './algorithm.model'; import { Author } from './author.model'; @ObjectType() @@ -21,6 +20,23 @@ export class Formula { interactions: string[][]; } +@ObjectType() +export class ParamValue { + @Field() + name: string; + + @Field() + value: string; +} +@ObjectType() +export class AlgorithmResult { + @Field() + name: string; + + @Field(() => [ParamValue], { nullable: true, defaultValue: [] }) + parameters?: ParamValue[]; +} + @ObjectType() export class Experiment { @Field() @@ -75,7 +91,7 @@ export class Experiment { formula?: Formula; @Field() - algorithm: Algorithm; + algorithm: AlgorithmResult; } @ObjectType() diff --git a/api/src/engine/models/experiment/input/algorithm-parameter.input.ts b/api/src/engine/models/experiment/input/algorithm-parameter.input.ts index 8cd3ec29ddfecb22c1574ea339cd96b4a010f79a..62a0ab458497bbecc9387434f741821ddab9728a 100644 --- a/api/src/engine/models/experiment/input/algorithm-parameter.input.ts +++ b/api/src/engine/models/experiment/input/algorithm-parameter.input.ts @@ -1,25 +1,10 @@ -import { Field, InputType, registerEnumType } from '@nestjs/graphql'; - -export enum ParamType { - STRING, - NUMBER, -} - -registerEnumType(ParamType, { - name: 'ParamType', -}); +import { Field, InputType } from '@nestjs/graphql'; @InputType() export class AlgorithmParamInput { @Field() id: string; - @Field(() => ParamType, { - nullable: true, - defaultValue: ParamType.STRING, - }) - type?: ParamType; - @Field(() => String) value: string; } diff --git a/api/src/engine/models/experiment/input/algorithm.input.ts b/api/src/engine/models/experiment/input/algorithm.input.ts index d87abd4561f975dbb9dfc00964f658265a340085..e815f47f2eee1d40781187d324be32df4d279399 100644 --- a/api/src/engine/models/experiment/input/algorithm.input.ts +++ b/api/src/engine/models/experiment/input/algorithm.input.ts @@ -9,6 +9,6 @@ export class AlgorithmInput { @Field(() => [AlgorithmParamInput], { nullable: true, defaultValue: [] }) parameters: AlgorithmParamInput[]; - @Field() - type: string; + @Field({ nullable: true }) + type?: string; } diff --git a/api/src/schema.gql b/api/src/schema.gql index 33142a24cc95cbc543605924b31ff76083003968..8265eb01a39457cb2d9420de4625a1b937ac26d7 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -2,6 +2,17 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ +interface BaseParameter { + name: String! + label: String + + """Small hint (description) for the end user""" + hint: String + isRequired: Boolean + hasMultiple: Boolean + defaultValue: String +} + type User { id: String! username: String! @@ -29,6 +40,7 @@ type Configuration { skipTos: Boolean enableSSO: Boolean matomo: Matomo + ontologyUrl: String } type Dataset { @@ -51,7 +63,7 @@ type Group { } type Category { - id: String! + value: String! label: String } @@ -77,24 +89,72 @@ type Domain { rootGroup: Group! } -type AlgorithmParameter { - id: String! - value: String +type OptionValue { + value: String! + label: String! +} + +type NominalParameter implements BaseParameter { + name: String! label: String - description: String + + """Small hint (description) for the end user""" + hint: String + isRequired: Boolean + hasMultiple: Boolean defaultValue: String - isMultiple: Boolean + + """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 - min: String - max: String - type: String + 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 + allowedTypes: [String!] } type Algorithm { id: String! - parameters: [AlgorithmParameter!] - label: String + parameters: [BaseParameter!] + variable: VariableParameter! + coVariable: VariableParameter + hasFormula: Boolean type: String + label: String description: String } @@ -226,6 +286,16 @@ type Formula { interactions: [[String!]!] } +type ParamValue { + name: String! + value: String! +} + +type AlgorithmResult { + name: String! + parameters: [ParamValue!] +} + type Experiment { id: String! name: String! @@ -244,7 +314,7 @@ type Experiment { coVariables: [String!] filterVariables: [String!] formula: Formula - algorithm: Algorithm! + algorithm: AlgorithmResult! } type PartialExperiment { @@ -265,7 +335,7 @@ type PartialExperiment { coVariables: [String!] filterVariables: [String!] formula: Formula - algorithm: Algorithm + algorithm: AlgorithmResult } type ListExperiments { @@ -308,20 +378,14 @@ input ExperimentCreateInput { input AlgorithmInput { id: String! parameters: [AlgorithmParamInput!] = [] - type: String! + type: String } input AlgorithmParamInput { id: String! - type: ParamType = STRING value: String! } -enum ParamType { - STRING - NUMBER -} - input FormulaTransformation { id: String! operation: String! diff --git a/docs/for-developers/configuration/gateway.md b/docs/for-developers/configuration/gateway.md index 2b16dacfcbdfbc415f9091c3086a000032787d21..7f879ca3369f383b0f2b26fbacc31d35d889258b 100644 --- a/docs/for-developers/configuration/gateway.md +++ b/docs/for-developers/configuration/gateway.md @@ -18,6 +18,7 @@ description: >- | GATEWAY\_PORT | number | 8081 | Indicate the port that should be used by the gateway | | NODE\_ENV | string | dev | Value can be `prod` or `dev` | | BASE\_URL\_CONTEXT | string | null | Define context of the gateway. E.g. `api` if the api is under `http://127.0.0.1/api/` | +| ONTOLOGY\_URL | string | null | Define ontology's url | #### Authentication