diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0977baa835a8232a00b63cab804093a3a179018c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ +# ðŸ Home + +[](https://github.com/semantic-release/semantic-release) [](https://gitlab.com/sibmip/gateway/-/commits/main) [](http://www.apache.org/licenses/LICENSE-2.0) [](https://hub.docker.com/r/hbpmip/gateway/) + +## Introduction + +The MIP Gateway is a middleware layer between the [MIP Frontend](https://github.com/HBPMedical/portal-frontend) and a federate analytic engine (Exareme, Datashield, etc...). + +## Contact + +* [Manuel Spuhler](https://github.com/nicedexter) ([manuel.spuhler@chuv.ch](mailto:manuel.spuhler@chuv.ch)) +* [Steve Mendes Reis](https://github.com/M4n0x) ([steve.mendes-reis@chuv.ch](mailto:steve.mendes-reis@chuv.ch)) + +## Technical documentation + +Technical documentation can be found at [https://mip-front.gitbook.io/mip-gateway-doc/](https://mip-front.gitbook.io/mip-gateway-doc/) diff --git a/docs/for-developers/configuration/gateway.md b/docs/for-developers/configuration/gateway.md index 2b16dacfcbdfbc415f9091c3086a000032787d21..0e1b5f32d7e3345350ad6a80633375e6bfbe261f 100644 --- a/docs/for-developers/configuration/gateway.md +++ b/docs/for-developers/configuration/gateway.md @@ -21,14 +21,16 @@ description: >- #### Authentication -| name | type | default | description | -| ----------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| AUTH\_SKIP | boolean | false | Allow to skip authentication. Warn: all routes will be accessible without authentication. | -| AUTH\_JWT\_SECRET | string | N/A | Secret that should be used to generate JWT token | -| AUTH\_JWT\_TOKEN\_EXPIRES\_IN | string | '2d' | <p>JWT token time to live.</p><p>Expressed in seconds or a string describing a time span <a href="https://github.com/vercel/ms">vercel/ms</a></p> | -| AUTH\_COOKIE\_SAME\_SITE | string | 'strict' | Specify the cookie same site option. Value can be `lax`, `strict` or `none` | -| AUTH\_COOKIE\_SECURE | boolean | true | Specify the cookie secure option. Should be set to true if same site is not set to `strict`. | -| AUTH\_ENABLE\_SSO | boolean | false | Enable SSO login process, this variable will be provided to the frontend in order to perform the login. | +| name | type | default | description | +| -------------------------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| AUTH\_SKIP | boolean | false | Allow to skip authentication. Warn: all routes will be accessible without authentication. | +| AUTH\_JWT\_SECRET | string | N/A | Secret that should be used to generate JWT | +| AUTH\_JWT\_REFRESH\_SECRET | string | N/A | Secret that should be used to generate Refresh Token | +| AUTH\_JWT\_TOKEN\_EXPIRES\_IN | string | '1h' | <p>JWT time to live.</p><p>Expressed in seconds or a string describing a time span <a href="https://github.com/vercel/ms">vercel/ms</a></p> | +| AUTH\_JWT\_REFRESH\_TOKEN\_EXPIRES\_IN | | '2d' | <p>Refresh token time to live.</p><p>Expressed in seconds or a string describing a time span <a href="https://github.com/vercel/ms">vercel/ms</a></p> | +| AUTH\_COOKIE\_SAME\_SITE | string | 'strict' | Specify the cookie same site option. Value can be `lax`, `strict` or `none` | +| AUTH\_COOKIE\_SECURE | boolean | true | Specify the cookie secure option. Should be set to true if same site is not set to `strict`. | +| AUTH\_ENABLE\_SSO | boolean | false | Enable SSO login process, this variable will be provided to the frontend in order to perform the login. | #### Database @@ -50,6 +52,16 @@ Matomo is an open source alternative to Google Analytics. The gateway provide th | MATOMO\_URL | string \| undefined | undefined | Base url for matomo scripts and data reporting. This parameter is `required` if Matomo is `enabled` | | MATOMO\_SITE\_ID | string \| undefined | undefined | Matomo Website ID. This parameter is required if `Matomo` is `enabled`. | +#### Cache + +The Gateway offers the possibility to cache some of the most used queries (domains and algorithms queries). This cache use In-Memory data store. + +| name | type | default | description | +| ----------------- | ------- | ------- | ------------------------------------------------------ | +| CACHE\_ENABLED | boolean | true | Enable or disable the cache | +| CACHE\_TTL | number | 1800 | Define (in seconds) time to live for cached elements. | +| CACHE\_MAX\_ITEMS | number | 100 | Max items that can be cached at the same time | + ### Overwrite parameters These parameters can be overwrite by either diff --git a/docs/for-developers/connector/Parsing-response-with-JSONata.md b/docs/for-developers/connector/Parsing-response-with-JSONata.md index 4394d3c09d4ab064f29fe249402855ea3fdcc3f7..284456f2e0315f116f4bc8e2be04a2a9654d32db 100644 --- a/docs/for-developers/connector/Parsing-response-with-JSONata.md +++ b/docs/for-developers/connector/Parsing-response-with-JSONata.md @@ -23,7 +23,7 @@ It makes the transformation really easy to do, more readable and thus maintainab File _transformation.ts_ -```ts +```typescript import * as jsonata from 'jsonata'; export const expression = jsonata(` @@ -33,7 +33,7 @@ export const expression = jsonata(` File _converter.ts_ -```ts +```typescript import { expression } from './transformations'; const data = ` diff --git a/docs/for-developers/connector/README.md b/docs/for-developers/connector/README.md index ac68aea26c481fac80b2d2d781254c095177f7c7..e7dc8cf6e87c0e3d881700b99d06cc14e112a3a1 100644 --- a/docs/for-developers/connector/README.md +++ b/docs/for-developers/connector/README.md @@ -1,2 +1,2 @@ -# Connector +# 🔌 Connectors diff --git a/docs/for-developers/frontend/graphql/README.md b/docs/for-developers/frontend/graphql/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8c5b682092b4d3c5932143fa6bb8934aea2630b8 --- /dev/null +++ b/docs/for-developers/frontend/graphql/README.md @@ -0,0 +1,2 @@ +# GraphQL + diff --git a/docs/for-developers/frontend/graphql/unions-and-interfaces.md b/docs/for-developers/frontend/graphql/unions-and-interfaces.md new file mode 100644 index 0000000000000000000000000000000000000000..2d03765f996b1c6201df4f65ccb68899ef176c8d --- /dev/null +++ b/docs/for-developers/frontend/graphql/unions-and-interfaces.md @@ -0,0 +1,16 @@ +--- +description: Abstract schema types +--- + +# Unions and interfaces + +**Unions** and **interfaces** are abstract GraphQL types that enable a schema field to return one of multiple object types. + +Read more about [unions and interface](https://www.apollographql.com/docs/apollo-server/schema/unions-interfaces/). + +In order to be correctly interpreted in the Frontend, especially when dealing [with fragments](https://github.com/apollographql/apollo-client/issues/7050), we have to provide to the cache the possible types for a fragment.  + +{% hint style="warning" %} +In the file `cache.tsx`, within the parameter `possiblesTypes` you should provide a list of types that you expect to see in the GraphQL responses, if a type is missing this will result in a empty response for the specific type missing.  +{% endhint %} + diff --git a/docs/for-developers/frontend/visualisations.md b/docs/for-developers/frontend/visualisations.md index 56395ce99f2db62122194abc64401a12c204838d..ba489645d4f6924bed01aa25c03f4042cd4ab90e 100644 --- a/docs/for-developers/frontend/visualisations.md +++ b/docs/for-developers/frontend/visualisations.md @@ -1,6 +1,6 @@ -# 📊 Visualisations +# 📖 Storybook -To see the different possible visualisations in the frontend we have integrated [storybook.js](https://storybook.js.org) directly in the frontend. +To see the different possible visualisations in the frontend we have integrated [storybook.js](https://storybook.js.org/) directly in the frontend. Start the storybook by launching this command in the frontend folder diff --git a/docs/for-developers/gateway/cache.md b/docs/for-developers/gateway/cache.md new file mode 100644 index 0000000000000000000000000000000000000000..f8ce081527c84be00f16aecf30b86d82d704cf4c --- /dev/null +++ b/docs/for-developers/gateway/cache.md @@ -0,0 +1,27 @@ +--- +description: This page describe how the cache is managed inside the Gateway. +--- + +# 📠Cache + +The cache system is a In-Memory cache (but it can be changed to another cache system like Redis, see [nest documentation](https://docs.nestjs.com/techniques/caching)). + +In the following picture will see an overview of how a request is handled in the Gateway.  + + + +The engine service is like a proxy class for the connector, it implements the `Connector` interface as each connector does but it adds some overall functionality to all the connector like the cache. + +#### What is cached ? + +Currently only `domains` and `algorithms` are cached as they are the most used functions inside the Gateway. + +#### Cache life cycle + +The cache is create whenever a user makes a query to `domain` or `algorithms` and is user based, so a cache entry is created for every pair of `domains|algorithms-user.id`.  + +When the user logout, all entries linked to the user are cleared. + + + +  diff --git a/docs/for-developers/gateway/connector/create-a-connector.md b/docs/for-developers/gateway/connector/create-a-connector.md new file mode 100644 index 0000000000000000000000000000000000000000..71c907536dd9dcabfae049ee584a8748178d96e6 --- /dev/null +++ b/docs/for-developers/gateway/connector/create-a-connector.md @@ -0,0 +1,154 @@ +--- +description: This page describe how to create a connector +--- + +# Create a connector + +All connectors have a folder under `src/engine/connectors/`. If you want to create a new connector, the first thing to do is to create a folder with the name of your connector. Take care that the connector name should be unique.  + +Inside your folder `src/engine/connectors/example/` you need a file name after your connector for e.g. `src/engine/connectors/example/example.connector.ts`. The connector should have the same name as your folder. + +The connector instantiation is managed by the `engine.service.ts`. + +Here is a minimal implementation of a connector + +{% code title="example.connector.ts" %} +```typescript +import { NotImplementedException } from '@nestjs/common'; +import { ExperimentResult } from 'src/common/interfaces/utilities.interface'; +import Connector from 'src/engine/interfaces/connector.interface'; +import { Domain } from 'src/engine/models/domain.model'; +import { Algorithm } from 'src/engine/models/experiment/algorithm.model'; +import { User } from 'src/users/models/user.model'; + +export default class LocalConnector implements Connector { + async login(): Promise<User> { + throw new NotImplementedException(); + } + + async getAlgorithms(): Promise<Algorithm[]> { + throw new NotImplementedException(); + } + + async runExperiment(): Promise<ExperimentResult[]> { + throw new NotImplementedException(); + } + + async getDomains(): Promise<Domain[]> { + throw new NotImplementedException(); + } + + async getActiveUser(): Promise<User> { + throw new NotImplementedException(); + } +} +``` +{% endcode %} + +### Constructor + +The engine service will inject some properties into the connector through the constructor that the connector is free to use or not. + +```typescript + constructor( + private readonly options: EngineOptions, + private readonly httpService: HttpService, + private readonly engineService: EngineService, + ) {} +``` + +The first parameter, `options`, is a key-value store that contains the `ENGINE_TYPE` and the `ENGINE_BASE_URL`. + +The second parameter, `httpService`, is an `Axios` instance shared between all request. + +The third parameter, `engineService`, is the engine service which inject itself into the connector. This can be useful because engine service provide utility functions and use the cache system. So you should always call engine service when you want to access to `algorithms` and `domains` to avoid to access external resources. + +### Experiments + +The Gateway offers two possibilities to manage experiment  + +1. Experiments are managed (save, edit, delete, etc...) directly internally +2. Experiments are managed by the external engine. + +#### Case 1 + +In case 1, we assume that the external engine will only offers the possibility to run experiment (not save them). In this case the connector should only implements `runExperiment`. The gateway will recognize that `runExperiment` is implemented and that it needs to manage the experiments internally. + +This is done by using a `PostgreSQL` database. + +#### Case 2 + +In case 2, we let the external engine manage experiments by itself. This required that the connector implements `createExperiment` and not `runExperiment`. + +This also implies that the connector need to implements these functions + +* `getExperiment` +* `listExperiments` +* `removeExperiment` +* `editExperiment` + +This will delegate all the CRUD work to the external engine.  + +### Authentication & users + +There are two possibilities of authentication both relay on a external resource. The only difference will be about how the user token is managed. + +1. Inside the Gateway +2. Outside the Gateway + +#### Case 1 + +In case 1, the identification is managed inside the gateway and the connector is responsible for making the authentication through the function `login`.  + +The Gateway will then manage the token between the Frontend and the Gateway with a Json Web Token. + +The logout is managed by the method `logout`. After calling `logout` from the connector, the gateway will remove the token from the user's headers. + +#### Case 2 + +In case 2, identification and authentication will both be managed externally through a resource that is located under the URL `/services/sso` for example a [KeyCloak](https://www.keycloak.org/) (exareme is using this strategy). This implies that the `editActiveUser` is implemented and allows updating the user (needed if ToS is enabled). + +In both cases, the `getActiveUser` will retrieve the current user logged.  + +### Configuration + +The connector has it's own part of configuration, it's mainly parameters that are closely related to the connector and not really with the overall configuration. + +| name | default | description | +| ----------- | ------- | ------------------------------------------------------------------------------------------------------- | +| hasGrouping | false | Define if the connector is able to make query grouping by nominal variable (mainly used for histograms) | +| hasFilters | true | Define if the engine behind the connector is able to manage filters | +| hasGalaxy | false | `Deprecated`. Only used by Exareme engine | + +These elements can be configured by the function `getConfiguration`. + +#### Filter + +Filter configuration describe the types of variables that can be considered as number. These types are closely related to the connector as types is depending on the engine used.  + +The types that should be considered as numbers can be configured by defining the function `getFilterConfiguration`.  + +`['real', 'integer']` are the default types. + +#### Formula + +Formula configuration give a list of available variable operations. Each element contains two properties, the variable type and an operation's list. + +```json +[ + { + variableType: 'real', + operationTypes: ['log', 'center', ...] + }, + ... +] +``` + +To define operations, the connector should implements `getFormulaConfiguration`. + + + +  + +  + diff --git a/docs/for-developers/gateway/graphql/README.md b/docs/for-developers/gateway/graphql/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9d5694c90593b8a3e23d549f9de5ca8e798bf90a --- /dev/null +++ b/docs/for-developers/gateway/graphql/README.md @@ -0,0 +1,2 @@ +# 🧙♂ GraphQL + diff --git a/docs/for-developers/gateway/graphql/decorators.md b/docs/for-developers/gateway/graphql/decorators.md new file mode 100644 index 0000000000000000000000000000000000000000..8304f8b393b88462799b2ff774102b849056a44d --- /dev/null +++ b/docs/for-developers/gateway/graphql/decorators.md @@ -0,0 +1,6 @@ +# Decorators + +The request and response made by `GraphQL` are a bit different then what you should expect from a normal REST API. For this purpose and as this is something that is often used, two decorators have been implemented to inject the request. The request can be injected with the decorator `@GQLRequest` and the response with `@GQLResponse` and finally the user can also be injected by using the `@CurrentUser`.  + +These 3 decorators can only be use in a GraphQL context, it will fail if it's a standard API REST request. + diff --git a/docs/for-developers/gateway/static-files.md b/docs/for-developers/gateway/static-files.md new file mode 100644 index 0000000000000000000000000000000000000000..fea1d8a2b287c58f83b038808d1c7b9c913fb5aa --- /dev/null +++ b/docs/for-developers/gateway/static-files.md @@ -0,0 +1,20 @@ +--- +description: This page describe how the static files are managed +--- + +# 🗃 Static files + +As different connector implies different context, there is some cases where the Frontend needed to change depending on the connector currently used, the changes can be related to  + +1. Custom CSS +2. Favicon / logo +3. Terms of Service  +4. Login page  + +A module `files` has been create to allows dynamic resources based on the connector currently used.  + +All static assets are stored under `assets/engines/*`, in this path you have one folder for each connector. By default, all resources are retrieve in the `default` folder. If you want to override a specific file for your connector you just need to create a file with the same name under your connector's folder `assets/engines/yourconnector`. + +#### Markdown files and static files + +For markdown files, there is a specific process which allows to define a placeholder for relative path. For example, if you want to make a reference to a static image inside the Gateway you cannot just use an absolute link to the resource as you don't know the domain that will be used. So for this specific case, you can use the placeholder `$ASSETS_URL$`. Which will be replaced dynamically at the markdown file's rendering. diff --git a/docs/for-developers/gateway/users.md b/docs/for-developers/gateway/users.md index 40dd3a1cd50388ab5dec4d11b2eee1a139d6a81a..c6ed6fd54ccb19a93f1394c80f02c16a8cc34993 100644 --- a/docs/for-developers/gateway/users.md +++ b/docs/for-developers/gateway/users.md @@ -9,7 +9,7 @@ The gateway is not meant to manage users directly. This is the engine's role to ### How it works ? -Let's say we want to retrieve the current user, the gateway will ask through the connector for the user's data in the same time the gateway will look in his own database if it has some data for this user. Then both data are merged to fit the User model. Data from the engine have precedence over the gateway data in case of conflict. +Let's say we want to retrieve the current user, the Gateway will ask through the connector for the user's data in the same time the gateway will look in his own database if it has some data for this user. Then both data are merged to fit the User model. Data from the engine have precedence over the gateway data in case of conflict. {% code title="user.model.ts" %} ```typescript @@ -42,27 +42,4 @@ After merging data from both source and make some integrity check the gateway wi #### Update user profile -So now we know that the data can be retrieve through two different sources, how will we handle updating our user profile ? The system is simple, the gateway will ask the connector if he can handle the user's update by looking if the function `updateUser` is defined in the connector. If it's defined it means that the engine can handle at least some part of the update, so we delay the work to the engine. Now if the engine cannot handle all the update data, the connector can decide to return some attributes back to the gateway.  - -{% code title="example return update data" %} -```typescript - async updateUser( - request: Request, - userId: string, - data: UpdateUserInput - ): Promise<UpdateUserInput | undefined> { - const path = this.options.baseurl + 'user'; - const response = await firstValueFrom( - this.post<string>(request, path, { - prop1: data.attrib1, - prop2: data.attrib2 - }), - ); - - const { attrib1, attrib2, ...subset } = UpdateUserInput // Subset of updateData - return subset; - } -``` -{% endcode %} - -The returned attributes will be provided back to the gateway and will be handle internally as far as it can do it. +So now we know that the data can be retrieve through two different sources, how will we handle updating our user profile ? The system is simple, the gateway will check the connector if he can handle the user's update by looking at the function `updateUser` is defined in the connector. If it's defined it means that the connector can handle the update, so we delay the work to the connector. If the connector cannot handle the update, the Gateway will handle it by itself. diff --git a/docs/for-developers/get-started/Introduction.md b/docs/for-developers/get-started/Introduction.md index 8e731eb6b1fa6edd7da4e56ffe8d4aee96d1d0d0..988fe00eea7fe0e67a353558e10493477b001dd5 100644 --- a/docs/for-developers/get-started/Introduction.md +++ b/docs/for-developers/get-started/Introduction.md @@ -6,7 +6,7 @@ description: Introduction for developers The MIP is mainly composed by 3 components -.png>) +.png>) * **Frontend** : user interface (React.js) * **Gateway** : middleware used to abstract calls from an engine (Nest.js and GraphQL) @@ -16,7 +16,7 @@ The MIP is mainly composed by 3 components The Frontend and the Gateway are closely related and their communication are abstracted from the engine. This abstraction is performed by the Gateway. - + The Gateway is in charge of communications with various engines, it could make simple calls and pass it to the Frontend or make any transformations needed in order to fit the need in the Frontend. @@ -51,7 +51,7 @@ The code above is an example of a connector. ### Visualizations -.png>) +.png>) With the Frontend we will introduce a new way to deal with visualizations. Previously the visualizations were completely manage by the engine. As a part of abstraction from a specific engine we want to be able to delegate this task to the visualization components. diff --git a/docs/for-developers/get-started/Setup-development-environment.md b/docs/for-developers/get-started/Setup-development-environment.md index 287f73db845ec46cee62b5c768b778e30602e02d..0d3984048dd1d71249f998d8ccc13c20240d4403 100644 --- a/docs/for-developers/get-started/Setup-development-environment.md +++ b/docs/for-developers/get-started/Setup-development-environment.md @@ -21,9 +21,9 @@ In this guide we will see how to setup the last two elements. Make sure to have -* [Node.js](https://nodejs.org) -* [NPM](https://npmjs.com) -* [Yarn](https://yarnpkg.com) +* [Node.js](https://nodejs.org) (16.x) +* [NPM](https://npmjs.com) (8.x) +* [Yarn](https://yarnpkg.com) (1.22.x) * [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) installed in your computer. @@ -44,7 +44,7 @@ npm install #### Run the DB -The gateway need a DB in order to work. [TypeORM](https://typeorm.io) is used to make the DB calls agnostic from the real implementation.  +The gateway need a DB in order to work. [TypeORM](https://typeorm.io/) is used to make the DB calls agnostic from the real implementation.  We provide a docker-compose to run a `postgres` DB, you can use it by running the following command @@ -66,7 +66,7 @@ npm run start:dev Once you have started the Gateway, you can play with the GraphQL playground that is automatically integrated within the gateway, follow this link : [http://127.0.0.1:8081/graphql](http://127.0.0.1:8081/graphql). You should be able to see something like this : -.png>) +.png>) This environment is a tool provided by GraphQL to play with queries, mutations, etc... diff --git a/docs/for-developers/recipes/README.md b/docs/for-developers/recipes/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5d3d5fd1f1840ca95582ebeabfa31f67b0ae8109 --- /dev/null +++ b/docs/for-developers/recipes/README.md @@ -0,0 +1,2 @@ +# 👩🳠Recipes + diff --git a/docs/for-developers/recipes/add-a-result-type.md b/docs/for-developers/recipes/add-a-result-type.md new file mode 100644 index 0000000000000000000000000000000000000000..565d1e76fd2d71f14793b5409552607ba4d9adca --- /dev/null +++ b/docs/for-developers/recipes/add-a-result-type.md @@ -0,0 +1,96 @@ +--- +description: This page describe the process to add a type of result +--- + +# Add a result type + +Result's types are defined in the gateway, in order to add a new type of result for the Frontend we need to create a new kind of result inside the gateway.  + +All results are stored inside the `engine` module, specifically inside the `src/engine/models/result` folder.  + +Let's create a model for the demonstration + +{% code title="my-custom.model.ts" lineNumbers="true" %} +```typescript +@ObjectType() +export class MyCustomResult extends Result { + @Field() + name: string; + + @Field() + customProperty: string; +} +``` +{% endcode %} + +Annotations are related to GraphQL, see more about GraphQL's annotation [here](https://docs.nestjs.com/graphql/resolvers#object-types). + +Once our model is created we need to declare it as a part of the possible result types, for this open the file `src/engine/models/result/common/result-union.model.ts` and add the type to the list + +{% code title="result-union.model.ts" lineNumbers="true" %} +```typescript +export const ResultUnion = createUnionType({ + name: 'ResultUnion', + types: () => [ + ... + MyCustomResult, + ], + resolveType(value) { + ... + + if (value.customProperty) return MyCustomResult + + return RawResult; + }, +}); + +``` +{% endcode %} + +{% hint style="info" %} +The `resolveType` function help GraphQL determines the result's type when the object receive is a literal JavaScript object (not a class). +{% endhint %} + +#### Integration in the Frontend + +First of all, as describe in [this issue](https://github.com/apollographql/apollo-client/issues/7050), we need tell Apollo which types we should expect from this `union field`. Open the file `src/components/API/GraphQL/cache.tsx` and in the cache configuration, we need to add a mention to our new type + +{% code title="cache.tsx" lineNumbers="true" %} +```typescript +export const cacheConfig = { + possibleTypes: { + // https://github.com/apollographql/apollo-client/issues/7050 + ResultUnion: [ + ... + 'MyCustomResult' // <- Our new result type + ], + ... + }, + ... +} +``` +{% endcode %} + +Once the possible types is defined, we need to tell Apollo what we want to extract from the the specific result.  + +open the file `src/components/API/GraphQL/fragments.ts` and add the details of our new result.  + +{% code title="fragments.ts" lineNumbers="true" %} +```typescript +export const coreInfoResult = gql` + fragment coreInfoResult on ResultUnion { + ... on RawResult { + rawdata + } + ... on MyCustomResult { + name + customProperty + } + (...) + } +`; +``` +{% endcode %} + +After that we are all setup to receive a `MyCustomResult` from any Experiment. + diff --git a/docs/for-developers/recipes/add-an-algorithm-handler-on-exareme.md b/docs/for-developers/recipes/add-an-algorithm-handler-on-exareme.md new file mode 100644 index 0000000000000000000000000000000000000000..925014879ee2d64fb3c024f40d7b79a7064a29ed --- /dev/null +++ b/docs/for-developers/recipes/add-an-algorithm-handler-on-exareme.md @@ -0,0 +1,142 @@ +--- +description: >- + This page describe how algorithm's outputs are processed with Exareme (I & II) + connector +--- + +# Add an algorithm handler on Exareme + +Exareme algorithms' ouputs are processed based on the design pattern [chain of responsibility](https://refactoring.guru/design-patterns/chain-of-responsibility). Basically every algorithm has an handler that can decide to process the output. Each handler has the responsibility to know if it can handle the current output or not.  + +### Add a new handler  + +Each handler should implements the `ResultHandler` interface. The handler can also directly extends the class `BaseHandler` which provides default implementation of the previous interface. + +For this demonstration we will create a new Handler called `MyAlgorithm`. First we will create a new file inside the `src/engine/connectors/exareme/handlers/algorithms` folder. + +{% code title="my-algorithm.handler.ts" lineNumbers="true" %} +```typescript +export default class MyAlgorithmHandler extends BaseHandler { + + handle(experiment: Experiment, data: any, domain?: Domain): void { + ... + } + +} +``` +{% endcode %} + +The handler can either process the request or pass it to the next handler along the chain. For this concern we will define a new method in our algorithm to define the ability or not to handle the request. + +{% code lineNumbers="true" %} +```typescript +export default class MyAlgorithmHandler extends BaseHandler { + public static readonly ALGO_NAME = 'myAlgorithmName'; + + private canHandle(data: any) { + return ( + data && + data.mySuperProperty && + data.algorithmName = MyAlgorithmHandler.ALGO_NAME + ); + } + + handle(experiment: Experiment, data: any, domain? Domain) { + if (!this.canHandle(data: any)) return super.handle(experiment, data, domain); + + //Process the data + } +} +``` +{% endcode %} + +{% hint style="warning" %} +Make sure to call the next handler either by calling directly `this.next(...)` or `super.handler(...)` otherwise it will end the chaining system +{% endhint %} + +The idea of the chaining system is to improve the `Experiment` object that is passed along the chain. This object is acting like a Request object, it's mutable, then you can modify it to fit the need of your handler. + +Here we will add a result inside our experiment for the purpose of the demonstration.  + +{% code lineNumbers="true" %} +```typescript +export default class MyAlgorithmHandler extends BaseHandler { + public static readonly ALGO_NAME = 'myAlgorithmName'; + + private canHandle(data: any) { + return ( + data && + data.mySuperProperty && + data.algorithmName = MyAlgorithmHandler.ALGO_NAME + ); + } + + private getTableResult(data: any): TableResult { + const tableResult: TableResult = { + name: 'Results', + tableStyle: TableStyle.NORMAL, + headers: ['name', 'value'].map((name) => ({ name, type: 'string' })), + data: [ + 'n_obs', + 't_value', + 'ci_upper', + 'cohens_d', + ].map((name) => [ + name, + data[name], + ]), + }; + + return tableResult; + } + + handle(experiment: Experiment, data: any, domain? Domain) { + if (!this.canHandle(data: any)) return super.handle(experiment, data, domain); + + const tableResult = this.getTableResult(data); + if (tableResult) exp.results.push(tableResult); + } +} +``` +{% endcode %} + +In lines 34 and 35 we know that our handler is up to deal with the request, so our handle can produce the result wanted and add it the experiment.  + +Now we can decide to either end the request here or pass it the next handler. As we know that only one algorithm should process the output we can end the request here by not calling the next handle. + +#### Add it to the chaining system + +Now that we have created our algorithm we should add it to the chaining system, open the file `src/engine/connectors/exareme/handlers/index.ts` and simply add our new class to the list + +{% code title="index.ts" lineNumbers="true" %} +```typescript +const start = new PearsonHandler(); + +start + .setNext(new DescriptiveHandler()) + .setNext(new AnovaOneWayHandler()) + .setNext(new PCAHandler()) + .setNext(new LinearRegressionHandler()) + .setNext(new MyAlgorithmHandler()) + .setNext(new RawHandler()); // should be last handler as it works as a fallback (if other handlers could not process the results) + +export default (exp: Experiment, data: unknown, domain: Domain): Experiment => { + start.handle(exp, data, domain); + return exp; +}; + +``` +{% endcode %} + +The handlers' order is important, if you want to check something in priority, just put it in the top of the list.  + +{% hint style="info" %} +This order can be useful if you want to process error at first. You can easily create a connector that could handle global error before passing along the chain system +{% endhint %} + +{% hint style="warning" %} +Don't forget to write a unit test along of your handler, conventionally called `my-algorithm.handler.spec.ts`. +{% endhint %} + + +