diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts index 4ee4587a888c9a10e849dba104bcb7707e5404ec..45bf43a1df7f8fc7877f297a69e5edf6eec50d6d 100644 --- a/api/src/engine/connectors/datashield/main.connector.ts +++ b/api/src/engine/connectors/datashield/main.connector.ts @@ -2,21 +2,42 @@ import { Observable } from 'rxjs'; import { IEngineService } from 'src/engine/engine.interfaces'; import { Domain } from 'src/engine/models/domain.model'; import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; -import { Experiment } from 'src/engine/models/experiment/experiment.model'; +import { + Experiment, + PartialExperiment, +} 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'; export default class DataShieldService implements IEngineService { createExperiment( data: ExperimentCreateInput, + isTransient: boolean, ): Experiment | Promise<Experiment> { throw new Error('Method not implemented.'); } - - getDomains(): Domain[] { + listExperiments( + page: number, + name: string, + ): ListExperiments | Promise<ListExperiments> { throw new Error('Method not implemented.'); } - - demo(): string { - return 'datashield'; + getExperiment(uuid: string): Experiment | Promise<Experiment> { + throw new Error('Method not implemented.'); + } + removeExperiment( + uuid: string, + ): PartialExperiment | Promise<PartialExperiment> { + throw new Error('Method not implemented.'); + } + editExperient( + uuid: string, + expriment: ExperimentEditInput, + ): Experiment | Promise<Experiment> { + throw new Error('Method not implemented.'); + } + getDomains(): Domain[] { + throw new Error('Method not implemented.'); } getActiveUser(): Observable<string> { @@ -27,7 +48,7 @@ export default class DataShieldService implements IEngineService { throw new Error('Method not implemented.'); } - getExperiment(): Observable<string> { + getExperimentAPI(): Observable<string> { throw new Error('Method not implemented.'); } @@ -35,7 +56,7 @@ export default class DataShieldService implements IEngineService { throw new Error('Method not implemented.'); } - editExperiment(): Observable<string> { + editExperimentAPI(): 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 24a6c2110faa9f15dc3796148b487a9b8e406125..63d689f3fecf19bf9987d640bf916df3ec464a0f 100644 --- a/api/src/engine/connectors/exareme/converters.ts +++ b/api/src/engine/connectors/exareme/converters.ts @@ -1,21 +1,20 @@ import { Category } from 'src/engine/models/category.model'; +import { AlgorithmParameter } from 'src/engine/models/experiment/algorithm-parameter.model'; import { Experiment, ResultUnion, } from 'src/engine/models/experiment/experiment.model'; -import { AlgorithmParameter } from 'src/engine/models/experiment/algorithm-parameter.model'; import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; import { Group } from 'src/engine/models/group.model'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; import { TableResult } from 'src/engine/models/result/table-result.model'; import { Variable } from 'src/engine/models/variable.model'; import { Entity } from './interfaces/entity.interface'; -import { Hierarchy } from './interfaces/hierarchy.interface'; -import { VariableEntity } from './interfaces/variable-entity.interface'; -import { transientToTable } from './transformations'; import { ExperimentData } from './interfaces/experiment/experiment.interface'; import { ResultExperiment } from './interfaces/experiment/result-experiment.interface'; -import { RawResult } from 'src/engine/models/result/raw-result.model'; -import { TransientDataResult } from './interfaces/transient/transient-data-result.interface'; +import { Hierarchy } from './interfaces/hierarchy.interface'; +import { VariableEntity } from './interfaces/variable-entity.interface'; +import { transformToExperiment, transientToTable } from './transformations'; export const dataToGroup = (data: Hierarchy): Group => { return { @@ -81,35 +80,57 @@ export const experimentInputToData = (data: ExperimentCreateInput) => { }; }; -export const dataToTransient = ( - input: ExperimentCreateInput, - data: TransientDataResult, -): Experiment => { - const tabs: TableResult[] = transientToTable.evaluate(data); - - return { - ...input, - results: tabs, - }; +export const descriptiveDataToTableResult = ( + data: ResultExperiment, +): TableResult[] => { + return transientToTable.evaluate(data); }; export const dataToExperiment = (data: ExperimentData): Experiment => { - const exp: Experiment = dataToExperiment(data); + const expTransform = transformToExperiment.evaluate(data); + + const exp: Experiment = { + ...expTransform, + results: [], + }; - exp.results = data.result.map((result) => dataToResult(result)); + exp.results = data.result + ? data.result + .map((result) => dataToResult(result, exp.algorithm.name)) + .flat() + : []; return exp; }; -export const dataToRaw = (result: ResultExperiment): RawResult => { - return { - data: result.data, - }; +export const dataToRaw = (result: ResultExperiment): RawResult[] => { + return [ + { + data: result.data, + }, + ]; }; -export const dataToResult = (result: ResultExperiment): typeof ResultUnion => { - switch (result.type) { +export const dataToResult = ( + result: ResultExperiment, + algo: string, +): Array<typeof ResultUnion> => { + switch (result.type.toLowerCase()) { + case 'application/json': + return dataJSONtoResult(result, algo); default: return dataToRaw(result); } }; + +export const dataJSONtoResult = ( + result: ResultExperiment, + algo: string, +): Array<typeof ResultUnion> => { + switch (algo.toLowerCase()) { + case 'descriptive_stats': + return descriptiveDataToTableResult(result); + default: + return []; + } +}; diff --git a/api/src/engine/connectors/exareme/interfaces/experiment/experiment.interface.ts b/api/src/engine/connectors/exareme/interfaces/experiment/experiment.interface.ts index b9203f86e1528c73302928d02f2aa9b41b108625..7412cb0560c9eda88fe61dd4ebbf8128c94e2c62 100644 --- a/api/src/engine/connectors/exareme/interfaces/experiment/experiment.interface.ts +++ b/api/src/engine/connectors/exareme/interfaces/experiment/experiment.interface.ts @@ -2,5 +2,10 @@ import { ResultExperiment } from './result-experiment.interface'; export interface ExperimentData { name: string; - result: ResultExperiment[]; + uuid?: string; + status?: string; + createdBy?: string; + shared?: boolean; + viewed?: boolean; + result?: ResultExperiment[]; } diff --git a/api/src/engine/connectors/exareme/interfaces/experiment/experiments.interface.ts b/api/src/engine/connectors/exareme/interfaces/experiment/experiments.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ef0c0f84568d7a7e87dd1f759c1db3b17e8066a --- /dev/null +++ b/api/src/engine/connectors/exareme/interfaces/experiment/experiments.interface.ts @@ -0,0 +1,8 @@ +import { ExperimentData } from './experiment.interface'; + +export interface ExperimentsData { + experiments: ExperimentData[]; + currentpage?: number; + totalExperiments?: number; + totalPages?: number; +} diff --git a/api/src/engine/connectors/exareme/main.connector.ts b/api/src/engine/connectors/exareme/main.connector.ts index 4b06e931e04ea0a5c33fc922bd5b7aca61b6a8c0..595fc22b1226f30382df604b866303be54c6b397 100644 --- a/api/src/engine/connectors/exareme/main.connector.ts +++ b/api/src/engine/connectors/exareme/main.connector.ts @@ -1,29 +1,36 @@ import { HttpService } from '@nestjs/axios'; -import { HttpException, HttpStatus } from '@nestjs/common'; +import { BadRequestException, HttpException, HttpStatus } from '@nestjs/common'; 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 { Experiment } from 'src/engine/models/experiment/experiment.model'; +import { + Experiment, + PartialExperiment, +} from 'src/engine/models/experiment/experiment.model'; import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; +import { ExperimentEditInput } from 'src/engine/models/experiment/input/experiment-edit.input'; +import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model'; import { Group } from 'src/engine/models/group.model'; import { Variable } from 'src/engine/models/variable.model'; import { dataToCategory, + dataToExperiment, dataToGroup, - dataToTransient, dataToVariable, experimentInputToData, } from './converters'; +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 { TransientDataResult } from './interfaces/transient/transient-data-result.interface'; export default class ExaremeService implements IEngineService { constructor( private readonly options: IEngineOptions, private readonly httpService: HttpService, ) {} + async createExperiment( data: ExperimentCreateInput, isTransient = false, @@ -34,10 +41,59 @@ export default class ExaremeService implements IEngineService { this.options.baseurl + `experiments${isTransient ? '/transient' : ''}`; const resultAPI = await firstValueFrom( - this.httpService.post<TransientDataResult>(path, form), + this.httpService.post<ExperimentData>(path, form), + ); + + return dataToExperiment(resultAPI.data); + } + + async listExperiments(page: number, name: string): Promise<ListExperiments> { + const path = this.options.baseurl + 'experiments'; + + const resultAPI = await firstValueFrom( + this.httpService.get<ExperimentsData>(path, { params: { page, name } }), + ); + + return { + ...resultAPI.data, + experiments: resultAPI.data.experiments.map(dataToExperiment), + }; + } + + async getExperiment(uuid: string): Promise<Experiment> { + const path = this.options.baseurl + `experiments/${uuid}`; + + const resultAPI = await firstValueFrom( + this.httpService.get<ExperimentData>(path), ); - return dataToTransient(data, resultAPI.data); + return dataToExperiment(resultAPI.data); + } + + async editExperient( + uuid: string, + expriment: ExperimentEditInput, + ): Promise<Experiment> { + const path = this.options.baseurl + `experiments/${uuid}`; + + const resultAPI = await firstValueFrom( + this.httpService.patch<ExperimentData>(path, expriment), + ); + + return dataToExperiment(resultAPI.data); + } + + async removeExperiment(uuid: string): Promise<PartialExperiment> { + const path = this.options.baseurl + `experiments/${uuid}`; + + try { + await firstValueFrom(this.httpService.delete(path)); + return { + uuid: uuid, + }; + } catch (error) { + throw new BadRequestException(`${uuid} does not exists`); + } } async getDomains(ids: string[]): Promise<Domain[]> { @@ -88,7 +144,7 @@ export default class ExaremeService implements IEngineService { .pipe(map((response) => response.data)); } - getExperiment(uuid: string): Observable<string> { + getExperimentAPI(uuid: string): Observable<string> { const path = this.options.baseurl + `experiments/${uuid}`; return this.httpService @@ -102,7 +158,7 @@ export default class ExaremeService implements IEngineService { return this.httpService.delete(path).pipe(map((response) => response.data)); } - editExperiment(uuid: string, request: Request): Observable<string> { + editExperimentAPI(uuid: string, request: Request): Observable<string> { const path = this.options.baseurl + `experiments/${uuid}`; return this.httpService @@ -142,6 +198,7 @@ export default class ExaremeService implements IEngineService { .pipe(map((response) => response.data)); } + // UTILITIES private flattenGroups = (data: Hierarchy): Group[] => { let groups: Group[] = [dataToGroup(data)]; diff --git a/api/src/engine/connectors/exareme/transformations.ts b/api/src/engine/connectors/exareme/transformations.ts index cbf9017a51859b3e0bdf41f3c721c090a37dffd5..4f574db08df84e0023b924ec987809d025e6e4e1 100644 --- a/api/src/engine/connectors/exareme/transformations.ts +++ b/api/src/engine/connectors/exareme/transformations.ts @@ -3,8 +3,34 @@ import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' -export const dataToExperiment = jsonata(`( - $ +export const transformToExperiment = jsonata(` +( + $params := ["y", "pathology", "dataset", "filter"]; + + { + "name": name, + "uuid": uuid, + "author": createdBy, + "viewed": viewed, + "status": status, + "createdAt": created, + "finishedAt": finished, + "shared": shared, + "updateAt": updated, + "domains": algorithm.parameters[name = "pathology"].value, + "variables": $split(algorithm.parameters[name = "y"].value, ','), + "filter": algorithm.parameters[name = "filter"].value, + "datasets": $split(algorithm.parameters[name = "dataset"].value, ','), + "algorithm": { + "name": algorithm.name, + "parameters" : + algorithm.parameters[$not(name in $params)].({ + "name": name, + "label": label, + "value": value + }) + } + } ) `); @@ -22,7 +48,7 @@ export const transientToTable = jsonata(` : {} }; - result.data.[ + data.[ $.single.*@$p#$i.( $ks := $keys($p.*.data[$type($) = 'object']); { diff --git a/api/src/engine/engine.controller.ts b/api/src/engine/engine.controller.ts index c54f0ce0fa44732ed4fc614caf5ff0509d6e0068..b3e12d13ff2b2004e576f05b4358991f27eabd08 100644 --- a/api/src/engine/engine.controller.ts +++ b/api/src/engine/engine.controller.ts @@ -31,7 +31,7 @@ export class EngineController { @Get('/experiments/:uuid') getExperiment(@Param('uuid') uuid: string): Observable<string> { - return this.engineService.getExperiment(uuid); + return this.engineService.getExperimentAPI(uuid); } @Delete('/experiments/:uuid') @@ -47,7 +47,7 @@ export class EngineController { @Param('uuid') uuid: string, @Req() request: Request, ): Observable<string> { - return this.engineService.editExperiment(uuid, request); + return this.engineService.editExperimentAPI(uuid, request); } @Post('experiments/transient') diff --git a/api/src/engine/engine.interfaces.ts b/api/src/engine/engine.interfaces.ts index 0fcf9f1410ebeb4302b2d78c88f61fc82a7ad496..e754a1fd068a47d1f235fb4ca54a692fd4470342 100644 --- a/api/src/engine/engine.interfaces.ts +++ b/api/src/engine/engine.interfaces.ts @@ -1,8 +1,13 @@ import { Request } from 'express'; import { Observable } from 'rxjs'; import { Domain } from './models/domain.model'; +import { + Experiment, + PartialExperiment, +} from './models/experiment/experiment.model'; import { ExperimentCreateInput } from './models/experiment/input/experiment-create.input'; -import { Experiment } from './models/experiment/experiment.model'; +import { ExperimentEditInput } from './models/experiment/input/experiment-edit.input'; +import { ListExperiments } from './models/experiment/list-experiments.model'; export interface IEngineOptions { type: string; @@ -18,16 +23,32 @@ export interface IEngineService { isTransient: boolean, ): Promise<Experiment> | Experiment; + listExperiments( + page: number, + name: string, + ): Promise<ListExperiments> | ListExperiments; + + getExperiment(uuid: string): Promise<Experiment> | Experiment; + + removeExperiment( + uuid: string, + ): Promise<PartialExperiment> | PartialExperiment; + + editExperient( + uuid: string, + expriment: ExperimentEditInput, + ): Promise<Experiment> | Experiment; + // Standard REST API call getAlgorithms(request: Request): Observable<string>; getExperiments(request: Request): Observable<string>; - getExperiment(uuid: string): Observable<string>; + getExperimentAPI(uuid: string): Observable<string>; deleteExperiment(uuid: string, request: Request): Observable<string>; - editExperiment(uuid: string, request: Request): Observable<string>; + editExperimentAPI(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 fba8296cdf7f69186dd3552f1dd465ccff2e6904..dfd7dc73ca78f22ce7378ef82895bb99db54f42b 100644 --- a/api/src/engine/engine.resolver.ts +++ b/api/src/engine/engine.resolver.ts @@ -3,8 +3,13 @@ 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 { + Experiment, + PartialExperiment, +} from './models/experiment/experiment.model'; import { ExperimentCreateInput } from './models/experiment/input/experiment-create.input'; -import { Experiment } from './models/experiment/experiment.model'; +import { ExperimentEditInput } from './models/experiment/input/experiment-edit.input'; +import { ListExperiments } from './models/experiment/list-experiments.model'; @Resolver() export class EngineResolver { @@ -20,6 +25,19 @@ export class EngineResolver { return this.engineService.getDomains(ids); } + @Query(() => ListExperiments) + async experiments( + @Args('page', { nullable: true, defaultValue: 0 }) page: number, + @Args('name', { nullable: true, defaultValue: '' }) name: string, + ) { + return this.engineService.listExperiments(page, name); + } + + @Query(() => Experiment) + async expriment(@Args('uuid') uuid: string) { + return this.engineService.getExperiment(uuid); + } + @Mutation(() => Experiment) async createExperiment( @Args('data') experimentCreateInput: ExperimentCreateInput, @@ -31,4 +49,19 @@ export class EngineResolver { isTransient, ); } + + @Mutation(() => Experiment) + async editExperiment( + @Args('uuid') uuid: string, + @Args('data') experiment: ExperimentEditInput, + ) { + return this.engineService.editExperient(uuid, experiment); + } + + @Mutation(() => PartialExperiment) + async removeExperiment( + @Args('uuid') uuid: string, + ): Promise<PartialExperiment> { + return this.engineService.removeExperiment(uuid); + } } diff --git a/api/src/engine/models/experiment/experiment.model.ts b/api/src/engine/models/experiment/experiment.model.ts index 91ee6c4033c1806b0660457e8aa2316a321beaac..2ac6ec204f2f1021f8f5963771b37362cbce7b18 100644 --- a/api/src/engine/models/experiment/experiment.model.ts +++ b/api/src/engine/models/experiment/experiment.model.ts @@ -1,8 +1,8 @@ import { createUnionType, Field, - GraphQLISODateTime, ObjectType, + PartialType, } from '@nestjs/graphql'; import { RawResult } from '../result/raw-result.model'; import { TableResult } from '../result/table-result.model'; @@ -28,16 +28,19 @@ export class Experiment { @Field({ nullable: true }) uuid?: string; - @Field(() => GraphQLISODateTime, { nullable: true }) - created_at?: Date; + @Field({ nullable: true, defaultValue: '' }) + author?: string; - @Field(() => GraphQLISODateTime, { nullable: true }) - update_at?: Date; + @Field({ nullable: true }) + createdAt?: number; - @Field(() => GraphQLISODateTime, { nullable: true }) - finished_at?: Date; + @Field({ nullable: true }) + updateAt?: number; - @Field({ defaultValue: false }) + @Field({ nullable: true }) + finishedAt?: number; + + @Field({ nullable: true, defaultValue: false }) viewed?: boolean; @Field({ nullable: true }) @@ -46,14 +49,14 @@ export class Experiment { @Field({ defaultValue: false }) shared?: boolean; - @Field(() => [ResultUnion]) - results: Array<typeof ResultUnion>; + @Field(() => [ResultUnion], { nullable: true, defaultValue: [] }) + results?: Array<typeof ResultUnion>; @Field(() => [String]) datasets: string[]; @Field(() => String, { nullable: true }) - filter: string; + filter?: string; @Field() domain: string; @@ -67,3 +70,6 @@ export class Experiment { @Field() name: string; } + +@ObjectType() +export class PartialExperiment extends PartialType(Experiment) {} diff --git a/api/src/engine/models/experiment/input/experiment-edit.input.ts b/api/src/engine/models/experiment/input/experiment-edit.input.ts new file mode 100644 index 0000000000000000000000000000000000000000..d37deb1e8d008d746fda7214bb6890142b74a12a --- /dev/null +++ b/api/src/engine/models/experiment/input/experiment-edit.input.ts @@ -0,0 +1,10 @@ +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class ExperimentEditInput { + @Field({ nullable: true }) + name?: string; + + @Field({ nullable: true }) + viewed?: boolean; +} diff --git a/api/src/engine/models/experiment/list-experiments.model.ts b/api/src/engine/models/experiment/list-experiments.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..8f8592e52f1e3d85946d373a73b4edd00ac318f5 --- /dev/null +++ b/api/src/engine/models/experiment/list-experiments.model.ts @@ -0,0 +1,17 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { Experiment } from './experiment.model'; + +@ObjectType() +export class ListExperiments { + @Field({ nullable: true, defaultValue: 0 }) + currentPage?: number; + + @Field({ nullable: true }) + totalPages?: number; + + @Field({ nullable: true }) + totalExperiments?: number; + + @Field(() => [Experiment]) + experiments: Experiment[]; +} diff --git a/api/src/schema.gql b/api/src/schema.gql index ba240a7927778b33acb82b9f9b079bb7ac62137a..7f7184b1a4848433930c010edd5aa3090207fd79 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -52,13 +52,14 @@ type Algorithm { type Experiment { uuid: String - created_at: DateTime - update_at: DateTime - finished_at: DateTime - viewed: Boolean! + author: String + createdAt: Float + updateAt: Float + finishedAt: Float + viewed: Boolean status: String shared: Boolean! - results: [ResultUnion!]! + results: [ResultUnion!] datasets: [String!]! filter: String domain: String! @@ -67,11 +68,6 @@ type Experiment { name: String! } -""" -A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. -""" -scalar DateTime - union ResultUnion = TableResult | RawResult type TableResult { @@ -92,12 +88,41 @@ The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404]( """ scalar JSONObject @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") +type PartialExperiment { + uuid: String + author: String + createdAt: Float + updateAt: Float + finishedAt: Float + viewed: Boolean + status: String + shared: Boolean + results: [ResultUnion!] + datasets: [String!] + filter: String + domain: String + variables: [String!] + algorithm: Algorithm + name: String +} + +type ListExperiments { + currentPage: Float + totalPages: Float + totalExperiments: Float + experiments: [Experiment!]! +} + type Query { domains(ids: [String!] = []): [Domain!]! + experiments(name: String = "", page: Float = 0): ListExperiments! + expriment(uuid: String!): Experiment! } type Mutation { createExperiment(transient: Boolean = false, data: ExperimentCreateInput!): Experiment! + editExperiment(data: ExperimentEditInput!, uuid: String!): Experiment! + removeExperiment(uuid: String!): PartialExperiment! } input ExperimentCreateInput { @@ -119,3 +144,8 @@ input AlgorithmParamInput { name: String! value: [String!]! } + +input ExperimentEditInput { + name: String + viewed: Boolean +}