mirror of
https://github.com/zadam/trilium.git
synced 2025-11-17 18:50:41 +01:00
chore(nx): move all monorepo-style in subfolder for processing
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
import type { Router } from "express";
|
||||
import appInfo from "../services/app_info.js";
|
||||
import eu from "./etapi_utils.js";
|
||||
|
||||
function register(router: Router) {
|
||||
eu.route(router, "get", "/etapi/app-info", (req, res, next) => {
|
||||
res.status(200).json(appInfo);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,108 +0,0 @@
|
||||
import becca from "../becca/becca.js";
|
||||
import eu from "./etapi_utils.js";
|
||||
import mappers from "./mappers.js";
|
||||
import v from "./validators.js";
|
||||
import utils from "../services/utils.js";
|
||||
import type { Router } from "express";
|
||||
import type { AttachmentRow } from "@triliumnext/commons";
|
||||
import type { ValidatorMap } from "./etapi-interface.js";
|
||||
|
||||
function register(router: Router) {
|
||||
const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT: ValidatorMap = {
|
||||
ownerId: [v.notNull, v.isNoteId],
|
||||
role: [v.notNull, v.isString],
|
||||
mime: [v.notNull, v.isString],
|
||||
title: [v.notNull, v.isString],
|
||||
position: [v.notNull, v.isInteger],
|
||||
content: [v.isString]
|
||||
};
|
||||
|
||||
eu.route(router, "post", "/etapi/attachments", (req, res, next) => {
|
||||
const _params: Partial<AttachmentRow> = {};
|
||||
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT);
|
||||
const params = _params as AttachmentRow;
|
||||
|
||||
try {
|
||||
if (!params.ownerId) {
|
||||
throw new Error("Missing owner ID.");
|
||||
}
|
||||
const note = becca.getNoteOrThrow(params.ownerId);
|
||||
const attachment = note.saveAttachment(params);
|
||||
|
||||
res.status(201).json(mappers.mapAttachmentToPojo(attachment));
|
||||
} catch (e: any) {
|
||||
throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/attachments/:attachmentId", (req, res, next) => {
|
||||
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
|
||||
|
||||
res.json(mappers.mapAttachmentToPojo(attachment));
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
||||
role: [v.notNull, v.isString],
|
||||
mime: [v.notNull, v.isString],
|
||||
title: [v.notNull, v.isString],
|
||||
position: [v.notNull, v.isInteger]
|
||||
};
|
||||
|
||||
eu.route(router, "patch", "/etapi/attachments/:attachmentId", (req, res, next) => {
|
||||
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
|
||||
|
||||
if (attachment.isProtected) {
|
||||
throw new eu.EtapiError(400, "ATTACHMENT_IS_PROTECTED", `Attachment '${req.params.attachmentId}' is protected and cannot be modified through ETAPI.`);
|
||||
}
|
||||
|
||||
eu.validateAndPatch(attachment, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
||||
attachment.save();
|
||||
|
||||
res.json(mappers.mapAttachmentToPojo(attachment));
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
|
||||
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
|
||||
|
||||
if (attachment.isProtected) {
|
||||
throw new eu.EtapiError(400, "ATTACHMENT_IS_PROTECTED", `Attachment '${req.params.attachmentId}' is protected and content cannot be read through ETAPI.`);
|
||||
}
|
||||
|
||||
const filename = utils.formatDownloadTitle(attachment.title, attachment.role, attachment.mime);
|
||||
|
||||
res.setHeader("Content-Disposition", utils.getContentDisposition(filename));
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Content-Type", attachment.mime);
|
||||
|
||||
res.send(attachment.getContent());
|
||||
});
|
||||
|
||||
eu.route(router, "put", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
|
||||
const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
|
||||
|
||||
if (attachment.isProtected) {
|
||||
throw new eu.EtapiError(400, "ATTACHMENT_IS_PROTECTED", `Attachment '${req.params.attachmentId}' is protected and cannot be modified through ETAPI.`);
|
||||
}
|
||||
|
||||
attachment.setContent(req.body);
|
||||
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
eu.route(router, "delete", "/etapi/attachments/:attachmentId", (req, res, next) => {
|
||||
const attachment = becca.getAttachment(req.params.attachmentId);
|
||||
|
||||
if (!attachment) {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
attachment.markAsDeleted();
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
import becca from "../becca/becca.js";
|
||||
import eu from "./etapi_utils.js";
|
||||
import mappers from "./mappers.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import v from "./validators.js";
|
||||
import type { Router } from "express";
|
||||
import type { AttributeRow } from "@triliumnext/commons";
|
||||
import type { ValidatorMap } from "./etapi-interface.js";
|
||||
|
||||
function register(router: Router) {
|
||||
eu.route(router, "get", "/etapi/attributes/:attributeId", (req, res, next) => {
|
||||
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
|
||||
|
||||
res.json(mappers.mapAttributeToPojo(attribute));
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE: ValidatorMap = {
|
||||
attributeId: [v.mandatory, v.notNull, v.isValidEntityId],
|
||||
noteId: [v.mandatory, v.notNull, v.isNoteId],
|
||||
type: [v.mandatory, v.notNull, v.isAttributeType],
|
||||
name: [v.mandatory, v.notNull, v.isString],
|
||||
value: [v.notNull, v.isString],
|
||||
isInheritable: [v.notNull, v.isBoolean],
|
||||
position: [v.notNull, v.isInteger]
|
||||
};
|
||||
|
||||
eu.route(router, "post", "/etapi/attributes", (req, res, next) => {
|
||||
if (req.body.type === "relation") {
|
||||
eu.getAndCheckNote(req.body.value);
|
||||
}
|
||||
|
||||
const _params = {};
|
||||
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE);
|
||||
const params: AttributeRow = _params as AttributeRow;
|
||||
|
||||
try {
|
||||
const attr = attributeService.createAttribute(params);
|
||||
|
||||
res.status(201).json(mappers.mapAttributeToPojo(attr));
|
||||
} catch (e: any) {
|
||||
throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH_LABEL = {
|
||||
value: [v.notNull, v.isString],
|
||||
position: [v.notNull, v.isInteger]
|
||||
};
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH_RELATION = {
|
||||
position: [v.notNull, v.isInteger]
|
||||
};
|
||||
|
||||
eu.route(router, "patch", "/etapi/attributes/:attributeId", (req, res, next) => {
|
||||
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
|
||||
|
||||
if (attribute.type === "label") {
|
||||
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_LABEL);
|
||||
} else if (attribute.type === "relation") {
|
||||
eu.getAndCheckNote(req.body.value);
|
||||
|
||||
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_RELATION);
|
||||
}
|
||||
|
||||
attribute.save();
|
||||
|
||||
res.json(mappers.mapAttributeToPojo(attribute));
|
||||
});
|
||||
|
||||
eu.route(router, "delete", "/etapi/attributes/:attributeId", (req, res, next) => {
|
||||
const attribute = becca.getAttribute(req.params.attributeId);
|
||||
|
||||
if (!attribute) {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
attribute.markAsDeleted();
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
import becca from "../becca/becca.js";
|
||||
import eu from "./etapi_utils.js";
|
||||
import passwordEncryptionService from "../services/encryption/password_encryption.js";
|
||||
import etapiTokenService from "../services/etapi_tokens.js";
|
||||
import type { RequestHandler, Router } from "express";
|
||||
|
||||
function register(router: Router, loginMiddleware: RequestHandler[]) {
|
||||
eu.NOT_AUTHENTICATED_ROUTE(router, "post", "/etapi/auth/login", loginMiddleware, (req, res, next) => {
|
||||
const { password, tokenName } = req.body;
|
||||
|
||||
if (!passwordEncryptionService.verifyPassword(password)) {
|
||||
throw new eu.EtapiError(401, "WRONG_PASSWORD", "Wrong password.");
|
||||
}
|
||||
|
||||
const { authToken } = etapiTokenService.createToken(tokenName || "ETAPI login");
|
||||
|
||||
res.status(201).json({
|
||||
authToken
|
||||
});
|
||||
});
|
||||
|
||||
eu.route(router, "post", "/etapi/auth/logout", (req, res, next) => {
|
||||
const parsed = etapiTokenService.parseAuthToken(req.headers.authorization);
|
||||
|
||||
if (!parsed || !parsed.etapiTokenId) {
|
||||
throw new eu.EtapiError(400, eu.GENERIC_CODE, "Cannot logout this token.");
|
||||
}
|
||||
|
||||
const etapiToken = becca.getEtapiToken(parsed.etapiTokenId);
|
||||
|
||||
if (!etapiToken) {
|
||||
// shouldn't happen since this already passed auth validation
|
||||
throw new Error(`Cannot find the token '${parsed.etapiTokenId}'.`);
|
||||
}
|
||||
|
||||
etapiToken.markAsDeletedSimple();
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { Router } from "express";
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import type { Router } from "express";
|
||||
|
||||
import becca from "../becca/becca.js";
|
||||
import eu from "./etapi_utils.js";
|
||||
import mappers from "./mappers.js";
|
||||
import BBranch from "../becca/entities/bbranch.js";
|
||||
import entityChangesService from "../services/entity_changes.js";
|
||||
import v from "./validators.js";
|
||||
import type { BranchRow } from "@triliumnext/commons";
|
||||
|
||||
function register(router: Router) {
|
||||
eu.route(router, "get", "/etapi/branches/:branchId", (req, res, next) => {
|
||||
const branch = eu.getAndCheckBranch(req.params.branchId);
|
||||
|
||||
res.json(mappers.mapBranchToPojo(branch));
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_CREATE_BRANCH = {
|
||||
noteId: [v.mandatory, v.notNull, v.isNoteId],
|
||||
parentNoteId: [v.mandatory, v.notNull, v.isNoteId],
|
||||
notePosition: [v.notNull, v.isInteger],
|
||||
prefix: [v.isString],
|
||||
isExpanded: [v.notNull, v.isBoolean]
|
||||
};
|
||||
|
||||
eu.route(router, "post", "/etapi/branches", (req, res, next) => {
|
||||
const _params = {};
|
||||
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_BRANCH);
|
||||
const params: BranchRow = _params as BranchRow;
|
||||
|
||||
const existing = becca.getBranchFromChildAndParent(params.noteId, params.parentNoteId);
|
||||
|
||||
if (existing) {
|
||||
existing.notePosition = params.notePosition as number;
|
||||
existing.prefix = params.prefix as string;
|
||||
existing.isExpanded = params.isExpanded as boolean;
|
||||
existing.save();
|
||||
|
||||
return res.status(200).json(mappers.mapBranchToPojo(existing));
|
||||
} else {
|
||||
try {
|
||||
const branch = new BBranch(params).save();
|
||||
|
||||
res.status(201).json(mappers.mapBranchToPojo(branch));
|
||||
} catch (e: any) {
|
||||
throw new eu.EtapiError(400, eu.GENERIC_CODE, e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
||||
notePosition: [v.notNull, v.isInteger],
|
||||
prefix: [v.isString],
|
||||
isExpanded: [v.notNull, v.isBoolean]
|
||||
};
|
||||
|
||||
eu.route(router, "patch", "/etapi/branches/:branchId", (req, res, next) => {
|
||||
const branch = eu.getAndCheckBranch(req.params.branchId);
|
||||
|
||||
eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
||||
branch.save();
|
||||
|
||||
res.json(mappers.mapBranchToPojo(branch));
|
||||
});
|
||||
|
||||
eu.route(router, "delete", "/etapi/branches/:branchId", (req, res, next) => {
|
||||
const branch = becca.getBranch(req.params.branchId);
|
||||
|
||||
if (!branch) {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
branch.deleteBranch();
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
eu.route(router, "post", "/etapi/refresh-note-ordering/:parentNoteId", (req, res, next) => {
|
||||
eu.getAndCheckNote(req.params.parentNoteId);
|
||||
|
||||
entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi");
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export type ValidatorFunc = (obj: unknown) => string | undefined;
|
||||
|
||||
export type ValidatorMap = Record<string, ValidatorFunc[]>;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,156 +0,0 @@
|
||||
import cls from "../services/cls.js";
|
||||
import sql from "../services/sql.js";
|
||||
import log from "../services/log.js";
|
||||
import becca from "../becca/becca.js";
|
||||
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/routes.js";
|
||||
const GENERIC_CODE = "GENERIC";
|
||||
|
||||
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
|
||||
|
||||
const noAuthentication = config.General && config.General.noAuthentication === true;
|
||||
|
||||
class EtapiError extends Error {
|
||||
statusCode: number;
|
||||
code: string;
|
||||
|
||||
constructor(statusCode: number, code: string, message: string) {
|
||||
super(message);
|
||||
|
||||
// Set the prototype explicitly.
|
||||
Object.setPrototypeOf(this, EtapiError.prototype);
|
||||
|
||||
this.statusCode = statusCode;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function sendError(res: Response, statusCode: number, code: string, message: string) {
|
||||
return res
|
||||
.set("Content-Type", "application/json")
|
||||
.status(statusCode)
|
||||
.send(
|
||||
JSON.stringify({
|
||||
status: statusCode,
|
||||
code: code,
|
||||
message: message
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function checkEtapiAuth(req: Request, res: Response, next: NextFunction) {
|
||||
if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
|
||||
next();
|
||||
} else {
|
||||
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
|
||||
}
|
||||
}
|
||||
|
||||
function processRequest(req: Request, res: Response, routeHandler: ApiRequestHandler, next: NextFunction, method: string, path: string) {
|
||||
try {
|
||||
cls.namespace.bindEmitter(req);
|
||||
cls.namespace.bindEmitter(res);
|
||||
|
||||
cls.init(() => {
|
||||
cls.set("componentId", "etapi");
|
||||
cls.set("localNowDateTime", req.headers["trilium-local-now-datetime"]);
|
||||
|
||||
const cb = () => routeHandler(req, res, next);
|
||||
|
||||
return sql.transactional(cb);
|
||||
});
|
||||
} catch (e: any) {
|
||||
log.error(`${method} ${path} threw exception ${e.message} with stacktrace: ${e.stack}`);
|
||||
|
||||
if (e instanceof EtapiError) {
|
||||
sendError(res, e.statusCode, e.code, e.message);
|
||||
} else {
|
||||
sendError(res, 500, GENERIC_CODE, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function route(router: Router, method: HttpMethod, path: string, routeHandler: ApiRequestHandler) {
|
||||
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) {
|
||||
router[method](path, ...middleware, (req: Request, res: Response, next: NextFunction) => processRequest(req, res, routeHandler, next, method, path));
|
||||
}
|
||||
|
||||
function getAndCheckNote(noteId: string) {
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (note) {
|
||||
return note;
|
||||
} else {
|
||||
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
function getAndCheckAttachment(attachmentId: string) {
|
||||
const attachment = becca.getAttachment(attachmentId, { includeContentLength: true });
|
||||
|
||||
if (attachment) {
|
||||
return attachment;
|
||||
} else {
|
||||
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
function getAndCheckBranch(branchId: string) {
|
||||
const branch = becca.getBranch(branchId);
|
||||
|
||||
if (branch) {
|
||||
return branch;
|
||||
} else {
|
||||
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
function getAndCheckAttribute(attributeId: string) {
|
||||
const attribute = becca.getAttribute(attributeId);
|
||||
|
||||
if (attribute) {
|
||||
return attribute;
|
||||
} else {
|
||||
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateAndPatch(target: any, source: any, allowedProperties: ValidatorMap) {
|
||||
for (const key of Object.keys(source)) {
|
||||
if (!(key in allowedProperties)) {
|
||||
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for this method.`);
|
||||
} else {
|
||||
for (const validator of allowedProperties[key]) {
|
||||
const validationResult = validator(source[key]);
|
||||
|
||||
if (validationResult) {
|
||||
throw new EtapiError(400, "PROPERTY_VALIDATION_ERROR", `Validation failed on property '${key}': ${validationResult}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validation passed, let's patch
|
||||
for (const propName of Object.keys(source)) {
|
||||
target[propName] = source[propName];
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
EtapiError,
|
||||
sendError,
|
||||
route,
|
||||
NOT_AUTHENTICATED_ROUTE,
|
||||
GENERIC_CODE,
|
||||
validateAndPatch,
|
||||
getAndCheckNote,
|
||||
getAndCheckBranch,
|
||||
getAndCheckAttribute,
|
||||
getAndCheckAttachment
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import type BAttachment from "../becca/entities/battachment.js";
|
||||
import type BAttribute from "../becca/entities/battribute.js";
|
||||
import type BBranch from "../becca/entities/bbranch.js";
|
||||
import type BNote from "../becca/entities/bnote.js";
|
||||
|
||||
function mapNoteToPojo(note: BNote) {
|
||||
return {
|
||||
noteId: note.noteId,
|
||||
isProtected: note.isProtected,
|
||||
title: note.title,
|
||||
type: note.type,
|
||||
mime: note.mime,
|
||||
blobId: note.blobId,
|
||||
dateCreated: note.dateCreated,
|
||||
dateModified: note.dateModified,
|
||||
utcDateCreated: note.utcDateCreated,
|
||||
utcDateModified: note.utcDateModified,
|
||||
parentNoteIds: note.getParentNotes().map((p) => p.noteId),
|
||||
childNoteIds: note.getChildNotes().map((ch) => ch.noteId),
|
||||
parentBranchIds: note.getParentBranches().map((p) => p.branchId),
|
||||
childBranchIds: note.getChildBranches().map((ch) => ch.branchId),
|
||||
attributes: note.getAttributes().map((attr) => mapAttributeToPojo(attr))
|
||||
};
|
||||
}
|
||||
|
||||
function mapBranchToPojo(branch: BBranch) {
|
||||
return {
|
||||
branchId: branch.branchId,
|
||||
noteId: branch.noteId,
|
||||
parentNoteId: branch.parentNoteId,
|
||||
prefix: branch.prefix,
|
||||
notePosition: branch.notePosition,
|
||||
isExpanded: branch.isExpanded,
|
||||
utcDateModified: branch.utcDateModified
|
||||
};
|
||||
}
|
||||
|
||||
function mapAttributeToPojo(attr: BAttribute) {
|
||||
return {
|
||||
attributeId: attr.attributeId,
|
||||
noteId: attr.noteId,
|
||||
type: attr.type,
|
||||
name: attr.name,
|
||||
value: attr.value,
|
||||
position: attr.position,
|
||||
isInheritable: attr.isInheritable,
|
||||
utcDateModified: attr.utcDateModified
|
||||
};
|
||||
}
|
||||
|
||||
function mapAttachmentToPojo(attachment: BAttachment) {
|
||||
return {
|
||||
attachmentId: attachment.attachmentId,
|
||||
ownerId: attachment.ownerId,
|
||||
role: attachment.role,
|
||||
mime: attachment.mime,
|
||||
title: attachment.title,
|
||||
position: attachment.position,
|
||||
blobId: attachment.blobId,
|
||||
dateModified: attachment.dateModified,
|
||||
utcDateModified: attachment.utcDateModified,
|
||||
utcDateScheduledForErasureSince: attachment.utcDateScheduledForErasureSince,
|
||||
contentLength: attachment.contentLength
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
mapNoteToPojo,
|
||||
mapBranchToPojo,
|
||||
mapAttributeToPojo,
|
||||
mapAttachmentToPojo
|
||||
};
|
||||
@@ -1,267 +0,0 @@
|
||||
import becca from "../becca/becca.js";
|
||||
import utils from "../services/utils.js";
|
||||
import eu from "./etapi_utils.js";
|
||||
import mappers from "./mappers.js";
|
||||
import noteService from "../services/notes.js";
|
||||
import TaskContext from "../services/task_context.js";
|
||||
import v from "./validators.js";
|
||||
import searchService from "../services/search/services/search.js";
|
||||
import SearchContext from "../services/search/search_context.js";
|
||||
import zipExportService from "../services/export/zip.js";
|
||||
import zipImportService from "../services/import/zip.js";
|
||||
import type { Request, Router } from "express";
|
||||
import type { ParsedQs } from "qs";
|
||||
import type { NoteParams } from "../services/note-interface.js";
|
||||
import type { SearchParams } from "../services/search/services/types.js";
|
||||
import type { ValidatorMap } from "./etapi-interface.js";
|
||||
|
||||
function register(router: Router) {
|
||||
eu.route(router, "get", "/etapi/notes", (req, res, next) => {
|
||||
const { search } = req.query;
|
||||
|
||||
if (typeof search !== "string" || !search?.trim()) {
|
||||
throw new eu.EtapiError(400, "SEARCH_QUERY_PARAM_MANDATORY", "'search' query parameter is mandatory.");
|
||||
}
|
||||
|
||||
const searchParams = parseSearchParams(req);
|
||||
const searchContext = new SearchContext(searchParams);
|
||||
|
||||
const searchResults = searchService.findResultsWithQuery(search, searchContext);
|
||||
const foundNotes = searchResults.map((sr) => becca.notes[sr.noteId]);
|
||||
|
||||
const resp: any = {
|
||||
results: foundNotes.map((note) => mappers.mapNoteToPojo(note))
|
||||
};
|
||||
|
||||
if (searchContext.debugInfo) {
|
||||
resp.debugInfo = searchContext.debugInfo;
|
||||
}
|
||||
|
||||
res.json(resp);
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/notes/:noteId", (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_CREATE_NOTE: ValidatorMap = {
|
||||
parentNoteId: [v.mandatory, v.notNull, v.isNoteId],
|
||||
title: [v.mandatory, v.notNull, v.isString],
|
||||
type: [v.mandatory, v.notNull, v.isNoteType],
|
||||
mime: [v.notNull, v.isString],
|
||||
content: [v.notNull, v.isString],
|
||||
notePosition: [v.notNull, v.isInteger],
|
||||
prefix: [v.notNull, v.isString],
|
||||
isExpanded: [v.notNull, v.isBoolean],
|
||||
noteId: [v.notNull, v.isValidEntityId],
|
||||
dateCreated: [v.notNull, v.isString, v.isLocalDateTime],
|
||||
utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime]
|
||||
};
|
||||
|
||||
eu.route(router, "post", "/etapi/create-note", (req, res, next) => {
|
||||
const _params = {};
|
||||
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE);
|
||||
const params = _params as NoteParams;
|
||||
|
||||
try {
|
||||
const resp = noteService.createNewNote(params);
|
||||
|
||||
res.status(201).json({
|
||||
note: mappers.mapNoteToPojo(resp.note),
|
||||
branch: mappers.mapBranchToPojo(resp.branch)
|
||||
});
|
||||
} catch (e: any) {
|
||||
return eu.sendError(res, 500, eu.GENERIC_CODE, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
||||
title: [v.notNull, v.isString],
|
||||
type: [v.notNull, v.isString],
|
||||
mime: [v.notNull, v.isString],
|
||||
dateCreated: [v.notNull, v.isString, v.isLocalDateTime],
|
||||
utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime]
|
||||
};
|
||||
|
||||
eu.route(router, "patch", "/etapi/notes/:noteId", (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
|
||||
if (note.isProtected) {
|
||||
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`);
|
||||
}
|
||||
|
||||
eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
||||
note.save();
|
||||
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
eu.route(router, "delete", "/etapi/notes/:noteId", (req, res, next) => {
|
||||
const { noteId } = req.params;
|
||||
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
note.deleteNote(null, new TaskContext("no-progress-reporting"));
|
||||
|
||||
res.sendStatus(204);
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/notes/:noteId/content", (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
|
||||
if (note.isProtected) {
|
||||
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and content cannot be read through ETAPI.`);
|
||||
}
|
||||
|
||||
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
|
||||
|
||||
res.setHeader("Content-Disposition", utils.getContentDisposition(filename));
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Content-Type", note.mime);
|
||||
|
||||
res.send(note.getContent());
|
||||
});
|
||||
|
||||
eu.route(router, "put", "/etapi/notes/:noteId/content", (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
|
||||
if (note.isProtected) {
|
||||
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`);
|
||||
}
|
||||
|
||||
note.setContent(req.body);
|
||||
|
||||
noteService.asyncPostProcessContent(note, req.body);
|
||||
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/notes/:noteId/export", (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
const format = req.query.format || "html";
|
||||
|
||||
if (typeof format !== "string" || !["html", "markdown"].includes(format)) {
|
||||
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`);
|
||||
}
|
||||
|
||||
const taskContext = new TaskContext("no-progress-reporting");
|
||||
|
||||
// technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain
|
||||
// (e.g. branchIds are not seen in UI), that we export "note export" instead.
|
||||
const branch = note.getParentBranches()[0];
|
||||
|
||||
zipExportService.exportToZip(taskContext, branch, format as "html" | "markdown", res);
|
||||
});
|
||||
|
||||
eu.route(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
const taskContext = new TaskContext("no-progress-reporting");
|
||||
|
||||
zipImportService.importZip(taskContext, req.body, note).then((importedNote) => {
|
||||
res.status(201).json({
|
||||
note: mappers.mapNoteToPojo(importedNote),
|
||||
branch: mappers.mapBranchToPojo(importedNote.getParentBranches()[0])
|
||||
});
|
||||
}); // we need better error handling here, async errors won't be properly processed.
|
||||
});
|
||||
|
||||
eu.route(router, "post", "/etapi/notes/:noteId/revision", (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
|
||||
note.saveRevision();
|
||||
|
||||
return res.sendStatus(204);
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
|
||||
const note = eu.getAndCheckNote(req.params.noteId);
|
||||
const attachments = note.getAttachments({ includeContentLength: true });
|
||||
|
||||
res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment)));
|
||||
});
|
||||
}
|
||||
|
||||
function parseSearchParams(req: Request) {
|
||||
const rawSearchParams: SearchParams = {
|
||||
fastSearch: parseBoolean(req.query, "fastSearch"),
|
||||
includeArchivedNotes: parseBoolean(req.query, "includeArchivedNotes"),
|
||||
ancestorNoteId: parseString(req.query["ancestorNoteId"]),
|
||||
ancestorDepth: parseString(req.query["ancestorDepth"]), // e.g. "eq5"
|
||||
orderBy: parseString(req.query["orderBy"]),
|
||||
// TODO: Check why the order direction was provided as a number, but it's a string everywhere else.
|
||||
orderDirection: parseOrderDirection(req.query, "orderDirection") as unknown as string,
|
||||
limit: parseInteger(req.query, "limit"),
|
||||
debug: parseBoolean(req.query, "debug")
|
||||
};
|
||||
|
||||
const searchParams: SearchParams = {};
|
||||
|
||||
for (const paramName of Object.keys(rawSearchParams) as (keyof SearchParams)[]) {
|
||||
if (rawSearchParams[paramName] !== undefined) {
|
||||
(searchParams as any)[paramName] = rawSearchParams[paramName];
|
||||
}
|
||||
}
|
||||
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
const SEARCH_PARAM_ERROR = "SEARCH_PARAM_VALIDATION_ERROR";
|
||||
|
||||
function parseString(value: string | ParsedQs | (string | ParsedQs)[] | undefined): string | undefined {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseBoolean(obj: any, name: string) {
|
||||
if (!(name in obj)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!["true", "false"].includes(obj[name])) {
|
||||
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'.`);
|
||||
}
|
||||
|
||||
return obj[name] === "true";
|
||||
}
|
||||
|
||||
function parseOrderDirection(obj: any, name: string) {
|
||||
if (!(name in obj)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const integer = parseInt(obj[name]);
|
||||
|
||||
if (!["asc", "desc"].includes(obj[name])) {
|
||||
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'.`);
|
||||
}
|
||||
|
||||
return integer;
|
||||
}
|
||||
|
||||
function parseInteger(obj: any, name: string) {
|
||||
if (!(name in obj)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const integer = parseInt(obj[name]);
|
||||
|
||||
if (Number.isNaN(integer)) {
|
||||
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse integer '${name}' value '${obj[name]}'.`);
|
||||
}
|
||||
|
||||
return integer;
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import type { Router } from "express";
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
const specPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "etapi.openapi.yaml");
|
||||
let spec: string | null = null;
|
||||
|
||||
function register(router: Router) {
|
||||
router.get("/etapi/etapi.openapi.yaml", (req, res, next) => {
|
||||
if (!spec) {
|
||||
spec = fs.readFileSync(specPath, "utf8");
|
||||
}
|
||||
|
||||
res.header("Content-Type", "text/plain"); // so that it displays in browser
|
||||
res.status(200).send(spec);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
import specialNotesService from "../services/special_notes.js";
|
||||
import dateNotesService from "../services/date_notes.js";
|
||||
import eu from "./etapi_utils.js";
|
||||
import mappers from "./mappers.js";
|
||||
import type { Router } from "express";
|
||||
|
||||
const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
|
||||
const getWeekInvalidError = (week: string) => new eu.EtapiError(400, "WEEK_INVALID", `Week "${week}" is not valid.`);
|
||||
const getWeekNotFoundError = (week: string) => new eu.EtapiError(404, "WEEK_NOT_FOUND", `Week "${week}" not found. Check if week note is enabled.`);
|
||||
const getMonthInvalidError = (month: string) => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
|
||||
const getYearInvalidError = (year: string) => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
|
||||
|
||||
function isValidDate(date: string) {
|
||||
return /[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date) && !!Date.parse(date);
|
||||
}
|
||||
|
||||
function register(router: Router) {
|
||||
eu.route(router, "get", "/etapi/inbox/:date", async (req, res, next) => {
|
||||
const { date } = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
throw getDateInvalidError(date);
|
||||
}
|
||||
const note = await specialNotesService.getInboxNote(date);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/calendar/days/:date", async (req, res, next) => {
|
||||
const { date } = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
throw getDateInvalidError(date);
|
||||
}
|
||||
|
||||
const note = await dateNotesService.getDayNote(date);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/calendar/week-first-day/:date", async (req, res, next) => {
|
||||
const { date } = req.params;
|
||||
|
||||
if (!isValidDate(date)) {
|
||||
throw getDateInvalidError(date);
|
||||
}
|
||||
|
||||
const note = await dateNotesService.getWeekFirstDayNote(date);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/calendar/weeks/:week", async (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);
|
||||
|
||||
if (!note) {
|
||||
throw getWeekNotFoundError(week);
|
||||
}
|
||||
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/calendar/months/:month", async (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);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
|
||||
eu.route(router, "get", "/etapi/calendar/years/:year", (req, res, next) => {
|
||||
const { year } = req.params;
|
||||
|
||||
if (!/[0-9]{4}/.test(year)) {
|
||||
throw getYearInvalidError(year);
|
||||
}
|
||||
|
||||
const note = dateNotesService.getYearNote(year);
|
||||
res.json(mappers.mapNoteToPojo(note));
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
import noteTypeService from "../services/note_types.js";
|
||||
import dateUtils from "../services/date_utils.js";
|
||||
import becca from "../becca/becca.js";
|
||||
|
||||
function mandatory(obj: unknown) {
|
||||
if (obj === undefined) {
|
||||
return `mandatory, but not set`;
|
||||
}
|
||||
}
|
||||
|
||||
function notNull(obj: unknown) {
|
||||
if (obj === null) {
|
||||
return `cannot be null`;
|
||||
}
|
||||
}
|
||||
|
||||
function isString(obj: unknown) {
|
||||
if (obj === undefined || obj === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj !== "string") {
|
||||
return `'${obj}' is not a string`;
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalDateTime(obj: unknown) {
|
||||
if (typeof obj !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
return dateUtils.validateLocalDateTime(obj);
|
||||
}
|
||||
|
||||
function isUtcDateTime(obj: unknown) {
|
||||
if (typeof obj !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
return dateUtils.validateUtcDateTime(obj);
|
||||
}
|
||||
|
||||
function isBoolean(obj: unknown) {
|
||||
if (obj === undefined || obj === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj !== "boolean") {
|
||||
return `'${obj}' is not a boolean`;
|
||||
}
|
||||
}
|
||||
|
||||
function isInteger(obj: unknown) {
|
||||
if (obj === undefined || obj === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(obj)) {
|
||||
return `'${obj}' is not an integer`;
|
||||
}
|
||||
}
|
||||
|
||||
function isNoteId(obj: unknown) {
|
||||
if (obj === undefined || obj === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj !== "string") {
|
||||
return `'${obj}' is not a valid noteId`;
|
||||
}
|
||||
|
||||
if (!(obj in becca.notes)) {
|
||||
return `Note '${obj}' does not exist`;
|
||||
}
|
||||
}
|
||||
|
||||
function isNoteType(obj: unknown) {
|
||||
if (obj === undefined || obj === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteTypes = noteTypeService.getNoteTypeNames();
|
||||
|
||||
if (typeof obj !== "string" || !noteTypes.includes(obj)) {
|
||||
return `'${obj}' is not a valid note type, allowed types are: ${noteTypes.join(", ")}`;
|
||||
}
|
||||
}
|
||||
|
||||
function isAttributeType(obj: unknown) {
|
||||
if (obj === undefined || obj === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj !== "string" || !["label", "relation"].includes(obj)) {
|
||||
return `'${obj}' is not a valid attribute type, allowed types are: label, relation`;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidEntityId(obj: unknown) {
|
||||
if (obj === undefined || obj === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof obj !== "string" || !/^[A-Za-z0-9_]{4,128}$/.test(obj)) {
|
||||
return `'${obj}' is not a valid entityId. Only alphanumeric characters are allowed of length 4 to 32.`;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
mandatory,
|
||||
notNull,
|
||||
isString,
|
||||
isBoolean,
|
||||
isInteger,
|
||||
isNoteId,
|
||||
isNoteType,
|
||||
isAttributeType,
|
||||
isValidEntityId,
|
||||
isLocalDateTime,
|
||||
isUtcDateTime
|
||||
};
|
||||
Reference in New Issue
Block a user