diff --git a/api/src/common/interfaces/utilities.interface.ts b/api/src/common/interfaces/utilities.interface.ts index efcb1cd621628c288d0f7d6d5b674275add8d4b8..db02ef99e3f4c67f65f320f1d0bfe1a2a6b832f5 100644 --- a/api/src/common/interfaces/utilities.interface.ts +++ b/api/src/common/interfaces/utilities.interface.ts @@ -1,5 +1,9 @@ +import { ResultUnion } from 'src/engine/models/result/common/result-union.model'; + export type Dictionary<T> = { [key: string]: T }; +export type ExperimentResult = typeof ResultUnion; + export enum MIME_TYPES { ERROR = 'text/plain+error', WARNING = 'text/plain+warning', diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts index 0b884a9061ef1ad789cb6886f4d125670476337b..010e000a5868fd8e1d3b37a9b44611d4a26b79b5 100644 --- a/api/src/engine/connectors/datashield/main.connector.ts +++ b/api/src/engine/connectors/datashield/main.connector.ts @@ -1,22 +1,31 @@ +import { HttpService } from '@nestjs/axios'; +import { Inject, Logger } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { Request } from 'express'; import { firstValueFrom, Observable } from 'rxjs'; +import { MIME_TYPES } from 'src/common/interfaces/utilities.interface'; +import { ENGINE_MODULE_OPTIONS } from 'src/engine/engine.constants'; import { IEngineOptions, 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 { Algorithm } from 'src/engine/models/experiment/algorithm.model'; import { Experiment, PartialExperiment, } from 'src/engine/models/experiment/experiment.model'; -import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model'; +import { ExperimentCreateInput } from 'src/engine/models/experiment/input/experiment-create.input'; import { ExperimentEditInput } from 'src/engine/models/experiment/input/experiment-edit.input'; -import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; -import { ENGINE_MODULE_OPTIONS } from 'src/engine/engine.constants'; -import { Inject } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; -import { REQUEST } from '@nestjs/core'; -import { Request } from 'express'; -import { transformToDomains } from './transformations'; +import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model'; +import { RawResult } from 'src/engine/models/result/raw-result.model'; +import { TableResult } from 'src/engine/models/result/table-result.model'; +import { + transformToDomains, + transformToHisto, + transformToTable, +} from './transformations'; export default class DataShieldService implements IEngineService { + private static readonly logger = new Logger(DataShieldService.name); + headers = {}; constructor( @Inject(ENGINE_MODULE_OPTIONS) private readonly options: IEngineOptions, private readonly httpService: HttpService, @@ -31,18 +40,104 @@ export default class DataShieldService implements IEngineService { throw new Error('Method not implemented.'); } - createExperiment( + async getHistogram(variable: string): Promise<RawResult> { + const path = + this.options.baseurl + `histogram?var=${variable}&type=combine`; + + const response = await firstValueFrom( + this.httpService.get(path, { + headers: { + cookie: this.req['req'].headers['cookie'], + }, + }), + ); + + if (response.data['breaks'] === undefined) { + DataShieldService.logger.warn('Inconsistency on histogram result'); + DataShieldService.logger.verbose(path); + return { + rawdata: { + data: response.data[0], + type: MIME_TYPES.ERROR, + }, + }; + } + + const title = variable.replace(/\./g, ' ').trim(); + const data = { ...response.data, title }; + const chart = transformToHisto.evaluate(data); + + return { + rawdata: { + data: chart, + type: 'application/vnd.highcharts+json', + }, + }; + } + + async getDescriptiveStats(variable: string): Promise<TableResult> { + const path = this.options.baseurl + `quantiles?var=${variable}&type=split`; + + const response = await firstValueFrom( + this.httpService.get(path, { + headers: { + cookie: this.req['req'].headers['cookie'], + }, + }), + ); + + const title = variable.replace(/\./g, ' ').trim(); + const data = { ...response.data, title }; + return transformToTable.evaluate(data); + } + + async createExperiment( data: ExperimentCreateInput, isTransient: boolean, - ): Experiment | Promise<Experiment> { - throw new Error('Method not implemented.'); + ): Promise<Experiment> { + const expResult: Experiment = { + id: `${data.algorithm.id}-${Date.now()}`, + variables: data.variables, + name: data.name, + domain: data.domain, + datasets: data.datasets, + algorithm: { + id: data.algorithm.id, + }, + }; + + switch (data.algorithm.id) { + case 'MULTIPLE_HISTOGRAMS': { + expResult.results = await Promise.all<RawResult>( + data.variables.map( + async (variable) => await this.getHistogram(variable), + ), + ); + break; + } + case 'DESCRIPTIVE_STATS': { + expResult.results = await Promise.all<TableResult>( + [...data.variables, ...data.coVariables].map( + async (variable) => await this.getDescriptiveStats(variable), + ), + ); + break; + } + } + + return expResult; } listExperiments( page: number, name: string, ): ListExperiments | Promise<ListExperiments> { - throw new Error('Method not implemented.'); + return { + totalExperiments: 0, + experiments: [], + totalPages: 0, + currentPage: 0, + }; } getExperiment(id: string): Experiment | Promise<Experiment> { @@ -63,12 +158,24 @@ export default class DataShieldService implements IEngineService { async getDomains(): Promise<Domain[]> { const path = this.options.baseurl + 'start'; - const data = await firstValueFrom( + const response = await firstValueFrom( this.httpService.get(path, { auth: { username: 'guest', password: 'guest123' }, }), ); - return [transformToDomains.evaluate(data.data)]; + + if (response.headers && response.headers['set-cookie']) { + const cookies = response.headers['set-cookie'] as string[]; + cookies.forEach((cookie) => { + const [key, value] = cookie.split(/={1}/); + this.req.res.cookie(key, value, { + httpOnly: true, + //sameSite: 'none', + }); + }); + } + + return [transformToDomains.evaluate(response.data)]; } getActiveUser(): string { diff --git a/api/src/engine/connectors/datashield/transformations.ts b/api/src/engine/connectors/datashield/transformations.ts index 77302eb169ec09ceca138657252d428364dde62c..5ab91c5b5b3f27673a6f0b1b0cd73d3b1cf21a3e 100644 --- a/api/src/engine/connectors/datashield/transformations.ts +++ b/api/src/engine/connectors/datashield/transformations.ts @@ -22,7 +22,91 @@ export const transformToDomains = jsonata(` }, "variables": $distinct(groups.variables).{ "id": $, - "label": $ + "label": $trim($replace($ & '', '.', ' ')), + "type": "Number" } } `); + +export const transformToHisto = jsonata(` +( + $nbBreaks := $count(breaks); + + { + "chart": { + "type": 'column' + }, + "legend": { + "enabled": false + }, + "series": [ + { + "data": counts, + "dataLabels": { + "enabled": true + } + } + ], + "title": { + "text": title ? title : '' + }, + "tooltip": { + "enabled": true + }, + "xAxis": { + "categories": breaks#$i[$i < $nbBreaks-1].[$ & ' - ' & %.*[$i+1]] + }, + "yAxis": { + "min": 0, + "minRange": 0.1, + "allowDecimals": true + } +}) + `); + +export const transformToTable = jsonata(` +{ + "name": "Descriptive Statistics", + "headers": $append(title, ['5%','10%','25%','50%','75%','90%','95%','Mean']).{ + "name": $, + "type": "string" + }, + "data": $.message.$each(function($v, $k) { + $append($k,$v) + }) +} +`); + +/*export const transformToTable = jsonata(` +( + $params := ["xname", "equidist", "breaks"]; + $concat := function($i, $j) { + $append($i, $j) + }; + + { + "name": "test", + "headers": $append('', $.*[0].breaks).{ + "name": $, + "type": "string" + }, + "data": $.$each(function($v, $k) { + [[$k],$v.$each(function($v, $k) {$not($k in $params) ? $append($k, $v) : undefined})] + }) ~> $reduce($concat, []) + } +) +`);*/ + +/* export const transformToTable = jsonata(` +( + $params := ["xname", "equidist", "breaks"]; + + $.$each(function($v, $k) { + { + "name": $k, + "headers": $v.breaks, + "data": $v.$each(function($v, $k) {$not($k in $params) ? $append($k, $v) : undefined}) + } + }) +) +`); */ diff --git a/api/src/engine/engine.module.ts b/api/src/engine/engine.module.ts index 6f8d33c6a63b40a9ac2c07a5bf3983ae0e148775..f00097a2949274285bbaf3349bc61fbb504889e8 100644 --- a/api/src/engine/engine.module.ts +++ b/api/src/engine/engine.module.ts @@ -2,6 +2,7 @@ import { HttpModule, HttpService } from '@nestjs/axios'; import { DynamicModule, Global, Logger, Module } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { GraphQLModule } from '@nestjs/graphql'; +import { Request } from 'express'; import { join } from 'path'; import { ENGINE_MODULE_OPTIONS, ENGINE_SERVICE } from './engine.constants'; import { EngineController } from './engine.controller'; @@ -33,6 +34,14 @@ export class EngineModule { HttpModule, GraphQLModule.forRoot({ autoSchemaFile: join(process.cwd(), 'src/schema.gql'), + context: ({ req, res }) => ({ req, res }), + cors: { + credentials: true, + origin: [ + /http:\/\/localhost($|:\d*)/, + /http:\/\/127.0.0.1($|:\d*)/, + ], + }, }), ], providers: [optionsProvider, engineProvider, EngineResolver], diff --git a/api/src/main.ts b/api/src/main.ts index d6f85b9d78faf28723a2b38bda2452c0866a8af4..ec0635cd5c5b6d7e254dcd525f7e3838945ccd57 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -1,8 +1,19 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './main/app.module'; +const CORS_URL = process.env.CORS_URL ?? process.env.ENGINE_BASE_URL; + async function bootstrap() { - const app = await NestFactory.create(AppModule, { cors: true }); + const app = await NestFactory.create(AppModule, { + cors: { + credentials: true, + origin: [ + /http:\/\/localhost($|:\d*)/, + /http:\/\/127.0.0.1($|:\d*)/, + CORS_URL, + ], + }, + }); await app.listen(process.env.GATEWAY_PORT); } bootstrap();