diff --git a/_regroup/package.json b/_regroup/package.json index ee454569d..2157ad9df 100644 --- a/_regroup/package.json +++ b/_regroup/package.json @@ -40,7 +40,7 @@ "@types/express": "5.0.1", "@types/node": "22.15.29", "@types/yargs": "17.0.33", - "@vitest/coverage-v8": "3.2.0", + "@vitest/coverage-v8": "3.2.1", "eslint": "9.28.0", "eslint-plugin-simple-import-sort": "12.1.1", "esm": "3.2.25", diff --git a/_regroup/test-etapi/delete-attachment.http b/_regroup/test-etapi/delete-attachment.http deleted file mode 100644 index d12e8de43..000000000 --- a/_regroup/test-etapi/delete-attachment.http +++ /dev/null @@ -1,52 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attachments -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "text/plain", - "title": "my attachment", - "content": "text" -} - -> {% client.global.set("createdAttachmentId", response.body.attachmentId); %} - -### - -DELETE {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "ATTACHMENT_NOT_FOUND"); -%} diff --git a/_regroup/test-etapi/delete-attribute.http b/_regroup/test-etapi/delete-attribute.http deleted file mode 100644 index d61b75ba2..000000000 --- a/_regroup/test-etapi/delete-attribute.http +++ /dev/null @@ -1,52 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### - -DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "ATTRIBUTE_NOT_FOUND"); -%} diff --git a/_regroup/test-etapi/delete-cloned-branch.http b/_regroup/test-etapi/delete-cloned-branch.http deleted file mode 100644 index a87a6fa4d..000000000 --- a/_regroup/test-etapi/delete-cloned-branch.http +++ /dev/null @@ -1,87 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### Clone to another location - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "parentNoteId": "_hidden" -} - -> {% client.global.set("clonedBranchId", response.body.branchId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "BRANCH_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} diff --git a/_regroup/test-etapi/delete-note-with-all-branches.http b/_regroup/test-etapi/delete-note-with-all-branches.http deleted file mode 100644 index 5a50bc4a9..000000000 --- a/_regroup/test-etapi/delete-note-with-all-branches.http +++ /dev/null @@ -1,126 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### Clone to another location - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "parentNoteId": "_hidden" -} - -> {% client.global.set("clonedBranchId", response.body.branchId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "BRANCH_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code == "BRANCH_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "NOTE_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "ATTRIBUTE_NOT_FOUND"); -%} diff --git a/_regroup/test-etapi/get-date-notes.http b/_regroup/test-etapi/get-date-notes.http deleted file mode 100644 index 19f0b4fc9..000000000 --- a/_regroup/test-etapi/get-date-notes.http +++ /dev/null @@ -1,72 +0,0 @@ -GET {{triliumHost}}/etapi/inbox/2022-01-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/days/2022-01-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/days/2022-1 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "DATE_INVALID"); -%} - -### - -GET {{triliumHost}}/etapi/calendar/weeks/2022-01-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/weeks/2022-1 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "DATE_INVALID"); -%} - -### - -GET {{triliumHost}}/etapi/calendar/months/2022-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/months/2022-1 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "MONTH_INVALID"); -%} - -### - -GET {{triliumHost}}/etapi/calendar/years/2022 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/years/202 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "YEAR_INVALID"); -%} diff --git a/_regroup/test-etapi/get-inherited-attribute-cloned.http b/_regroup/test-etapi/get-inherited-attribute-cloned.http deleted file mode 100644 index eaf8d91b1..000000000 --- a/_regroup/test-etapi/get-inherited-attribute-cloned.http +++ /dev/null @@ -1,116 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello parent", - "type": "text", - "content": "Hi there!" -} - -> {% -client.assert(response.status === 201); -client.global.set("parentNoteId", response.body.note.noteId); -client.global.set("parentBranchId", response.body.branch.branchId); -%} - -### Create inheritable parent attribute - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{parentNoteId}}", - "type": "label", - "name": "mylabel", - "value": "", - "isInheritable": true, - "position": 10 -} - -> {% -client.assert(response.status === 201); -client.global.set("parentAttributeId", response.body.attributeId); -%} - -### Create child note under root - -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello child", - "type": "text", - "content": "Hi there!" -} - -> {% -client.assert(response.status === 201); -client.global.set("childNoteId", response.body.note.noteId); -client.global.set("childBranchId", response.body.branch.branchId); -%} - -### Create child attribute - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{childNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": false, - "position": 10 -} - -> {% -client.assert(response.status === 201); -client.global.set("childAttributeId", response.body.attributeId); -%} - -### Clone child to parent - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{childNoteId}}", - "parentNoteId": "{{parentNoteId}}" -} - -> {% -client.assert(response.status === 201); -client.assert(response.body.parentNoteId == client.global.get("parentNoteId")); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{childNoteId}} -Authorization: {{authToken}} - -> {% - -function hasAttribute(list, attributeId) { - for (let i = 0; i < list.length; i++) { - if (list[i]["attributeId"] === attributeId) { - return true; - } - } - return false; -} - -client.log(JSON.stringify(response.body.attributes)); - -client.assert(response.status === 200); -client.assert(response.body.noteId == client.global.get("childNoteId")); -client.assert(response.body.attributes.length == 2); -client.assert(hasAttribute(response.body.attributes, client.global.get("parentAttributeId"))); -client.assert(hasAttribute(response.body.attributes, client.global.get("childAttributeId"))); -%} diff --git a/_regroup/test-etapi/get-inherited-attribute.http b/_regroup/test-etapi/get-inherited-attribute.http deleted file mode 100644 index 26e9af854..000000000 --- a/_regroup/test-etapi/get-inherited-attribute.http +++ /dev/null @@ -1,61 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "GetInheritedAttributes Test Note", - "type": "text", - "content": "Hi there!" -} - -> {% - client.assert(response.status === 201); - client.global.set("parentNoteId", response.body.note.noteId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{parentNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### - -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "{{parentNoteId}}", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% -client.global.set("createdNoteId", response.body.note.noteId); -client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.noteId == client.global.get("createdNoteId")); -client.assert(response.body.attributes.length == 1); -client.assert(response.body.attributes[0].attributeId == client.global.get("createdAttributeId")); -%} diff --git a/_regroup/test-etapi/http-client.env.json b/_regroup/test-etapi/http-client.env.json deleted file mode 100644 index 8ede0719c..000000000 --- a/_regroup/test-etapi/http-client.env.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dev": { - "triliumHost": "http://localhost:37740" - } -} diff --git a/_regroup/test-etapi/patch-attribute.http b/_regroup/test-etapi/patch-attribute.http deleted file mode 100644 index 625c19446..000000000 --- a/_regroup/test-etapi/patch-attribute.http +++ /dev/null @@ -1,80 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### - -PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "value": "CHANGED" -} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% -client.assert(response.body.value === "CHANGED"); -%} - -### - -PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "root" -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_NOT_ALLOWED"); -%} - -### - -PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "value": null -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR"); -%} \ No newline at end of file diff --git a/apps/client/src/widgets/type_widgets/ckeditor/config.ts b/apps/client/src/widgets/type_widgets/ckeditor/config.ts index 5a3262a1c..bd74d0512 100644 --- a/apps/client/src/widgets/type_widgets/ckeditor/config.ts +++ b/apps/client/src/widgets/type_widgets/ckeditor/config.ts @@ -222,7 +222,6 @@ export function buildFloatingToolbar() { "|", "code", "link", - "bookmark", "removeFormat", "internallink", "cuttonote" @@ -244,7 +243,7 @@ export function buildFloatingToolbar() { { label: "Insert", icon: "plus", - items: ["internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak"] + items: ["bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak"] }, "|", "outdent", diff --git a/apps/server/package.json b/apps/server/package.json index fec1872bd..89a19b068 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -85,10 +85,10 @@ "jsdom": "26.1.0", "marked": "15.0.12", "mime-types": "3.0.1", - "multer": "2.0.0", + "multer": "2.0.1", "normalize-strings": "1.1.1", "ollama": "0.5.16", - "openai": "5.0.2", + "openai": "5.1.0", "rand-token": "1.0.1", "safe-compare": "1.1.4", "sanitize-filename": "1.6.3", diff --git a/apps/server/spec/etapi/create-backup.spec.ts b/apps/server/spec/etapi/create-backup.spec.ts index efab80218..00c8751aa 100644 --- a/apps/server/spec/etapi/create-backup.spec.ts +++ b/apps/server/spec/etapi/create-backup.spec.ts @@ -21,6 +21,6 @@ describe("etapi/backup", () => { const response = await supertest(app) .put("/etapi/backup/etapi_test") .auth(USER, token, { "type": "basic"}) - .expect(201); + .expect(204); }); }); diff --git a/apps/server/spec/etapi/delete-entities.spec.ts b/apps/server/spec/etapi/delete-entities.spec.ts new file mode 100644 index 000000000..581b1e693 --- /dev/null +++ b/apps/server/spec/etapi/delete-entities.spec.ts @@ -0,0 +1,172 @@ +import { Application } from "express"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; +import { randomInt } from "crypto"; + +let app: Application; +let token: string; +let createdNoteId: string; +let createdBranchId: string; + +const USER = "etapi"; + +type EntityType = "attachments" | "attributes" | "branches" | "notes"; + +describe("etapi/delete-entities", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + beforeEach(async () => { + ({ createdNoteId, createdBranchId } = await createNote()); + }); + + it("deletes attachment", async () => { + const attachmentId = await createAttachment(); + await deleteEntity("attachments", attachmentId); + await expectNotFound("attachments", attachmentId); + }); + + it("deletes attribute", async () => { + const attributeId = await createAttribute(); + await deleteEntity("attributes", attributeId); + await expectNotFound("attributes", attributeId); + }); + + it("deletes cloned branch", async () => { + const clonedBranchId = await createClone(); + + await expectFound("branches", createdBranchId); + await expectFound("branches", clonedBranchId); + + await deleteEntity("branches", createdBranchId); + await expectNotFound("branches", createdBranchId); + + await expectFound("branches", clonedBranchId); + await expectFound("notes", createdNoteId); + }); + + it("deletes note with all branches", async () => { + const attributeId = await createAttribute(); + + const clonedBranchId = await createClone(); + + await expectFound("notes", createdNoteId); + await expectFound("branches", createdBranchId); + await expectFound("branches", clonedBranchId); + await expectFound("attributes", attributeId); + await deleteEntity("notes", createdNoteId); + + await expectNotFound("branches", createdBranchId); + await expectNotFound("branches", clonedBranchId); + await expectNotFound("notes", createdNoteId); + await expectNotFound("attributes", attributeId); + }); +}); + +async function createNote() { + const noteId = `forcedId${randomInt(1000)}`; + const response = await supertest(app) + .post("/etapi/create-note") + .auth(USER, token, { "type": "basic"}) + .send({ + "noteId": noteId, + "parentNoteId": "root", + "title": "Hello", + "type": "text", + "content": "Hi there!", + "dateCreated": "2023-08-21 23:38:51.123+0200", + "utcDateCreated": "2023-08-21 23:38:51.123Z" + }) + .expect(201); + expect(response.body.note.noteId).toStrictEqual(noteId); + + return { + createdNoteId: response.body.note.noteId, + createdBranchId: response.body.branch.branchId + }; +} + +async function createClone() { + const response = await supertest(app) + .post("/etapi/branches") + .auth(USER, token, { "type": "basic"}) + .send({ + noteId: createdNoteId, + parentNoteId: "_hidden" + }) + .expect(201); + expect(response.body.parentNoteId).toStrictEqual("_hidden"); + return response.body.branchId; +} + +async function createAttribute() { + const attributeId = `forcedId${randomInt(1000)}`; + const response = await supertest(app) + .post("/etapi/attributes") + .auth(USER, token, { "type": "basic"}) + .send({ + "attributeId": attributeId, + "noteId": createdNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true + }) + .expect(201); + expect(response.body.attributeId).toStrictEqual(attributeId); + return response.body.attributeId; +} + +async function createAttachment() { + const response = await supertest(app) + .post("/etapi/attachments") + .auth(USER, token, { "type": "basic"}) + .send({ + "ownerId": createdNoteId, + "role": "file", + "mime": "plain/text", + "title": "my attachment", + "content": "my text" + }) + .expect(201); + return response.body.attachmentId; +} + +async function deleteEntity(entity: EntityType, id: string) { + // Delete twice to test idempotency. + for (let i=0; i < 2; i++) { + await supertest(app) + .delete(`/etapi/${entity}/${id}`) + .auth(USER, token, { "type": "basic"}) + .expect(204); + } +} + +const MISSING_ENTITY_ERROR_CODES: Record = { + attachments: "ATTACHMENT_NOT_FOUND", + attributes: "ATTRIBUTE_NOT_FOUND", + branches: "BRANCH_NOT_FOUND", + notes: "NOTE_NOT_FOUND" +} + +async function expectNotFound(entity: EntityType, id: string) { + const response = await supertest(app) + .get(`/etapi/${entity}/${id}`) + .auth(USER, token, { "type": "basic"}) + .expect(404); + + expect(response.body.code).toStrictEqual(MISSING_ENTITY_ERROR_CODES[entity]); +} + +async function expectFound(entity: EntityType, id: string) { + await supertest(app) + .get(`/etapi/${entity}/${id}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); +} diff --git a/apps/server/spec/etapi/get-date-notes.spec.ts b/apps/server/spec/etapi/get-date-notes.spec.ts new file mode 100644 index 000000000..e1f67fec6 --- /dev/null +++ b/apps/server/spec/etapi/get-date-notes.spec.ts @@ -0,0 +1,103 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import config from "../../src/services/config.js"; +import { login } from "./utils.js"; +import { Application } from "express"; +import supertest from "supertest"; +import date_notes from "../../src/services/date_notes.js"; +import cls from "../../src/services/cls.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; + +describe("etapi/get-date-notes", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("obtains inbox", async () => { + await supertest(app) + .get("/etapi/inbox/2022-01-01") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + describe("days", () => { + it("obtains day from calendar", async () => { + await supertest(app) + .get("/etapi/calendar/days/2022-01-01") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("detects invalid date", async () => { + const response = await supertest(app) + .get("/etapi/calendar/days/2022-1") + .auth(USER, token, { "type": "basic"}) + .expect(400); + expect(response.body.code).toStrictEqual("DATE_INVALID"); + }); + }); + + describe("weeks", () => { + beforeAll(() => { + cls.init(() => { + const rootCalendarNote = date_notes.getRootCalendarNote(); + rootCalendarNote.setLabel("enableWeekNote"); + }); + }); + + it("obtains week calendar", async () => { + await supertest(app) + .get("/etapi/calendar/weeks/2022-W01") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("detects invalid date", async () => { + const response = await supertest(app) + .get("/etapi/calendar/weeks/2022-1") + .auth(USER, token, { "type": "basic"}) + .expect(400); + expect(response.body.code).toStrictEqual("WEEK_INVALID"); + }); + }); + + describe("months", () => { + it("obtains month calendar", async () => { + await supertest(app) + .get("/etapi/calendar/months/2022-01") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("detects invalid month", async () => { + const response = await supertest(app) + .get("/etapi/calendar/months/2022-1") + .auth(USER, token, { "type": "basic"}) + .expect(400); + expect(response.body.code).toStrictEqual("MONTH_INVALID"); + }); + }); + + describe("years", () => { + it("obtains year calendar", async () => { + await supertest(app) + .get("/etapi/calendar/years/2022") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("detects invalid year", async () => { + const response = await supertest(app) + .get("/etapi/calendar/years/202") + .auth(USER, token, { "type": "basic"}) + .expect(400); + expect(response.body.code).toStrictEqual("YEAR_INVALID"); + }); + }); +}); diff --git a/apps/server/spec/etapi/get-inherited-attribute-cloned.spec.ts b/apps/server/spec/etapi/get-inherited-attribute-cloned.spec.ts new file mode 100644 index 000000000..5d882746f --- /dev/null +++ b/apps/server/spec/etapi/get-inherited-attribute-cloned.spec.ts @@ -0,0 +1,98 @@ +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; + +let parentNoteId: string; + +describe("etapi/get-inherited-attribute-cloned", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + parentNoteId = await createNote(app, token); + }); + + it("gets inherited attribute", async () => { + // Create an inheritable attribute on the parent note. + let response = await supertest(app) + .post("/etapi/attributes") + .auth("etapi", token, { "type": "basic"}) + .send({ + "noteId": parentNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true, + "position": 10 + }) + .expect(201); + const parentAttributeId = response.body.attributeId; + expect(parentAttributeId).toBeTruthy(); + + // Create a subnote. + response = await supertest(app) + .post("/etapi/create-note") + .auth("etapi", token, { "type": "basic"}) + .send({ + "parentNoteId": parentNoteId, + "title": "Hello", + "type": "text", + "content": "Hi there!" + }) + .expect(201); + const childNoteId = response.body.note.noteId; + + // Create child attribute + response = await supertest(app) + .post("/etapi/attributes") + .auth("etapi", token, { "type": "basic"}) + .send({ + "noteId": childNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": false, + "position": 10 + }) + .expect(201); + const childAttributeId = response.body.attributeId; + expect(parentAttributeId).toBeTruthy(); + + // Clone child to parent + response = await supertest(app) + .post("/etapi/branches") + .auth("etapi", token, { "type": "basic"}) + .send({ + noteId: childNoteId, + parentNoteId: parentNoteId + }) + .expect(200); + parentNoteId = response.body.parentNoteId; + + // Check attribute IDs + response = await supertest(app) + .get(`/etapi/notes/${childNoteId}`) + .auth("etapi", token, { "type": "basic"}) + .expect(200); + expect(response.body.noteId).toStrictEqual(childNoteId); + expect(response.body.attributes).toHaveLength(2); + expect(hasAttribute(response.body.attributes, parentAttributeId)); + expect(hasAttribute(response.body.attributes, childAttributeId)); + }); + + function hasAttribute(list: object[], attributeId: string) { + for (let i = 0; i < list.length; i++) { + if (list[i]["attributeId"] === attributeId) { + return true; + } + } + return false; + } +}); diff --git a/apps/server/spec/etapi/get-inherited-attribute.spec.ts b/apps/server/spec/etapi/get-inherited-attribute.spec.ts new file mode 100644 index 000000000..c0e92dde1 --- /dev/null +++ b/apps/server/spec/etapi/get-inherited-attribute.spec.ts @@ -0,0 +1,60 @@ +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; + +let parentNoteId: string; + +describe("etapi/get-inherited-attribute", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + parentNoteId = await createNote(app, token); + }); + + it("gets inherited attribute", async () => { + // Create an inheritable attribute on the parent note. + let response = await supertest(app) + .post("/etapi/attributes") + .auth("etapi", token, { "type": "basic"}) + .send({ + "noteId": parentNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true + }) + .expect(201); + const createdAttributeId = response.body.attributeId; + expect(createdAttributeId).toBeTruthy(); + + // Create a subnote. + response = await supertest(app) + .post("/etapi/create-note") + .auth("etapi", token, { "type": "basic"}) + .send({ + "parentNoteId": parentNoteId, + "title": "Hello", + "type": "text", + "content": "Hi there!" + }) + .expect(201); + const createdNoteId = response.body.note.noteId; + + // Check the attribute is inherited. + response = await supertest(app) + .get(`/etapi/notes/${createdNoteId}`) + .auth("etapi", token, { "type": "basic"}) + .expect(200); + expect(response.body.noteId).toStrictEqual(createdNoteId); + expect(response.body.attributes).toHaveLength(1); + expect(response.body.attributes[0].attributeId === createdAttributeId); + }); +}); diff --git a/apps/server/spec/etapi/other.spec.ts b/apps/server/spec/etapi/other.spec.ts index 733ab12a6..d7c1d38b3 100644 --- a/apps/server/spec/etapi/other.spec.ts +++ b/apps/server/spec/etapi/other.spec.ts @@ -21,6 +21,6 @@ describe("etapi/refresh-note-ordering/root", () => { await supertest(app) .post("/etapi/refresh-note-ordering/root") .auth(USER, token, { "type": "basic"}) - .expect(200); + .expect(204); }); }); diff --git a/apps/server/spec/etapi/patch-attribute.spec.ts b/apps/server/spec/etapi/patch-attribute.spec.ts new file mode 100644 index 000000000..821a4e3db --- /dev/null +++ b/apps/server/spec/etapi/patch-attribute.spec.ts @@ -0,0 +1,77 @@ +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 createdAttributeId: string; + +describe("etapi/patch-attribute", () => { + 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 attribute + const response = await supertest(app) + .post(`/etapi/attributes`) + .auth(USER, token, { "type": "basic"}) + .send({ + "noteId": createdNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true + }); + createdAttributeId = response.body.attributeId; + expect(createdAttributeId).toBeTruthy(); + }); + + it("changes name and value", async () => { + const state = { + value: "CHANGED" + }; + await supertest(app) + .patch(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}) + .send(state) + .expect(200); + + // Ensure it got changed. + const response = await supertest(app) + .get(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}); + expect(response.body).toMatchObject(state); + }); + + it("forbids setting disallowed property", async () => { + const response = await supertest(app) + .patch(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}) + .send({ + noteId: "root" + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_NOT_ALLOWED"); + }); + + it("forbids setting wrong data type", async () => { + const response = await supertest(app) + .patch(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}) + .send({ + value: null + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_VALIDATION_ERROR"); + }); + +}); diff --git a/apps/server/spec/etapi/patch-note.spec.ts b/apps/server/spec/etapi/patch-note.spec.ts index 6b833a43c..178808762 100644 --- a/apps/server/spec/etapi/patch-note.spec.ts +++ b/apps/server/spec/etapi/patch-note.spec.ts @@ -34,7 +34,7 @@ describe("etapi/patch-note", () => { }); it("obtains correct note information", async () => { - expectNoteToMatch({ + await expectNoteToMatch({ title: "Hello", type: "code", mime: "application/json" diff --git a/apps/server/src/assets/views/share/page.ejs b/apps/server/src/assets/views/share/page.ejs index d15cf515e..2fba4702b 100644 --- a/apps/server/src/assets/views/share/page.ejs +++ b/apps/server/src/assets/views/share/page.ejs @@ -13,6 +13,7 @@ <% } %> + <% if (!note.isLabelTruthy("shareOmitDefaultCss")) { %> <% } %> diff --git a/apps/server/src/etapi/backup.ts b/apps/server/src/etapi/backup.ts index d73bf1a33..9c986f45a 100644 --- a/apps/server/src/etapi/backup.ts +++ b/apps/server/src/etapi/backup.ts @@ -4,10 +4,10 @@ import eu from "./etapi_utils.js"; import backupService from "../services/backup.js"; function register(router: Router) { - eu.route(router, "put", "/etapi/backup/:backupName", async (req, res, next) => { - await backupService.backupNow(req.params.backupName); - - res.sendStatus(204); + eu.route(router, "put", "/etapi/backup/:backupName", (req, res, next) => { + backupService.backupNow(req.params.backupName) + .then(() => res.sendStatus(204)) + .catch(() => res.sendStatus(500)); }); } diff --git a/apps/server/src/etapi/etapi_utils.ts b/apps/server/src/etapi/etapi_utils.ts index e4f40c927..a50434f70 100644 --- a/apps/server/src/etapi/etapi_utils.ts +++ b/apps/server/src/etapi/etapi_utils.ts @@ -6,7 +6,7 @@ import etapiTokenService from "../services/etapi_tokens.js"; import config from "../services/config.js"; import type { NextFunction, Request, RequestHandler, Response, Router } from "express"; import type { ValidatorMap } from "./etapi-interface.js"; -import type { ApiRequestHandler } from "../routes/route_api.js"; +import type { ApiRequestHandler, SyncRouteRequestHandler } from "../routes/route_api.js"; const GENERIC_CODE = "GENERIC"; type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head"; @@ -73,11 +73,11 @@ function processRequest(req: Request, res: Response, routeHandler: ApiRequestHan } } -function route(router: Router, method: HttpMethod, path: string, routeHandler: ApiRequestHandler) { +function route(router: Router, method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler) { router[method](path, checkEtapiAuth, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path)); } -function NOT_AUTHENTICATED_ROUTE(router: Router, method: HttpMethod, path: string, middleware: RequestHandler[], routeHandler: RequestHandler) { +function NOT_AUTHENTICATED_ROUTE(router: Router, method: HttpMethod, path: string, middleware: RequestHandler[], routeHandler: SyncRouteRequestHandler) { router[method](path, ...middleware, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path)); } diff --git a/apps/server/src/etapi/special_notes.ts b/apps/server/src/etapi/special_notes.ts index df2f75f9a..043ce1d3a 100644 --- a/apps/server/src/etapi/special_notes.ts +++ b/apps/server/src/etapi/special_notes.ts @@ -15,46 +15,46 @@ function isValidDate(date: string) { } function register(router: Router) { - eu.route(router, "get", "/etapi/inbox/:date", async (req, res, next) => { + eu.route(router, "get", "/etapi/inbox/:date", (req, res, next) => { const { date } = req.params; if (!isValidDate(date)) { throw getDateInvalidError(date); } - const note = await specialNotesService.getInboxNote(date); + const note = specialNotesService.getInboxNote(date); res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, "get", "/etapi/calendar/days/:date", async (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/days/:date", (req, res, next) => { const { date } = req.params; if (!isValidDate(date)) { throw getDateInvalidError(date); } - const note = await dateNotesService.getDayNote(date); + const note = dateNotesService.getDayNote(date); res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, "get", "/etapi/calendar/week-first-day/:date", async (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/week-first-day/:date", (req, res, next) => { const { date } = req.params; if (!isValidDate(date)) { throw getDateInvalidError(date); } - const note = await dateNotesService.getWeekFirstDayNote(date); + const note = dateNotesService.getWeekFirstDayNote(date); res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, "get", "/etapi/calendar/weeks/:week", async (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/weeks/:week", (req, res, next) => { const { week } = req.params; if (!/[0-9]{4}-W[0-9]{2}/.test(week)) { throw getWeekInvalidError(week); } - const note = await dateNotesService.getWeekNote(week); + const note = dateNotesService.getWeekNote(week); if (!note) { throw getWeekNotFoundError(week); @@ -63,14 +63,14 @@ function register(router: Router) { res.json(mappers.mapNoteToPojo(note)); }); - eu.route(router, "get", "/etapi/calendar/months/:month", async (req, res, next) => { + eu.route(router, "get", "/etapi/calendar/months/:month", (req, res, next) => { const { month } = req.params; if (!/[0-9]{4}-[0-9]{2}/.test(month)) { throw getMonthInvalidError(month); } - const note = await dateNotesService.getMonthNote(month); + const note = dateNotesService.getMonthNote(month); res.json(mappers.mapNoteToPojo(note)); }); diff --git a/apps/server/src/services/special_notes.ts b/apps/server/src/services/special_notes.ts index df8b87cdc..4083b8fff 100644 --- a/apps/server/src/services/special_notes.ts +++ b/apps/server/src/services/special_notes.ts @@ -9,6 +9,7 @@ import searchService from "./search/services/search.js"; import SearchContext from "./search/search_context.js"; import hiddenSubtree from "./hidden_subtree.js"; import { t } from "i18next"; +import { BNote } from "./backend_script_entrypoint.js"; const { LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT } = hiddenSubtree; function getInboxNote(date: string) { @@ -17,7 +18,7 @@ function getInboxNote(date: string) { throw new Error("Unable to find workspace note"); } - let inbox; + let inbox: BNote; if (!workspaceNote.isRoot()) { inbox = workspaceNote.searchNoteInSubtree("#workspaceInbox"); diff --git a/apps/server/vite.config.mts b/apps/server/vite.config.mts index 0f290b08e..ffd7b7427 100644 --- a/apps/server/vite.config.mts +++ b/apps/server/vite.config.mts @@ -11,7 +11,6 @@ export default defineConfig(() => ({ setupFiles: ["./spec/setup.ts"], environment: "node", include: ['{src,spec}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], coverage: { reportsDirectory: './test-output/vitest/coverage', provider: 'v8' as const, diff --git a/flake.lock b/flake.lock index 07c8f8b86..0aabefa81 100644 --- a/flake.lock +++ b/flake.lock @@ -18,6 +18,24 @@ "type": "github" } }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1748437600, @@ -34,10 +52,32 @@ "type": "github" } }, + "pnpm2nix": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1748901165, + "narHash": "sha256-SctrxW5rVrROBLfh8p4kXfbF7NbJQDkse/Penu4PlEs=", + "owner": "FliegendeWurst", + "repo": "pnpm2nix-nzbr", + "rev": "cda68d63418896a58542f3310c1c757ae92b1f22", + "type": "github" + }, + "original": { + "owner": "FliegendeWurst", + "repo": "pnpm2nix-nzbr", + "type": "github" + } + }, "root": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "pnpm2nix": "pnpm2nix" } }, "systems": { @@ -54,6 +94,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 4ffa16168..3b3736081 100644 --- a/flake.nix +++ b/flake.nix @@ -4,6 +4,10 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05"; flake-utils.url = "github:numtide/flake-utils"; + pnpm2nix = { + url = "github:FliegendeWurst/pnpm2nix-nzbr"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = @@ -11,69 +15,171 @@ self, nixpkgs, flake-utils, + pnpm2nix, }: flake-utils.lib.eachDefaultSystem ( system: let - packageJSON = builtins.fromJSON (builtins.readFile ./package.json); pkgs = import nixpkgs { inherit system; }; electron = pkgs.electron_35; + nodejs = pkgs.nodejs_22; + pnpm = pkgs.pnpm_10; inherit (pkgs) copyDesktopItems + darwin lib makeBinaryWrapper makeDesktopItem - nodejs - pnpm + moreutils + removeReferencesTo stdenv wrapGAppsHook3 xcodebuild - darwin ; - desktop = stdenv.mkDerivation (finalAttrs: { - pname = "triliumnext-desktop"; - version = packageJSON.version; - src = lib.cleanSource ./.; - nativeBuildInputs = - [ - pnpm.configHook - nodejs - nodejs.python - copyDesktopItems - makeBinaryWrapper - wrapGAppsHook3 - ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ - xcodebuild - darwin.cctools + fullCleanSourceFilter = + name: type: + (lib.cleanSourceFilter name type) + || ( + let + baseName = baseNameOf (toString name); + in + # No need to copy the flake. + # Don't copy local development instance of NX cache. + baseName == "flake.nix" || baseName == "flake.lock" || baseName == ".nx" + ); + fullCleanSource = + src: + lib.cleanSourceWith { + filter = fullCleanSourceFilter; + src = src; + }; + packageJson = builtins.fromJSON (builtins.readFile ./package.json); + + makeApp = + { + app, + buildTask, + mainProgram, + installCommands, + preBuildCommands ? "", + }: + pnpm2nix.packages.${system}.mkPnpmPackage rec { + pname = "triliumnext-${app}"; + version = packageJson.version + (lib.optionalString (self ? shortRev) "-${self.shortRev}"); + + src = fullCleanSource ./.; + packageJSON = ./package.json; + pnpmLockYaml = ./pnpm-lock.yaml; + + workspace = fullCleanSource ./.; + pnpmWorkspaceYaml = ./pnpm-workspace.yaml; + + inherit nodejs pnpm; + + extraNodeModuleSources = [ + rec { + name = "patches"; + value = ./patches; + } ]; - dontWrapGApps = true; + # remove pnpm version override + preConfigure = '' + cat package.json | grep -v 'packageManager' | sponge package.json + ''; - preBuild = lib.optionalString stdenv.hostPlatform.isLinux '' - patchelf --set-interpreter $(cat $NIX_CC/nix-support/dynamic-linker) \ - node_modules/.pnpm/sass-embedded-linux-x64@*/node_modules/sass-embedded-linux-x64/dart-sass/src/dart - ''; + postConfigure = + '' + chmod +x node_modules/.pnpm/electron@*/node_modules/electron/install.js + patchShebangs --build node_modules + '' + + lib.optionalString stdenv.hostPlatform.isLinux '' + patchelf --set-interpreter $(cat $NIX_CC/nix-support/dynamic-linker) \ + node_modules/.pnpm/sass-embedded-linux-x64@*/node_modules/sass-embedded-linux-x64/dart-sass/src/dart + ''; - buildPhase = '' - runHook preBuild + extraNativeBuildInputs = + [ + makeBinaryWrapper + moreutils # sponge + nodejs.python + removeReferencesTo + ] + ++ lib.optionals (app == "desktop") [ + copyDesktopItems + wrapGAppsHook3 + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + xcodebuild + darwin.cctools + ]; + dontWrapGApps = true; - # Disable NX interaction - export NX_TUI=false - export NX_DAEMON=false + env.ELECTRON_SKIP_BINARY_DOWNLOAD = "1"; - pnpm nx run desktop:build --outputStyle stream --verbose + preBuild = '' + ${preBuildCommands} + ''; - # Rebuild dependencies - export npm_config_nodedir=${electron.headers} - pnpm nx run desktop:rebuild-deps --outputStyle stream --verbose + scriptFull = "pnpm nx ${buildTask} --outputStyle stream --verbose"; - runHook postBuild - ''; + installPhase = '' + runHook preInstall - installPhase = '' - runHook preInstall + ${installCommands} + + runHook postInstall + ''; + + components = [ + "packages/ckeditor5" + "packages/ckeditor5-admonition" + "packages/ckeditor5-footnotes" + "packages/ckeditor5-keyboard-marker" + "packages/ckeditor5-math" + "packages/ckeditor5-mermaid" + "packages/codemirror" + "packages/commons" + "packages/express-partial-content" + "packages/highlightjs" + "packages/turndown-plugin-gfm" + + "apps/client" + "apps/db-compare" + "apps/desktop" + "apps/dump-db" + "apps/edit-docs" + "apps/server" + "apps/server-e2e" + ]; + + desktopItems = lib.optionals (app == "desktop") [ + (makeDesktopItem { + name = "TriliumNext Notes"; + exec = meta.mainProgram; + icon = "trilium"; + comment = meta.description; + desktopName = "TriliumNext Notes"; + categories = [ "Office" ]; + startupWMClass = "Trilium Notes Next"; + }) + ]; + + meta = { + description = "TriliumNext: ${app}"; + inherit mainProgram; + }; + }; + + desktop = makeApp { + app = "desktop"; + preBuildCommands = "export npm_config_nodedir=${electron.headers}"; + buildTask = "run desktop:rebuild-deps"; + mainProgram = "trilium"; + installCommands = '' + remove-references-to -t ${electron.headers} apps/desktop/dist/node_modules/better-sqlite3/build/config.gypi + remove-references-to -t ${nodejs.python} apps/desktop/dist/node_modules/better-sqlite3/build/config.gypi mkdir -p $out/{bin,share/icons/hicolor/512x512/apps,opt/trilium} cp --archive apps/desktop/dist/* $out/opt/trilium @@ -82,34 +188,37 @@ "''${gappsWrapperArgs[@]}" \ --set-default ELECTRON_IS_DEV 0 \ --add-flags $out/opt/trilium/main.cjs - - runHook postInstall ''; + }; - desktopItems = [ - (makeDesktopItem { - name = "TriliumNext Notes"; - exec = finalAttrs.meta.mainProgram; - icon = "trilium"; - comment = finalAttrs.meta.description; - desktopName = "TriliumNext Notes"; - categories = [ "Office" ]; - startupWMClass = "Trilium Notes Next"; - }) - ]; + server = makeApp { + app = "server"; + preBuildCommands = "pushd apps/server; pnpm rebuild; popd"; + buildTask = "--project=server build"; + mainProgram = "trilium-server"; + installCommands = '' + remove-references-to -t ${nodejs.python} apps/server/dist/node_modules/better-sqlite3/build/config.gypi + remove-references-to -t ${pnpm} apps/server/dist/node_modules/better-sqlite3/build/config.gypi - pnpmDeps = pnpm.fetchDeps { - inherit (finalAttrs) pname version src; - hash = "sha256-xC0u1h92wtthylOAw+IF9mpFi0c4xajJhUcA9pqzcAw="; - }; + pushd apps/server/dist + rm -rf node_modules/better-sqlite3/build/Release/obj \ + node_modules/better-sqlite3/build/Release/obj.target \ + node_modules/better-sqlite3/build/Release/sqlite3.a \ + node_modules/better-sqlite3/build/{Makefile,better_sqlite3.target.mk,test_extension.target.mk,binding.Makefile} \ + node_modules/better-sqlite3/deps/sqlite3 + popd - meta = { - description = "Free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases"; - mainProgram = "trilium"; - }; - }); + mkdir -p $out/{bin,opt/trilium-server} + cp --archive apps/server/dist/* $out/opt/trilium-server + makeWrapper ${lib.getExe nodejs} $out/bin/trilium-server \ + --add-flags $out/opt/trilium-server/main.cjs + ''; + }; in { + packages.desktop = desktop; + packages.server = server; + packages.default = desktop; } ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52f6e624b..d56ba38e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,7 +65,7 @@ importers: version: 21.1.2(@babel/traverse@7.27.0)(@playwright/test@1.52.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.28.0(jiti@2.4.2))(nx@21.1.2(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.8.3) '@nx/vite': specifier: 21.1.2 - version: 21.1.2(@babel/traverse@7.27.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))(nx@21.1.2(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.0) + version: 21.1.2(@babel/traverse@7.27.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))(nx@21.1.2(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.1) '@nx/web': specifier: 21.1.2 version: 21.1.2(@babel/traverse@7.27.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))(nx@21.1.2(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))) @@ -83,10 +83,10 @@ importers: version: 22.15.29 '@vitest/coverage-v8': specifier: ^3.0.5 - version: 3.2.0(@vitest/browser@3.2.0)(vitest@3.2.0) + version: 3.2.1(vitest@3.2.1) '@vitest/ui': specifier: ^3.0.0 - version: 3.2.0(vitest@3.2.0) + version: 3.2.1(vitest@3.2.1) chalk: specifier: 5.4.1 version: 5.4.1 @@ -146,7 +146,7 @@ importers: version: 4.5.4(@types/node@22.15.29)(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0)) vitest: specifier: ^3.0.0 - version: 3.2.0(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/browser@3.2.0)(@vitest/ui@3.2.0)(happy-dom@17.6.1)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.1)(happy-dom@17.6.1)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) apps/client: dependencies: @@ -699,8 +699,8 @@ importers: specifier: 3.0.1 version: 3.0.1 multer: - specifier: 2.0.0 - version: 2.0.0 + specifier: 2.0.1 + version: 2.0.1 normalize-strings: specifier: 1.1.1 version: 1.1.1 @@ -708,8 +708,8 @@ importers: specifier: 0.5.16 version: 0.5.16 openai: - specifier: 5.0.2 - version: 5.0.2(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.24.4) + specifier: 5.1.0 + version: 5.1.0(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.24.4) rand-token: specifier: 1.0.1 version: 1.0.1 @@ -1426,8 +1426,8 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@7.27.4': - resolution: {integrity: sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==} + '@babel/parser@7.27.5': + resolution: {integrity: sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==} engines: {node: '>=6.0.0'} hasBin: true @@ -4864,11 +4864,11 @@ packages: peerDependencies: vitest: 3.2.0 - '@vitest/coverage-v8@3.2.0': - resolution: {integrity: sha512-HjgvaokAiHxRMI5ioXl4WmgAi4zQtKtnltOOlmpzUqApdcTTZrZJAastbbRGydtiqwtYLFaIb6Jpo3PzowZ0cg==} + '@vitest/coverage-v8@3.2.1': + resolution: {integrity: sha512-6dy0uF/0BE3jpUW9bFzg0V2S4F7XVaZHL/7qma1XANvHPQGoJuc3wtx911zSoAgUnpfvcLVK1vancNJ95d+uxQ==} peerDependencies: - '@vitest/browser': 3.2.0 - vitest: 3.2.0 + '@vitest/browser': 3.2.1 + vitest: 3.2.1 peerDependenciesMeta: '@vitest/browser': optional: true @@ -4876,6 +4876,9 @@ packages: '@vitest/expect@3.2.0': resolution: {integrity: sha512-0v4YVbhDKX3SKoy0PHWXpKhj44w+3zZkIoVES9Ex2pq+u6+Bijijbi2ua5kE+h3qT6LBWFTNZSCOEU37H8Y5sA==} + '@vitest/expect@3.2.1': + resolution: {integrity: sha512-FqS/BnDOzV6+IpxrTg5GQRyLOCtcJqkwMwcS8qGCI2IyRVDwPAtutztaf1CjtPHlZlWtl1yUPCd7HM0cNiDOYw==} + '@vitest/mocker@3.2.0': resolution: {integrity: sha512-HFcW0lAMx3eN9vQqis63H0Pscv0QcVMo1Kv8BNysZbxcmHu3ZUYv59DS6BGYiGQ8F5lUkmsfMMlPm4DJFJdf/A==} peerDependencies: @@ -4887,26 +4890,57 @@ packages: vite: optional: true + '@vitest/mocker@3.2.1': + resolution: {integrity: sha512-OXxMJnx1lkB+Vl65Re5BrsZEHc90s5NMjD23ZQ9NlU7f7nZiETGoX4NeKZSmsKjseuMq2uOYXdLOeoM0pJU+qw==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@3.2.0': resolution: {integrity: sha512-gUUhaUmPBHFkrqnOokmfMGRBMHhgpICud9nrz/xpNV3/4OXCn35oG+Pl8rYYsKaTNd/FAIrqRHnwpDpmYxCYZw==} + '@vitest/pretty-format@3.2.1': + resolution: {integrity: sha512-xBh1X2GPlOGBupp6E1RcUQWIxw0w/hRLd3XyBS6H+dMdKTAqHDNsIR2AnJwPA3yYe9DFy3VUKTe3VRTrAiQ01g==} + '@vitest/runner@3.2.0': resolution: {integrity: sha512-bXdmnHxuB7fXJdh+8vvnlwi/m1zvu+I06i1dICVcDQFhyV4iKw2RExC/acavtDn93m/dRuawUObKsrNE1gJacA==} + '@vitest/runner@3.2.1': + resolution: {integrity: sha512-kygXhNTu/wkMYbwYpS3z/9tBe0O8qpdBuC3dD/AW9sWa0LE/DAZEjnHtWA9sIad7lpD4nFW1yQ+zN7mEKNH3yA==} + '@vitest/snapshot@3.2.0': resolution: {integrity: sha512-z7P/EneBRMe7hdvWhcHoXjhA6at0Q4ipcoZo6SqgxLyQQ8KSMMCmvw1cSt7FHib3ozt0wnRHc37ivuUMbxzG/A==} + '@vitest/snapshot@3.2.1': + resolution: {integrity: sha512-5xko/ZpW2Yc65NVK9Gpfg2y4BFvcF+At7yRT5AHUpTg9JvZ4xZoyuRY4ASlmNcBZjMslV08VRLDrBOmUe2YX3g==} + '@vitest/spy@3.2.0': resolution: {integrity: sha512-s3+TkCNUIEOX99S0JwNDfsHRaZDDZZR/n8F0mop0PmsEbQGKZikCGpTGZ6JRiHuONKew3Fb5//EPwCP+pUX9cw==} + '@vitest/spy@3.2.1': + resolution: {integrity: sha512-Nbfib34Z2rfcJGSetMxjDCznn4pCYPZOtQYox2kzebIJcgH75yheIKd5QYSFmR8DIZf2M8fwOm66qSDIfRFFfQ==} + '@vitest/ui@3.2.0': resolution: {integrity: sha512-cYFZZSl1usgzsHoGF66GHfYXlEwc06ggapS1TaSLMKCzhTPWBPI9b/t1RvKIsLSjdKUakpSPf33jQMvRjMvvlQ==} peerDependencies: vitest: 3.2.0 + '@vitest/ui@3.2.1': + resolution: {integrity: sha512-xT93aOcPn2wn8vvw4T6rZAK9WjGEHdYrEjN3OJ1zcDpl2UInxvcD9fYI10nmPAERNEK6jUVcSCIPAIfNuaRX6Q==} + peerDependencies: + vitest: 3.2.1 + '@vitest/utils@3.2.0': resolution: {integrity: sha512-gXXOe7Fj6toCsZKVQouTRLJftJwmvbhH5lKOBR6rlP950zUq9AitTUjnFoXS/CqjBC2aoejAztLPzzuva++XBw==} + '@vitest/utils@3.2.1': + resolution: {integrity: sha512-KkHlGhePEKZSub5ViknBcN5KEF+u7dSUr9NW8QsVICusUojrgrOnnY3DEWWO877ax2Pyopuk2qHmt+gkNKnBVw==} + '@volar/language-core@2.4.13': resolution: {integrity: sha512-MnQJ7eKchJx5Oz+YdbqyFUk8BN6jasdJv31n/7r6/WwlOOv7qzvot6B66887l2ST3bUW4Mewml54euzpJWA6bg==} @@ -5951,6 +5985,10 @@ packages: resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} engines: {'0': node >= 0.8} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -9262,8 +9300,8 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - multer@2.0.0: - resolution: {integrity: sha512-bS8rPZurbAuHGAnApbM9d4h1wSoYqrOqkE+6a64KLMK9yWU7gJXBDDVklKQ3TPi9DRb85cRs6yXaC0+cjxRtRg==} + multer@2.0.1: + resolution: {integrity: sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==} engines: {node: '>= 10.16.0'} multicast-dns@7.2.5: @@ -9544,8 +9582,8 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} - openai@5.0.2: - resolution: {integrity: sha512-NN7LAAImgBmd4RIe6WyRpLmwCbn+HQ1iaXeIG7K9DM3Auy/G2waKFhrDfRgaEeY0UUPnm6nohaCsqcS+zO8+2g==} + openai@5.1.0: + resolution: {integrity: sha512-YQBgPJykHrDOlngB/8QpOsFNg36yofBatpeDWg1zejl9R59/ELuN7AMPSU95ZIdChbKc/o5vg1UnBJ1OEB0IJA==} hasBin: true peerDependencies: ws: ^8.18.0 @@ -12386,6 +12424,11 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-node@3.2.1: + resolution: {integrity: sha512-V4EyKQPxquurNJPtQJRZo8hKOoKNBRIhxcDbQFPFig0JdoWcUhwRgK8yoCXXrfYVPKS6XwirGHPszLnR8FbjCA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-dts@4.5.4: resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: @@ -12475,6 +12518,34 @@ packages: jsdom: optional: true + vitest@3.2.1: + resolution: {integrity: sha512-VZ40MBnlE1/V5uTgdqY3DmjUgZtIzsYq758JGlyQrv5syIsaYcabkfPkEuWML49Ph0D/SoqpVFd0dyVTr551oA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.1 + '@vitest/ui': 3.2.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-jsonrpc@8.2.0: resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} engines: {node: '>=14.0.0'} @@ -13128,7 +13199,7 @@ snapshots: dependencies: '@babel/types': 7.27.1 - '@babel/parser@7.27.4': + '@babel/parser@7.27.5': dependencies: '@babel/types': 7.27.3 @@ -16377,7 +16448,7 @@ snapshots: - typescript - verdaccio - '@nx/vite@21.1.2(@babel/traverse@7.27.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))(nx@21.1.2(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.0)': + '@nx/vite@21.1.2(@babel/traverse@7.27.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))(nx@21.1.2(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17)))(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.1)': dependencies: '@nx/devkit': 21.1.2(nx@21.1.2(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))) '@nx/js': 21.1.2(@babel/traverse@7.27.0)(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))(nx@21.1.2(@swc-node/register@1.10.10(@swc/core@1.11.29(@swc/helpers@0.5.17))(@swc/types@0.1.21)(typescript@5.8.3))(@swc/core@1.11.29(@swc/helpers@0.5.17))) @@ -16389,7 +16460,7 @@ snapshots: semver: 7.7.2 tsconfig-paths: 4.2.0 vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) - vitest: 3.2.0(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/browser@3.2.0)(@vitest/ui@3.2.0)(happy-dom@17.6.1)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.1)(happy-dom@17.6.1)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -17241,7 +17312,7 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.27.4 + '@babel/parser': 7.27.5 '@babel/types': 7.27.3 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 @@ -17253,7 +17324,7 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.27.4 + '@babel/parser': 7.27.5 '@babel/types': 7.27.3 '@types/babel__traverse@7.20.7': @@ -17921,7 +17992,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.0(@vitest/browser@3.2.0)(vitest@3.2.0)': + '@vitest/coverage-v8@3.2.1(vitest@3.2.1)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -17936,9 +18007,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.0(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/browser@3.2.0)(@vitest/ui@3.2.0)(happy-dom@17.6.1)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) - optionalDependencies: - '@vitest/browser': 3.2.0(bufferutil@4.0.9)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(playwright@1.52.0)(utf-8-validate@6.0.5)(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))(vitest@3.2.0)(webdriverio@9.15.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)) + vitest: 3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.1)(happy-dom@17.6.1)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -17950,6 +18019,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/expect@3.2.1': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.1 + '@vitest/utils': 3.2.1 + chai: 5.2.0 + tinyrainbow: 2.0.0 + '@vitest/mocker@3.2.0(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.0 @@ -17959,25 +18036,53 @@ snapshots: msw: 2.7.5(@types/node@22.15.29)(typescript@5.8.3) vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) + '@vitest/mocker@3.2.1(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0))': + dependencies: + '@vitest/spy': 3.2.1 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + msw: 2.7.5(@types/node@22.15.29)(typescript@5.8.3) + vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) + '@vitest/pretty-format@3.2.0': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@3.2.1': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@3.2.0': dependencies: '@vitest/utils': 3.2.0 pathe: 2.0.3 + '@vitest/runner@3.2.1': + dependencies: + '@vitest/utils': 3.2.1 + pathe: 2.0.3 + '@vitest/snapshot@3.2.0': dependencies: '@vitest/pretty-format': 3.2.0 magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/snapshot@3.2.1': + dependencies: + '@vitest/pretty-format': 3.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + '@vitest/spy@3.2.0': dependencies: tinyspy: 4.0.3 + '@vitest/spy@3.2.1': + dependencies: + tinyspy: 4.0.3 + '@vitest/ui@3.2.0(vitest@3.2.0)': dependencies: '@vitest/utils': 3.2.0 @@ -17988,6 +18093,18 @@ snapshots: tinyglobby: 0.2.14 tinyrainbow: 2.0.0 vitest: 3.2.0(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/browser@3.2.0)(@vitest/ui@3.2.0)(happy-dom@17.6.1)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) + optional: true + + '@vitest/ui@3.2.1(vitest@3.2.1)': + dependencies: + '@vitest/utils': 3.2.1 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.1 + tinyglobby: 0.2.14 + tinyrainbow: 2.0.0 + vitest: 3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.1)(happy-dom@17.6.1)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.0': dependencies: @@ -17995,6 +18112,12 @@ snapshots: loupe: 3.1.3 tinyrainbow: 2.0.0 + '@vitest/utils@3.2.1': + dependencies: + '@vitest/pretty-format': 3.2.1 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + '@volar/language-core@2.4.13': dependencies: '@volar/source-map': 2.4.13 @@ -18009,7 +18132,7 @@ snapshots: '@vue/compiler-core@3.5.14': dependencies: - '@babel/parser': 7.27.4 + '@babel/parser': 7.27.5 '@vue/shared': 3.5.14 entities: 4.5.0 estree-walker: 2.0.2 @@ -19290,6 +19413,13 @@ snapshots: readable-stream: 2.3.8 typedarray: 0.0.6 + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + confbox@0.1.8: {} confbox@0.2.2: {} @@ -22034,7 +22164,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.26.10 - '@babel/parser': 7.27.4 + '@babel/parser': 7.27.5 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 6.3.1 @@ -22839,7 +22969,7 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.27.4 + '@babel/parser': 7.27.5 '@babel/types': 7.27.3 source-map-js: 1.2.1 @@ -23428,11 +23558,11 @@ snapshots: muggle-string@0.4.1: {} - multer@2.0.0: + multer@2.0.1: dependencies: append-field: 1.0.0 busboy: 1.6.0 - concat-stream: 1.6.2 + concat-stream: 2.0.0 mkdirp: 0.5.6 object-assign: 4.1.1 type-is: 1.6.18 @@ -23761,7 +23891,7 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - openai@5.0.2(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.24.4): + openai@5.1.0(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5))(zod@3.24.4): optionalDependencies: ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5) zod: 3.24.4 @@ -26868,6 +26998,27 @@ snapshots: - tsx - yaml + vite-node@3.2.1(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1(supports-color@6.0.0) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-dts@4.5.4(@types/node@22.15.29)(rollup@4.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0)): dependencies: '@microsoft/api-extractor': 7.52.8(@types/node@22.15.29) @@ -26969,6 +27120,51 @@ snapshots: - tsx - yaml + vitest@3.2.1(@types/debug@4.1.12)(@types/node@22.15.29)(@vitest/ui@3.2.1)(happy-dom@17.6.1)(jiti@2.4.2)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.1.3)(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.1 + '@vitest/mocker': 3.2.1(msw@2.7.5(@types/node@22.15.29)(typescript@5.8.3))(vite@6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.1 + '@vitest/runner': 3.2.1 + '@vitest/snapshot': 3.2.1 + '@vitest/spy': 3.2.1 + '@vitest/utils': 3.2.1 + chai: 5.2.0 + debug: 4.4.1(supports-color@6.0.0) + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.0 + tinyrainbow: 2.0.0 + vite: 6.3.5(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.2.1(@types/node@22.15.29)(jiti@2.4.2)(less@4.1.3)(sass-embedded@1.87.0)(sass@1.87.0)(stylus@0.64.0)(sugarss@4.0.1(postcss@8.5.3))(terser@5.39.0)(tsx@4.19.4)(yaml@2.8.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 22.15.29 + '@vitest/ui': 3.2.1(vitest@3.2.1) + happy-dom: 17.6.1 + jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-jsonrpc@8.2.0: {} vscode-languageserver-protocol@3.17.5: