From 808625e564b7fd890ee5d04d49f3ee4f2abbb526 Mon Sep 17 00:00:00 2001 From: perfectra1n Date: Sun, 1 Feb 2026 09:19:37 -0800 Subject: [PATCH 1/2] feat(etapi): add attachments etapi endpoint --- apps/server/etapi.openapi.yaml | 25 +++++++++++++++++++++++++ apps/server/src/etapi/attachments.ts | 6 ++++++ 2 files changed, 31 insertions(+) diff --git a/apps/server/etapi.openapi.yaml b/apps/server/etapi.openapi.yaml index af05bdbe57..8b8a65f2b3 100644 --- a/apps/server/etapi.openapi.yaml +++ b/apps/server/etapi.openapi.yaml @@ -362,6 +362,31 @@ paths: application/json; charset=utf-8: schema: $ref: "#/components/schemas/Error" + /notes/{noteId}/attachments: + parameters: + - name: noteId + in: path + required: true + schema: + $ref: "#/components/schemas/EntityId" + get: + description: Returns all attachments for a note identified by its ID + operationId: getNoteAttachments + responses: + "200": + description: list of attachments + content: + application/json; charset=utf-8: + schema: + type: array + items: + $ref: "#/components/schemas/Attachment" + default: + description: unexpected error + content: + application/json; charset=utf-8: + schema: + $ref: "#/components/schemas/Error" /notes/{noteId}/undelete: parameters: - name: noteId diff --git a/apps/server/src/etapi/attachments.ts b/apps/server/src/etapi/attachments.ts index f8fd9c16dc..48cccec29a 100644 --- a/apps/server/src/etapi/attachments.ts +++ b/apps/server/src/etapi/attachments.ts @@ -8,6 +8,12 @@ import type { AttachmentRow } from "@triliumnext/commons"; import type { ValidatorMap } from "./etapi-interface.js"; function register(router: Router) { + eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => { + const note = eu.getAndCheckNote(req.params.noteId); + const attachments = note.getAttachments(); + res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment))); + }); + const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT: ValidatorMap = { ownerId: [v.notNull, v.isNoteId], role: [v.notNull, v.isString], From c702fb273ce9cf1b6a7f48a1959909537df79dfa Mon Sep 17 00:00:00 2001 From: perfectra1n Date: Sun, 1 Feb 2026 11:46:03 -0800 Subject: [PATCH 2/2] feat(tests): add tests for new attachments endpoint --- .../spec/etapi/get-note-attachments.spec.ts | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 apps/server/spec/etapi/get-note-attachments.spec.ts diff --git a/apps/server/spec/etapi/get-note-attachments.spec.ts b/apps/server/spec/etapi/get-note-attachments.spec.ts new file mode 100644 index 0000000000..5e95463bba --- /dev/null +++ b/apps/server/spec/etapi/get-note-attachments.spec.ts @@ -0,0 +1,82 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let createdNoteId: string; +let createdAttachmentId: string; + +describe("etapi/get-note-attachments", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + createdNoteId = await createNote(app, token); + + // Create an attachment for the note + const response = await supertest(app) + .post(`/etapi/attachments`) + .auth(USER, token, { "type": "basic" }) + .send({ + "ownerId": createdNoteId, + "role": "file", + "mime": "text/plain", + "title": "test-attachment.txt", + "content": "test content", + "position": 10 + }); + createdAttachmentId = response.body.attachmentId; + expect(createdAttachmentId).toBeTruthy(); + }); + + it("gets attachments for a note", async () => { + const response = await supertest(app) + .get(`/etapi/notes/${createdNoteId}/attachments`) + .auth(USER, token, { "type": "basic" }) + .expect(200); + + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBeGreaterThan(0); + + const attachment = response.body[0]; + expect(attachment).toHaveProperty("attachmentId", createdAttachmentId); + expect(attachment).toHaveProperty("ownerId", createdNoteId); + expect(attachment).toHaveProperty("role", "file"); + expect(attachment).toHaveProperty("mime", "text/plain"); + expect(attachment).toHaveProperty("title", "test-attachment.txt"); + expect(attachment).toHaveProperty("position", 10); + expect(attachment).toHaveProperty("blobId"); + expect(attachment).toHaveProperty("dateModified"); + expect(attachment).toHaveProperty("utcDateModified"); + expect(attachment).toHaveProperty("contentLength"); + }); + + it("returns empty array for note with no attachments", async () => { + // Create a new note without any attachments + const newNoteId = await createNote(app, token, "Note without attachments"); + + const response = await supertest(app) + .get(`/etapi/notes/${newNoteId}/attachments`) + .auth(USER, token, { "type": "basic" }) + .expect(200); + + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBe(0); + }); + + it("returns 404 for non-existent note", async () => { + const response = await supertest(app) + .get("/etapi/notes/nonexistentnote/attachments") + .auth(USER, token, { "type": "basic" }) + .expect(404); + + expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND"); + }); +});