Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.

Commit 221cfe2

Browse files
authored
Merge pull request #1935 from TriliumNext/feature/fix_transaction_issues
Fix transaction issues
2 parents 35c9f10 + a333f8a commit 221cfe2

File tree

3 files changed

+263
-242
lines changed

3 files changed

+263
-242
lines changed

apps/server/src/routes/api/import.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ async function importNotesToBranch(req: Request) {
101101
return note.getPojo();
102102
}
103103

104-
async function importAttachmentsToNote(req: Request) {
104+
function importAttachmentsToNote(req: Request) {
105105
const { parentNoteId } = req.params;
106106
const { taskId, last } = req.body;
107107

@@ -121,7 +121,7 @@ async function importAttachmentsToNote(req: Request) {
121121
// unlike in note import, we let the events run, because a huge number of attachments is not likely
122122

123123
try {
124-
await singleImportService.importAttachment(taskContext, file, parentNote);
124+
singleImportService.importAttachment(taskContext, file, parentNote);
125125
} catch (e: unknown) {
126126
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
127127

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import express from "express";
2+
import multer from "multer";
3+
import log from "../services/log.js";
4+
import cls from "../services/cls.js";
5+
import sql from "../services/sql.js";
6+
import entityChangesService from "../services/entity_changes.js";
7+
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
8+
import NotFoundError from "../errors/not_found_error.js";
9+
import ValidationError from "../errors/validation_error.js";
10+
import auth from "../services/auth.js";
11+
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
12+
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
13+
14+
const MAX_ALLOWED_FILE_SIZE_MB = 250;
15+
export const router = express.Router();
16+
17+
// TODO: Deduplicate with etapi_utils.ts afterwards.
18+
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
19+
20+
export type ApiResultHandler = (req: express.Request, res: express.Response, result: unknown) => number;
21+
22+
type NotAPromise<T> = T & { then?: void };
23+
export type ApiRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => unknown;
24+
export type SyncRouteRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => NotAPromise<object> | number | string | void | null;
25+
26+
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
27+
function convertEntitiesToPojo(result: unknown) {
28+
if (result instanceof AbstractBeccaEntity) {
29+
result = result.getPojo();
30+
} else if (Array.isArray(result)) {
31+
for (const idx in result) {
32+
if (result[idx] instanceof AbstractBeccaEntity) {
33+
result[idx] = result[idx].getPojo();
34+
}
35+
}
36+
} else if (result && typeof result === "object") {
37+
if ("note" in result && result.note instanceof AbstractBeccaEntity) {
38+
result.note = result.note.getPojo();
39+
}
40+
41+
if ("branch" in result && result.branch instanceof AbstractBeccaEntity) {
42+
result.branch = result.branch.getPojo();
43+
}
44+
}
45+
46+
if (result && typeof result === "object" && "executionResult" in result) {
47+
// from runOnBackend()
48+
result.executionResult = convertEntitiesToPojo(result.executionResult);
49+
}
50+
51+
return result;
52+
}
53+
54+
export function apiResultHandler(req: express.Request, res: express.Response, result: unknown) {
55+
res.setHeader("trilium-max-entity-change-id", entityChangesService.getMaxEntityChangeId());
56+
57+
result = convertEntitiesToPojo(result);
58+
59+
// if it's an array and the first element is integer, then we consider this to be [statusCode, response] format
60+
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
61+
const [statusCode, response] = result;
62+
63+
if (statusCode !== 200 && statusCode !== 201 && statusCode !== 204) {
64+
log.info(`${req.method} ${req.originalUrl} returned ${statusCode} with response ${JSON.stringify(response)}`);
65+
}
66+
67+
return send(res, statusCode, response);
68+
} else if (result === undefined) {
69+
return send(res, 204, "");
70+
} else {
71+
return send(res, 200, result);
72+
}
73+
}
74+
75+
function send(res: express.Response, statusCode: number, response: unknown) {
76+
if (typeof response === "string") {
77+
if (statusCode >= 400) {
78+
res.setHeader("Content-Type", "text/plain");
79+
}
80+
81+
res.status(statusCode).send(response);
82+
83+
return response.length;
84+
} else {
85+
const json = JSON.stringify(response);
86+
87+
res.setHeader("Content-Type", "application/json");
88+
res.status(statusCode).send(json);
89+
90+
return json.length;
91+
}
92+
}
93+
94+
export function apiRoute(method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler) {
95+
route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
96+
}
97+
98+
export function asyncApiRoute(method: HttpMethod, path: string, routeHandler: ApiRequestHandler) {
99+
asyncRoute(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
100+
}
101+
102+
export function route(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: SyncRouteRequestHandler, resultHandler: ApiResultHandler | null = null) {
103+
internalRoute(method, path, middleware, routeHandler, resultHandler, true);
104+
}
105+
106+
export function asyncRoute(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null) {
107+
internalRoute(method, path, middleware, routeHandler, resultHandler, false);
108+
}
109+
110+
function internalRoute(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null, transactional: boolean) {
111+
router[method](path, ...(middleware as express.Handler[]), (req: express.Request, res: express.Response, next: express.NextFunction) => {
112+
const start = Date.now();
113+
114+
try {
115+
cls.namespace.bindEmitter(req);
116+
cls.namespace.bindEmitter(res);
117+
118+
const result = cls.init(() => {
119+
cls.set("componentId", req.headers["trilium-component-id"]);
120+
cls.set("localNowDateTime", req.headers["trilium-local-now-datetime"]);
121+
cls.set("hoistedNoteId", req.headers["trilium-hoisted-note-id"] || "root");
122+
123+
const cb = () => routeHandler(req, res, next);
124+
125+
return transactional ? sql.transactional(cb) : cb();
126+
});
127+
128+
if (!resultHandler) {
129+
return;
130+
}
131+
132+
if (result?.then) {
133+
// promise
134+
result.then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start)).catch((e: unknown) => handleException(e, method, path, res));
135+
} else {
136+
handleResponse(resultHandler, req, res, result, start);
137+
}
138+
} catch (e) {
139+
handleException(e, method, path, res);
140+
}
141+
});
142+
}
143+
144+
function handleResponse(resultHandler: ApiResultHandler, req: express.Request, res: express.Response, result: unknown, start: number) {
145+
// Skip result handling if the response has already been handled
146+
if ((res as any).triliumResponseHandled) {
147+
// Just log the request without additional processing
148+
log.request(req, res, Date.now() - start, 0);
149+
return;
150+
}
151+
152+
const responseLength = resultHandler(req, res, result);
153+
log.request(req, res, Date.now() - start, responseLength);
154+
}
155+
156+
function handleException(e: unknown | Error, method: HttpMethod, path: string, res: express.Response) {
157+
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
158+
159+
log.error(`${method} ${path} threw exception: '${errMessage}', stack: ${errStack}`);
160+
161+
const resStatusCode = (e instanceof ValidationError || e instanceof NotFoundError) ? e.statusCode : 500;
162+
163+
res.status(resStatusCode).json({
164+
message: errMessage
165+
});
166+
167+
}
168+
169+
export function createUploadMiddleware() {
170+
const multerOptions: multer.Options = {
171+
fileFilter: (req: express.Request, file, cb) => {
172+
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
173+
// See https://github.com/expressjs/multer/pull/1102.
174+
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
175+
cb(null, true);
176+
}
177+
};
178+
179+
if (!process.env.TRILIUM_NO_UPLOAD_LIMIT) {
180+
multerOptions.limits = {
181+
fileSize: MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
182+
};
183+
}
184+
185+
return multer(multerOptions).single("upload");
186+
}
187+
188+
const uploadMiddleware = createUploadMiddleware();
189+
190+
export const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
191+
uploadMiddleware(req, res, function (err) {
192+
if (err?.code === "LIMIT_FILE_SIZE") {
193+
res.setHeader("Content-Type", "text/plain").status(400).send(`Cannot upload file because it excceeded max allowed file size of ${MAX_ALLOWED_FILE_SIZE_MB} MiB`);
194+
} else {
195+
next();
196+
}
197+
});
198+
};

0 commit comments

Comments
 (0)