diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts index 45bf43a1df7f8fc7877f297a69e5edf6eec50d6d..4a0f2220c806b480d30a07cb1634e1aac953d943 100644 --- a/api/src/engine/connectors/datashield/main.connector.ts +++ b/api/src/engine/connectors/datashield/main.connector.ts @@ -8,8 +8,12 @@ import { } from 'src/engine/models/experiment/experiment.model'; import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model'; import { ExperimentEditInput } from 'src/engine/models/experiment/input/experiment-edit.input'; +import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; export default class DataShieldService implements IEngineService { + getAlgorithms(): Algorithm[] | Promise<Algorithm[]> { + throw new Error('Method not implemented.'); + } createExperiment( data: ExperimentCreateInput, isTransient: boolean, @@ -48,7 +52,7 @@ export default class DataShieldService implements IEngineService { throw new Error('Method not implemented.'); } - getExperimentAPI(): Observable<string> { + getExperimentREST(): Observable<string> { throw new Error('Method not implemented.'); } @@ -56,7 +60,7 @@ export default class DataShieldService implements IEngineService { throw new Error('Method not implemented.'); } - editExperimentAPI(): Observable<string> { + editExperimentREST(): Observable<string> { throw new Error('Method not implemented.'); } @@ -72,7 +76,7 @@ export default class DataShieldService implements IEngineService { throw new Error('Method not implemented.'); } - getAlgorithms(): Observable<string> { + getAlgorithmsREST(): Observable<string> { throw new Error('Method not implemented.'); } } diff --git a/api/src/engine/connectors/exareme/converters.ts b/api/src/engine/connectors/exareme/converters.ts index 05b9711b0a989e4df868fa53c480c881cb18b2ed..80811045c0862864d4ba704712a9b0efa606685b 100644 --- a/api/src/engine/connectors/exareme/converters.ts +++ b/api/src/engine/connectors/exareme/converters.ts @@ -1,5 +1,6 @@ import { Category } from 'src/engine/models/category.model'; import { AlgorithmParameter } from 'src/engine/models/experiment/algorithm-parameter.model'; +import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; import { Experiment } from 'src/engine/models/experiment/experiment.model'; import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; import { Group } from 'src/engine/models/group.model'; @@ -18,6 +19,7 @@ import { VariableEntity } from './interfaces/variable-entity.interface'; import { descriptiveModelToTables, descriptiveSingleToTables, + transformToAlgorithms, transformToExperiment, } from './transformations'; @@ -92,7 +94,8 @@ export const descriptiveDataToTableResult = ( result.groups = [ new GroupResult({ - name: 'Single', + name: 'Variables', + description: 'Descriptive statistics for the variables of interest.', results: descriptiveSingleToTables.evaluate(data), }), ]; @@ -100,6 +103,8 @@ export const descriptiveDataToTableResult = ( result.groups.push( new GroupResult({ name: 'Model', + description: + 'Intersection table for the variables of interest as it appears in the experiment.', results: descriptiveModelToTables.evaluate(data), }), ); @@ -124,10 +129,14 @@ export const dataToExperiment = (data: ExperimentData): Experiment => { return exp; }; +export const dataToAlgorithms = (data: string): Algorithm[] => { + return transformToAlgorithms.evaluate(data); +}; + export const dataToRaw = (result: ResultExperiment): RawResult[] => { return [ { - data: result.data, + rawdata: result.data, }, ]; }; diff --git a/api/src/engine/connectors/exareme/main.connector.ts b/api/src/engine/connectors/exareme/main.connector.ts index 81ed14ab64999708295fd7bbbc38678994d1ba86..11f2ed6c18118d813a830ad2cb34c808203be43b 100644 --- a/api/src/engine/connectors/exareme/main.connector.ts +++ b/api/src/engine/connectors/exareme/main.connector.ts @@ -4,6 +4,7 @@ import { Request } from 'express'; import { firstValueFrom, map, Observable } from 'rxjs'; import { IEngineOptions, IEngineService } from 'src/engine/engine.interfaces'; import { Domain } from 'src/engine/models/domain.model'; +import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; import { Experiment, PartialExperiment, @@ -14,6 +15,7 @@ import { ListExperiments } from 'src/engine/models/experiment/list-experiments.m import { Group } from 'src/engine/models/group.model'; import { Variable } from 'src/engine/models/variable.model'; import { + dataToAlgorithms, dataToCategory, dataToExperiment, dataToGroup, @@ -60,6 +62,14 @@ export default class ExaremeService implements IEngineService { }; } + async getAlgorithms(): Promise<Algorithm[]> { + const path = this.options.baseurl + 'algorithms'; + + const resultAPI = await firstValueFrom(this.httpService.get<string>(path)); + + return dataToAlgorithms(resultAPI.data); + } + async getExperiment(uuid: string): Promise<Experiment> { const path = this.options.baseurl + `experiments/${uuid}`; @@ -144,7 +154,7 @@ export default class ExaremeService implements IEngineService { .pipe(map((response) => response.data)); } - getExperimentAPI(uuid: string): Observable<string> { + getExperimentREST(uuid: string): Observable<string> { const path = this.options.baseurl + `experiments/${uuid}`; return this.httpService @@ -158,7 +168,7 @@ export default class ExaremeService implements IEngineService { return this.httpService.delete(path).pipe(map((response) => response.data)); } - editExperimentAPI(uuid: string, request: Request): Observable<string> { + editExperimentREST(uuid: string, request: Request): Observable<string> { const path = this.options.baseurl + `experiments/${uuid}`; return this.httpService @@ -190,7 +200,7 @@ export default class ExaremeService implements IEngineService { .pipe(map((response) => response.data)); } - getAlgorithms(request: Request): Observable<string> { + getAlgorithmsREST(request: Request): Observable<string> { const path = this.options.baseurl + 'algorithms'; return this.httpService diff --git a/api/src/engine/connectors/exareme/transformations.ts b/api/src/engine/connectors/exareme/transformations.ts index 8df362c42f17978d07357fdee8e6d793c48dab09..b124790ce8d25fe226df4d68d489b72256b611ea 100644 --- a/api/src/engine/connectors/exareme/transformations.ts +++ b/api/src/engine/connectors/exareme/transformations.ts @@ -3,6 +3,31 @@ import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' +export const transformToAlgorithms = jsonata(` +( + $params := ["y", "pathology", "dataset", "filter"]; + + $toArray := function($x) { $type($x) = 'array' ? $x : [$x]}; + + *.{ + 'name': name, + 'label': label, + 'description': desc, + 'parameters': $toArray(parameters[$not(name in $params)].{ + 'name': 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"]; diff --git a/api/src/engine/engine.controller.ts b/api/src/engine/engine.controller.ts index b3e12d13ff2b2004e576f05b4358991f27eabd08..6c5949493e6c59ddc759d0f83b9c4d363a578a9e 100644 --- a/api/src/engine/engine.controller.ts +++ b/api/src/engine/engine.controller.ts @@ -21,7 +21,7 @@ export class EngineController { @Get('/algorithms') getAlgorithms(@Req() request: Request): Observable<string> { - return this.engineService.getAlgorithms(request); + return this.engineService.getAlgorithmsREST(request); } @Get('/experiments') @@ -31,7 +31,7 @@ export class EngineController { @Get('/experiments/:uuid') getExperiment(@Param('uuid') uuid: string): Observable<string> { - return this.engineService.getExperimentAPI(uuid); + return this.engineService.getExperimentREST(uuid); } @Delete('/experiments/:uuid') @@ -47,7 +47,7 @@ export class EngineController { @Param('uuid') uuid: string, @Req() request: Request, ): Observable<string> { - return this.engineService.editExperimentAPI(uuid, request); + return this.engineService.editExperimentREST(uuid, request); } @Post('experiments/transient') diff --git a/api/src/engine/engine.interfaces.ts b/api/src/engine/engine.interfaces.ts index e754a1fd068a47d1f235fb4ca54a692fd4470342..26818b01868edbd7191461de19ed904310a15d1e 100644 --- a/api/src/engine/engine.interfaces.ts +++ b/api/src/engine/engine.interfaces.ts @@ -1,6 +1,7 @@ import { Request } from 'express'; import { Observable } from 'rxjs'; import { Domain } from './models/domain.model'; +import { Algorithm } from './models/experiment/algorithm.model'; import { Experiment, PartialExperiment, @@ -39,16 +40,18 @@ export interface IEngineService { expriment: ExperimentEditInput, ): Promise<Experiment> | Experiment; + getAlgorithms(): Promise<Algorithm[]> | Algorithm[]; + // Standard REST API call - getAlgorithms(request: Request): Observable<string>; + getAlgorithmsREST(request: Request): Observable<string>; getExperiments(request: Request): Observable<string>; - getExperimentAPI(uuid: string): Observable<string>; + getExperimentREST(uuid: string): Observable<string>; deleteExperiment(uuid: string, request: Request): Observable<string>; - editExperimentAPI(uuid: string, request: Request): Observable<string>; + editExperimentREST(uuid: string, request: Request): Observable<string>; startExperimentTransient(request: Request): Observable<string>; diff --git a/api/src/engine/engine.resolver.ts b/api/src/engine/engine.resolver.ts index a954ed3b004e46c8b2cce19ebbc327ac24c2abfe..0cfac3a5b1fdc02c82b74cd31928a554c45bc550 100644 --- a/api/src/engine/engine.resolver.ts +++ b/api/src/engine/engine.resolver.ts @@ -3,6 +3,7 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { ENGINE_SERVICE } from './engine.constants'; import { IEngineService } from './engine.interfaces'; import { Domain } from './models/domain.model'; +import { Algorithm } from './models/experiment/algorithm.model'; import { Experiment, PartialExperiment, @@ -38,6 +39,11 @@ export class EngineResolver { return this.engineService.getExperiment(uuid); } + @Query(() => [Algorithm]) + async algorithms() { + return this.engineService.getAlgorithms(); + } + @Mutation(() => Experiment) async createExperiment( @Args('data') experimentCreateInput: ExperimentCreateInput, diff --git a/api/src/engine/models/experiment/algorithm-parameter.model.ts b/api/src/engine/models/experiment/algorithm-parameter.model.ts index 79666de79a81e2a7d3c2e2c0756404a995479735..d5db6bfc11634ae3ec054c4c6eef9a5441f74931 100644 --- a/api/src/engine/models/experiment/algorithm-parameter.model.ts +++ b/api/src/engine/models/experiment/algorithm-parameter.model.ts @@ -5,6 +5,30 @@ export class AlgorithmParameter { @Field() name: string; - @Field(() => [String]) - value: string[]; + @Field(() => [String], { nullable: true }) + value?: string[]; + + @Field({ nullable: true }) + label?: string; + + @Field({ nullable: true }) + description?: string; + + @Field({ nullable: true }) + defaultValue?: string; + + @Field({ defaultValue: false, nullable: true }) + isMultiple?: boolean; + + @Field({ defaultValue: false, nullable: true }) + isRequired?: boolean; + + @Field({ nullable: true }) + min?: string; + + @Field({ nullable: true }) + max?: string; + + @Field({ nullable: true }) + type?: string; } diff --git a/api/src/engine/models/experiment/algorithm.model.ts b/api/src/engine/models/experiment/algorithm.model.ts index 30087c5b55d07d213508d125ba9eb491920073c2..ec26d747fc8019a29822bf673aa82b29162f5757 100644 --- a/api/src/engine/models/experiment/algorithm.model.ts +++ b/api/src/engine/models/experiment/algorithm.model.ts @@ -7,8 +7,14 @@ export class Algorithm { name: string; @Field(() => [AlgorithmParameter], { nullable: true, defaultValue: [] }) - parameters: AlgorithmParameter[]; + parameters?: AlgorithmParameter[]; - @Field() - type: string; + @Field({ nullable: true }) + label?: string; + + @Field({ nullable: true }) + type?: string; + + @Field({ nullable: true }) + description?: string; } diff --git a/api/src/engine/models/result/common/result-union.model.ts b/api/src/engine/models/result/common/result-union.model.ts index 1f7b522c0a06a1c12f13cf2e608b49f2da233c77..9957a6068e693c7c99cc9ffd59ebec2ddc61a635 100644 --- a/api/src/engine/models/result/common/result-union.model.ts +++ b/api/src/engine/models/result/common/result-union.model.ts @@ -11,7 +11,7 @@ export const ResultUnion = createUnionType({ return TableResult; } - if (value.listMax) { + if (value.rawdata) { return RawResult; } diff --git a/api/src/engine/models/result/groups-result.model.ts b/api/src/engine/models/result/groups-result.model.ts index e3de47038bb69b09615831c8c5dac1cf456d93df..5ea51cff97c998309df74518f1efee0efcb0c3bb 100644 --- a/api/src/engine/models/result/groups-result.model.ts +++ b/api/src/engine/models/result/groups-result.model.ts @@ -11,6 +11,9 @@ export class GroupResult { @Field() name: string; + @Field({ nullable: true }) + description?: string; + @Field(() => [ResultUnion]) results: Array<typeof ResultUnion>; } diff --git a/api/src/engine/models/result/raw-result.model.ts b/api/src/engine/models/result/raw-result.model.ts index 5074e015cc483a3b4fe899f9608d8ddba76f0bfa..853362293bbfe6da230e487f8ea139ba36a16f3b 100644 --- a/api/src/engine/models/result/raw-result.model.ts +++ b/api/src/engine/models/result/raw-result.model.ts @@ -1,12 +1,12 @@ import { Field, ObjectType } from '@nestjs/graphql'; -import { GraphQLJSONObject } from 'graphql-type-json'; +import GraphQLJSON from 'graphql-type-json'; import { Result } from './common/result.model'; +// field name 'rawdata' was used instead of 'data' because of typing problem on union with same field name +// see :https://stackoverflow.com/questions/44170603/graphql-using-same-field-names-in-different-types-within-union + @ObjectType() export class RawResult extends Result { - @Field(() => GraphQLJSONObject) - data: unknown; - - @Field(() => [String], { defaultValue: [] }) - listMax?: string[]; + @Field(() => GraphQLJSON) + rawdata: unknown; } diff --git a/api/src/schema.gql b/api/src/schema.gql index fc53b738b631a6dc35e5e162889f12f1c726df06..aad684192e33c70fa81041ad58ef4c8d0b3bd6c8 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -34,8 +34,30 @@ type Domain { rootGroup: Group! } +type AlgorithmParameter { + name: String! + value: [String!] + label: String + description: String + defaultValue: String + isMultiple: Boolean + isRequired: Boolean + min: String + max: String + type: String +} + +type Algorithm { + name: String! + parameters: [AlgorithmParameter!] + label: String + type: String + description: String +} + type GroupResult { name: String! + description: String results: [ResultUnion!]! } @@ -48,14 +70,13 @@ type TableResult { } type RawResult { - data: JSONObject! - listMax: [String!]! + rawdata: JSON! } """ -The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). """ -scalar JSONObject @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") +scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") type GroupsResult { groups: [GroupResult!]! @@ -66,17 +87,6 @@ type Header { type: String! } -type AlgorithmParameter { - name: String! - value: [String!]! -} - -type Algorithm { - name: String! - parameters: [AlgorithmParameter!] - type: String! -} - type Experiment { uuid: String author: String @@ -124,6 +134,7 @@ type Query { domains(ids: [String!] = []): [Domain!]! experiments(name: String = "", page: Float = 0): ListExperiments! expriment(uuid: String!): Experiment! + algorithms: [Algorithm!]! } type Mutation {