diff --git a/lib/domain/dtos/GetAllFlpsDto.js b/lib/domain/dtos/GetAllFlpsDto.js index f6a651d8bf..63abaa06c9 100644 --- a/lib/domain/dtos/GetAllFlpsDto.js +++ b/lib/domain/dtos/GetAllFlpsDto.js @@ -17,7 +17,11 @@ const PaginationDto = require('./PaginationDto'); const SortDto = Joi.object({ id: Joi.string() .valid('asc', 'desc'), - text: Joi.string() + name: Joi.string() + .valid('asc', 'desc'), + hostname: Joi.string() + .valid('asc', 'desc'), + runNumber: Joi.string() .valid('asc', 'desc'), }); diff --git a/lib/domain/dtos/GetFlpDto.js b/lib/domain/dtos/GetFlpDto.js index d0be2aa07e..4fa30def35 100644 --- a/lib/domain/dtos/GetFlpDto.js +++ b/lib/domain/dtos/GetFlpDto.js @@ -15,7 +15,7 @@ const Joi = require('joi'); const EntityIdDto = require('./EntityIdDto'); const ParamsDto = Joi.object({ - flpId: EntityIdDto, + flpId: EntityIdDto.required(), }); const QueryDto = Joi.object({ diff --git a/lib/domain/dtos/UpdateFlpDto.js b/lib/domain/dtos/UpdateFlpDto.js index 9b90381d9e..973a1d5994 100644 --- a/lib/domain/dtos/UpdateFlpDto.js +++ b/lib/domain/dtos/UpdateFlpDto.js @@ -29,20 +29,18 @@ const BodyDto = Joi.object({ }); const QueryDto = Joi.object({ - flpName: Joi.string(), - runNumber: EntityIdDto, token: Joi.string(), }); const ParamsDto = Joi.object({ - flpName: Joi.string(), - runNumber: EntityIdDto, + flpName: Joi.string().required(), + runNumber: EntityIdDto.required(), }); -const CreateLogDto = Joi.object({ +const UpdateFlpDto = Joi.object({ query: QueryDto, body: BodyDto, params: ParamsDto, }); -module.exports = CreateLogDto; +module.exports = UpdateFlpDto; diff --git a/lib/server/api-docs/API.md b/lib/server/api-docs/API.md index 39a512754a..89b7a39809 100644 --- a/lib/server/api-docs/API.md +++ b/lib/server/api-docs/API.md @@ -738,3 +738,121 @@ Responses: ```bash curl -g "$BOOKKEEPING_URL/api/dpl-process/executions?runNumber=106&detectorId=1&processId=1&hostId=1&token=$TOKEN" ``` + +## FLPs + +Roles of FLPs in runs, with their data counters. + +### `GET /flps` - List the FLP roles + +Operation id: `listFlps` + +Returns the roles of FLPs (one entry per FLP and per run) with their data counters. Paginated, sorted by name descending by default. + +| Parameter | In | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| `page[limit]` | query | integer | no | Maximum number of items to return. LLMs and agents must use 100 or less. | +| `page[offset]` | query | integer | no | Number of items to skip. | +| `sort[id]` | query | string | no | Sort by identifier. | +| `sort[name]` | query | string | no | Sort by FLP name. | +| `sort[hostname]` | query | string | no | Sort by host name. | +| `sort[runNumber]` | query | string | no | Sort by run number. | + +Responses: + +- `200`: A page of FLP roles. +- `400`: The request failed validation. HTTP status is 400 although each error's `status` field is `"422"`. `source.pointer` has the form `/data/attributes//`. + +```bash +curl -g "$BOOKKEEPING_URL/api/flps?page[limit]=100&token=$TOKEN" +``` + +### `POST /flps` - Create an FLP role (not for LLM use) + +Operation id: `createFlp` + +Creates the role of an FLP in a run. Responds 404 if the run does not exist. + +**Not meant for LLM or MCP tool use** (`x-llm-exclude`). + +Request body (JSON): + +| Field | Type | Required | Notes | +| --- | --- | --- | --- | +| `name` | string | yes | min length 1 | +| `hostname` | string | yes | min length 1 | +| `runNumber` | integer | yes | | +| `nTimeframes` | number | no | | +| `bytesProcessed` | number | no | | +| `bytesEquipmentReadOut` | number | no | | +| `bytesRecordingReadOut` | number | no | | +| `bytesFairMQReadOut` | number | no | | +| `subtype` | string | no | | + +Responses: + +- `201`: The created FLP role. +- `400`: The request failed validation. HTTP status is 400 although each error's `status` field is `"422"`. `source.pointer` has the form `/data/attributes//`. +- `404`: The FLP role (or, when creating, the run) does not exist. + +```bash +curl -g -X POST "$BOOKKEEPING_URL/api/flps?token=$TOKEN" \ + -H 'content-type: application/json' \ + -d '{"name":"FLP-1","hostname":"flp001.cern.ch","runNumber":106}' +``` + +### `GET /flps/{flpId}` - Get an FLP role + +Operation id: `getFlp` + +Returns one FLP role, with its run. + +| Parameter | In | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| `flpId` | path | integer | yes | FLP role identifier. | + +Responses: + +- `200`: The FLP role. +- `400`: The request failed validation. HTTP status is 400 although each error's `status` field is `"422"`. `source.pointer` has the form `/data/attributes//`. +- `404`: The FLP role (or, when creating, the run) does not exist. + +```bash +curl -g "$BOOKKEEPING_URL/api/flps/1?token=$TOKEN" +``` + +### `PATCH /flps/{flpName}/runs/{runNumber}` - Update the counters of an FLP in a run (not for LLM use) + +Operation id: `updateFlp` + +Updates the data counters of the FLP role identified by FLP name and run number. Counters absent from the body are left unchanged. +Responds 404 if there is no such FLP in this run. + +**Not meant for LLM or MCP tool use** (`x-llm-exclude`). + +| Parameter | In | Type | Required | Notes | +| --- | --- | --- | --- | --- | +| `flpName` | path | string | yes | Name of the FLP. | +| `runNumber` | path | integer | yes | Run number. | + +Request body (JSON): + +| Field | Type | Required | Notes | +| --- | --- | --- | --- | +| `nTimeframes` | integer | no | | +| `bytesProcessed` | integer | no | | +| `bytesEquipmentReadOut` | integer | no | | +| `bytesRecordingReadOut` | integer | no | | +| `bytesFairMQReadOut` | integer | no | | + +Responses: + +- `201`: The updated FLP role. +- `400`: The request failed validation. HTTP status is 400 although each error's `status` field is `"422"`. `source.pointer` has the form `/data/attributes//`. +- `404`: The FLP role (or, when creating, the run) does not exist. + +```bash +curl -g -X PATCH "$BOOKKEEPING_URL/api/flps/1/runs/1?token=$TOKEN" \ + -H 'content-type: application/json' \ + -d '{"nTimeframes":1000,"bytesProcessed":2048}' +``` diff --git a/lib/server/api-docs/index.html b/lib/server/api-docs/index.html index 421ae26956..597c764468 100644 --- a/lib/server/api-docs/index.html +++ b/lib/server/api-docs/index.html @@ -1334,6 +1334,272 @@

DPL processes

curl -g "$BOOKKEEPING_URL/api/dpl-process/executions?runNumber=106&detectorId=1&processId=1&hostId=1&token=$TOKEN"
 
+

FLPs

+

Roles of FLPs in runs, with their data counters.

+
+

GET /flps - List the FLP roles

+ +

Operation id: listFlps

+

Returns the roles of FLPs (one entry per FLP and per run) with their data counters. Paginated, sorted by name descending by default.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterInTypeRequiredNotes
page[limit]queryintegernoMaximum number of items to return. LLMs and agents must use 100 or less.
page[offset]queryintegernoNumber of items to skip.
sort[id]querystringnoSort by identifier.
sort[name]querystringnoSort by FLP name.
sort[hostname]querystringnoSort by host name.
sort[runNumber]querystringnoSort by run number.
+

Responses:

+
    +
  • 200: A page of FLP roles.
  • +
  • 400: The request failed validation. HTTP status is 400 although each error's status field is "422". source.pointer has the form /data/attributes/<query|params|body>/<field>.
  • +
+
curl -g "$BOOKKEEPING_URL/api/flps?page[limit]=100&token=$TOKEN"
+
+
+
+

POST /flps - Create an FLP role not for LLM use

+ +

Operation id: createFlp

+

Creates the role of an FLP in a run. Responds 404 if the run does not exist.

+

Not meant for LLM or MCP tool use (x-llm-exclude).

+

Request body (JSON):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeRequiredNotes
namestringyesmin length 1
hostnamestringyesmin length 1
runNumberintegeryes
nTimeframesnumberno
bytesProcessednumberno
bytesEquipmentReadOutnumberno
bytesRecordingReadOutnumberno
bytesFairMQReadOutnumberno
subtypestringno
+

Responses:

+
    +
  • 201: The created FLP role.
  • +
  • 400: The request failed validation. HTTP status is 400 although each error's status field is "422". source.pointer has the form /data/attributes/<query|params|body>/<field>.
  • +
  • 404: The FLP role (or, when creating, the run) does not exist.
  • +
+
curl -g -X POST "$BOOKKEEPING_URL/api/flps?token=$TOKEN" \
+  -H 'content-type: application/json' \
+  -d '{"name":"FLP-1","hostname":"flp001.cern.ch","runNumber":106}'
+
+
+
+

GET /flps/{flpId} - Get an FLP role

+ +

Operation id: getFlp

+

Returns one FLP role, with its run.

+ + + + + + + + + + + + + + + + + +
ParameterInTypeRequiredNotes
flpIdpathintegeryesFLP role identifier.
+

Responses:

+
    +
  • 200: The FLP role.
  • +
  • 400: The request failed validation. HTTP status is 400 although each error's status field is "422". source.pointer has the form /data/attributes/<query|params|body>/<field>.
  • +
  • 404: The FLP role (or, when creating, the run) does not exist.
  • +
+
curl -g "$BOOKKEEPING_URL/api/flps/1?token=$TOKEN"
+
+
+
+

PATCH /flps/{flpName}/runs/{runNumber} - Update the counters of an FLP in a run not for LLM use

+ +

Operation id: updateFlp

+

Updates the data counters of the FLP role identified by FLP name and run number. Counters absent from the body are left unchanged. +Responds 404 if there is no such FLP in this run.

+

Not meant for LLM or MCP tool use (x-llm-exclude).

+ + + + + + + + + + + + + + + + + + + + + + + + +
ParameterInTypeRequiredNotes
flpNamepathstringyesName of the FLP.
runNumberpathintegeryesRun number.
+

Request body (JSON):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeRequiredNotes
nTimeframesintegerno
bytesProcessedintegerno
bytesEquipmentReadOutintegerno
bytesRecordingReadOutintegerno
bytesFairMQReadOutintegerno
+

Responses:

+
    +
  • 201: The updated FLP role.
  • +
  • 400: The request failed validation. HTTP status is 400 although each error's status field is "422". source.pointer has the form /data/attributes/<query|params|body>/<field>.
  • +
  • 404: The FLP role (or, when creating, the run) does not exist.
  • +
+
curl -g -X PATCH "$BOOKKEEPING_URL/api/flps/1/runs/1?token=$TOKEN" \
+  -H 'content-type: application/json' \
+  -d '{"nTimeframes":1000,"bytesProcessed":2048}'
+
+
Generated from openapi.yaml by npm run docs:api · ALICE Bookkeeping · internal use only
diff --git a/lib/server/api-docs/openapi.yaml b/lib/server/api-docs/openapi.yaml index ac5613c322..2fb04a7c15 100644 --- a/lib/server/api-docs/openapi.yaml +++ b/lib/server/api-docs/openapi.yaml @@ -61,6 +61,8 @@ tags: description: Files attached to log entries. - name: DPL processes description: Processes of the data processing layer (DPL) executed during the runs, by detector, process and host. + - name: FLPs + description: Roles of FLPs in runs, with their data counters. paths: /tags: @@ -888,6 +890,116 @@ paths: '400': { $ref: '#/components/responses/ValidationError' } '404': { $ref: '#/components/responses/DplNotFound' } + /flps: + get: + tags: [FLPs] + operationId: listFlps + summary: List the FLP roles + description: | + Returns the roles of FLPs (one entry per FLP and per run) with their data counters. Paginated, sorted by name descending by default. + parameters: + - $ref: '#/components/parameters/PageLimit' + - $ref: '#/components/parameters/PageOffset' + - name: sort[id] + in: query + description: Sort by identifier. + schema: { $ref: '#/components/schemas/SortOrder' } + - name: sort[name] + in: query + description: Sort by FLP name. + schema: { $ref: '#/components/schemas/SortOrder' } + - name: sort[hostname] + in: query + description: Sort by host name. + schema: { $ref: '#/components/schemas/SortOrder' } + - name: sort[runNumber] + in: query + description: Sort by run number. + schema: { $ref: '#/components/schemas/SortOrder' } + responses: + '200': + description: A page of FLP roles. + content: + application/json: + schema: { $ref: '#/components/schemas/FlpListResponse' } + '400': { $ref: '#/components/responses/ValidationError' } + post: + tags: [FLPs] + operationId: createFlp + summary: Create an FLP role + description: Creates the role of an FLP in a run. Responds 404 if the run does not exist. + x-llm-exclude: true + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/CreateFlp' } + example: { name: FLP-1, hostname: flp001.cern.ch, runNumber: 106 } + responses: + '201': + description: The created FLP role. + content: + application/json: + schema: { $ref: '#/components/schemas/FlpResponse' } + '400': { $ref: '#/components/responses/ValidationError' } + '404': { $ref: '#/components/responses/FlpNotFound' } + + /flps/{flpId}: + get: + tags: [FLPs] + operationId: getFlp + summary: Get an FLP role + description: Returns one FLP role, with its run. + parameters: + - name: flpId + in: path + required: true + description: FLP role identifier. + schema: { type: integer, minimum: 1 } + responses: + '200': + description: The FLP role. + content: + application/json: + schema: { $ref: '#/components/schemas/FlpResponse' } + '400': { $ref: '#/components/responses/ValidationError' } + '404': { $ref: '#/components/responses/FlpNotFound' } + + /flps/{flpName}/runs/{runNumber}: + patch: + tags: [FLPs] + operationId: updateFlp + summary: Update the counters of an FLP in a run + description: | + Updates the data counters of the FLP role identified by FLP name and run number. Counters absent from the body are left unchanged. + Responds 404 if there is no such FLP in this run. + x-llm-exclude: true + parameters: + - name: flpName + in: path + required: true + description: Name of the FLP. + schema: { type: string } + - name: runNumber + in: path + required: true + description: Run number. + schema: { type: integer, minimum: 1 } + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/UpdateFlp' } + example: { nTimeframes: 1000, bytesProcessed: 2048 } + responses: + '201': + description: The updated FLP role. + content: + application/json: + schema: { $ref: '#/components/schemas/FlpResponse' } + '400': { $ref: '#/components/responses/ValidationError' } + '404': { $ref: '#/components/responses/FlpNotFound' } + components: securitySchemes: jwtQuery: @@ -973,6 +1085,15 @@ components: errors: - { status: 404, title: Not found, detail: Run with this run number (999) could not be found } + FlpNotFound: + description: The FLP role (or, when creating, the run) does not exist. + content: + application/json: + schema: { $ref: '#/components/schemas/ErrorResponse' } + example: + errors: + - { status: 404, title: Not found, detail: FLP with this id (999999999) could not be found } + schemas: Tag: type: object @@ -1426,3 +1547,55 @@ components: data: type: array items: { $ref: '#/components/schemas/DplProcessExecution' } + Flp: + type: object + required: [id, runNumber, name, hostname] + properties: + id: { type: integer } + runNumber: { type: integer } + name: { type: [string, 'null'], examples: [FLP-1] } + hostname: { type: [string, 'null'], examples: [flp001.cern.ch] } + nTimeframes: { type: [integer, 'null'] } + rootFlpId: { type: [integer, 'null'], description: Identifier of the root FLP role; its own id when it has none. } + bytesProcessed: { type: [integer, 'null'] } + bytesEquipmentReadOut: { type: [integer, 'null'] } + bytesRecordingReadOut: { type: [integer, 'null'] } + bytesFairMQReadOut: { type: [integer, 'null'] } + run: { type: [object, 'null'], additionalProperties: true, description: 'Minified run, only when fetching one FLP role.' } + FlpResponse: + type: object + required: [data] + properties: + data: { $ref: '#/components/schemas/Flp' } + FlpListResponse: + type: object + required: [data, meta] + properties: + data: + type: array + items: { $ref: '#/components/schemas/Flp' } + meta: + type: object + properties: + page: { $ref: '#/components/schemas/PageMeta' } + CreateFlp: + type: object + required: [name, hostname, runNumber] + properties: + name: { type: string, minLength: 1 } + hostname: { type: string, minLength: 1 } + runNumber: { type: integer, minimum: 1 } + nTimeframes: { type: number } + bytesProcessed: { type: number } + bytesEquipmentReadOut: { type: number } + bytesRecordingReadOut: { type: number } + bytesFairMQReadOut: { type: number } + subtype: { type: string } + UpdateFlp: + type: object + properties: + nTimeframes: { type: integer, minimum: 0 } + bytesProcessed: { type: integer, minimum: 0 } + bytesEquipmentReadOut: { type: integer, minimum: 0 } + bytesRecordingReadOut: { type: integer, minimum: 0 } + bytesFairMQReadOut: { type: integer, minimum: 0 } diff --git a/lib/server/controllers/flp.controller.js b/lib/server/controllers/flp.controller.js deleted file mode 100644 index 38c843750a..0000000000 --- a/lib/server/controllers/flp.controller.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -/** - * Read flp. - * - * @param {Object} request The *request* object represents the HTTP request and has properties for the request query - * string, parameters, body, HTTP headers, and so on. - * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets an - * HTTP request. - * @param {Function} next The *next* object represents the next middleware function which is used to pass control to the - * next middleware function. - * @returns {undefined} - */ -const read = (request, response, next) => { - response.status(200).json({ - errors: [ - { - status: '200', - title: 'Not implemented in bookkeeping yet.', - }, - ], - }); -}; - -module.exports = { - read, -}; diff --git a/lib/server/controllers/flps.controller.js b/lib/server/controllers/flps.controller.js index 2a7ffab757..b1c471ab4e 100644 --- a/lib/server/controllers/flps.controller.js +++ b/lib/server/controllers/flps.controller.js @@ -11,14 +11,6 @@ * or submit itself to any jurisdiction. */ -const { - flp: { - GetAllFlpsUseCase, - GetFlpUseCase, - CreateFlpUseCase, - UpdateFlpUseCase, - }, -} = require('../../usecases'); const { dtos: { GetAllFlpsDto, @@ -29,15 +21,15 @@ const { } = require('../../domain'); const { dtoValidator } = require('../utilities'); const { ApiConfig } = require('../../config/index.js'); +const { flpRoleService } = require('../services/flp/FlpRoleService.js'); +const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError.js'); /** * Get all flps. * - * @param {Object} request The *request* object represents the HTTP request and has properties for the request query - * string, parameters, body, HTTP headers, and so on. - * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets an - * HTTP request. - * @returns {undefined} + * @param {Request} request the express request + * @param {Response} response the express response + * @returns {Promise} resolves once the response is sent */ const listFlps = async (request, response) => { const value = await dtoValidator(GetAllFlpsDto, request, response); @@ -45,31 +37,30 @@ const listFlps = async (request, response) => { return; } - const { count, flps } = await new GetAllFlpsUseCase() - .execute(value); + try { + const { query: { page: { limit = ApiConfig.pagination.limit, offset } = {}, sort } } = value; + const { count, rows } = await flpRoleService.getAll({ limit, offset, sort }); - const { query: { page: { limit = ApiConfig.pagination.limit } = {} } } = value; - const totalPages = Math.ceil(count / limit); - - response.status(200).json({ - data: flps, - meta: { - page: { - pageCount: totalPages, - totalCount: count, + response.status(200).json({ + data: rows, + meta: { + page: { + pageCount: Math.ceil(count / limit), + totalCount: count, + }, }, - }, - }); + }); + } catch (error) { + updateExpressResponseFromNativeError(response, error); + } }; /** * Get flp. * - * @param {Object} request The *request* object represents the HTTP request and has properties for the request query - * string, parameters, body, HTTP headers, and so on. - * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets an - * HTTP request. - * @returns {undefined} + * @param {Request} request the express request + * @param {Response} response the express response + * @returns {Promise} resolves once the response is sent */ const getFlpById = async (request, response) => { const value = await dtoValidator(GetFlpDto, request, response); @@ -77,33 +68,19 @@ const getFlpById = async (request, response) => { return; } - const flp = await new GetFlpUseCase() - .execute(value); - - if (flp === null) { - response.status(404).json({ - errors: [ - { - status: '404', - title: `Flp with this id (${value.params.flpId}) could not be found`, - }, - ], - }); - } else { - response.status(200).json({ - data: flp, - }); + try { + response.status(200).json({ data: await flpRoleService.getOneOrFail({ flpRoleId: value.params.flpId }) }); + } catch (error) { + updateExpressResponseFromNativeError(response, error); } }; /** * Create a new flp. * - * @param {Object} request The *request* object represents the HTTP request and has properties for the request query - * string, parameters, body, HTTP headers, and so on. - * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets an - * HTTP request. - * @returns {undefined} + * @param {Request} request the express request + * @param {Response} response the express response + * @returns {Promise} resolves once the response is sent */ const create = async (request, response) => { const value = await dtoValidator(CreateFlpDto, request, response); @@ -111,24 +88,19 @@ const create = async (request, response) => { return; } - const { result, error } = await new CreateFlpUseCase() - .execute(value); - - if (error) { - response.status(Number(error.status)).json({ errors: [error] }); - } else { - response.status(201).json({ data: result }); + try { + response.status(201).json({ data: await flpRoleService.create(value.body) }); + } catch (error) { + updateExpressResponseFromNativeError(response, error); } }; /** * Update flp. * - * @param {Object} request The *request* object represents the HTTP request and has properties for the request query - * string, parameters, body, HTTP headers, and so on. - * @param {Object} response The *response* object represents the HTTP response that an Express app sends when it gets an - * HTTP request. - * @returns {undefined} + * @param {Request} request the express request + * @param {Response} response the express response + * @returns {Promise} resolves once the response is sent */ const update = async (request, response) => { const value = await dtoValidator(UpdateFlpDto, request, response); @@ -136,13 +108,11 @@ const update = async (request, response) => { return; } - const { result, error } = await new UpdateFlpUseCase() - .execute(value); - - if (error) { - response.status(Number(error.status)).json({ errors: [error] }); - } else { - response.status(201).json({ data: result }); + try { + const { params: { flpName, runNumber }, body } = value; + response.status(201).json({ data: await flpRoleService.update({ flpName, runNumber }, body) }); + } catch (error) { + updateExpressResponseFromNativeError(response, error); } }; diff --git a/lib/server/services/flp/FlpRoleService.js b/lib/server/services/flp/FlpRoleService.js index 02bd691d27..78d2754473 100644 --- a/lib/server/services/flp/FlpRoleService.js +++ b/lib/server/services/flp/FlpRoleService.js @@ -15,6 +15,9 @@ const { createFlpRole } = require('./createFlpRole.js'); const { getFlpRole } = require('./getFlpRole.js'); const { flpRoleAdapter } = require('../../../database/adapters/index.js'); const { updateFlpRole } = require('./updateFlpRole.js'); +const { getAllFlpRoles } = require('./getAllFlpRoles.js'); +const { getFlpRoleOrFail } = require('./getFlpRoleOrFail.js'); +const { ApiConfig } = require('../../../config/index.js'); /** * @typedef FlpRoleIdentifier object to uniquely identify an FLP role, either by its id either by the FLP's name and a runNumber @@ -45,6 +48,36 @@ class FlpRoleService { return flpRole ? flpRoleAdapter.toEntity(flpRole) : null; } + /** + * Return the FLP role matching the given identifier, with its run, or reject if there is none + * + * @param {FlpRoleIdentifier} flpRoleIdentifier the identifier of the FLP role to find + * @return {Promise} resolves with the FLP role found or rejects with a NotFoundError + */ + async getOneOrFail(flpRoleIdentifier) { + return flpRoleAdapter.toEntity(await getFlpRoleOrFail(flpRoleIdentifier, (queryBuilder) => { + queryBuilder.include('run'); + })); + } + + /** + * Return a page of FLP roles, with the total number of FLP roles + * + * @param {Object} [options] the pagination and sort options + * @param {number} [options.limit] the maximum number of FLP roles to return, the API default limit if not specified + * @param {number} [options.offset=0] the number of FLP roles to skip + * @param {Object} [options.sort] the sort to apply as a map of field name to order, by default name descending + * @return {Promise<{count: number, rows: FlpRole[]}>} resolves with the FLP roles of the page and the total count + */ + async getAll({ limit = ApiConfig.pagination.limit, offset = 0, sort = { name: 'desc' } } = {}) { + const { count, rows } = await getAllFlpRoles((queryBuilder) => { + queryBuilder.limit(limit); + queryBuilder.offset(offset); + Object.entries(sort).forEach(([field, order]) => queryBuilder.orderBy(field, order)); + }); + return { count, rows: rows.map(flpRoleAdapter.toEntity) }; + } + /** * Create an FLP role in the database and return the created instance * diff --git a/lib/server/services/flp/getAllFlpRoles.js b/lib/server/services/flp/getAllFlpRoles.js new file mode 100644 index 0000000000..0598569740 --- /dev/null +++ b/lib/server/services/flp/getAllFlpRoles.js @@ -0,0 +1,32 @@ +/** + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". + * + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. + */ + +const { dataSource } = require('../../../database/DataSource.js'); +const { FlpRoleRepository } = require('../../../database/repositories/index.js'); + +/** + * Find and return the FLP roles (with the number of matching rows) + * + * @param {(function(QueryBuilder):void)|null} [qbConfiguration=null] function called with the FLP roles find query builder as parameter to add + * specific configuration to the query + * @return {Promise<{count: number, rows: SequelizeFlpRole[]}>} resolves with the FLP roles found and their count + */ +exports.getAllFlpRoles = (qbConfiguration = null) => { + const queryBuilder = dataSource.createQueryBuilder(); + + if (qbConfiguration) { + qbConfiguration(queryBuilder); + } + + return FlpRoleRepository.findAndCountAll(queryBuilder); +}; diff --git a/lib/server/services/flp/getFlpRoleOrFail.js b/lib/server/services/flp/getFlpRoleOrFail.js index f4fa1582d8..5dc95508f3 100644 --- a/lib/server/services/flp/getFlpRoleOrFail.js +++ b/lib/server/services/flp/getFlpRoleOrFail.js @@ -18,10 +18,12 @@ const { getFlpRole } = require('./getFlpRole.js'); * Find an FLP role model by its identifier and reject with a NotFoundError if none is found * * @param {FlpRoleIdentifier} identifier the identifier of the FLP role to find + * @param {Function|null} qbConfiguration function called with the FLP role find query builder as parameter to add specific configuration to the + * query * @return {Promise} resolve with the FLP role model found or reject with a NotFoundError */ -exports.getFlpRoleOrFail = async (identifier) => { - const flpRoleModel = await getFlpRole(identifier); +exports.getFlpRoleOrFail = async (identifier, qbConfiguration = null) => { + const flpRoleModel = await getFlpRole(identifier, qbConfiguration); if (flpRoleModel !== null) { return flpRoleModel; } else { diff --git a/lib/server/services/flp/updateFlpRole.js b/lib/server/services/flp/updateFlpRole.js index e9ff34d398..5849ea6ec0 100644 --- a/lib/server/services/flp/updateFlpRole.js +++ b/lib/server/services/flp/updateFlpRole.js @@ -1,10 +1,16 @@ /** - * Update a given FLP role + * @license + * Copyright CERN and copyright holders of ALICE O2. This software is + * distributed under the terms of the GNU General Public License v3 (GPL + * Version 3), copied verbatim in the file "COPYING". * - * @param {FlpRoleIdentifier} identifier the identifier of the FLP role to update - * @param {Partial} patch the patch to apply - * @return {Promise} the updated FLP role + * See http://alice-o2.web.cern.ch/license for full licensing information. + * + * In applying this license CERN does not waive the privileges and immunities + * granted to it by virtue of its status as an Intergovernmental Organization + * or submit itself to any jurisdiction. */ + const { getFlpRoleOrFail } = require('./getFlpRoleOrFail.js'); const { FlpRoleRepository } = require('../../../database/repositories/index.js'); @@ -17,6 +23,6 @@ const { FlpRoleRepository } = require('../../../database/repositories/index.js') */ exports.updateFlpRole = async (identifier, patch) => { const flpRole = await getFlpRoleOrFail(identifier); - await FlpRoleRepository.update(flpRole, patch); + await FlpRoleRepository.update(flpRole, Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined))); return flpRole; }; diff --git a/lib/usecases/flp/CreateFlpUseCase.js b/lib/usecases/flp/CreateFlpUseCase.js deleted file mode 100644 index 7ff1741765..0000000000 --- a/lib/usecases/flp/CreateFlpUseCase.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { flpRoleService } = require('../../server/services/flp/FlpRoleService.js'); - -/** - * CreateFlpUseCase - */ -class CreateFlpUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The CreateFlpDto containing all data. - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute(dto) { - const { body } = dto; - - try { - const flp = await flpRoleService.create(body); - return { result: flp }; - } catch (e) { - return { - error: { - status: '400', - title: e.message, - }, - }; - } - } -} - -module.exports = CreateFlpUseCase; diff --git a/lib/usecases/flp/GetAllFlpsUseCase.js b/lib/usecases/flp/GetAllFlpsUseCase.js deleted file mode 100644 index b2083c6168..0000000000 --- a/lib/usecases/flp/GetAllFlpsUseCase.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { - repositories: { - FlpRoleRepository, - }, - utilities: { - QueryBuilder, - TransactionHelper, - }, -} = require('../../database'); -const { flpRoleAdapter } = require('../../database/adapters/index.js'); -const { ApiConfig } = require('../../config/index.js'); - -/** - * GetAllFlpsUseCase - */ -class GetAllFlpsUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The GetAllFlps DTO which contains all request data. - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute(dto = {}) { - const queryBuilder = new QueryBuilder(); - const { query = {} } = dto; - const { page = {}, sort = { name: 'desc' } } = query; - - const { limit = ApiConfig.pagination.limit, offset = 0 } = page; - queryBuilder.limit(limit); - queryBuilder.offset(offset); - - Object.keys(sort).forEach((s) => queryBuilder.orderBy(s, sort[s])); - - const { - count, - rows, - } = await TransactionHelper.provide(async () => FlpRoleRepository.findAndCountAll(queryBuilder)); - - return { - count, - flps: rows.map(flpRoleAdapter.toEntity), - }; - } -} - -module.exports = GetAllFlpsUseCase; diff --git a/lib/usecases/flp/GetFlpUseCase.js b/lib/usecases/flp/GetFlpUseCase.js deleted file mode 100644 index 18bc19c6aa..0000000000 --- a/lib/usecases/flp/GetFlpUseCase.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { flpRoleService } = require('../../server/services/flp/FlpRoleService.js'); - -/** - * GetFlpUseCase - */ -class GetFlpUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The GetFlpDto containing all data. - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute(dto) { - const { params } = dto; - const { flpId: flpRoleId } = params; - - return flpRoleService.get({ flpRoleId }, { run: true }); - } -} - -module.exports = GetFlpUseCase; diff --git a/lib/usecases/flp/UpdateFlpUseCase.js b/lib/usecases/flp/UpdateFlpUseCase.js deleted file mode 100644 index 5176216f43..0000000000 --- a/lib/usecases/flp/UpdateFlpUseCase.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { - repositories: { - FlpRoleRepository, - }, - utilities: { - TransactionHelper, - QueryBuilder, - }, -} = require('../../database'); - -const { - models: { - FlpRole, - }, -} = require('../../database'); - -const GetRunByRunNumberUseCase = require('../run/GetRunByRunNumberUseCase'); -const { flpRoleAdapter } = require('../../database/adapters/index.js'); - -/** - * UpdateFlpUseCase - */ -class UpdateFlpUseCase { - /** - * Executes this use case. - * - * @param {Object} dto The UpdateFlpUseCase containing all data. - * @returns {Promise} Promise object represents the result of this use case. - */ - async execute(dto) { - const { body, params } = dto; - const { flpName, runNumber } = params; - const { nTimeframes, bytesProcessed, bytesEquipmentReadOut, bytesRecordingReadOut, bytesFairMQReadOut } = body; - - return await TransactionHelper.provide(async () => { - body.userId = dto?.session?.id || 0; - - const runId = await new GetRunByRunNumberUseCase() - .execute({ params: { runNumber } }); - - const queryBuilder = new QueryBuilder() - .setModel(FlpRole) - .include('runs') - .whereAssociation('runs', 'id').is(runId.id) - .where('name').is(flpName); - - const flpEntity = await FlpRoleRepository.findOne(queryBuilder); - - if (flpEntity) { - flpEntity.nTimeframes = nTimeframes; - flpEntity.bytesProcessed = bytesProcessed; - flpEntity.bytesEquipmentReadOut = bytesEquipmentReadOut; - flpEntity.bytesRecordingReadOut = bytesRecordingReadOut; - flpEntity.bytesFairMQReadOut = bytesFairMQReadOut; - - await flpEntity.save(); - } else { - return { - error: { - status: '400', - title: `flp with name (${flpName}) and the runNumber (${runNumber}) could not be found`, - }, - }; - } - - return flpRoleAdapter.toEntity(flpEntity); - }); - } -} - -module.exports = UpdateFlpUseCase; diff --git a/lib/usecases/flp/index.js b/lib/usecases/flp/index.js deleted file mode 100644 index 5b455bbcd0..0000000000 --- a/lib/usecases/flp/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const GetAllFlpsUseCase = require('./GetAllFlpsUseCase'); -const GetFlpUseCase = require('./GetFlpUseCase'); -const CreateFlpUseCase = require('./CreateFlpUseCase'); -const UpdateFlpUseCase = require('./UpdateFlpUseCase'); - -module.exports = { - GetAllFlpsUseCase, - GetFlpUseCase, - CreateFlpUseCase, - UpdateFlpUseCase, -}; diff --git a/lib/usecases/index.js b/lib/usecases/index.js index fe043e6a26..d1a8813608 100644 --- a/lib/usecases/index.js +++ b/lib/usecases/index.js @@ -12,7 +12,6 @@ */ const environment = require('./environment'); -const flp = require('./flp'); const lhcFill = require('./lhcFill'); const log = require('./log'); const run = require('./run'); @@ -22,7 +21,6 @@ const tag = require('./tag'); module.exports = { environment, - flp, lhcFill, log, run, diff --git a/test/api/flps.test.js b/test/api/flps.test.js index 721b72c749..2be67815d5 100644 --- a/test/api/flps.test.js +++ b/test/api/flps.test.js @@ -175,7 +175,8 @@ module.exports = () => { return; } - expect(res.body.errors[0].title).to.equal('Flp with this id (999999999) could not be found'); + expect(res.body.errors[0].title).to.equal('Not found'); + expect(res.body.errors[0].detail).to.equal('FLP with this id (999999999) could not be found'); done(); }); @@ -243,4 +244,27 @@ module.exports = () => { }); }); }); + + describe('PATCH api/flps/:flpName/runs/:runNumber', () => { + it('should return 400 if the run number is not a number', async () => { + const response = await request(server).patch('/api/flps/FLP/runs/abc').send({}); + expect(response.status).to.equal(400); + const error = response.body.errors.find(({ source }) => source.pointer === '/data/attributes/params/runNumber'); + expect(error.detail).to.equal('"params.runNumber" must be a number'); + }); + + it('should return 404 if the FLP of the run does not exist', async () => { + const response = await request(server).patch('/api/flps/FLP/runs/999999999').send({ nTimeframes: 1 }); + expect(response.status).to.equal(404); + expect(response.body.errors[0].detail).to.equal('FLP with this name (FLP) and run number (999999999) could not be found'); + }); + + it('should only update the counters of the body', async () => { + const { bytesProcessed } = (await request(server).get('/api/flps/1')).body.data; + const response = await request(server).patch('/api/flps/FLP-TPC-1/runs/1').send({ nTimeframes: 77 }); + expect(response.status).to.equal(201); + expect(response.body.data.nTimeframes).to.equal(77); + expect(response.body.data.bytesProcessed).to.equal(bytesProcessed); + }); + }); }; diff --git a/test/lib/server/services/flpRole/FlpRoleService.test.js b/test/lib/server/services/flpRole/FlpRoleService.test.js index 4d161c0b41..0a3263e849 100644 --- a/test/lib/server/services/flpRole/FlpRoleService.test.js +++ b/test/lib/server/services/flpRole/FlpRoleService.test.js @@ -13,6 +13,7 @@ const { getFlpRole } = require('../../../../../lib/server/services/flp/getFlpRole.js'); const { expect } = require('chai'); +const { NotFoundError } = require('../../../../../lib/server/errors/NotFoundError.js'); const { flpRoleService } = require('../../../../../lib/server/services/flp/FlpRoleService.js'); module.exports = () => { @@ -69,4 +70,44 @@ module.exports = () => { expect(updatedFlpRole.bytesFairMQReadOut).to.equal(flpRole.bytesFairMQReadOut); } }); + + it('should successfully get one FLP role with its run', async () => { + const flpRole = await flpRoleService.getOneOrFail({ flpRoleId: 1 }); + expect(flpRole.id).to.equal(1); + expect(flpRole.run).to.not.be.null; + }); + + it('should fail to get an FLP role that does not exist', async () => { + let error; + try { + await flpRoleService.getOneOrFail({ flpRoleId: 999999999 }); + } catch (e) { + error = e; + } + expect(error).to.be.instanceOf(NotFoundError); + expect(error.message).to.equal('FLP with this id (999999999) could not be found'); + }); + + it('should successfully get a page of FLP roles, sorted by name descending by default', async () => { + const { count, rows } = await flpRoleService.getAll({ limit: 2 }); + expect(count).to.be.greaterThan(2); + expect(rows).to.have.lengthOf(2); + expect(rows[0].name >= rows[1].name).to.be.true; + + const { rows: ascendingRows } = await flpRoleService.getAll({ limit: 2, sort: { name: 'asc' } }); + expect(ascendingRows[0].name <= ascendingRows[1].name).to.be.true; + }); + + it('should not modify the counters absent from the patch', async () => { + const { bytesProcessed } = await flpRoleService.getOneOrFail({ flpRoleId: 1 }); + const updated = await flpRoleService.update({ flpRoleId: 1 }, { nTimeframes: 12 }); + expect(updated.nTimeframes).to.equal(12); + expect(updated.bytesProcessed).to.equal(bytesProcessed); + }); + + it('should successfully create an FLP role', async () => { + const created = await flpRoleService.create({ name: 'FlpTest', hostname: 'TestFlp', runNumber: 36 }); + expect(created).to.deep.include({ name: 'FlpTest', hostname: 'TestFlp', runNumber: 36 }); + expect((await flpRoleService.getOneOrFail({ flpRoleId: created.id })).id).to.equal(created.id); + }); }; diff --git a/test/lib/usecases/flp/CreateFlpUseCase.test.js b/test/lib/usecases/flp/CreateFlpUseCase.test.js deleted file mode 100644 index 4209fb65e1..0000000000 --- a/test/lib/usecases/flp/CreateFlpUseCase.test.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { repositories: { FlpRoleRepository } } = require('../../../../lib/database/index.js'); -const { flp: { CreateFlpUseCase } } = require('../../../../lib/usecases/index.js'); -const { dtos: { CreateFlpDto } } = require('../../../../lib/domain/index.js'); -const chai = require('chai'); - -const { expect } = chai; - -module.exports = () => { - let createFlpDto; - - beforeEach(async () => { - createFlpDto = await CreateFlpDto.validateAsync({ - body: { - name: 'FlpTest', - hostname: 'TestFlp', - runNumber: 36, - }, - }); - createFlpDto.session = { - personid: 1, - id: 1, - name: 'John Doe', - }; - }); - - it('should insert a new Flp', async () => { - const nFlpsBefore = await FlpRoleRepository.count(); - - await new CreateFlpUseCase().execute(createFlpDto); - - const nFlpsAfter = await FlpRoleRepository.count(); - expect(nFlpsAfter).to.be.greaterThan(nFlpsBefore); - }); -}; diff --git a/test/lib/usecases/flp/GetFlpUseCase.test.js b/test/lib/usecases/flp/GetFlpUseCase.test.js deleted file mode 100644 index a5aead2bbd..0000000000 --- a/test/lib/usecases/flp/GetFlpUseCase.test.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const { flp: { GetFlpUseCase } } = require('../../../../lib/usecases/index.js'); -const { dtos: { GetFlpDto } } = require('../../../../lib/domain/index.js'); -const chai = require('chai'); - -const { expect } = chai; - -module.exports = () => { - let getFlpDto; - - beforeEach(async () => { - getFlpDto = await GetFlpDto.validateAsync({ - params: { - flpId: 1, - }, - }); - }); - - it('should return an object that has the `id` property', async () => { - const result = await new GetFlpUseCase() - .execute(getFlpDto); - - expect(result).to.have.ownProperty('id'); - expect(result.id).to.equal(1); - }); -}; diff --git a/test/lib/usecases/flp/index.js b/test/lib/usecases/flp/index.js deleted file mode 100644 index 3ebb8af279..0000000000 --- a/test/lib/usecases/flp/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright CERN and copyright holders of ALICE O2. This software is - * distributed under the terms of the GNU General Public License v3 (GPL - * Version 3), copied verbatim in the file "COPYING". - * - * See http://alice-o2.web.cern.ch/license for full licensing information. - * - * In applying this license CERN does not waive the privileges and immunities - * granted to it by virtue of its status as an Intergovernmental Organization - * or submit itself to any jurisdiction. - */ - -const CreateFlpUseCase = require('./CreateFlpUseCase.test.js'); -const GetFlpUseCase = require('./GetFlpUseCase.test.js'); - -module.exports = () => { - describe('CreateFlpUseCase', CreateFlpUseCase); - describe('GetFlpUseCase', GetFlpUseCase); -}; diff --git a/test/lib/usecases/index.js b/test/lib/usecases/index.js index bab1e09d0a..23ea3f78ef 100644 --- a/test/lib/usecases/index.js +++ b/test/lib/usecases/index.js @@ -16,7 +16,6 @@ const StatusSuite = require('./status/index.js'); const LhcFillSuite = require('./lhcFill/index.js'); const LogSuite = require('./log/index.js'); const RunSuite = require('./run/index.js'); -const FlpSuite = require('./flp/index.js'); const ServerSuite = require('./server/index.js'); const TagSuite = require('./tag/index.js'); const { resetDatabaseContent } = require('../../utilities/resetDatabaseContent.js'); @@ -27,7 +26,6 @@ module.exports = () => { describe('Log use-case', LogSuite); describe('LhcFill use-case', LhcFillSuite); describe('Run use-case', RunSuite); - describe('Flp use-case', FlpSuite); describe('Server use-case', ServerSuite); describe('Tag use-case', TagSuite); describe('Environment use-case', EnvironmentSuite);