Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions lib/database/adapters/AttachmentAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ class AttachmentAdapter {
path,
encoding,
logId,
createdAt,
updatedAt,
} = databaseObject;

return {
Expand All @@ -52,8 +50,6 @@ class AttachmentAdapter {
path,
encoding,
logId,
createdAt: new Date(createdAt).getTime(),
updatedAt: new Date(updatedAt).getTime(),
};
}

Expand Down
2 changes: 1 addition & 1 deletion lib/domain/dtos/CreateAttachmentDto.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const BodyDto = Joi.object({
log: EntityIdDto,
});

const FilesDto = Joi.array().items(FileDto);
const FilesDto = Joi.array().items(FileDto).min(1);

const QueryDto = Joi.object({
token: Joi.string(),
Expand Down
4 changes: 2 additions & 2 deletions lib/domain/dtos/GetAttachmentDto.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ const ParamsDto = Joi.object({
attachmentId: EntityIdDto,
});

const GetLogAttachmentDto = Joi.object({
const GetAttachmentDto = Joi.object({
body: Joi.object({}),
query: QueryDto,
params: ParamsDto,
});

module.exports = GetLogAttachmentDto;
module.exports = GetAttachmentDto;
2 changes: 0 additions & 2 deletions lib/domain/entities/Attachment.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,4 @@
* @property {string} path
* @property {string} encoding
* @property {number} logId
* @property {number} createdAt
* @property {number} updatedAt
*/
53 changes: 53 additions & 0 deletions lib/server/api-docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,56 @@ curl -g -X POST "$BOOKKEEPING_URL/api/eos-report?reportType=ECS&token=$TOKEN" \
-H 'content-type: application/json' \
-d '{"shifterName":"John Doe","shiftFlow":"The shift flow","infoForNextShifter":"Info for the next shifter","typeSpecific":{"environmentComments":{"ENV1":"A comment about the environment"},"runComments":{"200":"A comment about the run"}}}'
```

## Attachments

Files attached to log entries.

### `POST /attachments` - Upload a file as an attachment (not for LLM use)

Operation id: `createAttachment`

Uploads **one** file (multipart request) and stores it as an attachment, linked to a log if `log` is given.

The files of a request are all written on the server, but only the first one is stored as an attachment. A request without any file is rejected with a `400`.
Logs and their attachments are usually created together with `POST /logs`.

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

Request body (multipart/form-data):

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `log` | integer | no | Identifier of the log to attach the file to. |
| `attachments` | string (binary) | yes | The file to upload. |

Responses:

- `201`: The stored attachment.
- `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>`.

```bash
curl -g -X POST "$BOOKKEEPING_URL/api/attachments?token=$TOKEN" \
-F 'log=1' \
-F 'attachments=@/path/to/file.png'
```

### `GET /attachments/{attachmentId}` - Download the file of an attachment

Operation id: `getAttachmentFile`

Returns the content of the file, with the type of the file as `Content-Type`. Use `curl -OJ` to save it with the name given by the server.

| Parameter | In | Type | Required | Notes |
| --- | --- | --- | --- | --- |
| `attachmentId` | path | integer | yes | Attachment identifier. |

Responses:

- `200`: The file.
- `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`: No attachment has this id.

```bash
curl -g -OJ "$BOOKKEEPING_URL/api/attachments/1?token=$TOKEN"
```
75 changes: 75 additions & 0 deletions lib/server/api-docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,81 @@ <h2>End of shift reports</h2>
-d &#39;{&quot;shifterName&quot;:&quot;John Doe&quot;,&quot;shiftFlow&quot;:&quot;The shift flow&quot;,&quot;infoForNextShifter&quot;:&quot;Info for the next shifter&quot;,&quot;typeSpecific&quot;:{&quot;environmentComments&quot;:{&quot;ENV1&quot;:&quot;A comment about the environment&quot;},&quot;runComments&quot;:{&quot;200&quot;:&quot;A comment about the run&quot;}}}&#39;
</code></pre>
</details>
<h2>Attachments</h2>
<p>Files attached to log entries.</p>
<details>
<summary><h3><span class="method post">POST</span> <code>/attachments</code> - Upload a file as an attachment <span class="no-llm" title="Changes data: not meant for LLM or MCP tool use (x-llm-exclude)">not for LLM use</span></h3></summary>

<p>Operation id: <code>createAttachment</code></p>
<p>Uploads <strong>one</strong> file (multipart request) and stores it as an attachment, linked to a log if <code>log</code> is given.</p>
<p>The files of a request are all written on the server, but only the first one is stored as an attachment. A request without any file is rejected with a <code>400</code>.
Logs and their attachments are usually created together with <code>POST /logs</code>.</p>
<p><strong>Not meant for LLM or MCP tool use</strong> (<code>x-llm-exclude</code>).</p>
<p>Request body (multipart/form-data):</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td><code>log</code></td>
<td>integer</td>
<td>no</td>
<td>Identifier of the log to attach the file to.</td>
</tr>
<tr>
<td><code>attachments</code></td>
<td>string (binary)</td>
<td>yes</td>
<td>The file to upload.</td>
</tr>
</tbody></table>
<p>Responses:</p>
<ul>
<li><code>201</code>: The stored attachment.</li>
<li><code>400</code>: The request failed validation. HTTP status is 400 although each error&#39;s <code>status</code> field is <code>&quot;422&quot;</code>. <code>source.pointer</code> has the form <code>/data/attributes/&lt;query|params|body&gt;/&lt;field&gt;</code>.</li>
</ul>
<pre><code class="language-bash">curl -g -X POST &quot;$BOOKKEEPING_URL/api/attachments?token=$TOKEN&quot; \
-F &#39;log=1&#39; \
-F &#39;attachments=@/path/to/file.png&#39;
</code></pre>
</details>
<details>
<summary><h3><span class="method get">GET</span> <code>/attachments/{attachmentId}</code> - Download the file of an attachment</h3></summary>

<p>Operation id: <code>getAttachmentFile</code></p>
<p>Returns the content of the file, with the type of the file as <code>Content-Type</code>. Use <code>curl -OJ</code> to save it with the name given by the server.</p>
<table>
<thead>
<tr>
<th>Parameter</th>
<th>In</th>
<th>Type</th>
<th>Required</th>
<th>Notes</th>
</tr>
</thead>
<tbody><tr>
<td><code>attachmentId</code></td>
<td>path</td>
<td>integer</td>
<td>yes</td>
<td>Attachment identifier.</td>
</tr>
</tbody></table>
<p>Responses:</p>
<ul>
<li><code>200</code>: The file.</li>
<li><code>400</code>: The request failed validation. HTTP status is 400 although each error&#39;s <code>status</code> field is <code>&quot;422&quot;</code>. <code>source.pointer</code> has the form <code>/data/attributes/&lt;query|params|body&gt;/&lt;field&gt;</code>.</li>
<li><code>404</code>: No attachment has this id.</li>
</ul>
<pre><code class="language-bash">curl -g -OJ &quot;$BOOKKEEPING_URL/api/attachments/1?token=$TOKEN&quot;
</code></pre>
</details>

</main>
<footer>Generated from openapi.yaml by <code>npm run docs:api</code> · ALICE Bookkeeping · internal use only</footer>
Expand Down
79 changes: 79 additions & 0 deletions lib/server/api-docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ tags:
description: Detectors whose QC flags contribute to the global aggregated quality (GAQ) of a run in a data pass.
- name: End of shift reports
description: End-of-shift (EoS) reports written by the shifters.
- name: Attachments
description: Files attached to log entries.

paths:
/tags:
Expand Down Expand Up @@ -736,6 +738,60 @@ paths:
errors:
- { status: 404, title: Not found, detail: User with this external id (1) could not be found }

/attachments:
post:
tags: [Attachments]
operationId: createAttachment
x-llm-exclude: true
summary: Upload a file as an attachment
description: |
Uploads **one** file (multipart request) and stores it as an attachment, linked to a log if `log` is given.

The files of a request are all written on the server, but only the first one is stored as an attachment. A request without any file is rejected with a `400`.
Logs and their attachments are usually created together with `POST /logs`.
requestBody:
required: true
content:
multipart/form-data:
schema: { $ref: '#/components/schemas/CreateAttachment' }
example:
log: 1
attachments: '@/path/to/file.png'
responses:
'201':
description: The stored attachment.
content:
application/json:
schema: { $ref: '#/components/schemas/StoredAttachmentResponse' }
'400': { $ref: '#/components/responses/ValidationError' }

/attachments/{attachmentId}:
get:
tags: [Attachments]
operationId: getAttachmentFile
summary: Download the file of an attachment
description: Returns the content of the file, with the type of the file as `Content-Type`. Use `curl -OJ` to save it with the name given by the server.
parameters:
- name: attachmentId
in: path
required: true
description: Attachment identifier.
schema: { type: integer, minimum: 1 }
responses:
'200':
description: The file.
content:
application/octet-stream:
schema: { type: string, format: binary }
'400': { $ref: '#/components/responses/ValidationError' }
'404':
description: No attachment has this id.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
example:
errors: [{ status: 404, title: Not found, detail: Attachment with this id (999999999) could not be found }]

components:
securitySchemes:
jwtQuery:
Expand Down Expand Up @@ -1152,3 +1208,26 @@ components:
id: { type: integer }
title: { type: string, description: 'Title of the report, e.g. `End of shift report - ECS - 17/03/2023 Morning`.' }
additionalProperties: true
CreateAttachment:
type: object
required: [attachments]
properties:
log: { type: integer, minimum: 1, description: Identifier of the log to attach the file to. }
attachments: { type: string, format: binary, description: The file to upload. }
StoredAttachment:
type: object
description: An attachment as stored on the server.
properties:
id: { type: integer }
fileName: { type: string, description: Name of the file on the server (the upload time is prepended to the original name). }
originalName: { type: string }
mimeType: { type: string, examples: [image/png] }
encoding: { type: string, examples: [7bit] }
size: { type: integer, description: Size of the file in bytes. }
path: { type: string, description: Path of the file on the server. }
logId: { type: [integer, 'null'] }
StoredAttachmentResponse:
type: object
required: [data]
properties:
data: { $ref: '#/components/schemas/StoredAttachment' }
43 changes: 18 additions & 25 deletions lib/server/controllers/attachments.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/
const { attachment: { CreateAttachmentUseCase, GetAttachmentUseCase } } = require('../../usecases');
const { dtos: { CreateAttachmentDto, GetAttachmentDto } } = require('../../domain');
const { dtoValidator } = require('../utilities');
const { attachmentService } = require('../services/attachment/AttachmentService.js');
const { updateExpressResponseFromNativeError } = require('../express/updateExpressResponseFromNativeError.js');

/**
* Create new attachment on log
Expand All @@ -21,52 +22,44 @@ const { dtoValidator } = require('../utilities');
* 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}
* @returns {Promise<void>} resolves once the response has been sent
*/
const createAttachment = async (request, response) => {
const value = await dtoValidator(CreateAttachmentDto, request, response);
if (!value) {
return;
}

const attachment = await new CreateAttachmentUseCase().execute(value);

response.status(201).json({
data: attachment,
});
try {
const { files: [file], body: { log: logId } } = value;
response.status(201).json({ data: await attachmentService.create(file, logId) });
} catch (error) {
updateExpressResponseFromNativeError(response, error);
}
};

/**
* Find all Attachments that belong to a certain log item
* Send the file of an attachment
*
* @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}
* @returns {Promise<void>} resolves once the response has been sent
*/
const readAttachment = async (request, response, next) => {
const readAttachment = async (request, response) => {
const value = await dtoValidator(GetAttachmentDto, request, response);
if (!value) {
return;
}

const { attachment } = await new GetAttachmentUseCase()
.execute(value);
if (!attachment) {
response.status(400).json({
errors: [
{
status: '404',
title: `Attachment with this id (${request.params.attachmentId}) could not be found`,
},
],
});
return;
try {
const { params: { attachmentId } } = value;
const attachment = await attachmentService.getOneOrFail(attachmentId);
response.status(200).sendFile(attachment.path);
} catch (error) {
updateExpressResponseFromNativeError(response, error);
}
response.status(200).sendFile(attachment.path);
};

module.exports = {
Expand Down
Loading
Loading