diff --git a/api/package-lock.json b/api/package-lock.json index 6f50c98fcb8f360b97b14a52408e77063f952e33..88182859b9a42510792332bff648111830b823b3 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -19,6 +19,7 @@ "apollo-server-express": "^3.3.0", "axios": "^0.21.1", "graphql": "^15.5.3", + "graphql-type-json": "^0.3.2", "jsonata": "^1.8.5", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -5582,6 +5583,14 @@ "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" } }, + "node_modules/graphql-type-json": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", + "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==", + "peerDependencies": { + "graphql": ">=0.8.0" + } + }, "node_modules/graphql-ws": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.4.0.tgz", @@ -14888,6 +14897,12 @@ "tslib": "^2.1.0" } }, + "graphql-type-json": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz", + "integrity": "sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==", + "requires": {} + }, "graphql-ws": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.4.0.tgz", diff --git a/api/package.json b/api/package.json index fd94af7dcea0a56fcf453b755939a3a373c8b459..1885442d70f4ac545ac7fc662ec9bef27d1d21de 100644 --- a/api/package.json +++ b/api/package.json @@ -32,6 +32,7 @@ "apollo-server-express": "^3.3.0", "axios": "^0.21.1", "graphql": "^15.5.3", + "graphql-type-json": "^0.3.2", "jsonata": "^1.8.5", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts index 1cdbb509c93a78794235c371c6fb3a62076b4792..45bf43a1df7f8fc7877f297a69e5edf6eec50d6d 100644 --- a/api/src/engine/connectors/datashield/main.connector.ts +++ b/api/src/engine/connectors/datashield/main.connector.ts @@ -1,21 +1,43 @@ 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/experiment-create.input'; -import { Experiment } from 'src/engine/models/experiment/experiment.model'; +import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; +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 { - createTransient( + 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> { @@ -26,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.'); } @@ -34,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 cb1c884c6101bec8de814accda529690cb92896f..05b9711b0a989e4df868fa53c480c881cb18b2ed 100644 --- a/api/src/engine/connectors/exareme/converters.ts +++ b/api/src/engine/connectors/exareme/converters.ts @@ -1,14 +1,25 @@ import { Category } from 'src/engine/models/category.model'; -import { ExperimentCreateInput } from 'src/engine/models/experiment/experiment-create.input'; +import { AlgorithmParameter } from 'src/engine/models/experiment/algorithm-parameter.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'; -import { TableResult } from 'src/engine/models/result/table-result.model'; +import { ResultUnion } from 'src/engine/models/result/common/result-union.model'; +import { + GroupResult, + GroupsResult, +} from 'src/engine/models/result/groups-result.model'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; import { Variable } from 'src/engine/models/variable.model'; import { Entity } from './interfaces/entity.interface'; +import { ExperimentData } from './interfaces/experiment/experiment.interface'; +import { ResultExperiment } from './interfaces/experiment/result-experiment.interface'; import { Hierarchy } from './interfaces/hierarchy.interface'; -import { TransientDataResult } from './interfaces/transient/transient-data-result.interface'; import { VariableEntity } from './interfaces/variable-entity.interface'; -import { transientToTable } from './transformations'; +import { + descriptiveModelToTables, + descriptiveSingleToTables, + transformToExperiment, +} from './transformations'; export const dataToGroup = (data: Hierarchy): Group => { return { @@ -39,6 +50,13 @@ export const dataToVariable = (data: VariableEntity): Variable => { }; }; +const algoParamInputToData = (param: AlgorithmParameter) => { + return { + name: param.name, + value: param.value.join(','), + }; +}; + export const experimentInputToData = (data: ExperimentCreateInput) => { return { algorithm: { @@ -47,10 +65,6 @@ export const experimentInputToData = (data: ExperimentCreateInput) => { name: 'dataset', value: data.datasets.join(','), }, - { - name: 'y', - value: data.variables.join(','), - }, { name: 'filter', value: data.filter, @@ -59,19 +73,85 @@ export const experimentInputToData = (data: ExperimentCreateInput) => { name: 'pathology', value: data.domain, }, - ], - type: 'string', - name: data.algorithm, + { + name: 'y', + value: data.variables.join(','), + }, + ].concat(data.algorithm.parameters.map(algoParamInputToData)), + type: data.algorithm.type ?? 'string', + name: data.algorithm.name, }, name: data.name, }; }; -export const dataToTransient = (data: TransientDataResult): Experiment => { - const tabs: TableResult[] = transientToTable.evaluate(data); +export const descriptiveDataToTableResult = ( + data: ResultExperiment, +): GroupsResult[] => { + const result = new GroupsResult(); - return { - title: data.name, - results: tabs, + result.groups = [ + new GroupResult({ + name: 'Single', + results: descriptiveSingleToTables.evaluate(data), + }), + ]; + + result.groups.push( + new GroupResult({ + name: 'Model', + results: descriptiveModelToTables.evaluate(data), + }), + ); + + return [result]; +}; + +export const dataToExperiment = (data: ExperimentData): Experiment => { + const expTransform = transformToExperiment.evaluate(data); + + const exp: Experiment = { + ...expTransform, + results: [], }; + + 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 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 new file mode 100644 index 0000000000000000000000000000000000000000..7412cb0560c9eda88fe61dd4ebbf8128c94e2c62 --- /dev/null +++ b/api/src/engine/connectors/exareme/interfaces/experiment/experiment.interface.ts @@ -0,0 +1,11 @@ +import { ResultExperiment } from './result-experiment.interface'; + +export interface ExperimentData { + name: string; + 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/interfaces/experiment/result-experiment.interface.ts b/api/src/engine/connectors/exareme/interfaces/experiment/result-experiment.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..e655151c9e7b113a2dbff0402d8e99c61534adee --- /dev/null +++ b/api/src/engine/connectors/exareme/interfaces/experiment/result-experiment.interface.ts @@ -0,0 +1,4 @@ +export interface ResultExperiment { + data: unknown; + type: string; +} diff --git a/api/src/engine/connectors/exareme/interfaces/transient/transient-data-result.interface.ts b/api/src/engine/connectors/exareme/interfaces/transient/transient-data-result.interface.ts deleted file mode 100644 index 37a62f96b071c594696f01bd3b27d75d2bc25f67..0000000000000000000000000000000000000000 --- a/api/src/engine/connectors/exareme/interfaces/transient/transient-data-result.interface.ts +++ /dev/null @@ -1,42 +0,0 @@ -export interface NumericalData { - [key: string]: number; -} - -export interface CategoricalData { - [key: string]: { - count: number; - percentage: number; - }; -} - -export interface TransientDataResult { - name: string; - result: [ - { - data: { - single: { - [variable: string]: { - [dataset: string]: { - data: NumericalData | CategoricalData; - num_datapoints: number; - num_total: number; - num_nulls: number; - }; - }; - }; - model: { - [dataset: string]: { - data: { - [variable: string]: { - [key: string]: number; - }; - }; - num_datapoints: number; - num_total: number; - num_nulls: number; - }; - }; - }; - }, - ]; -} diff --git a/api/src/engine/connectors/exareme/main.connector.ts b/api/src/engine/connectors/exareme/main.connector.ts index 75f21b56698ea59dbc65a88b4edf1ea014163e48..81ed14ab64999708295fd7bbbc38678994d1ba86 100644 --- a/api/src/engine/connectors/exareme/main.connector.ts +++ b/api/src/engine/connectors/exareme/main.connector.ts @@ -1,23 +1,29 @@ 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 { ExperimentCreateInput } from 'src/engine/models/experiment/experiment-create.input'; -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( @@ -25,16 +31,69 @@ export default class ExaremeService implements IEngineService { private readonly httpService: HttpService, ) {} - async createTransient(data: ExperimentCreateInput): Promise<Experiment> { + async createExperiment( + data: ExperimentCreateInput, + isTransient = false, + ): Promise<Experiment> { const form = experimentInputToData(data); - const path = this.options.baseurl + 'experiments/transient'; + const path = + this.options.baseurl + `experiments${isTransient ? '/transient' : ''}`; + + const resultAPI = await firstValueFrom( + 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.post<TransientDataResult>(path, form), + this.httpService.get<ExperimentData>(path), ); - return dataToTransient(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[]> { @@ -85,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 @@ -99,11 +158,11 @@ 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 - .post(path, request.body) + .patch(path, request.body) .pipe(map((response) => response.data)); } @@ -123,22 +182,23 @@ export default class ExaremeService implements IEngineService { .pipe(map((response) => response.data)); } - getExperiments(): Observable<string> { + getExperiments(request: Request): Observable<string> { const path = this.options.baseurl + 'experiments'; return this.httpService - .get<string>(path) + .get<string>(path, { params: request.query }) .pipe(map((response) => response.data)); } - getAlgorithms(): Observable<string> { + getAlgorithms(request: Request): Observable<string> { const path = this.options.baseurl + 'algorithms'; return this.httpService - .get<string>(path) + .get<string>(path, { params: request.query }) .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 5a03098b673d393dcffa8eb33aada2c71bef65d1..8df362c42f17978d07357fdee8e6d793c48dab09 100644 --- a/api/src/engine/connectors/exareme/transformations.ts +++ b/api/src/engine/connectors/exareme/transformations.ts @@ -3,27 +3,93 @@ import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata' -export const transientToTable = jsonata(` +export const transformToExperiment = jsonata(` ( - $trim := function($v) {$v.$string().$replace(/(\\d+\\.\\d{1,3})(\\d*)/, "$1")}; + $params := ["y", "pathology", "dataset", "filter"]; - $e := function($x, $r) {($x != null) ? $trim($x) : ($r ? $r : '')}; + { + "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 + }) + } + } +) +`); + +const headerDescriptivie = ` +$e := function($x, $r) {($x != null) ? $fnum($x) : ($r ? $r : '')}; + +$fnum := function($x) { $type($x) = 'number' ? $round($number($x),3) : $x }; + +$fn := function($o, $prefix) { + $type($o) = 'object' ? + $each($o, function($v, $k) {( + $type($v) = 'object' ? { $k: $v.count & ' (' & $v.percentage & '%)' } : { + $k: $v + } + )}) ~> $merge() + : {} +};`; + +export const descriptiveModelToTables = jsonata(` +( + ${headerDescriptivie} + + $vars := $count(data.single.*)-1; + $varName := $keys(data.single); + $model := data.model; + + [[0..$vars].( + $i := $; + $ks := $keys($model.*.data.*[$i][$type($) = 'object']); + { + 'name': $varName[$i], + 'headers': $append("", $keys($$.data.model)).{ + 'name': $, + 'type': 'string' + }, + 'data': [ + [$varName[$i], $model.*.($e(num_total))], + ['Datapoints', $model.*.($e(num_datapoints))], + ['Nulls', $model.*.($e(num_nulls))], + $model.*.data.($fn($.*[$i])) ~> $reduce(function($a, $b) { + $map($ks, function($k) {( + { + $k : [$e($lookup($a,$k), "No data"), $e($lookup($b,$k), "No data")] + } + )}) ~> $merge() + }) ~> $each(function($v, $k) {$append($k,$v)}) + ] + } + )] +)`); - $fn := function($o, $prefix) { - $type($o) = 'object' ? - $each($o, function($v, $k) {( - $type($v) = 'object' ? { $k: $v.count & ' (' & $v.percentage & '%)' } : { - $k: $v - } - )}) ~> $merge() - : {} - }; +export const descriptiveSingleToTables = jsonata(` +( + ${headerDescriptivie} - result.data.[ + data.[ $.single.*@$p#$i.( $ks := $keys($p.*.data[$type($) = 'object']); { - 'groupBy' : 'single', 'name': $keys(%)[$i], 'headers': $append("", $keys(*)).{ 'name': $, 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 0d90e92e62de3e342cc206f4579a2bf399df1f36..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 { ExperimentCreateInput } from './models/experiment/experiment-create.input'; -import { Experiment } from './models/experiment/experiment.model'; +import { + Experiment, + PartialExperiment, +} from './models/experiment/experiment.model'; +import { ExperimentCreateInput } from './models/experiment/input/experiment-create.input'; +import { ExperimentEditInput } from './models/experiment/input/experiment-edit.input'; +import { ListExperiments } from './models/experiment/list-experiments.model'; export interface IEngineOptions { type: string; @@ -10,24 +15,43 @@ export interface IEngineOptions { } export interface IEngineService { + //GraphQL getDomains(ids: string[]): Domain[] | Promise<Domain[]>; + createExperiment( + data: ExperimentCreateInput, + 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>; - createTransient( - data: ExperimentCreateInput, - ): Promise<Experiment> | Experiment; - startExperiment(request: Request): Observable<string>; getActiveUser(request: Request): Observable<string>; diff --git a/api/src/engine/engine.resolver.ts b/api/src/engine/engine.resolver.ts index e3de433e113e0e13577372240cda4114aa02a193..a954ed3b004e46c8b2cce19ebbc327ac24c2abfe 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 { ExperimentCreateInput } from './models/experiment/experiment-create.input'; -import { Experiment } from './models/experiment/experiment.model'; +import { + Experiment, + PartialExperiment, +} from './models/experiment/experiment.model'; +import { ExperimentCreateInput } from './models/experiment/input/experiment-create.input'; +import { ExperimentEditInput } from './models/experiment/input/experiment-edit.input'; +import { ListExperiments } from './models/experiment/list-experiments.model'; @Resolver() export class EngineResolver { @@ -20,10 +25,43 @@ 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 createTransient( + async createExperiment( @Args('data') experimentCreateInput: ExperimentCreateInput, + @Args('isTransient', { nullable: true, defaultValue: false }) + isTransient: boolean, ) { - return this.engineService.createTransient(experimentCreateInput); + return this.engineService.createExperiment( + experimentCreateInput, + 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/algorithm-parameter.model.ts b/api/src/engine/models/experiment/algorithm-parameter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..79666de79a81e2a7d3c2e2c0756404a995479735 --- /dev/null +++ b/api/src/engine/models/experiment/algorithm-parameter.model.ts @@ -0,0 +1,10 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class AlgorithmParameter { + @Field() + name: string; + + @Field(() => [String]) + value: string[]; +} diff --git a/api/src/engine/models/experiment/algorithm.model.ts b/api/src/engine/models/experiment/algorithm.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..30087c5b55d07d213508d125ba9eb491920073c2 --- /dev/null +++ b/api/src/engine/models/experiment/algorithm.model.ts @@ -0,0 +1,14 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { AlgorithmParameter } from './algorithm-parameter.model'; + +@ObjectType() +export class Algorithm { + @Field() + name: string; + + @Field(() => [AlgorithmParameter], { nullable: true, defaultValue: [] }) + parameters: AlgorithmParameter[]; + + @Field() + type: string; +} diff --git a/api/src/engine/models/experiment/experiment.model.ts b/api/src/engine/models/experiment/experiment.model.ts index 4e7abaeb1ef440bd449b548069e2187a14057000..06e28b4b2e1c93e8dac7c6e7dc828c706f370c8a 100644 --- a/api/src/engine/models/experiment/experiment.model.ts +++ b/api/src/engine/models/experiment/experiment.model.ts @@ -1,44 +1,54 @@ -import { - createUnionType, - Field, - GraphQLISODateTime, - ObjectType, -} from '@nestjs/graphql'; -import { DummyResult } from '../result/dummy-result.model'; -import { TableResult } from '../result/table-result.model'; - -export const ResultUnion = createUnionType({ - name: 'ResultUnion', - types: () => [TableResult, DummyResult], - resolveType(value) { - if (value.headers) { - return TableResult; - } - if (value.listMax) { - return DummyResult; - } - - return null; - }, -}); +import { Field, ObjectType, PartialType } from '@nestjs/graphql'; +import { ResultUnion } from '../result/common/result-union.model'; +import { Algorithm } from './algorithm.model'; @ObjectType() export class Experiment { - @Field() - title: string; - @Field({ nullable: true }) uuid?: string; - @Field(() => GraphQLISODateTime, { nullable: true }) - created_at?: Date; + @Field({ nullable: true, defaultValue: '' }) + author?: string; + + @Field({ nullable: true }) + createdAt?: number; + + @Field({ nullable: true }) + updateAt?: number; + + @Field({ nullable: true }) + finishedAt?: number; + + @Field({ nullable: true, defaultValue: false }) + viewed?: boolean; + + @Field({ nullable: true }) + status?: string; - @Field(() => GraphQLISODateTime, { nullable: true }) - update_at?: Date; + @Field({ defaultValue: false }) + shared?: boolean; - @Field(() => GraphQLISODateTime, { nullable: true }) - finished_at?: Date; + @Field(() => [ResultUnion], { nullable: true, defaultValue: [] }) + results?: Array<typeof ResultUnion>; - @Field(() => [ResultUnion]) - results: Array<typeof ResultUnion>; + @Field(() => [String]) + datasets: string[]; + + @Field(() => String, { nullable: true }) + filter?: string; + + @Field() + domain: string; + + @Field(() => [String]) + variables: string[]; + + @Field() + algorithm: Algorithm; + + @Field() + name: string; } + +@ObjectType() +export class PartialExperiment extends PartialType(Experiment) {} diff --git a/api/src/engine/models/experiment/input/algorithm-parameter.input.ts b/api/src/engine/models/experiment/input/algorithm-parameter.input.ts new file mode 100644 index 0000000000000000000000000000000000000000..8eb4cb26b5dade932440b6fef169275988d33b03 --- /dev/null +++ b/api/src/engine/models/experiment/input/algorithm-parameter.input.ts @@ -0,0 +1,10 @@ +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class AlgorithmParamInput { + @Field() + name: string; + + @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 new file mode 100644 index 0000000000000000000000000000000000000000..54d26b7686559c39b311cee5e5496bce530b0cf5 --- /dev/null +++ b/api/src/engine/models/experiment/input/algorithm.input.ts @@ -0,0 +1,14 @@ +import { Field, InputType } from '@nestjs/graphql'; +import { AlgorithmParamInput } from './algorithm-parameter.input'; + +@InputType() +export class AlgorithmInput { + @Field() + name: string; + + @Field(() => [AlgorithmParamInput], { nullable: true, defaultValue: [] }) + parameters: AlgorithmParamInput[]; + + @Field() + type: string; +} diff --git a/api/src/engine/models/experiment/experiment-create.input.ts b/api/src/engine/models/experiment/input/experiment-create.input.ts similarity index 80% rename from api/src/engine/models/experiment/experiment-create.input.ts rename to api/src/engine/models/experiment/input/experiment-create.input.ts index d871eead0d40690fda47ec6b8ee5ff76bd173e6d..85a54049cffde188cd770237ba53ef1ccf847027 100644 --- a/api/src/engine/models/experiment/experiment-create.input.ts +++ b/api/src/engine/models/experiment/input/experiment-create.input.ts @@ -1,21 +1,22 @@ import { Field, InputType } from '@nestjs/graphql'; +import { AlgorithmInput } from './algorithm.input'; @InputType() export class ExperimentCreateInput { @Field(() => [String]) datasets: string[]; - @Field(() => [String]) - variables: string[]; - @Field(() => String, { nullable: true }) filter: string; @Field() domain: string; + @Field(() => [String]) + variables: string[]; + @Field() - algorithm: string; + algorithm: AlgorithmInput; @Field() name: string; 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/engine/models/result/common/result-union.model.ts b/api/src/engine/models/result/common/result-union.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f7b522c0a06a1c12f13cf2e608b49f2da233c77 --- /dev/null +++ b/api/src/engine/models/result/common/result-union.model.ts @@ -0,0 +1,24 @@ +import { createUnionType } from '@nestjs/graphql'; +import { GroupsResult } from '../groups-result.model'; +import { RawResult } from '../raw-result.model'; +import { TableResult } from '../table-result.model'; + +export const ResultUnion = createUnionType({ + name: 'ResultUnion', + types: () => [TableResult, RawResult, GroupsResult], + resolveType(value) { + if (value.headers) { + return TableResult; + } + + if (value.listMax) { + return RawResult; + } + + if (value.groups) { + return GroupsResult; + } + + return null; + }, +}); diff --git a/api/src/engine/models/result/common/result.model.ts b/api/src/engine/models/result/common/result.model.ts index ca86bfb4e109c44d0d4c06ea0e70ac05e3bf656a..e5e059738c16bb8297558add9aa51e7bce9791e2 100644 --- a/api/src/engine/models/result/common/result.model.ts +++ b/api/src/engine/models/result/common/result.model.ts @@ -1,7 +1,4 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { ObjectType } from '@nestjs/graphql'; @ObjectType() -export abstract class Result { - @Field({ nullable: true }) - groupBy?: string; -} +export abstract class Result {} diff --git a/api/src/engine/models/result/dummy-result.model.ts b/api/src/engine/models/result/dummy-result.model.ts deleted file mode 100644 index 99e36f5c90b9100e1cd46ed9fa67f613a46864b5..0000000000000000000000000000000000000000 --- a/api/src/engine/models/result/dummy-result.model.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Field, ObjectType } from '@nestjs/graphql'; -import { Result } from './common/result.model'; - -@ObjectType() -export class DummyResult extends Result { - @Field() - name: string; - - @Field(() => [[String]]) - data: string[][]; - - @Field(() => [String]) - listMax: string[]; -} diff --git a/api/src/engine/models/result/groups-result.model.ts b/api/src/engine/models/result/groups-result.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3de47038bb69b09615831c8c5dac1cf456d93df --- /dev/null +++ b/api/src/engine/models/result/groups-result.model.ts @@ -0,0 +1,22 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { ResultUnion } from './common/result-union.model'; +import { Result } from './common/result.model'; + +@ObjectType() +export class GroupResult { + public constructor(init?: Partial<GroupResult>) { + Object.assign(this, init); + } + + @Field() + name: string; + + @Field(() => [ResultUnion]) + results: Array<typeof ResultUnion>; +} + +@ObjectType() +export class GroupsResult extends Result { + @Field(() => [GroupResult]) + groups: GroupResult[]; +} diff --git a/api/src/engine/models/result/raw-result.model.ts b/api/src/engine/models/result/raw-result.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..5074e015cc483a3b4fe899f9608d8ddba76f0bfa --- /dev/null +++ b/api/src/engine/models/result/raw-result.model.ts @@ -0,0 +1,12 @@ +import { Field, ObjectType } from '@nestjs/graphql'; +import { GraphQLJSONObject } from 'graphql-type-json'; +import { Result } from './common/result.model'; + +@ObjectType() +export class RawResult extends Result { + @Field(() => GraphQLJSONObject) + data: unknown; + + @Field(() => [String], { defaultValue: [] }) + listMax?: string[]; +} diff --git a/api/src/schema.gql b/api/src/schema.gql index c2ebe062948d88d817911b41942288f96c926b25..fc53b738b631a6dc35e5e162889f12f1c726df06 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -34,54 +34,125 @@ type Domain { rootGroup: Group! } -type Header { +type GroupResult { name: String! - type: String! + results: [ResultUnion!]! } -type Experiment { - title: String! - uuid: String - created_at: DateTime - update_at: DateTime - finished_at: DateTime - results: [ResultUnion!]! +union ResultUnion = TableResult | RawResult | GroupsResult + +type TableResult { + name: String! + data: [[String!]!]! + headers: [Header!]! +} + +type RawResult { + data: JSONObject! + listMax: [String!]! } """ -A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. +The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). """ -scalar DateTime +scalar JSONObject @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf") -union ResultUnion = TableResult | DummyResult +type GroupsResult { + groups: [GroupResult!]! +} -type TableResult { - groupBy: String +type Header { name: String! - data: [[String!]!]! - headers: [Header!]! + type: String! } -type DummyResult { - groupBy: String +type AlgorithmParameter { name: String! - data: [[String!]!]! - listMax: [String!]! + value: [String!]! +} + +type Algorithm { + name: String! + parameters: [AlgorithmParameter!] + type: String! +} + +type Experiment { + 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 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 { - createTransient(data: ExperimentCreateInput!): Experiment! + createExperiment(isTransient: Boolean = false, data: ExperimentCreateInput!): Experiment! + editExperiment(data: ExperimentEditInput!, uuid: String!): Experiment! + removeExperiment(uuid: String!): PartialExperiment! } input ExperimentCreateInput { datasets: [String!]! - variables: [String!]! filter: String domain: String! - algorithm: String! + variables: [String!]! + algorithm: AlgorithmInput! + name: String! +} + +input AlgorithmInput { name: String! + parameters: [AlgorithmParamInput!] = [] + type: String! +} + +input AlgorithmParamInput { + name: String! + value: [String!]! +} + +input ExperimentEditInput { + name: String + viewed: Boolean }