From 8ca0a11a6f3b6cc956b8765954f99b18982ceed8 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 16:01:30 +0200 Subject: [PATCH 1/7] [O2B-1623] Clean up the FLP endpoints: remove the dead controller, handle errors, tighten the DTOs Co-Authored-By: Claude Sonnet 5.5 --- lib/domain/dtos/GetAllFlpsDto.js | 6 +- lib/domain/dtos/GetFlpDto.js | 2 +- lib/domain/dtos/UpdateFlpDto.js | 10 +- lib/server/controllers/flp.controller.js | 38 ------- lib/server/controllers/flps.controller.js | 127 ++++++++++++---------- lib/server/services/flp/updateFlpRole.js | 14 ++- lib/usecases/flp/UpdateFlpUseCase.js | 9 ++ test/api/flps.test.js | 15 +++ 8 files changed, 112 insertions(+), 109 deletions(-) delete mode 100644 lib/server/controllers/flp.controller.js 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/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..f4d9d71edc 100644 --- a/lib/server/controllers/flps.controller.js +++ b/lib/server/controllers/flps.controller.js @@ -29,15 +29,14 @@ const { } = require('../../domain'); const { dtoValidator } = require('../utilities'); const { ApiConfig } = require('../../config/index.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 +44,33 @@ const listFlps = async (request, response) => { return; } - const { count, flps } = await new GetAllFlpsUseCase() - .execute(value); + try { + const { count, flps } = await new GetAllFlpsUseCase() + .execute(value); - const { query: { page: { limit = ApiConfig.pagination.limit } = {} } } = value; - const totalPages = Math.ceil(count / limit); + 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: flps, + meta: { + page: { + pageCount: totalPages, + 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 +78,35 @@ const getFlpById = async (request, response) => { return; } - const flp = await new GetFlpUseCase() - .execute(value); + try { + 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, - }); + 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, + }); + } + } 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 +114,26 @@ const create = async (request, response) => { return; } - const { result, error } = await new CreateFlpUseCase() - .execute(value); + try { + 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 }); + if (error) { + response.status(Number(error.status)).json({ errors: [error] }); + } else { + response.status(201).json({ data: result }); + } + } 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 +141,17 @@ const update = async (request, response) => { return; } - const { result, error } = await new UpdateFlpUseCase() - .execute(value); + try { + 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 }); + if (error) { + response.status(Number(error.status)).json({ errors: [error] }); + } else { + response.status(201).json({ data: result }); + } + } catch (error) { + updateExpressResponseFromNativeError(response, error); } }; diff --git a/lib/server/services/flp/updateFlpRole.js b/lib/server/services/flp/updateFlpRole.js index e9ff34d398..d29d8a4074 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'); diff --git a/lib/usecases/flp/UpdateFlpUseCase.js b/lib/usecases/flp/UpdateFlpUseCase.js index 5176216f43..ce40d86fff 100644 --- a/lib/usecases/flp/UpdateFlpUseCase.js +++ b/lib/usecases/flp/UpdateFlpUseCase.js @@ -51,6 +51,15 @@ class UpdateFlpUseCase { const runId = await new GetRunByRunNumberUseCase() .execute({ params: { runNumber } }); + if (!runId) { + return { + error: { + status: '400', + title: `flp with name (${flpName}) and the runNumber (${runNumber}) could not be found`, + }, + }; + } + const queryBuilder = new QueryBuilder() .setModel(FlpRole) .include('runs') diff --git a/test/api/flps.test.js b/test/api/flps.test.js index 721b72c749..bba5b9402c 100644 --- a/test/api/flps.test.js +++ b/test/api/flps.test.js @@ -243,4 +243,19 @@ 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 400 if 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(400); + expect(response.body.errors[0].title).to.contain('could not be found'); + }); + }); }; From 07117eeec781d90178c8520f856adf91dd847bb7 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 16:01:30 +0200 Subject: [PATCH 2/7] [O2B-1623] Document the flps endpoints in the HTTP API specification Co-Authored-By: Claude Sonnet 5.5 --- lib/server/api-docs/openapi.yaml | 162 +++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/lib/server/api-docs/openapi.yaml b/lib/server/api-docs/openapi.yaml index ac5613c322..ddd6c4d879 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,114 @@ 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. + 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' } + + /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/NotFound' } + + /flps/{flpName}/runs/{runNumber}: + patch: + tags: [FLPs] + operationId: updateFlp + summary: Update the counters of an FLP in a run + description: | + Replaces the data counters of the FLP role identified by FLP name and run number. Counters absent from the body are set to null. + Responds 400 if the run or the FLP in this run does not exist. + 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' } + components: securitySchemes: jwtQuery: @@ -1426,3 +1536,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 } From d825ddb5f0ea644baaf4e91b0a46630519634a83 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 16:01:30 +0200 Subject: [PATCH 3/7] [O2B-1623] Regenerate the HTTP API documentation with the flps endpoints Co-Authored-By: Claude Sonnet 5.5 --- lib/server/api-docs/API.md | 116 +++++++++++++++ lib/server/api-docs/index.html | 264 +++++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) diff --git a/lib/server/api-docs/API.md b/lib/server/api-docs/API.md index 39a512754a..46261691ec 100644 --- a/lib/server/api-docs/API.md +++ b/lib/server/api-docs/API.md @@ -738,3 +738,119 @@ 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. + +**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//`. + +```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 requested entity 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` + +Replaces the data counters of the FLP role identified by FLP name and run number. Counters absent from the body are set to null. +Responds 400 if the run or the FLP in this run does not exist. + +**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//`. + +```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..77162f9f9f 100644 --- a/lib/server/api-docs/index.html +++ b/lib/server/api-docs/index.html @@ -1334,6 +1334,270 @@

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.

+

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>.
  • +
+
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 requested entity 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

+

Replaces the data counters of the FLP role identified by FLP name and run number. Counters absent from the body are set to null. +Responds 400 if the run or the FLP in this run does not exist.

+

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>.
  • +
+
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
From 194f96badbf0926efa8c18a9f48c9a5fc8ea43a5 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 16:34:53 +0200 Subject: [PATCH 4/7] [O2B-1623] Migrate the FLP use cases to the FlpRoleService Co-Authored-By: Claude Sonnet 5.5 --- lib/server/controllers/flps.controller.js | 57 ++---------- lib/server/services/flp/FlpRoleService.js | 33 +++++++ lib/server/services/flp/getAllFlpRoles.js | 32 +++++++ lib/server/services/flp/getFlpRoleOrFail.js | 6 +- lib/server/services/flp/updateFlpRole.js | 2 +- lib/usecases/flp/CreateFlpUseCase.js | 43 --------- lib/usecases/flp/GetAllFlpsUseCase.js | 59 ------------ lib/usecases/flp/GetFlpUseCase.js | 34 ------- lib/usecases/flp/UpdateFlpUseCase.js | 93 ------------------- lib/usecases/flp/index.js | 24 ----- lib/usecases/index.js | 2 - test/api/flps.test.js | 17 +++- .../services/flpRole/FlpRoleService.test.js | 35 +++++++ 13 files changed, 127 insertions(+), 310 deletions(-) create mode 100644 lib/server/services/flp/getAllFlpRoles.js delete mode 100644 lib/usecases/flp/CreateFlpUseCase.js delete mode 100644 lib/usecases/flp/GetAllFlpsUseCase.js delete mode 100644 lib/usecases/flp/GetFlpUseCase.js delete mode 100644 lib/usecases/flp/UpdateFlpUseCase.js delete mode 100644 lib/usecases/flp/index.js diff --git a/lib/server/controllers/flps.controller.js b/lib/server/controllers/flps.controller.js index f4d9d71edc..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,6 +21,7 @@ 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'); /** @@ -45,17 +38,14 @@ const listFlps = async (request, response) => { } try { - const { count, flps } = await new GetAllFlpsUseCase() - .execute(value); - - const { query: { page: { limit = ApiConfig.pagination.limit } = {} } } = value; - const totalPages = Math.ceil(count / limit); + const { query: { page: { limit = ApiConfig.pagination.limit, offset } = {}, sort } } = value; + const { count, rows } = await flpRoleService.getAll({ limit, offset, sort }); response.status(200).json({ - data: flps, + data: rows, meta: { page: { - pageCount: totalPages, + pageCount: Math.ceil(count / limit), totalCount: count, }, }, @@ -79,23 +69,7 @@ const getFlpById = async (request, response) => { } try { - 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, - }); - } + response.status(200).json({ data: await flpRoleService.getOneOrFail({ flpRoleId: value.params.flpId }) }); } catch (error) { updateExpressResponseFromNativeError(response, error); } @@ -115,14 +89,7 @@ const create = async (request, response) => { } try { - 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 }); - } + response.status(201).json({ data: await flpRoleService.create(value.body) }); } catch (error) { updateExpressResponseFromNativeError(response, error); } @@ -142,14 +109,8 @@ const update = async (request, response) => { } try { - 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 }); - } + 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 d29d8a4074..5849ea6ec0 100644 --- a/lib/server/services/flp/updateFlpRole.js +++ b/lib/server/services/flp/updateFlpRole.js @@ -23,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 ce40d86fff..0000000000 --- a/lib/usecases/flp/UpdateFlpUseCase.js +++ /dev/null @@ -1,93 +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 } }); - - if (!runId) { - return { - error: { - status: '400', - title: `flp with name (${flpName}) and the runNumber (${runNumber}) could not be found`, - }, - }; - } - - 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 bba5b9402c..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(); }); @@ -252,10 +253,18 @@ module.exports = () => { expect(error.detail).to.equal('"params.runNumber" must be a number'); }); - it('should return 400 if the run does not exist', async () => { + 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(400); - expect(response.body.errors[0].title).to.contain('could not be found'); + 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..005128add6 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,38 @@ 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); + }); }; From 5d4c185c7b6ab73d5007885564594bb47e8fe267 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 16:34:53 +0200 Subject: [PATCH 5/7] [O2B-1623] Update the flps endpoints in the HTTP API specification Co-Authored-By: Claude Sonnet 5.5 --- lib/server/api-docs/openapi.yaml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/server/api-docs/openapi.yaml b/lib/server/api-docs/openapi.yaml index ddd6c4d879..2fb04a7c15 100644 --- a/lib/server/api-docs/openapi.yaml +++ b/lib/server/api-docs/openapi.yaml @@ -927,7 +927,7 @@ paths: tags: [FLPs] operationId: createFlp summary: Create an FLP role - description: Creates the role of an FLP in a run. + 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 @@ -942,6 +942,7 @@ paths: application/json: schema: { $ref: '#/components/schemas/FlpResponse' } '400': { $ref: '#/components/responses/ValidationError' } + '404': { $ref: '#/components/responses/FlpNotFound' } /flps/{flpId}: get: @@ -962,7 +963,7 @@ paths: application/json: schema: { $ref: '#/components/schemas/FlpResponse' } '400': { $ref: '#/components/responses/ValidationError' } - '404': { $ref: '#/components/responses/NotFound' } + '404': { $ref: '#/components/responses/FlpNotFound' } /flps/{flpName}/runs/{runNumber}: patch: @@ -970,8 +971,8 @@ paths: operationId: updateFlp summary: Update the counters of an FLP in a run description: | - Replaces the data counters of the FLP role identified by FLP name and run number. Counters absent from the body are set to null. - Responds 400 if the run or the FLP in this run does not exist. + 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 @@ -997,6 +998,7 @@ paths: application/json: schema: { $ref: '#/components/schemas/FlpResponse' } '400': { $ref: '#/components/responses/ValidationError' } + '404': { $ref: '#/components/responses/FlpNotFound' } components: securitySchemes: @@ -1083,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 From f0fa4c2d5db832bcfa6349ec29477c7ab000bfc9 Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 5.5" Date: Thu, 22 Oct 2026 16:34:53 +0200 Subject: [PATCH 6/7] [O2B-1623] Regenerate the HTTP API documentation after the FLP service migration Co-Authored-By: Claude Sonnet 5.5 --- lib/server/api-docs/API.md | 10 ++++++---- lib/server/api-docs/index.html | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/server/api-docs/API.md b/lib/server/api-docs/API.md index 46261691ec..89b7a39809 100644 --- a/lib/server/api-docs/API.md +++ b/lib/server/api-docs/API.md @@ -771,7 +771,7 @@ curl -g "$BOOKKEEPING_URL/api/flps?page[limit]=100&token=$TOKEN" Operation id: `createFlp` -Creates the role of an FLP in a run. +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`). @@ -793,6 +793,7 @@ 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" \ @@ -814,7 +815,7 @@ 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 requested entity does not exist. +- `404`: The FLP role (or, when creating, the run) does not exist. ```bash curl -g "$BOOKKEEPING_URL/api/flps/1?token=$TOKEN" @@ -824,8 +825,8 @@ curl -g "$BOOKKEEPING_URL/api/flps/1?token=$TOKEN" Operation id: `updateFlp` -Replaces the data counters of the FLP role identified by FLP name and run number. Counters absent from the body are set to null. -Responds 400 if the run or the FLP in this run does not exist. +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`). @@ -848,6 +849,7 @@ 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" \ diff --git a/lib/server/api-docs/index.html b/lib/server/api-docs/index.html index 77162f9f9f..597c764468 100644 --- a/lib/server/api-docs/index.html +++ b/lib/server/api-docs/index.html @@ -1406,7 +1406,7 @@

FLPs

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

Operation id: createFlp

-

Creates the role of an FLP in a run.

+

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):

@@ -1477,6 +1477,7 @@

FLPs

  • 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' \
@@ -1510,7 +1511,7 @@ 

FLPs

  • 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 requested entity does not exist.
  • +
  • 404: The FLP role (or, when creating, the run) does not exist.
curl -g "$BOOKKEEPING_URL/api/flps/1?token=$TOKEN"
 
@@ -1519,8 +1520,8 @@

FLPs

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

Operation id: updateFlp

-

Replaces the data counters of the FLP role identified by FLP name and run number. Counters absent from the body are set to null. -Responds 400 if the run or the FLP in this run does not exist.

+

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).

@@ -1592,6 +1593,7 @@

FLPs

  • 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' \

From 3f093b0904acc5b6360f0fda3bf46081135dc6d6 Mon Sep 17 00:00:00 2001
From: "Claude Sonnet 5.5" 
Date: Thu, 22 Oct 2026 16:35:28 +0200
Subject: [PATCH 7/7] [O2B-1623] Replace the FLP use case tests with
 FlpRoleService tests

Co-Authored-By: Claude Sonnet 5.5 
---
 .../services/flpRole/FlpRoleService.test.js   |  6 +++
 .../lib/usecases/flp/CreateFlpUseCase.test.js | 47 -------------------
 test/lib/usecases/flp/GetFlpUseCase.test.js   | 38 ---------------
 test/lib/usecases/flp/index.js                | 20 --------
 test/lib/usecases/index.js                    |  2 -
 5 files changed, 6 insertions(+), 107 deletions(-)
 delete mode 100644 test/lib/usecases/flp/CreateFlpUseCase.test.js
 delete mode 100644 test/lib/usecases/flp/GetFlpUseCase.test.js
 delete mode 100644 test/lib/usecases/flp/index.js

diff --git a/test/lib/server/services/flpRole/FlpRoleService.test.js b/test/lib/server/services/flpRole/FlpRoleService.test.js
index 005128add6..0a3263e849 100644
--- a/test/lib/server/services/flpRole/FlpRoleService.test.js
+++ b/test/lib/server/services/flpRole/FlpRoleService.test.js
@@ -104,4 +104,10 @@ module.exports = () => {
         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);