mirror of
https://github.com/TriliumNext/Notes.git
synced 2026-01-27 23:59:37 -06:00
chore(nx): move all monorepo-style in subfolder for processing
This commit is contained in:
@@ -1,34 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderCode, type Result } from "./content_renderer.js";
|
||||
|
||||
describe("content_renderer", () => {
|
||||
describe("renderCode", () => {
|
||||
it("identifies empty content", () => {
|
||||
const emptyResult: Result = {
|
||||
header: "",
|
||||
content: " "
|
||||
};
|
||||
renderCode(emptyResult);
|
||||
expect(emptyResult.isEmpty).toBeTruthy();
|
||||
});
|
||||
|
||||
it("identifies unsupported content type", () => {
|
||||
const emptyResult: Result = {
|
||||
header: "",
|
||||
content: Buffer.from("Hello world")
|
||||
};
|
||||
renderCode(emptyResult);
|
||||
expect(emptyResult.isEmpty).toBeTruthy();
|
||||
});
|
||||
|
||||
it("wraps code in <pre>", () => {
|
||||
const result: Result = {
|
||||
header: "",
|
||||
content: "\tHello\nworld"
|
||||
};
|
||||
renderCode(result);
|
||||
expect(result.isEmpty).toBeFalsy();
|
||||
expect(result.content).toBe("<pre>\tHello\nworld</pre>");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,189 +0,0 @@
|
||||
import { JSDOM } from "jsdom";
|
||||
import shaca from "./shaca/shaca.js";
|
||||
import assetPath from "../services/asset_path.js";
|
||||
import shareRoot from "./share_root.js";
|
||||
import escapeHtml from "escape-html";
|
||||
import type SNote from "./shaca/entities/snote.js";
|
||||
import { t } from "i18next";
|
||||
|
||||
/**
|
||||
* Represents the output of the content renderer.
|
||||
*/
|
||||
export interface Result {
|
||||
header: string;
|
||||
content: string | Buffer | undefined;
|
||||
/** Set to `true` if the provided content should be rendered as empty. */
|
||||
isEmpty?: boolean;
|
||||
}
|
||||
|
||||
function getContent(note: SNote) {
|
||||
if (note.isProtected) {
|
||||
return {
|
||||
header: "",
|
||||
content: "<p>Protected note cannot be displayed</p>",
|
||||
isEmpty: false
|
||||
};
|
||||
}
|
||||
|
||||
const result: Result = {
|
||||
content: note.getContent(),
|
||||
header: "",
|
||||
isEmpty: false
|
||||
};
|
||||
|
||||
if (note.type === "text") {
|
||||
renderText(result, note);
|
||||
} else if (note.type === "code") {
|
||||
renderCode(result);
|
||||
} else if (note.type === "mermaid") {
|
||||
renderMermaid(result, note);
|
||||
} else if (["image", "canvas", "mindMap"].includes(note.type)) {
|
||||
renderImage(result, note);
|
||||
} else if (note.type === "file") {
|
||||
renderFile(note, result);
|
||||
} else if (note.type === "book") {
|
||||
result.isEmpty = true;
|
||||
} else {
|
||||
result.content = `<p>${t("content_renderer.note-cannot-be-displayed")}</p>`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderIndex(result: Result) {
|
||||
result.content += '<ul id="index">';
|
||||
|
||||
const rootNote = shaca.getNote(shareRoot.SHARE_ROOT_NOTE_ID);
|
||||
|
||||
for (const childNote of rootNote.getChildNotes()) {
|
||||
const isExternalLink = childNote.hasLabel("shareExternalLink");
|
||||
const href = isExternalLink ? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId}`;
|
||||
const target = isExternalLink ? `target="_blank" rel="noopener noreferrer"` : "";
|
||||
result.content += `<li><a class="${childNote.type}" href="${href}" ${target}>${childNote.escapedTitle}</a></li>`;
|
||||
}
|
||||
|
||||
result.content += "</ul>";
|
||||
}
|
||||
|
||||
function renderText(result: Result, note: SNote) {
|
||||
const document = new JSDOM(result.content || "").window.document;
|
||||
|
||||
result.isEmpty = document.body.textContent?.trim().length === 0 && document.querySelectorAll("img").length === 0;
|
||||
|
||||
if (!result.isEmpty) {
|
||||
for (const linkEl of document.querySelectorAll("a")) {
|
||||
const href = linkEl.getAttribute("href");
|
||||
|
||||
// Preserve footnotes.
|
||||
if (href?.startsWith("#fn")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (href?.startsWith("#")) {
|
||||
handleAttachmentLink(linkEl, href);
|
||||
}
|
||||
}
|
||||
|
||||
result.content = document.body.innerHTML;
|
||||
|
||||
if (result.content.includes(`<span class="math-tex">`)) {
|
||||
result.header += `
|
||||
<script src="../${assetPath}/node_modules/katex/dist/katex.min.js"></script>
|
||||
<link rel="stylesheet" href="../${assetPath}/node_modules/katex/dist/katex.min.css">
|
||||
<script src="../${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
|
||||
<script src="../${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
renderMathInElement(document.getElementById('content'));
|
||||
});
|
||||
</script>`;
|
||||
}
|
||||
|
||||
if (note.hasLabel("shareIndex")) {
|
||||
renderIndex(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleAttachmentLink(linkEl: HTMLAnchorElement, href: string) {
|
||||
const linkRegExp = /attachmentId=([a-zA-Z0-9_]+)/g;
|
||||
let attachmentMatch;
|
||||
if ((attachmentMatch = linkRegExp.exec(href))) {
|
||||
const attachmentId = attachmentMatch[1];
|
||||
const attachment = shaca.getAttachment(attachmentId);
|
||||
|
||||
if (attachment) {
|
||||
linkEl.setAttribute("href", `api/attachments/${attachmentId}/download`);
|
||||
linkEl.classList.add(`attachment-link`);
|
||||
linkEl.classList.add(`role-${attachment.role}`);
|
||||
linkEl.innerText = attachment.title;
|
||||
} else {
|
||||
linkEl.removeAttribute("href");
|
||||
}
|
||||
} else {
|
||||
const [notePath] = href.split("?");
|
||||
const notePathSegments = notePath.split("/");
|
||||
const noteId = notePathSegments[notePathSegments.length - 1];
|
||||
const linkedNote = shaca.getNote(noteId);
|
||||
if (linkedNote) {
|
||||
const isExternalLink = linkedNote.hasLabel("shareExternalLink");
|
||||
const href = isExternalLink ? linkedNote.getLabelValue("shareExternalLink") : `./${linkedNote.shareId}`;
|
||||
if (href) {
|
||||
linkEl.setAttribute("href", href);
|
||||
}
|
||||
if (isExternalLink) {
|
||||
linkEl.setAttribute("target", "_blank");
|
||||
linkEl.setAttribute("rel", "noopener noreferrer");
|
||||
}
|
||||
linkEl.classList.add(`type-${linkedNote.type}`);
|
||||
} else {
|
||||
linkEl.removeAttribute("href");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a code note.
|
||||
*/
|
||||
export function renderCode(result: Result) {
|
||||
if (typeof result.content !== "string" || !result.content?.trim()) {
|
||||
result.isEmpty = true;
|
||||
} else {
|
||||
const document = new JSDOM().window.document;
|
||||
|
||||
const preEl = document.createElement("pre");
|
||||
preEl.appendChild(document.createTextNode(result.content));
|
||||
|
||||
result.content = preEl.outerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
function renderMermaid(result: Result, note: SNote) {
|
||||
if (typeof result.content !== "string") {
|
||||
return;
|
||||
}
|
||||
|
||||
result.content = `
|
||||
<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">
|
||||
<hr>
|
||||
<details>
|
||||
<summary>Chart source</summary>
|
||||
<pre>${escapeHtml(result.content)}</pre>
|
||||
</details>`;
|
||||
}
|
||||
|
||||
function renderImage(result: Result, note: SNote) {
|
||||
result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`;
|
||||
}
|
||||
|
||||
function renderFile(note: SNote, result: Result) {
|
||||
if (note.mime === "application/pdf") {
|
||||
result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>`;
|
||||
} else {
|
||||
result.content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getContent
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import supertest from "supertest";
|
||||
import { initializeTranslations } from "../services/i18n.js";
|
||||
import type { Application, Request, Response, NextFunction } from "express";
|
||||
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||
|
||||
let app: Application;
|
||||
|
||||
describe("Share API test", () => {
|
||||
let cannotSetHeadersCount = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
initializeTranslations();
|
||||
app = (await import("../app.js")).default;
|
||||
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
|
||||
const [ errMessage ] = safeExtractMessageAndStackFromError(err);
|
||||
if (errMessage.includes("Cannot set headers after they are sent to the client")) {
|
||||
cannotSetHeadersCount++;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cannotSetHeadersCount = 0;
|
||||
});
|
||||
|
||||
it("requests password for password-protected share", async () => {
|
||||
await supertest(app)
|
||||
.get("/share/YjlPRj2E9fOV")
|
||||
.expect(401)
|
||||
.expect("WWW-Authenticate", 'Basic realm="User Visible Realm", charset="UTF-8"');
|
||||
expect(cannotSetHeadersCount).toBe(0);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,389 +0,0 @@
|
||||
import safeCompare from "safe-compare";
|
||||
import ejs from "ejs";
|
||||
|
||||
import type { Request, Response, Router } from "express";
|
||||
|
||||
import shaca from "./shaca/shaca.js";
|
||||
import shacaLoader from "./shaca/shaca_loader.js";
|
||||
import shareRoot from "./share_root.js";
|
||||
import contentRenderer from "./content_renderer.js";
|
||||
import assetPath from "../services/asset_path.js";
|
||||
import appPath from "../services/app_path.js";
|
||||
import searchService from "../services/search/services/search.js";
|
||||
import SearchContext from "../services/search/search_context.js";
|
||||
import log from "../services/log.js";
|
||||
import type SNote from "./shaca/entities/snote.js";
|
||||
import type SBranch from "./shaca/entities/sbranch.js";
|
||||
import type SAttachment from "./shaca/entities/sattachment.js";
|
||||
import utils, { safeExtractMessageAndStackFromError } from "../services/utils.js";
|
||||
import options from "../services/options.js";
|
||||
|
||||
function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } {
|
||||
if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
// share root itself is not shared
|
||||
return {};
|
||||
}
|
||||
|
||||
// every path leads to share root, but which one to choose?
|
||||
// for the sake of simplicity, URLs are not note paths
|
||||
const parentBranch = note.getParentBranches()[0];
|
||||
|
||||
if (parentBranch.parentNoteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
return {
|
||||
note,
|
||||
branch: parentBranch
|
||||
};
|
||||
}
|
||||
|
||||
return getSharedSubTreeRoot(parentBranch.getParentNote());
|
||||
}
|
||||
|
||||
function addNoIndexHeader(note: SNote, res: Response) {
|
||||
if (note.isLabelTruthy("shareDisallowRobotIndexing")) {
|
||||
res.setHeader("X-Robots-Tag", "noindex");
|
||||
}
|
||||
}
|
||||
|
||||
function requestCredentials(res: Response) {
|
||||
res.setHeader("WWW-Authenticate", 'Basic realm="User Visible Realm", charset="UTF-8"').sendStatus(401);
|
||||
}
|
||||
|
||||
function checkAttachmentAccess(attachmentId: string, req: Request, res: Response) {
|
||||
const attachment = shaca.getAttachment(attachmentId);
|
||||
|
||||
if (!attachment) {
|
||||
res.status(404).json({ message: `Attachment '${attachmentId}' not found.` });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const note = checkNoteAccess(attachment.ownerId, req, res);
|
||||
|
||||
// truthy note means the user has access, and we can return the attachment
|
||||
return note ? attachment : false;
|
||||
}
|
||||
|
||||
function checkNoteAccess(noteId: string, req: Request, res: Response) {
|
||||
const note = shaca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
res.status(404).json({ message: `Note '${noteId}' not found.` });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (noteId === "_share" && !shaca.shareIndexEnabled) {
|
||||
res.status(403).json({ message: `Accessing share index is forbidden.` });
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const credentials = note.getCredentials();
|
||||
|
||||
if (credentials.length === 0) {
|
||||
return note;
|
||||
}
|
||||
|
||||
const header = req.header("Authorization");
|
||||
|
||||
if (!header?.startsWith("Basic ")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const base64Str = header.substring("Basic ".length);
|
||||
const buffer = Buffer.from(base64Str, "base64");
|
||||
const authString = buffer.toString("utf-8");
|
||||
|
||||
for (const credentialLabel of credentials) {
|
||||
if (safeCompare(authString, credentialLabel.value)) {
|
||||
return note; // success;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderImageAttachment(image: SNote, res: Response, attachmentName: string) {
|
||||
let svgString = "<svg/>";
|
||||
const attachment = image.getAttachmentByTitle(attachmentName);
|
||||
if (!attachment) {
|
||||
res.status(404).render("share/404");
|
||||
return;
|
||||
}
|
||||
const content = attachment.getContent();
|
||||
if (typeof content === "string") {
|
||||
svgString = content;
|
||||
} else {
|
||||
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
|
||||
const possibleSvgContent = image.getJsonContentSafely();
|
||||
|
||||
const contentSvg = (typeof possibleSvgContent === "object"
|
||||
&& possibleSvgContent !== null
|
||||
&& "svg" in possibleSvgContent
|
||||
&& typeof possibleSvgContent.svg === "string")
|
||||
? possibleSvgContent.svg
|
||||
: null;
|
||||
|
||||
if (contentSvg) {
|
||||
svgString = contentSvg;
|
||||
}
|
||||
}
|
||||
|
||||
const svg = svgString;
|
||||
res.set("Content-Type", "image/svg+xml");
|
||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.send(svg);
|
||||
}
|
||||
|
||||
function register(router: Router) {
|
||||
function renderNote(note: SNote, req: Request, res: Response) {
|
||||
if (!note) {
|
||||
console.log("Unable to find note ", note);
|
||||
res.status(404).render("share/404");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkNoteAccess(note.noteId, req, res)) {
|
||||
requestCredentials(res);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
addNoIndexHeader(note, res);
|
||||
|
||||
if (note.isLabelTruthy("shareRaw") || typeof req.query.raw !== "undefined") {
|
||||
res.setHeader("Content-Type", note.mime).send(note.getContent());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { header, content, isEmpty } = contentRenderer.getContent(note);
|
||||
const subRoot = getSharedSubTreeRoot(note);
|
||||
const showLoginInShareTheme = options.getOption("showLoginInShareTheme");
|
||||
const opts = { note, header, content, isEmpty, subRoot, assetPath, appPath, showLoginInShareTheme };
|
||||
let useDefaultView = true;
|
||||
|
||||
// Check if the user has their own template
|
||||
if (note.hasRelation("shareTemplate")) {
|
||||
// Get the template note and content
|
||||
const templateId = note.getRelation("shareTemplate")?.value;
|
||||
const templateNote = templateId && shaca.getNote(templateId);
|
||||
|
||||
// Make sure the note type is correct
|
||||
if (templateNote && templateNote.type === "code" && templateNote.mime === "application/x-ejs") {
|
||||
// EJS caches the result of this so we don't need to pre-cache
|
||||
const includer = (path: string) => {
|
||||
const childNote = templateNote.children.find((n) => path === n.title);
|
||||
if (!childNote) throw new Error("Unable to find child note.");
|
||||
if (childNote.type !== "code" || childNote.mime !== "application/x-ejs") throw new Error("Incorrect child note type.");
|
||||
|
||||
const template = childNote.getContent();
|
||||
if (typeof template !== "string") throw new Error("Invalid template content type.");
|
||||
|
||||
return { template };
|
||||
};
|
||||
|
||||
// Try to render user's template, w/ fallback to default view
|
||||
try {
|
||||
const content = templateNote.getContent();
|
||||
if (typeof content === "string") {
|
||||
const ejsResult = ejs.render(content, opts, { includer });
|
||||
res.send(ejsResult);
|
||||
useDefaultView = false; // Rendering went okay, don't use default view
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
|
||||
log.error(`Rendering user provided share template (${templateId}) threw exception ${errMessage} with stacktrace: ${errStack}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (useDefaultView) {
|
||||
res.render("share/page", opts);
|
||||
}
|
||||
}
|
||||
|
||||
router.get("/share/", (req, res) => {
|
||||
if (req.path.substr(-1) !== "/") {
|
||||
res.redirect("../share/");
|
||||
return;
|
||||
}
|
||||
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
if (!shaca.shareRootNote) {
|
||||
res.status(404).json({ message: "Share root note not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
renderNote(shaca.shareRootNote, req, res);
|
||||
});
|
||||
|
||||
router.get("/share/:shareId", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
const { shareId } = req.params;
|
||||
|
||||
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
|
||||
|
||||
renderNote(note, req, res);
|
||||
});
|
||||
|
||||
router.get("/share/api/notes/:noteId", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
let note: SNote | boolean;
|
||||
|
||||
if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
|
||||
return;
|
||||
}
|
||||
|
||||
addNoIndexHeader(note, res);
|
||||
|
||||
res.json(note.getPojo());
|
||||
});
|
||||
|
||||
router.get("/share/api/notes/:noteId/download", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
let note: SNote | boolean;
|
||||
|
||||
if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
|
||||
return;
|
||||
}
|
||||
|
||||
addNoIndexHeader(note, res);
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
|
||||
router.get("/share/api/images/:noteId/:filename", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
let image: SNote | boolean;
|
||||
|
||||
if (!(image = checkNoteAccess(req.params.noteId, req, res))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (image.type === "image") {
|
||||
// normal image
|
||||
res.set("Content-Type", image.mime);
|
||||
addNoIndexHeader(image, res);
|
||||
res.send(image.getContent());
|
||||
} else if (image.type === "canvas") {
|
||||
renderImageAttachment(image, res, "canvas-export.svg");
|
||||
} else if (image.type === "mermaid") {
|
||||
renderImageAttachment(image, res, "mermaid-export.svg");
|
||||
} else if (image.type === "mindMap") {
|
||||
renderImageAttachment(image, res, "mindmap-export.svg");
|
||||
} else {
|
||||
res.status(400).json({ message: "Requested note is not a shareable image" });
|
||||
}
|
||||
});
|
||||
|
||||
// :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename
|
||||
router.get("/share/api/attachments/:attachmentId/image/:filename", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
let attachment: SAttachment | boolean;
|
||||
|
||||
if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attachment.role === "image") {
|
||||
res.set("Content-Type", attachment.mime);
|
||||
addNoIndexHeader(attachment.note, res);
|
||||
res.send(attachment.getContent());
|
||||
} else {
|
||||
res.status(400).json({ message: "Requested attachment is not a shareable image" });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/share/api/attachments/:attachmentId/download", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
let attachment: SAttachment | boolean;
|
||||
|
||||
if (!(attachment = checkAttachmentAccess(req.params.attachmentId, req, res))) {
|
||||
return;
|
||||
}
|
||||
|
||||
addNoIndexHeader(attachment.note, res);
|
||||
|
||||
const filename = utils.formatDownloadTitle(attachment.title, null, 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());
|
||||
});
|
||||
|
||||
// used for PDF viewing
|
||||
router.get("/share/api/notes/:noteId/view", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
let note: SNote | boolean;
|
||||
|
||||
if (!(note = checkNoteAccess(req.params.noteId, req, res))) {
|
||||
return;
|
||||
}
|
||||
|
||||
addNoIndexHeader(note, res);
|
||||
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Content-Type", note.mime);
|
||||
|
||||
res.send(note.getContent());
|
||||
});
|
||||
|
||||
// Used for searching, require noteId so we know the subTreeRoot
|
||||
router.get("/share/api/notes", (req, res) => {
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
const ancestorNoteId = req.query.ancestorNoteId ?? "_share";
|
||||
|
||||
if (typeof ancestorNoteId !== "string") {
|
||||
res.status(400).json({ message: "'ancestorNoteId' parameter is mandatory." });
|
||||
return;
|
||||
}
|
||||
|
||||
// This will automatically return if no ancestorNoteId is provided and there is no shareIndex
|
||||
if (!checkNoteAccess(ancestorNoteId, req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { search } = req.query;
|
||||
|
||||
if (typeof search !== "string" || !search?.trim()) {
|
||||
res.status(400).json({ message: "'search' parameter is mandatory." });
|
||||
return;
|
||||
}
|
||||
|
||||
const searchContext = new SearchContext({ ancestorNoteId: ancestorNoteId });
|
||||
const searchResults = searchService.findResultsWithQuery(search, searchContext);
|
||||
const filteredResults = searchResults.map((sr) => {
|
||||
const fullNote = shaca.notes[sr.noteId];
|
||||
const startIndex = sr.notePathArray.indexOf(ancestorNoteId);
|
||||
const localPathArray = sr.notePathArray.slice(startIndex + 1).filter((id) => shaca.notes[id]);
|
||||
const pathTitle = localPathArray.map((id) => shaca.notes[id].title).join(" / ");
|
||||
return { id: fullNote.shareId, title: fullNote.title, score: sr.score, path: pathTitle };
|
||||
});
|
||||
|
||||
res.json({ results: filteredResults });
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
register
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import shaca from "../shaca.js";
|
||||
import type Shaca from "../shaca-interface.js";
|
||||
|
||||
class AbstractShacaEntity {
|
||||
get shaca(): Shaca {
|
||||
return shaca;
|
||||
}
|
||||
}
|
||||
|
||||
export default AbstractShacaEntity;
|
||||
@@ -1,4 +0,0 @@
|
||||
export type SNoteRow = [string, string, string, string, string, string, boolean];
|
||||
export type SBranchRow = [string, string, string, string, string, boolean];
|
||||
export type SAttributeRow = [string, string, string, string, string, boolean, number];
|
||||
export type SAttachmentRow = [string, string, string, string, string, string, string];
|
||||
@@ -1,76 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import sql from "../../sql.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import AbstractShacaEntity from "./abstract_shaca_entity.js";
|
||||
import type SNote from "./snote.js";
|
||||
import type { Blob } from "../../../services/blob-interface.js";
|
||||
import type { SAttachmentRow } from "./rows.js";
|
||||
|
||||
class SAttachment extends AbstractShacaEntity {
|
||||
private attachmentId: string;
|
||||
ownerId: string;
|
||||
title: string;
|
||||
role: string;
|
||||
mime: string;
|
||||
private blobId: string;
|
||||
/** used for caching of images */
|
||||
private utcDateModified: string;
|
||||
|
||||
constructor([attachmentId, ownerId, role, mime, title, blobId, utcDateModified]: SAttachmentRow) {
|
||||
super();
|
||||
|
||||
this.attachmentId = attachmentId;
|
||||
this.ownerId = ownerId;
|
||||
this.title = title;
|
||||
this.role = role;
|
||||
this.mime = mime;
|
||||
this.blobId = blobId;
|
||||
this.utcDateModified = utcDateModified;
|
||||
|
||||
this.shaca.attachments[this.attachmentId] = this;
|
||||
this.shaca.notes[this.ownerId].attachments.push(this);
|
||||
}
|
||||
|
||||
get note(): SNote {
|
||||
return this.shaca.notes[this.ownerId];
|
||||
}
|
||||
|
||||
getContent(silentNotFoundError = false) {
|
||||
const row = sql.getRow<Pick<Blob, "content">>(/*sql*/`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
|
||||
|
||||
if (!row) {
|
||||
if (silentNotFoundError) {
|
||||
return undefined;
|
||||
} else {
|
||||
throw new Error(`Cannot find blob for attachment '${this.attachmentId}', blob '${this.blobId}'`);
|
||||
}
|
||||
}
|
||||
|
||||
const content = row.content;
|
||||
|
||||
if (this.hasStringContent()) {
|
||||
return content === null ? "" : content.toString("utf-8");
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns true if the attachment has string content (not binary) */
|
||||
hasStringContent() {
|
||||
return utils.isStringNote(undefined, this.mime);
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
attachmentId: this.attachmentId,
|
||||
role: this.role,
|
||||
mime: this.mime,
|
||||
title: this.title,
|
||||
blobId: this.blobId,
|
||||
utcDateModified: this.utcDateModified
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default SAttachment;
|
||||
@@ -1,111 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import AbstractShacaEntity from "./abstract_shaca_entity.js";
|
||||
import type { SAttributeRow } from "./rows.js";
|
||||
import type SNote from "./snote.js";
|
||||
|
||||
class SAttribute extends AbstractShacaEntity {
|
||||
attributeId: string;
|
||||
private noteId: string;
|
||||
type: string;
|
||||
name: string;
|
||||
private position: number;
|
||||
value: string;
|
||||
isInheritable: boolean;
|
||||
|
||||
constructor([attributeId, noteId, type, name, value, isInheritable, position]: SAttributeRow) {
|
||||
super();
|
||||
|
||||
this.attributeId = attributeId;
|
||||
this.noteId = noteId;
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
this.position = position;
|
||||
this.value = value;
|
||||
this.isInheritable = !!isInheritable;
|
||||
|
||||
this.shaca.attributes[this.attributeId] = this;
|
||||
this.shaca.notes[this.noteId].ownedAttributes.push(this);
|
||||
|
||||
const targetNote = this.targetNote;
|
||||
|
||||
if (targetNote) {
|
||||
targetNote.targetRelations.push(this);
|
||||
}
|
||||
|
||||
if (this.type === "relation" && this.name === "imageLink") {
|
||||
const linkedChildNote = this.note.getChildNotes().find((childNote) => childNote.noteId === this.value);
|
||||
|
||||
if (linkedChildNote) {
|
||||
const branch = this.shaca.getBranchFromChildAndParent(linkedChildNote.noteId, this.noteId);
|
||||
|
||||
branch.isHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.type === "label" && this.name === "shareAlias" && this.value.trim()) {
|
||||
this.shaca.aliasToNote[this.value.trim()] = this.note;
|
||||
}
|
||||
|
||||
if (this.type === "label" && this.name === "shareRoot") {
|
||||
this.shaca.shareRootNote = this.note;
|
||||
}
|
||||
|
||||
if (this.type === "label" && this.name === "shareIndex") {
|
||||
this.shaca.shareIndexEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
get isAffectingSubtree() {
|
||||
return this.isInheritable || (this.type === "relation" && ["template", "inherit"].includes(this.name));
|
||||
}
|
||||
|
||||
get targetNoteId() {
|
||||
// alias
|
||||
return this.type === "relation" ? this.value : undefined;
|
||||
}
|
||||
|
||||
isAutoLink() {
|
||||
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
|
||||
}
|
||||
|
||||
get note(): SNote {
|
||||
return this.shaca.notes[this.noteId];
|
||||
}
|
||||
|
||||
get targetNote(): SNote | null | undefined {
|
||||
if (this.type === "relation") {
|
||||
return this.shaca.notes[this.value];
|
||||
}
|
||||
}
|
||||
|
||||
getNote(): SNote | null {
|
||||
return this.shaca.getNote(this.noteId);
|
||||
}
|
||||
|
||||
getTargetNote(): SNote | null {
|
||||
if (this.type !== "relation") {
|
||||
throw new Error(`Attribute '${this.attributeId}' is not relation`);
|
||||
}
|
||||
|
||||
if (!this.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.shaca.getNote(this.value);
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
attributeId: this.attributeId,
|
||||
noteId: this.noteId,
|
||||
type: this.type,
|
||||
name: this.name,
|
||||
position: this.position,
|
||||
value: this.value,
|
||||
isInheritable: this.isInheritable
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default SAttribute;
|
||||
@@ -1,61 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import AbstractShacaEntity from "./abstract_shaca_entity.js";
|
||||
import type { SBranchRow } from "./rows.js";
|
||||
import type SNote from "./snote.js";
|
||||
|
||||
class SBranch extends AbstractShacaEntity {
|
||||
private branchId: string;
|
||||
private noteId: string;
|
||||
parentNoteId: string;
|
||||
private prefix: string;
|
||||
private isExpanded: boolean;
|
||||
isHidden: boolean;
|
||||
|
||||
constructor([branchId, noteId, parentNoteId, prefix, isExpanded]: SBranchRow) {
|
||||
super();
|
||||
|
||||
this.branchId = branchId;
|
||||
this.noteId = noteId;
|
||||
this.parentNoteId = parentNoteId;
|
||||
this.prefix = prefix;
|
||||
this.isExpanded = !!isExpanded;
|
||||
this.isHidden = false;
|
||||
|
||||
const childNote = this.childNote;
|
||||
const parentNote = this.parentNote;
|
||||
|
||||
if (!childNote.parents.includes(parentNote)) {
|
||||
childNote.parents.push(parentNote);
|
||||
}
|
||||
|
||||
if (!childNote.parentBranches.includes(this)) {
|
||||
childNote.parentBranches.push(this);
|
||||
}
|
||||
|
||||
if (!parentNote.children.includes(childNote)) {
|
||||
parentNote.children.push(childNote);
|
||||
}
|
||||
|
||||
this.shaca.branches[this.branchId] = this;
|
||||
this.shaca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
|
||||
}
|
||||
|
||||
get childNote(): SNote {
|
||||
return this.shaca.notes[this.noteId];
|
||||
}
|
||||
|
||||
getNote() {
|
||||
return this.childNote;
|
||||
}
|
||||
|
||||
get parentNote(): SNote {
|
||||
return this.shaca.notes[this.parentNoteId];
|
||||
}
|
||||
|
||||
getParentNote() {
|
||||
return this.parentNote;
|
||||
}
|
||||
}
|
||||
|
||||
export default SBranch;
|
||||
@@ -1,535 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import sql from "../../sql.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import AbstractShacaEntity from "./abstract_shaca_entity.js";
|
||||
import escape from "escape-html";
|
||||
import type { Blob } from "../../../services/blob-interface.js";
|
||||
import type SAttachment from "./sattachment.js";
|
||||
import type SAttribute from "./sattribute.js";
|
||||
import type SBranch from "./sbranch.js";
|
||||
import type { SNoteRow } from "./rows.js";
|
||||
|
||||
const LABEL = "label";
|
||||
const RELATION = "relation";
|
||||
const CREDENTIALS = "shareCredentials";
|
||||
|
||||
const isCredentials = (attr: SAttribute) => attr.type === "label" && attr.name === CREDENTIALS;
|
||||
|
||||
class SNote extends AbstractShacaEntity {
|
||||
noteId: string;
|
||||
title: string;
|
||||
type: string;
|
||||
mime: string;
|
||||
private blobId: string;
|
||||
utcDateModified: string;
|
||||
isProtected: boolean;
|
||||
parentBranches: SBranch[];
|
||||
parents: SNote[];
|
||||
children: SNote[];
|
||||
ownedAttributes: SAttribute[];
|
||||
private __attributeCache: SAttribute[] | null;
|
||||
private __inheritableAttributeCache: SAttribute[] | null;
|
||||
targetRelations: SAttribute[];
|
||||
attachments: SAttachment[];
|
||||
|
||||
constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]: SNoteRow) {
|
||||
super();
|
||||
|
||||
this.noteId = noteId;
|
||||
this.title = isProtected ? "[protected]" : title;
|
||||
this.type = type;
|
||||
this.mime = mime;
|
||||
this.blobId = blobId;
|
||||
this.utcDateModified = utcDateModified; // used for caching of images
|
||||
this.isProtected = isProtected;
|
||||
|
||||
this.parentBranches = [];
|
||||
this.parents = [];
|
||||
this.children = [];
|
||||
this.ownedAttributes = [];
|
||||
|
||||
this.__attributeCache = null;
|
||||
this.__inheritableAttributeCache = null;
|
||||
|
||||
this.targetRelations = [];
|
||||
this.attachments = [];
|
||||
|
||||
this.shaca.notes[this.noteId] = this;
|
||||
}
|
||||
|
||||
getParentBranches() {
|
||||
return this.parentBranches;
|
||||
}
|
||||
|
||||
getBranches() {
|
||||
return this.parentBranches;
|
||||
}
|
||||
|
||||
getChildBranches(): SBranch[] {
|
||||
return this.children.map((childNote) => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
|
||||
}
|
||||
|
||||
getVisibleChildBranches() {
|
||||
return this.getChildBranches().filter((branch) => !branch.isHidden && !branch.getNote().isLabelTruthy("shareHiddenFromTree"));
|
||||
}
|
||||
|
||||
getParentNotes() {
|
||||
return this.parents;
|
||||
}
|
||||
|
||||
getChildNotes() {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
getVisibleChildNotes() {
|
||||
return this.getVisibleChildBranches().map((branch) => branch.getNote());
|
||||
}
|
||||
|
||||
hasChildren() {
|
||||
return this.children && this.children.length > 0;
|
||||
}
|
||||
|
||||
hasVisibleChildren() {
|
||||
return this.getVisibleChildNotes().length > 0;
|
||||
}
|
||||
|
||||
getContent(silentNotFoundError = false) {
|
||||
const row = sql.getRow<Pick<Blob, "content">>(/*sql*/`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
|
||||
|
||||
if (!row) {
|
||||
if (silentNotFoundError) {
|
||||
return undefined;
|
||||
} else {
|
||||
throw new Error(`Cannot find note content for note '${this.noteId}', blob '${this.blobId}'`);
|
||||
}
|
||||
}
|
||||
|
||||
const content = row.content;
|
||||
|
||||
if (this.hasStringContent()) {
|
||||
return content === null ? "" : content.toString("utf-8");
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns true if the note has string content (not binary) */
|
||||
hasStringContent() {
|
||||
return utils.isStringNote(this.type, this.mime);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type - (optional) attribute type to filter
|
||||
* @param name - (optional) attribute name to filter
|
||||
* @returns all note's attributes, including inherited ones
|
||||
*/
|
||||
getAttributes(type?: string, name?: string) {
|
||||
let attributeCache = this.__attributeCache;
|
||||
if (!attributeCache) {
|
||||
attributeCache = this.__getAttributes([]);
|
||||
}
|
||||
|
||||
if (type && name) {
|
||||
return attributeCache.filter((attr) => attr.type === type && attr.name === name && !isCredentials(attr));
|
||||
} else if (type) {
|
||||
return attributeCache.filter((attr) => attr.type === type && !isCredentials(attr));
|
||||
} else if (name) {
|
||||
return attributeCache.filter((attr) => attr.name === name && !isCredentials(attr));
|
||||
} else {
|
||||
return attributeCache.filter((attr) => !isCredentials(attr));
|
||||
}
|
||||
}
|
||||
|
||||
getCredentials() {
|
||||
return this.__getAttributes([]).filter(isCredentials);
|
||||
}
|
||||
|
||||
__getAttributes(path: string[]) {
|
||||
if (path.includes(this.noteId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.__attributeCache) {
|
||||
const parentAttributes = this.ownedAttributes.slice();
|
||||
const newPath = [...path, this.noteId];
|
||||
|
||||
if (this.noteId !== "root") {
|
||||
for (const parentNote of this.parents) {
|
||||
parentAttributes.push(...parentNote.__getInheritableAttributes(newPath));
|
||||
}
|
||||
}
|
||||
|
||||
const templateAttributes: SAttribute[] = [];
|
||||
|
||||
for (const ownedAttr of parentAttributes) {
|
||||
// parentAttributes so we process also inherited templates
|
||||
if (ownedAttr.type === "relation" && ["template", "inherit"].includes(ownedAttr.name)) {
|
||||
const templateNote = this.shaca.notes[ownedAttr.value];
|
||||
|
||||
if (templateNote) {
|
||||
templateAttributes.push(...templateNote.__getAttributes(newPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.__attributeCache = [];
|
||||
|
||||
const addedAttributeIds = new Set();
|
||||
|
||||
for (const attr of parentAttributes.concat(templateAttributes)) {
|
||||
if (!addedAttributeIds.has(attr.attributeId)) {
|
||||
addedAttributeIds.add(attr.attributeId);
|
||||
|
||||
this.__attributeCache.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
this.__inheritableAttributeCache = [];
|
||||
|
||||
for (const attr of this.__attributeCache) {
|
||||
if (attr.isInheritable) {
|
||||
this.__inheritableAttributeCache.push(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.__attributeCache;
|
||||
}
|
||||
|
||||
__getInheritableAttributes(path: string[]) {
|
||||
if (path.includes(this.noteId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.__inheritableAttributeCache) {
|
||||
this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache
|
||||
}
|
||||
|
||||
return this.__inheritableAttributeCache || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error in case of invalid JSON
|
||||
*/
|
||||
getJsonContent(): unknown | null {
|
||||
const content = this.getContent();
|
||||
|
||||
if (typeof content !== "string" || !content || !content.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
/** @returns valid object or null if the content cannot be parsed as JSON */
|
||||
getJsonContentSafely() {
|
||||
try {
|
||||
return this.getJsonContent();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
hasAttribute(type: string, name: string) {
|
||||
return !!this.getAttributes().find((attr) => attr.type === type && attr.name === name);
|
||||
}
|
||||
|
||||
getRelationTarget(name: string) {
|
||||
const relation = this.getAttributes().find((attr) => attr.type === "relation" && attr.name === name);
|
||||
|
||||
return relation ? relation.targetNote : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name
|
||||
* @returns true if label exists (including inherited)
|
||||
*/
|
||||
hasLabel(name: string) {
|
||||
return this.hasAttribute(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name
|
||||
* @returns true if label exists (including inherited) and does not have "false" value.
|
||||
*/
|
||||
isLabelTruthy(name: string) {
|
||||
const label = this.getLabel(name);
|
||||
|
||||
if (!label) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!label && label.value !== "false";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name
|
||||
* @returns true if label exists (excluding inherited)
|
||||
*/
|
||||
hasOwnedLabel(name: string) {
|
||||
return this.hasOwnedAttribute(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - relation name
|
||||
* @returns true if relation exists (including inherited)
|
||||
*/
|
||||
hasRelation(name: string) {
|
||||
return this.hasAttribute(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - relation name
|
||||
* @returns true if relation exists (excluding inherited)
|
||||
*/
|
||||
hasOwnedRelation(name: string) {
|
||||
return this.hasOwnedAttribute(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name
|
||||
* @returns label if it exists, null otherwise
|
||||
*/
|
||||
getLabel(name: string) {
|
||||
return this.getAttribute(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name
|
||||
* @returns label if it exists, null otherwise
|
||||
*/
|
||||
getOwnedLabel(name: string) {
|
||||
return this.getOwnedAttribute(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - relation name
|
||||
* @returns relation if it exists, null otherwise
|
||||
*/
|
||||
getRelation(name: string) {
|
||||
return this.getAttribute(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - relation name
|
||||
* @returns relation if it exists, null otherwise
|
||||
*/
|
||||
getOwnedRelation(name: string) {
|
||||
return this.getOwnedAttribute(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name
|
||||
* @returns label value if label exists, null otherwise
|
||||
*/
|
||||
getLabelValue(name: string) {
|
||||
return this.getAttributeValue(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name
|
||||
* @returns label value if label exists, null otherwise
|
||||
*/
|
||||
getOwnedLabelValue(name: string) {
|
||||
return this.getOwnedAttributeValue(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - relation name
|
||||
* @returns relation value if relation exists, null otherwise
|
||||
*/
|
||||
getRelationValue(name: string) {
|
||||
return this.getAttributeValue(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - relation name
|
||||
* @returns relation value if relation exists, null otherwise
|
||||
*/
|
||||
getOwnedRelationValue(name: string) {
|
||||
return this.getOwnedAttributeValue(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns true if note has an attribute with given type and name (excluding inherited)
|
||||
*/
|
||||
hasOwnedAttribute(type: string, name: string) {
|
||||
return !!this.getOwnedAttribute(type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns attribute of the given type and name. If there are more such attributes, first is returned.
|
||||
* Returns null if there's no such attribute belonging to this note.
|
||||
*/
|
||||
getAttribute(type: string, name: string) {
|
||||
const attributes = this.getAttributes();
|
||||
|
||||
return attributes.find((attr) => attr.type === type && attr.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns attribute value of the given type and name or null if no such attribute exists.
|
||||
*/
|
||||
getAttributeValue(type: string, name: string) {
|
||||
const attr = this.getAttribute(type, name);
|
||||
|
||||
return attr ? attr.value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type - attribute type (label, relation, etc.)
|
||||
* @param name - attribute name
|
||||
* @returns attribute value of the given type and name or null if no such attribute exists.
|
||||
*/
|
||||
getOwnedAttributeValue(type: string, name: string) {
|
||||
const attr = this.getOwnedAttribute(type, name);
|
||||
|
||||
return attr ? (attr.value as string) : null; // FIXME
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name to filter
|
||||
* @returns all note's labels (attributes with type label), including inherited ones
|
||||
*/
|
||||
getLabels(name: string) {
|
||||
return this.getAttributes(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name to filter
|
||||
* @returns all note's label values, including inherited ones
|
||||
*/
|
||||
getLabelValues(name: string) {
|
||||
return this.getLabels(name).map((l) => l.value) as string[]; // FIXME
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name to filter
|
||||
* @returns all note's labels (attributes with type label), excluding inherited ones
|
||||
*/
|
||||
getOwnedLabels(name: string) {
|
||||
return this.getOwnedAttributes(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - label name to filter
|
||||
* @returns all note's label values, excluding inherited ones
|
||||
*/
|
||||
getOwnedLabelValues(name: string) {
|
||||
return this.getOwnedAttributes(LABEL, name).map((l) => l.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - relation name to filter
|
||||
* @returns all note's relations (attributes with type relation), including inherited ones
|
||||
*/
|
||||
getRelations(name: string) {
|
||||
return this.getAttributes(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name - relation name to filter
|
||||
* @returns all note's relations (attributes with type relation), excluding inherited ones
|
||||
*/
|
||||
getOwnedRelations(name: string) {
|
||||
return this.getOwnedAttributes(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type - (optional) attribute type to filter
|
||||
* @param name - (optional) attribute name to filter
|
||||
* @returns note's "owned" attributes - excluding inherited ones
|
||||
*/
|
||||
getOwnedAttributes(type: string, name: string) {
|
||||
// it's a common mistake to include # or ~ into attribute name
|
||||
if (name && ["#", "~"].includes(name[0])) {
|
||||
name = name.substr(1);
|
||||
}
|
||||
|
||||
if (type && name) {
|
||||
return this.ownedAttributes.filter((attr) => attr.type === type && attr.name === name);
|
||||
} else if (type) {
|
||||
return this.ownedAttributes.filter((attr) => attr.type === type);
|
||||
} else if (name) {
|
||||
return this.ownedAttributes.filter((attr) => attr.name === name);
|
||||
} else {
|
||||
return this.ownedAttributes.slice();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns attribute belonging to this specific note (excludes inherited attributes)
|
||||
*
|
||||
* This method can be significantly faster than the getAttribute()
|
||||
*/
|
||||
getOwnedAttribute(type: string, name: string) {
|
||||
const attrs = this.getOwnedAttributes(type, name);
|
||||
|
||||
return attrs.length > 0 ? attrs[0] : null;
|
||||
}
|
||||
|
||||
get isArchived() {
|
||||
return this.hasAttribute("label", "archived");
|
||||
}
|
||||
|
||||
isInherited() {
|
||||
return !!this.targetRelations.find((rel) => rel.name === "template" || rel.name === "inherit");
|
||||
}
|
||||
|
||||
getTargetRelations() {
|
||||
return this.targetRelations;
|
||||
}
|
||||
|
||||
getAttachments() {
|
||||
return this.attachments;
|
||||
}
|
||||
|
||||
getAttachmentByTitle(title: string) {
|
||||
return this.attachments.find((attachment) => attachment.title === title);
|
||||
}
|
||||
|
||||
get shareId() {
|
||||
if (this.hasOwnedLabel("shareRoot")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const sharedAlias = this.getOwnedLabelValue("shareAlias");
|
||||
|
||||
return sharedAlias || this.noteId;
|
||||
}
|
||||
|
||||
get escapedTitle() {
|
||||
return escape(this.title);
|
||||
}
|
||||
|
||||
get encodedTitle() {
|
||||
return encodeURIComponent(this.title);
|
||||
}
|
||||
|
||||
getPojo() {
|
||||
return {
|
||||
noteId: this.noteId,
|
||||
title: this.title,
|
||||
type: this.type,
|
||||
mime: this.mime,
|
||||
utcDateModified: this.utcDateModified,
|
||||
attributes: this.getAttributes()
|
||||
// relations could link across shared subtrees which might leak them
|
||||
// individual relations might be whitelisted based on needs #3434
|
||||
.filter((attr) => attr.type === "label")
|
||||
.map((attr) => attr.getPojo()),
|
||||
attachments: this.getAttachments().map((attachment) => attachment.getPojo()),
|
||||
parentNoteIds: this.parents.map((parentNote) => parentNote.noteId),
|
||||
childNoteIds: this.children.map((child) => child.noteId)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default SNote;
|
||||
@@ -1,90 +0,0 @@
|
||||
import type SAttachment from "./entities/sattachment.js";
|
||||
import type SAttribute from "./entities/sattribute.js";
|
||||
import type SBranch from "./entities/sbranch.js";
|
||||
import type SNote from "./entities/snote.js";
|
||||
|
||||
export default class Shaca {
|
||||
notes!: Record<string, SNote>;
|
||||
branches!: Record<string, SBranch>;
|
||||
childParentToBranch!: Record<string, SBranch>;
|
||||
attributes!: Record<string, SAttribute>;
|
||||
attachments!: Record<string, SAttachment>;
|
||||
aliasToNote!: Record<string, SNote>;
|
||||
shareRootNote!: SNote | null;
|
||||
/** true if the index of all shared subtrees is enabled */
|
||||
shareIndexEnabled!: boolean;
|
||||
loaded!: boolean;
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.notes = {};
|
||||
this.branches = {};
|
||||
this.childParentToBranch = {};
|
||||
this.attributes = {};
|
||||
this.attachments = {};
|
||||
this.aliasToNote = {};
|
||||
|
||||
this.shareRootNote = null;
|
||||
|
||||
this.shareIndexEnabled = false;
|
||||
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
getNote(noteId: string) {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
hasNote(noteId: string) {
|
||||
return noteId in this.notes;
|
||||
}
|
||||
|
||||
getNotes(noteIds: string[], ignoreMissing = false) {
|
||||
const filteredNotes = [];
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (!note) {
|
||||
if (ignoreMissing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Note '${noteId}' was not found in shaca.`);
|
||||
}
|
||||
|
||||
filteredNotes.push(note);
|
||||
}
|
||||
|
||||
return filteredNotes;
|
||||
}
|
||||
|
||||
getBranch(branchId: string) {
|
||||
return this.branches[branchId];
|
||||
}
|
||||
|
||||
getBranchFromChildAndParent(childNoteId: string, parentNoteId: string) {
|
||||
return this.childParentToBranch[`${childNoteId}-${parentNoteId}`];
|
||||
}
|
||||
|
||||
getAttribute(attributeId: string) {
|
||||
return this.attributes[attributeId];
|
||||
}
|
||||
|
||||
getAttachment(attachmentId: string) {
|
||||
return this.attachments[attachmentId];
|
||||
}
|
||||
|
||||
getEntity(entityName: string, entityId: string) {
|
||||
if (!entityName || !entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, (group) => group.toUpperCase().replace("_", ""));
|
||||
|
||||
return (this as any)[camelCaseEntityName][entityId];
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import Shaca from "./shaca-interface.js";
|
||||
|
||||
const shaca = new Shaca();
|
||||
|
||||
export default shaca;
|
||||
@@ -1,104 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import sql from "../sql.js";
|
||||
import shaca from "./shaca.js";
|
||||
import log from "../../services/log.js";
|
||||
import SNote from "./entities/snote.js";
|
||||
import SBranch from "./entities/sbranch.js";
|
||||
import SAttribute from "./entities/sattribute.js";
|
||||
import SAttachment from "./entities/sattachment.js";
|
||||
import shareRoot from "../share_root.js";
|
||||
import eventService from "../../services/events.js";
|
||||
import type { SAttachmentRow, SAttributeRow, SBranchRow, SNoteRow } from "./entities/rows.js";
|
||||
|
||||
function load() {
|
||||
const start = Date.now();
|
||||
shaca.reset();
|
||||
|
||||
// using a raw query and passing arrays to avoid allocating new objects
|
||||
|
||||
const noteIds = sql.getColumn(
|
||||
`
|
||||
WITH RECURSIVE
|
||||
tree(noteId) AS (
|
||||
SELECT ?
|
||||
UNION
|
||||
SELECT branches.noteId FROM branches
|
||||
JOIN tree ON branches.parentNoteId = tree.noteId
|
||||
WHERE branches.isDeleted = 0
|
||||
)
|
||||
SELECT noteId FROM tree`,
|
||||
[shareRoot.SHARE_ROOT_NOTE_ID]
|
||||
);
|
||||
|
||||
if (noteIds.length === 0) {
|
||||
shaca.loaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const noteIdStr = noteIds.map((noteId) => `'${noteId}'`).join(",");
|
||||
|
||||
const rawNoteRows = sql.getRawRows<SNoteRow>(`
|
||||
SELECT noteId, title, type, mime, blobId, utcDateModified, isProtected
|
||||
FROM notes
|
||||
WHERE isDeleted = 0
|
||||
AND noteId IN (${noteIdStr})`);
|
||||
|
||||
for (const row of rawNoteRows) {
|
||||
new SNote(row);
|
||||
}
|
||||
|
||||
const rawBranchRows = sql.getRawRows<SBranchRow>(`
|
||||
SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified
|
||||
FROM branches
|
||||
WHERE isDeleted = 0
|
||||
AND parentNoteId IN (${noteIdStr})
|
||||
ORDER BY notePosition`);
|
||||
|
||||
for (const row of rawBranchRows) {
|
||||
new SBranch(row);
|
||||
}
|
||||
|
||||
const rawAttributeRows = sql.getRawRows<SAttributeRow>(`
|
||||
SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified
|
||||
FROM attributes
|
||||
WHERE isDeleted = 0
|
||||
AND noteId IN (${noteIdStr})`);
|
||||
|
||||
for (const row of rawAttributeRows) {
|
||||
new SAttribute(row);
|
||||
}
|
||||
|
||||
const rawAttachmentRows = sql.getRawRows<SAttachmentRow>(`
|
||||
SELECT attachmentId, ownerId, role, mime, title, blobId, utcDateModified
|
||||
FROM attachments
|
||||
WHERE isDeleted = 0
|
||||
AND ownerId IN (${noteIdStr})`);
|
||||
|
||||
for (const row of rawAttachmentRows) {
|
||||
new SAttachment(row);
|
||||
}
|
||||
|
||||
shaca.loaded = true;
|
||||
|
||||
log.info(`Shaca loaded ${rawNoteRows.length} notes, ${rawBranchRows.length} branches, ${rawAttachmentRows.length} attributes took ${Date.now() - start}ms`);
|
||||
}
|
||||
|
||||
function ensureLoad() {
|
||||
if (!shaca.loaded) {
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
eventService.subscribe(
|
||||
[eventService.ENTITY_CREATED, eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED, eventService.ENTITY_CHANGE_SYNCED, eventService.ENTITY_DELETE_SYNCED],
|
||||
() => {
|
||||
shaca.reset();
|
||||
}
|
||||
);
|
||||
|
||||
export default {
|
||||
load,
|
||||
ensureLoad
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export default {
|
||||
SHARE_ROOT_NOTE_ID: "_share"
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
import Database from "better-sqlite3";
|
||||
import dataDir from "../services/data_dir.js";
|
||||
import sql_init from "../services/sql_init.js";
|
||||
|
||||
let dbConnection!: Database.Database;
|
||||
|
||||
sql_init.dbReady.then(() => {
|
||||
dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true });
|
||||
|
||||
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((eventType) => {
|
||||
process.on(eventType, () => {
|
||||
if (dbConnection) {
|
||||
// closing connection is especially important to fold -wal file into the main DB file
|
||||
// (see https://sqlite.org/tempfiles.html for details)
|
||||
dbConnection.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getRawRows<T>(query: string, params = []): T[] {
|
||||
return dbConnection.prepare(query).raw().all(params) as T[];
|
||||
}
|
||||
|
||||
function getRow<T>(query: string, params: string[] = []): T {
|
||||
return dbConnection.prepare(query).get(params) as T;
|
||||
}
|
||||
|
||||
function getColumn<T>(query: string, params: string[] = []): T[] {
|
||||
return dbConnection.prepare(query).pluck().all(params) as T[];
|
||||
}
|
||||
|
||||
export default {
|
||||
getRawRows,
|
||||
getRow,
|
||||
getColumn
|
||||
};
|
||||
Reference in New Issue
Block a user