mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 10:02:59 +08:00
chore(code): fix more js & ts files
This commit is contained in:
parent
b321d99076
commit
7a2b5e731e
@ -1,6 +1,6 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*.{js,ts}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
@ -23,7 +23,7 @@ async function copyNodeModuleFileOrFolder(source: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
for (const srcFile of fs.readdirSync("build")) {
|
for (const srcFile of fs.readdirSync("build")) {
|
||||||
const destFile = path.join(DEST_DIR, path.basename(srcFile));
|
const destFile = path.join(DEST_DIR, path.basename(srcFile));
|
||||||
log(`Copying source ${srcFile} -> ${destFile}.`);
|
log(`Copying source ${srcFile} -> ${destFile}.`);
|
||||||
fs.copySync(path.join("build", srcFile), destFile, { recursive: true });
|
fs.copySync(path.join("build", srcFile), destFile, { recursive: true });
|
||||||
@ -45,11 +45,11 @@ const copy = async () => {
|
|||||||
for (const dir of srcDirsToCopy) {
|
for (const dir of srcDirsToCopy) {
|
||||||
log(`Copying ${dir}`);
|
log(`Copying ${dir}`);
|
||||||
await fs.copy(dir, path.join(DEST_DIR_SRC, path.basename(dir)));
|
await fs.copy(dir, path.join(DEST_DIR_SRC, path.basename(dir)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directories to be copied relative to the project root into <resource_dir>/src/public/app-dist.
|
* Directories to be copied relative to the project root into <resource_dir>/src/public/app-dist.
|
||||||
*/
|
*/
|
||||||
const publicDirsToCopy = [ "./src/public/app/doc_notes" ];
|
const publicDirsToCopy = [ "./src/public/app/doc_notes" ];
|
||||||
const PUBLIC_DIR = path.join(DEST_DIR, "src", "public", "app-dist");
|
const PUBLIC_DIR = path.join(DEST_DIR, "src", "public", "app-dist");
|
||||||
for (const dir of publicDirsToCopy) {
|
for (const dir of publicDirsToCopy) {
|
||||||
|
@ -22,4 +22,4 @@ export default {
|
|||||||
};
|
};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
fs.writeFileSync("src/services/build.ts", output);
|
fs.writeFileSync("src/services/build.ts", output);
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* @module
|
* @module
|
||||||
*
|
*
|
||||||
* The nightly version works uses the version described in `package.json`, just like any release.
|
* The nightly version works uses the version described in `package.json`, just like any release.
|
||||||
* The problem with this approach is that production builds have a very aggressive cache, and
|
* The problem with this approach is that production builds have a very aggressive cache, and
|
||||||
* usually running the nightly with this cached version of the application will mean that the
|
* usually running the nightly with this cached version of the application will mean that the
|
||||||
* user might run into module not found errors or styling errors caused by an old cache.
|
* user might run into module not found errors or styling errors caused by an old cache.
|
||||||
*
|
*
|
||||||
* This script is supposed to be run in the CI, which will update locally the version field of
|
* This script is supposed to be run in the CI, which will update locally the version field of
|
||||||
* `package.json` to contain the date. For example, `0.90.9-beta` will become `0.90.9-test-YYMMDD-HHMMSS`.
|
* `package.json` to contain the date. For example, `0.90.9-beta` will become `0.90.9-test-YYMMDD-HHMMSS`.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
@ -33,7 +33,7 @@ function processVersion(version) {
|
|||||||
function main() {
|
function main() {
|
||||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||||
const packageJsonPath = join(scriptDir, "..", "package.json");
|
const packageJsonPath = join(scriptDir, "..", "package.json");
|
||||||
|
|
||||||
// Read the version from package.json and process it.
|
// Read the version from package.json and process it.
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
||||||
const currentVersion = packageJson.version;
|
const currentVersion = packageJson.version;
|
||||||
@ -43,7 +43,7 @@ function main() {
|
|||||||
|
|
||||||
// Write the adjusted version back in.
|
// Write the adjusted version back in.
|
||||||
packageJson.version = adjustedVersion;
|
packageJson.version = adjustedVersion;
|
||||||
const formattedJson = JSON.stringify(packageJson, null, 4);
|
const formattedJson = JSON.stringify(packageJson, null, 4);
|
||||||
fs.writeFileSync(packageJsonPath, formattedJson);
|
fs.writeFileSync(packageJsonPath, formattedJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,4 +15,4 @@ const sourceDir = "src/public";
|
|||||||
chokidar
|
chokidar
|
||||||
.watch(sourceDir)
|
.watch(sourceDir)
|
||||||
.on("change", onFileChanged);
|
.on("change", onFileChanged);
|
||||||
console.log(`Watching for changes to ${sourceDir}...`);
|
console.log(`Watching for changes to ${sourceDir}...`);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { initializeTranslations } from "./src/services/i18n.js";
|
import { initializeTranslations } from "./src/services/i18n.js";
|
||||||
|
|
||||||
await initializeTranslations();
|
await initializeTranslations();
|
||||||
await import("./electron.js")
|
await import("./electron.js")
|
||||||
|
@ -48,11 +48,11 @@ electron.app.on("ready", async () => {
|
|||||||
await windowService.createMainWindow(electron.app);
|
await windowService.createMainWindow(electron.app);
|
||||||
|
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
electron.app.on("activate", async () => {
|
electron.app.on("activate", async () => {
|
||||||
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
||||||
await windowService.createMainWindow(electron.app);
|
await windowService.createMainWindow(electron.app);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tray.createTray();
|
tray.createTray();
|
||||||
|
@ -5,7 +5,7 @@ const authFile = 'playwright/.auth/user.json';
|
|||||||
const ROOT_URL = "http://localhost:8082";
|
const ROOT_URL = "http://localhost:8082";
|
||||||
const LOGIN_PASSWORD = "demo1234";
|
const LOGIN_PASSWORD = "demo1234";
|
||||||
|
|
||||||
// Reference: https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
|
// Reference: https://playwright.dev/docs/auth#basic-shared-account-in-all-tests
|
||||||
|
|
||||||
setup("authenticate", async ({ page }) => {
|
setup("authenticate", async ({ page }) => {
|
||||||
await page.goto(ROOT_URL);
|
await page.goto(ROOT_URL);
|
||||||
@ -13,5 +13,5 @@ setup("authenticate", async ({ page }) => {
|
|||||||
|
|
||||||
await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD);
|
await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD);
|
||||||
await page.getByRole("button", { name: "Login"}).click();
|
await page.getByRole("button", { name: "Login"}).click();
|
||||||
await page.context().storageState({ path: authFile });
|
await page.context().storageState({ path: authFile });
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
|
|||||||
test("Can duplicate note with broken links", async ({ page }) => {
|
test("Can duplicate note with broken links", async ({ page }) => {
|
||||||
await page.goto(`http://localhost:8082/#2VammGGdG6Ie`);
|
await page.goto(`http://localhost:8082/#2VammGGdG6Ie`);
|
||||||
await page.locator('.tree-wrapper .fancytree-active').getByText('Note map').click({ button: 'right' });
|
await page.locator('.tree-wrapper .fancytree-active').getByText('Note map').click({ button: 'right' });
|
||||||
await page.getByText('Duplicate subtree').click();
|
await page.getByText('Duplicate subtree').click();
|
||||||
await expect(page.locator(".toast-body")).toBeHidden();
|
await expect(page.locator(".toast-body")).toBeHidden();
|
||||||
await expect(page.locator('.tree-wrapper').getByText('Note map (dup)')).toBeVisible();
|
await expect(page.locator('.tree-wrapper').getByText('Note map (dup)')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,6 @@ test('Complete help in search', async ({ page }) => {
|
|||||||
|
|
||||||
await page.locator('#launcher-container').getByRole('button', { name: '' }).first().click();
|
await page.locator('#launcher-container').getByRole('button', { name: '' }).first().click();
|
||||||
await page.getByRole('cell', { name: ' ' }).locator('span').first().click();
|
await page.getByRole('cell', { name: ' ' }).locator('span').first().click();
|
||||||
await page.getByRole('button', { name: 'complete help on search syntax' }).click();
|
await page.getByRole('button', { name: 'complete help on search syntax' }).click();
|
||||||
expect((await page.waitForEvent('popup')).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
|
expect((await page.waitForEvent('popup')).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@ test("User can change language from settings", async ({ page }) => {
|
|||||||
await page.locator('#center-pane').getByText('Appearance').click();
|
await page.locator('#center-pane').getByText('Appearance').click();
|
||||||
|
|
||||||
// Check that the default value (English) is set.
|
// Check that the default value (English) is set.
|
||||||
await expect(page.locator('#center-pane')).toContainText('Theme');
|
await expect(page.locator('#center-pane')).toContainText('Theme');
|
||||||
const languageCombobox = await page.getByRole('combobox').first();
|
const languageCombobox = await page.getByRole('combobox').first();
|
||||||
await expect(languageCombobox).toHaveValue("en");
|
await expect(languageCombobox).toHaveValue("en");
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ test("User can change language from settings", async ({ page }) => {
|
|||||||
languageCombobox.selectOption("en");
|
languageCombobox.selectOption("en");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Restores language on start-up on desktop", async ({ page, context }) => {
|
test("Restores language on start-up on desktop", async ({ page, context }) => {
|
||||||
await page.goto('http://localhost:8082');
|
await page.goto('http://localhost:8082');
|
||||||
await expect(page.locator('#launcher-pane').first()).toContainText("Open New Window");
|
await expect(page.locator('#launcher-pane').first()).toContainText("Open New Window");
|
||||||
});
|
});
|
||||||
@ -40,4 +40,4 @@ test("Restores language on start-up on mobile", async ({ page, context }) => {
|
|||||||
]);
|
]);
|
||||||
await page.goto('http://localhost:8082');
|
await page.goto('http://localhost:8082');
|
||||||
await expect(page.locator('#launcher-pane div').first()).toContainText("Open New Window");
|
await expect(page.locator('#launcher-pane div').first()).toContainText("Open New Window");
|
||||||
});
|
});
|
||||||
|
@ -9,10 +9,10 @@ test("Can insert equations", async ({ page }) => {
|
|||||||
|
|
||||||
// Create a new note
|
// Create a new note
|
||||||
// await page.locator("button.button-widget.bx-file-blank")
|
// await page.locator("button.button-widget.bx-file-blank")
|
||||||
// .click();
|
// .click();
|
||||||
|
|
||||||
const activeNote = page.locator(".component.note-split:visible");
|
const activeNote = page.locator(".component.note-split:visible");
|
||||||
const noteContent = activeNote
|
const noteContent = activeNote
|
||||||
.locator(".note-detail-editable-text-editor")
|
.locator(".note-detail-editable-text-editor")
|
||||||
await noteContent.press("Ctrl+M");
|
await noteContent.press("Ctrl+M");
|
||||||
});
|
});
|
||||||
|
@ -18,4 +18,4 @@ test("Spellcheck settings not displayed on web", async ({ page }) => {
|
|||||||
await expect(page.getByRole('heading', { name: 'Tray' })).toBeHidden();
|
await expect(page.getByRole('heading', { name: 'Tray' })).toBeHidden();
|
||||||
await expect(page.getByText('These options apply only for desktop builds')).toBeVisible();
|
await expect(page.getByText('These options apply only for desktop builds')).toBeVisible();
|
||||||
await expect(page.getByText('Enable spellcheck')).toBeHidden();
|
await expect(page.getByText('Enable spellcheck')).toBeHidden();
|
||||||
});
|
});
|
||||||
|
@ -15,4 +15,4 @@ test("Renders on mobile", async ({ page, context }) => {
|
|||||||
]);
|
]);
|
||||||
await page.goto('http://localhost:8082');
|
await page.goto('http://localhost:8082');
|
||||||
await expect(page.locator('.tree')).toContainText('Trilium Integration Test');
|
await expect(page.locator('.tree')).toContainText('Trilium Integration Test');
|
||||||
});
|
});
|
||||||
|
@ -9,4 +9,4 @@ test("Displays update badge when there is a version available", async ({ page })
|
|||||||
|
|
||||||
const page1 = await page.waitForEvent('popup');
|
const page1 = await page.waitForEvent('popup');
|
||||||
expect(page1.url()).toBe(`https://github.com/TriliumNext/Notes/releases/tag/v${expectedVersion}`);
|
expect(page1.url()).toBe(`https://github.com/TriliumNext/Notes/releases/tag/v${expectedVersion}`);
|
||||||
});
|
});
|
||||||
|
@ -7,4 +7,4 @@
|
|||||||
|
|
||||||
import { register } from 'node:module';
|
import { register } from 'node:module';
|
||||||
import { pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
register('ts-node/esm', pathToFileURL('./'));
|
register('ts-node/esm', pathToFileURL('./'));
|
||||||
|
@ -42,17 +42,17 @@ export default defineConfig({
|
|||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "setup",
|
name: "setup",
|
||||||
testMatch: /.*\.setup\.ts/
|
testMatch: /.*\.setup\.ts/
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "firefox",
|
name: "firefox",
|
||||||
use: {
|
use: {
|
||||||
...devices[ "Desktop Firefox" ],
|
...devices[ "Desktop Firefox" ],
|
||||||
storageState: "playwright/.auth/user.json"
|
storageState: "playwright/.auth/user.json"
|
||||||
},
|
},
|
||||||
dependencies: [ "setup" ]
|
dependencies: [ "setup" ]
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
/* Test against mobile viewports. */
|
||||||
|
@ -9,12 +9,12 @@ etapi.describeEtapi("import", () => {
|
|||||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const zipFileBuffer = fs.readFileSync(
|
const zipFileBuffer = fs.readFileSync(
|
||||||
path.resolve(scriptDir, "test-export.zip")
|
path.resolve(scriptDir, "test-export.zip")
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await etapi.postEtapiContent(
|
const response = await etapi.postEtapiContent(
|
||||||
"notes/root/import",
|
"notes/root/import",
|
||||||
zipFileBuffer
|
zipFileBuffer
|
||||||
);
|
);
|
||||||
expect(response.status).toEqual(201);
|
expect(response.status).toEqual(201);
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ etapi.describeEtapi("import", () => {
|
|||||||
expect(branch.parentNoteId).toEqual("root");
|
expect(branch.parentNoteId).toEqual("root");
|
||||||
|
|
||||||
const content = await (
|
const content = await (
|
||||||
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
|
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
|
||||||
).text();
|
).text();
|
||||||
expect(content).toContain("test export content");
|
expect(content).toContain("test export content");
|
||||||
});
|
});
|
||||||
|
@ -4,11 +4,11 @@ import etapi from "../support/etapi.js";
|
|||||||
etapi.describeEtapi("notes", () => {
|
etapi.describeEtapi("notes", () => {
|
||||||
it("create", async () => {
|
it("create", async () => {
|
||||||
const { note, branch } = await etapi.postEtapi("create-note", {
|
const { note, branch } = await etapi.postEtapi("create-note", {
|
||||||
parentNoteId: "root",
|
parentNoteId: "root",
|
||||||
type: "text",
|
type: "text",
|
||||||
title: "Hello World!",
|
title: "Hello World!",
|
||||||
content: "Content",
|
content: "Content",
|
||||||
prefix: "Custom prefix",
|
prefix: "Custom prefix",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(note.title).toEqual("Hello World!");
|
expect(note.title).toEqual("Hello World!");
|
||||||
@ -19,7 +19,7 @@ etapi.describeEtapi("notes", () => {
|
|||||||
expect(rNote.title).toEqual("Hello World!");
|
expect(rNote.title).toEqual("Hello World!");
|
||||||
|
|
||||||
const rContent = await (
|
const rContent = await (
|
||||||
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
|
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
|
||||||
).text();
|
).text();
|
||||||
expect(rContent).toEqual("Content");
|
expect(rContent).toEqual("Content");
|
||||||
|
|
||||||
@ -30,18 +30,18 @@ etapi.describeEtapi("notes", () => {
|
|||||||
|
|
||||||
it("patch", async () => {
|
it("patch", async () => {
|
||||||
const { note } = await etapi.postEtapi("create-note", {
|
const { note } = await etapi.postEtapi("create-note", {
|
||||||
parentNoteId: "root",
|
parentNoteId: "root",
|
||||||
type: "text",
|
type: "text",
|
||||||
title: "Hello World!",
|
title: "Hello World!",
|
||||||
content: "Content",
|
content: "Content",
|
||||||
});
|
});
|
||||||
|
|
||||||
await etapi.patchEtapi(`notes/${note.noteId}`, {
|
await etapi.patchEtapi(`notes/${note.noteId}`, {
|
||||||
title: "new title",
|
title: "new title",
|
||||||
type: "code",
|
type: "code",
|
||||||
mime: "text/apl",
|
mime: "text/apl",
|
||||||
dateCreated: "2000-01-01 12:34:56.999+0200",
|
dateCreated: "2000-01-01 12:34:56.999+0200",
|
||||||
utcDateCreated: "2000-01-01 10:34:56.999Z",
|
utcDateCreated: "2000-01-01 10:34:56.999Z",
|
||||||
});
|
});
|
||||||
|
|
||||||
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
|
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
|
||||||
@ -54,26 +54,26 @@ etapi.describeEtapi("notes", () => {
|
|||||||
|
|
||||||
it("update content", async () => {
|
it("update content", async () => {
|
||||||
const { note } = await etapi.postEtapi("create-note", {
|
const { note } = await etapi.postEtapi("create-note", {
|
||||||
parentNoteId: "root",
|
parentNoteId: "root",
|
||||||
type: "text",
|
type: "text",
|
||||||
title: "Hello World!",
|
title: "Hello World!",
|
||||||
content: "Content",
|
content: "Content",
|
||||||
});
|
});
|
||||||
|
|
||||||
await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content");
|
await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content");
|
||||||
|
|
||||||
const rContent = await (
|
const rContent = await (
|
||||||
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
|
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
|
||||||
).text();
|
).text();
|
||||||
expect(rContent).toEqual("new content");
|
expect(rContent).toEqual("new content");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("create / update binary content", async () => {
|
it("create / update binary content", async () => {
|
||||||
const { note } = await etapi.postEtapi("create-note", {
|
const { note } = await etapi.postEtapi("create-note", {
|
||||||
parentNoteId: "root",
|
parentNoteId: "root",
|
||||||
type: "file",
|
type: "file",
|
||||||
title: "Hello World!",
|
title: "Hello World!",
|
||||||
content: "ZZZ",
|
content: "ZZZ",
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedContent = crypto.randomBytes(16);
|
const updatedContent = crypto.randomBytes(16);
|
||||||
@ -81,17 +81,17 @@ etapi.describeEtapi("notes", () => {
|
|||||||
await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent);
|
await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent);
|
||||||
|
|
||||||
const rContent = await (
|
const rContent = await (
|
||||||
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
|
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
|
||||||
).arrayBuffer();
|
).arrayBuffer();
|
||||||
expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent);
|
expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("delete note", async () => {
|
it("delete note", async () => {
|
||||||
const { note } = await etapi.postEtapi("create-note", {
|
const { note } = await etapi.postEtapi("create-note", {
|
||||||
parentNoteId: "root",
|
parentNoteId: "root",
|
||||||
type: "text",
|
type: "text",
|
||||||
title: "Hello World!",
|
title: "Hello World!",
|
||||||
content: "Content",
|
content: "Content",
|
||||||
});
|
});
|
||||||
|
|
||||||
await etapi.deleteEtapi(`notes/${note.noteId}`);
|
await etapi.deleteEtapi(`notes/${note.noteId}`);
|
||||||
|
@ -24,12 +24,12 @@ class NoteBuilder {
|
|||||||
|
|
||||||
label(name: string, value = "", isInheritable = false) {
|
label(name: string, value = "", isInheritable = false) {
|
||||||
new BAttribute({
|
new BAttribute({
|
||||||
attributeId: id(),
|
attributeId: id(),
|
||||||
noteId: this.note.noteId,
|
noteId: this.note.noteId,
|
||||||
type: "label",
|
type: "label",
|
||||||
isInheritable,
|
isInheritable,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@ -37,11 +37,11 @@ class NoteBuilder {
|
|||||||
|
|
||||||
relation(name: string, targetNote: BNote) {
|
relation(name: string, targetNote: BNote) {
|
||||||
new BAttribute({
|
new BAttribute({
|
||||||
attributeId: id(),
|
attributeId: id(),
|
||||||
noteId: this.note.noteId,
|
noteId: this.note.noteId,
|
||||||
type: "relation",
|
type: "relation",
|
||||||
name,
|
name,
|
||||||
value: targetNote.noteId,
|
value: targetNote.noteId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@ -49,11 +49,11 @@ class NoteBuilder {
|
|||||||
|
|
||||||
child(childNoteBuilder: NoteBuilder, prefix = "") {
|
child(childNoteBuilder: NoteBuilder, prefix = "") {
|
||||||
new BBranch({
|
new BBranch({
|
||||||
branchId: id(),
|
branchId: id(),
|
||||||
noteId: childNoteBuilder.note.noteId,
|
noteId: childNoteBuilder.note.noteId,
|
||||||
parentNoteId: this.note.noteId,
|
parentNoteId: this.note.noteId,
|
||||||
prefix,
|
prefix,
|
||||||
notePosition: 10,
|
notePosition: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@ -67,10 +67,10 @@ function id() {
|
|||||||
function note(title: string, extraParams = {}) {
|
function note(title: string, extraParams = {}) {
|
||||||
const row = Object.assign(
|
const row = Object.assign(
|
||||||
{
|
{
|
||||||
noteId: id(),
|
noteId: id(),
|
||||||
title: title,
|
title: title,
|
||||||
type: "text" as NoteType,
|
type: "text" as NoteType,
|
||||||
mime: "text/html",
|
mime: "text/html",
|
||||||
},
|
},
|
||||||
extraParams
|
extraParams
|
||||||
);
|
);
|
||||||
|
@ -3,65 +3,65 @@ import lex from "../../src/services/search/services/lex.js";
|
|||||||
describe("Lexer fulltext", () => {
|
describe("Lexer fulltext", () => {
|
||||||
it("simple lexing", () => {
|
it("simple lexing", () => {
|
||||||
expect(lex("hello world").fulltextTokens.map((t) => t.token)).toEqual([
|
expect(lex("hello world").fulltextTokens.map((t) => t.token)).toEqual([
|
||||||
"hello",
|
"hello",
|
||||||
"world",
|
"world",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(lex("hello, world").fulltextTokens.map((t) => t.token)).toEqual([
|
expect(lex("hello, world").fulltextTokens.map((t) => t.token)).toEqual([
|
||||||
"hello",
|
"hello",
|
||||||
"world",
|
"world",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("use quotes to keep words together", () => {
|
it("use quotes to keep words together", () => {
|
||||||
expect(
|
expect(
|
||||||
lex("'hello world' my friend").fulltextTokens.map((t) => t.token)
|
lex("'hello world' my friend").fulltextTokens.map((t) => t.token)
|
||||||
).toEqual(["hello world", "my", "friend"]);
|
).toEqual(["hello world", "my", "friend"]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
lex('"hello world" my friend').fulltextTokens.map((t) => t.token)
|
lex('"hello world" my friend').fulltextTokens.map((t) => t.token)
|
||||||
).toEqual(["hello world", "my", "friend"]);
|
).toEqual(["hello world", "my", "friend"]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
lex("`hello world` my friend").fulltextTokens.map((t) => t.token)
|
lex("`hello world` my friend").fulltextTokens.map((t) => t.token)
|
||||||
).toEqual(["hello world", "my", "friend"]);
|
).toEqual(["hello world", "my", "friend"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("you can use different quotes and other special characters inside quotes", () => {
|
it("you can use different quotes and other special characters inside quotes", () => {
|
||||||
expect(
|
expect(
|
||||||
lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map(
|
lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map(
|
||||||
(t) => t.token
|
(t) => t.token
|
||||||
)
|
)
|
||||||
).toEqual(['i can use " or ` or #~=*', "without", "problem"]);
|
).toEqual(['i can use " or ` or #~=*', "without", "problem"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("I can use backslash to escape quotes", () => {
|
it("I can use backslash to escape quotes", () => {
|
||||||
expect(lex('hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual(
|
expect(lex('hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual(
|
||||||
["hello", '"world"']
|
["hello", '"world"']
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(lex("hello \\'world\\'").fulltextTokens.map((t) => t.token)).toEqual(
|
expect(lex("hello \\'world\\'").fulltextTokens.map((t) => t.token)).toEqual(
|
||||||
["hello", "'world'"]
|
["hello", "'world'"]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(lex("hello \\`world\\`").fulltextTokens.map((t) => t.token)).toEqual(
|
expect(lex("hello \\`world\\`").fulltextTokens.map((t) => t.token)).toEqual(
|
||||||
["hello", "`world`"]
|
["hello", "`world`"]
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
lex('"hello \\"world\\"').fulltextTokens.map((t) => t.token)
|
lex('"hello \\"world\\"').fulltextTokens.map((t) => t.token)
|
||||||
).toEqual(['hello "world"']);
|
).toEqual(['hello "world"']);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
lex("'hello \\'world\\''").fulltextTokens.map((t) => t.token)
|
lex("'hello \\'world\\''").fulltextTokens.map((t) => t.token)
|
||||||
).toEqual(["hello 'world'"]);
|
).toEqual(["hello 'world'"]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
lex("`hello \\`world\\``").fulltextTokens.map((t) => t.token)
|
lex("`hello \\`world\\``").fulltextTokens.map((t) => t.token)
|
||||||
).toEqual(["hello `world`"]);
|
).toEqual(["hello `world`"]);
|
||||||
|
|
||||||
expect(lex("\\#token").fulltextTokens.map((t) => t.token)).toEqual([
|
expect(lex("\\#token").fulltextTokens.map((t) => t.token)).toEqual([
|
||||||
"#token",
|
"#token",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -69,40 +69,40 @@ describe("Lexer fulltext", () => {
|
|||||||
const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan");
|
const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan");
|
||||||
|
|
||||||
expect(lexResult.fulltextTokens.map((t) => t.token)).toEqual([
|
expect(lexResult.fulltextTokens.map((t) => t.token)).toEqual([
|
||||||
"d'artagnan",
|
"d'artagnan",
|
||||||
"is",
|
"is",
|
||||||
"dead",
|
"dead",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(lexResult.expressionTokens.map((t) => t.token)).toEqual([
|
expect(lexResult.expressionTokens.map((t) => t.token)).toEqual([
|
||||||
"#hero",
|
"#hero",
|
||||||
"=",
|
"=",
|
||||||
"d'artagnan",
|
"d'artagnan",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("if quote is not ended then it's just one long token", () => {
|
it("if quote is not ended then it's just one long token", () => {
|
||||||
expect(lex("'unfinished quote").fulltextTokens.map((t) => t.token)).toEqual(
|
expect(lex("'unfinished quote").fulltextTokens.map((t) => t.token)).toEqual(
|
||||||
["unfinished quote"]
|
["unfinished quote"]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parenthesis and symbols in fulltext section are just normal characters", () => {
|
it("parenthesis and symbols in fulltext section are just normal characters", () => {
|
||||||
expect(
|
expect(
|
||||||
lex("what's u=p <b(r*t)h>").fulltextTokens.map((t) => t.token)
|
lex("what's u=p <b(r*t)h>").fulltextTokens.map((t) => t.token)
|
||||||
).toEqual(["what's", "u=p", "<b(r*t)h>"]);
|
).toEqual(["what's", "u=p", "<b(r*t)h>"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("operator characters in expressions are separate tokens", () => {
|
it("operator characters in expressions are separate tokens", () => {
|
||||||
expect(
|
expect(
|
||||||
lex("# abc+=-def**-+d").expressionTokens.map((t) => t.token)
|
lex("# abc+=-def**-+d").expressionTokens.map((t) => t.token)
|
||||||
).toEqual(["#", "abc", "+=-", "def", "**-+", "d"]);
|
).toEqual(["#", "abc", "+=-", "def", "**-+", "d"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("escaping special characters", () => {
|
it("escaping special characters", () => {
|
||||||
expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual([
|
expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual([
|
||||||
"hello",
|
"hello",
|
||||||
"#~'",
|
"#~'",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -110,132 +110,132 @@ describe("Lexer fulltext", () => {
|
|||||||
describe("Lexer expression", () => {
|
describe("Lexer expression", () => {
|
||||||
it("simple attribute existence", () => {
|
it("simple attribute existence", () => {
|
||||||
expect(
|
expect(
|
||||||
lex("#label ~relation").expressionTokens.map((t) => t.token)
|
lex("#label ~relation").expressionTokens.map((t) => t.token)
|
||||||
).toEqual(["#label", "~relation"]);
|
).toEqual(["#label", "~relation"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("simple label operators", () => {
|
it("simple label operators", () => {
|
||||||
expect(lex("#label*=*text").expressionTokens.map((t) => t.token)).toEqual([
|
expect(lex("#label*=*text").expressionTokens.map((t) => t.token)).toEqual([
|
||||||
"#label",
|
"#label",
|
||||||
"*=*",
|
"*=*",
|
||||||
"text",
|
"text",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("simple label operator with in quotes", () => {
|
it("simple label operator with in quotes", () => {
|
||||||
expect(lex("#label*=*'text'").expressionTokens).toEqual([
|
expect(lex("#label*=*'text'").expressionTokens).toEqual([
|
||||||
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
||||||
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
|
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
|
||||||
{ token: "text", inQuotes: true, startIndex: 10, endIndex: 13 },
|
{ token: "text", inQuotes: true, startIndex: 10, endIndex: 13 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("simple label operator with param without quotes", () => {
|
it("simple label operator with param without quotes", () => {
|
||||||
expect(lex("#label*=*text").expressionTokens).toEqual([
|
expect(lex("#label*=*text").expressionTokens).toEqual([
|
||||||
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
||||||
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
|
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
|
||||||
{ token: "text", inQuotes: false, startIndex: 9, endIndex: 12 },
|
{ token: "text", inQuotes: false, startIndex: 9, endIndex: 12 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("simple label operator with empty string param", () => {
|
it("simple label operator with empty string param", () => {
|
||||||
expect(lex("#label = ''").expressionTokens).toEqual([
|
expect(lex("#label = ''").expressionTokens).toEqual([
|
||||||
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
|
||||||
{ token: "=", inQuotes: false, startIndex: 7, endIndex: 7 },
|
{ token: "=", inQuotes: false, startIndex: 7, endIndex: 7 },
|
||||||
// weird case for empty strings which ends up with endIndex < startIndex :-(
|
// weird case for empty strings which ends up with endIndex < startIndex :-(
|
||||||
{ token: "", inQuotes: true, startIndex: 10, endIndex: 9 },
|
{ token: "", inQuotes: true, startIndex: 10, endIndex: 9 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("note. prefix also separates fulltext from expression", () => {
|
it("note. prefix also separates fulltext from expression", () => {
|
||||||
expect(
|
expect(
|
||||||
lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(
|
lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(
|
||||||
(t) => t.token
|
(t) => t.token
|
||||||
)
|
)
|
||||||
).toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]);
|
).toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("note. prefix in quotes will note start expression", () => {
|
it("note. prefix in quotes will note start expression", () => {
|
||||||
expect(
|
expect(
|
||||||
lex(`hello fulltext "note.txt"`).expressionTokens.map((t) => t.token)
|
lex(`hello fulltext "note.txt"`).expressionTokens.map((t) => t.token)
|
||||||
).toEqual([]);
|
).toEqual([]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
lex(`hello fulltext "note.txt"`).fulltextTokens.map((t) => t.token)
|
lex(`hello fulltext "note.txt"`).fulltextTokens.map((t) => t.token)
|
||||||
).toEqual(["hello", "fulltext", "note.txt"]);
|
).toEqual(["hello", "fulltext", "note.txt"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("complex expressions with and, or and parenthesis", () => {
|
it("complex expressions with and, or and parenthesis", () => {
|
||||||
expect(
|
expect(
|
||||||
lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map(
|
lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map(
|
||||||
(t) => t.token
|
(t) => t.token
|
||||||
)
|
)
|
||||||
).toEqual([
|
).toEqual([
|
||||||
"#",
|
"#",
|
||||||
"(",
|
"(",
|
||||||
"#label",
|
"#label",
|
||||||
"=",
|
"=",
|
||||||
"text",
|
"text",
|
||||||
"or",
|
"or",
|
||||||
"#second",
|
"#second",
|
||||||
"=",
|
"=",
|
||||||
"text",
|
"text",
|
||||||
")",
|
")",
|
||||||
"and",
|
"and",
|
||||||
"~relation",
|
"~relation",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dot separated properties", () => {
|
it("dot separated properties", () => {
|
||||||
expect(
|
expect(
|
||||||
lex(
|
lex(
|
||||||
`# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'`
|
`# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'`
|
||||||
).expressionTokens.map((t) => t.token)
|
).expressionTokens.map((t) => t.token)
|
||||||
).toEqual([
|
).toEqual([
|
||||||
"#",
|
"#",
|
||||||
"~author",
|
"~author",
|
||||||
".",
|
".",
|
||||||
"title",
|
"title",
|
||||||
"=",
|
"=",
|
||||||
"hugh howey",
|
"hugh howey",
|
||||||
"and",
|
"and",
|
||||||
"note",
|
"note",
|
||||||
".",
|
".",
|
||||||
"book title",
|
"book title",
|
||||||
"=",
|
"=",
|
||||||
"silo",
|
"silo",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("negation of label and relation", () => {
|
it("negation of label and relation", () => {
|
||||||
expect(
|
expect(
|
||||||
lex(`#!capital ~!neighbor`).expressionTokens.map((t) => t.token)
|
lex(`#!capital ~!neighbor`).expressionTokens.map((t) => t.token)
|
||||||
).toEqual(["#!capital", "~!neighbor"]);
|
).toEqual(["#!capital", "~!neighbor"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("negation of sub-expression", () => {
|
it("negation of sub-expression", () => {
|
||||||
expect(
|
expect(
|
||||||
lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map(
|
lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map(
|
||||||
(t) => t.token
|
(t) => t.token
|
||||||
)
|
)
|
||||||
).toEqual([
|
).toEqual([
|
||||||
"#",
|
"#",
|
||||||
"not",
|
"not",
|
||||||
"(",
|
"(",
|
||||||
"#capital",
|
"#capital",
|
||||||
")",
|
")",
|
||||||
"and",
|
"and",
|
||||||
"note",
|
"note",
|
||||||
".",
|
".",
|
||||||
"noteid",
|
"noteid",
|
||||||
"!=",
|
"!=",
|
||||||
"root",
|
"root",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("order by multiple labels", () => {
|
it("order by multiple labels", () => {
|
||||||
expect(lex(`# orderby #a,#b`).expressionTokens.map((t) => t.token)).toEqual(
|
expect(lex(`# orderby #a,#b`).expressionTokens.map((t) => t.token)).toEqual(
|
||||||
["#", "orderby", "#a", ",", "#b"]
|
["#", "orderby", "#a", ",", "#b"]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -243,14 +243,14 @@ describe("Lexer expression", () => {
|
|||||||
describe("Lexer invalid queries and edge cases", () => {
|
describe("Lexer invalid queries and edge cases", () => {
|
||||||
it("concatenated attributes", () => {
|
it("concatenated attributes", () => {
|
||||||
expect(lex("#label~relation").expressionTokens.map((t) => t.token)).toEqual(
|
expect(lex("#label~relation").expressionTokens.map((t) => t.token)).toEqual(
|
||||||
["#label", "~relation"]
|
["#label", "~relation"]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("trailing escape \\", () => {
|
it("trailing escape \\", () => {
|
||||||
expect(lex("abc \\").fulltextTokens.map((t) => t.token)).toEqual([
|
expect(lex("abc \\").fulltextTokens.map((t) => t.token)).toEqual([
|
||||||
"abc",
|
"abc",
|
||||||
"\\",
|
"\\",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -49,4 +49,4 @@ describe("Markdown export", () => {
|
|||||||
|
|
||||||
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
expect(markdownExportService.toMarkdown(html)).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -19,11 +19,11 @@ function describeEtapi(
|
|||||||
let appProcess: ReturnType<typeof child_process.spawn>;
|
let appProcess: ReturnType<typeof child_process.spawn>;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
specDefinitions();
|
specDefinitions();
|
||||||
@ -34,7 +34,7 @@ async function getEtapiResponse(url: string): Promise<Response> {
|
|||||||
return await fetch(`${HOST}/etapi/${url}`, {
|
return await fetch(`${HOST}/etapi/${url}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: getEtapiAuthorizationHeader(),
|
Authorization: getEtapiAuthorizationHeader(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -48,7 +48,7 @@ async function getEtapiContent(url: string): Promise<Response> {
|
|||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: getEtapiAuthorizationHeader(),
|
Authorization: getEtapiAuthorizationHeader(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -64,8 +64,8 @@ async function postEtapi(
|
|||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: getEtapiAuthorizationHeader(),
|
Authorization: getEtapiAuthorizationHeader(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
@ -79,8 +79,8 @@ async function postEtapiContent(
|
|||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
Authorization: getEtapiAuthorizationHeader(),
|
Authorization: getEtapiAuthorizationHeader(),
|
||||||
},
|
},
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
@ -97,8 +97,8 @@ async function putEtapi(
|
|||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: getEtapiAuthorizationHeader(),
|
Authorization: getEtapiAuthorizationHeader(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
@ -112,8 +112,8 @@ async function putEtapiContent(
|
|||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/octet-stream",
|
"Content-Type": "application/octet-stream",
|
||||||
Authorization: getEtapiAuthorizationHeader(),
|
Authorization: getEtapiAuthorizationHeader(),
|
||||||
},
|
},
|
||||||
body: data,
|
body: data,
|
||||||
});
|
});
|
||||||
@ -130,8 +130,8 @@ async function patchEtapi(
|
|||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: getEtapiAuthorizationHeader(),
|
Authorization: getEtapiAuthorizationHeader(),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
@ -142,7 +142,7 @@ async function deleteEtapi(url: string): Promise<any> {
|
|||||||
const response = await fetch(`${HOST}/etapi/${url}`, {
|
const response = await fetch(`${HOST}/etapi/${url}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: getEtapiAuthorizationHeader(),
|
Authorization: getEtapiAuthorizationHeader(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return await processEtapiResponse(response);
|
return await processEtapiResponse(response);
|
||||||
|
@ -11,4 +11,4 @@ Hello
|
|||||||
world
|
world
|
||||||
123`);
|
123`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export function trimIndentation(strings: TemplateStringsArray) {
|
export function trimIndentation(strings: TemplateStringsArray) {
|
||||||
const str = strings.toString();
|
const str = strings.toString();
|
||||||
|
|
||||||
// Count the number of spaces on the first line.
|
// Count the number of spaces on the first line.
|
||||||
@ -6,10 +6,10 @@ export function trimIndentation(strings: TemplateStringsArray) {
|
|||||||
while (str.charAt(numSpaces) == ' ' && numSpaces < str.length) {
|
while (str.charAt(numSpaces) == ' ' && numSpaces < str.length) {
|
||||||
numSpaces++;
|
numSpaces++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim the indentation of the first line in all the lines.
|
// Trim the indentation of the first line in all the lines.
|
||||||
const lines = str.split("\n");
|
const lines = str.split("\n");
|
||||||
const output = [];
|
const output = [];
|
||||||
for (let i=0; i<lines.length; i++) {
|
for (let i=0; i<lines.length; i++) {
|
||||||
let numSpacesLine = 0;
|
let numSpacesLine = 0;
|
||||||
while (str.charAt(numSpacesLine) == ' ' && numSpacesLine < str.length) {
|
while (str.charAt(numSpacesLine) == ' ' && numSpacesLine < str.length) {
|
||||||
@ -18,4 +18,4 @@ export function trimIndentation(strings: TemplateStringsArray) {
|
|||||||
output.push(lines[i].substring(numSpacesLine));
|
output.push(lines[i].substring(numSpacesLine));
|
||||||
}
|
}
|
||||||
return output.join("\n");
|
return output.join("\n");
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ export default class Becca {
|
|||||||
this.notes = {};
|
this.notes = {};
|
||||||
this.branches = {};
|
this.branches = {};
|
||||||
this.childParentToBranch = {};
|
this.childParentToBranch = {};
|
||||||
this.attributes = {};
|
this.attributes = {};
|
||||||
this.attributeIndex = {};
|
this.attributeIndex = {};
|
||||||
this.options = {};
|
this.options = {};
|
||||||
this.etapiTokens = {};
|
this.etapiTokens = {};
|
||||||
@ -172,9 +172,9 @@ export default class Becca {
|
|||||||
|
|
||||||
const query = opts.includeContentLength
|
const query = opts.includeContentLength
|
||||||
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
|
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
|
||||||
FROM attachments
|
FROM attachments
|
||||||
JOIN blobs USING (blobId)
|
JOIN blobs USING (blobId)
|
||||||
WHERE attachmentId = ? AND isDeleted = 0`
|
WHERE attachmentId = ? AND isDeleted = 0`
|
||||||
: `SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`;
|
: `SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`;
|
||||||
|
|
||||||
return sql.getRows<AttachmentRow>(query, [attachmentId])
|
return sql.getRows<AttachmentRow>(query, [attachmentId])
|
||||||
@ -279,7 +279,7 @@ export default class Becca {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface contains the data that is shared across all the objects of a given derived class of {@link AbstractBeccaEntity}.
|
* This interface contains the data that is shared across all the objects of a given derived class of {@link AbstractBeccaEntity}.
|
||||||
* For example, all BAttributes will share their content, but all BBranches will have another set of this data.
|
* For example, all BAttributes will share their content, but all BBranches will have another set of this data.
|
||||||
*/
|
*/
|
||||||
export interface ConstructorData<T extends AbstractBeccaEntity<T>> {
|
export interface ConstructorData<T extends AbstractBeccaEntity<T>> {
|
||||||
primaryKeyName: string;
|
primaryKeyName: string;
|
||||||
@ -299,4 +299,4 @@ export interface NotePojo {
|
|||||||
dateModified?: string;
|
dateModified?: string;
|
||||||
utcDateCreated: string;
|
utcDateCreated: string;
|
||||||
utcDateModified?: string;
|
utcDateModified?: string;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ interface ContentOpts {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for all backend entities.
|
* Base class for all backend entities.
|
||||||
*
|
*
|
||||||
* @type T the same entity type needed for self-reference in {@link ConstructorData}.
|
* @type T the same entity type needed for self-reference in {@link ConstructorData}.
|
||||||
*/
|
*/
|
||||||
abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
||||||
@ -27,7 +27,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
|||||||
utcDateModified?: string;
|
utcDateModified?: string;
|
||||||
dateCreated?: string;
|
dateCreated?: string;
|
||||||
dateModified?: string;
|
dateModified?: string;
|
||||||
|
|
||||||
utcDateCreated!: string;
|
utcDateCreated!: string;
|
||||||
|
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
@ -99,15 +99,15 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves entity - executes SQL, but doesn't commit the transaction on its own
|
* Saves entity - executes SQL, but doesn't commit the transaction on its own
|
||||||
*/
|
*/
|
||||||
save(opts?: {}): this {
|
save(opts?: {}): this {
|
||||||
const constructorData = (this.constructor as unknown as ConstructorData<T>);
|
const constructorData = (this.constructor as unknown as ConstructorData<T>);
|
||||||
const entityName = constructorData.entityName;
|
const entityName = constructorData.entityName;
|
||||||
const primaryKeyName = constructorData.primaryKeyName;
|
const primaryKeyName = constructorData.primaryKeyName;
|
||||||
|
|
||||||
const isNewEntity = !(this as any)[primaryKeyName];
|
const isNewEntity = !(this as any)[primaryKeyName];
|
||||||
|
|
||||||
this.beforeSaving(opts);
|
this.beforeSaving(opts);
|
||||||
|
|
||||||
const pojo = this.getPojoToSave();
|
const pojo = this.getPojoToSave();
|
||||||
@ -160,7 +160,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
|||||||
if (protectedSessionService.isProtectedSessionAvailable()) {
|
if (protectedSessionService.isProtectedSessionAvailable()) {
|
||||||
const encryptedContent = protectedSessionService.encrypt(content);
|
const encryptedContent = protectedSessionService.encrypt(content);
|
||||||
if (!encryptedContent) {
|
if (!encryptedContent) {
|
||||||
throw new Error(`Unable to encrypt the content of the entity.`);
|
throw new Error(`Unable to encrypt the content of the entity.`);
|
||||||
}
|
}
|
||||||
content = encryptedContent;
|
content = encryptedContent;
|
||||||
} else {
|
} else {
|
||||||
@ -216,11 +216,11 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
|||||||
|
|
||||||
private saveBlob(content: string | Buffer, unencryptedContentForHashCalculation: string | Buffer, opts: ContentOpts = {}) {
|
private saveBlob(content: string | Buffer, unencryptedContentForHashCalculation: string | Buffer, opts: ContentOpts = {}) {
|
||||||
/*
|
/*
|
||||||
* We're using the unencrypted blob for the hash calculation, because otherwise the random IV would
|
* We're using the unencrypted blob for the hash calculation, because otherwise the random IV would
|
||||||
* cause every content blob to be unique which would balloon the database size (esp. with revisioning).
|
* cause every content blob to be unique which would balloon the database size (esp. with revisioning).
|
||||||
* This has minor security implications (it's easy to infer that given content is shared between different
|
* This has minor security implications (it's easy to infer that given content is shared between different
|
||||||
* notes/attachments), but the trade-off comes out clearly positive.
|
* notes/attachments), but the trade-off comes out clearly positive.
|
||||||
*/
|
*/
|
||||||
const newBlobId = utils.hashedBlobId(unencryptedContentForHashCalculation);
|
const newBlobId = utils.hashedBlobId(unencryptedContentForHashCalculation);
|
||||||
const blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]);
|
const blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]);
|
||||||
|
|
||||||
@ -261,7 +261,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
|||||||
return newBlobId;
|
return newBlobId;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getContent(): string | Buffer {
|
protected _getContent(): string | Buffer {
|
||||||
const row = sql.getRow<{ content: string | Buffer }>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
|
const row = sql.getRow<{ content: string | Buffer }>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
@ -273,10 +273,10 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark the entity as (soft) deleted. It will be completely erased later.
|
* Mark the entity as (soft) deleted. It will be completely erased later.
|
||||||
*
|
*
|
||||||
* This is a low-level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
|
* This is a low-level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
|
||||||
*/
|
*/
|
||||||
markAsDeleted(deleteId: string | null = null) {
|
markAsDeleted(deleteId: string | null = null) {
|
||||||
const constructorData = (this.constructor as unknown as ConstructorData<T>);
|
const constructorData = (this.constructor as unknown as ConstructorData<T>);
|
||||||
const entityId = (this as any)[constructorData.primaryKeyName];
|
const entityId = (this as any)[constructorData.primaryKeyName];
|
||||||
@ -285,7 +285,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
|||||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||||
|
|
||||||
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ?
|
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ?
|
||||||
WHERE ${constructorData.primaryKeyName} = ?`,
|
WHERE ${constructorData.primaryKeyName} = ?`,
|
||||||
[deleteId, this.utcDateModified, entityId]);
|
[deleteId, this.utcDateModified, entityId]);
|
||||||
|
|
||||||
if (this.dateModified) {
|
if (this.dateModified) {
|
||||||
@ -310,7 +310,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
|
|||||||
this.utcDateModified = dateUtils.utcNowDateTime();
|
this.utcDateModified = dateUtils.utcNowDateTime();
|
||||||
|
|
||||||
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ?
|
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ?
|
||||||
WHERE ${constructorData.primaryKeyName} = ?`,
|
WHERE ${constructorData.primaryKeyName} = ?`,
|
||||||
[this.utcDateModified, entityId]);
|
[this.utcDateModified, entityId]);
|
||||||
|
|
||||||
log.info(`Marking ${entityName} ${entityId} as deleted`);
|
log.info(`Marking ${entityName} ${entityId} as deleted`);
|
||||||
|
@ -170,7 +170,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
|
|||||||
|
|
||||||
if (this.role === 'image' && parentNote.type === 'text') {
|
if (this.role === 'image' && parentNote.type === 'text') {
|
||||||
const origContent = parentNote.getContent();
|
const origContent = parentNote.getContent();
|
||||||
|
|
||||||
if (typeof origContent !== "string") {
|
if (typeof origContent !== "string") {
|
||||||
throw new Error(`Note with ID '${note.noteId} has a text type but non-string content.`);
|
throw new Error(`Note with ID '${note.noteId} has a text type but non-string content.`);
|
||||||
}
|
}
|
||||||
@ -201,8 +201,8 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
|
|||||||
|
|
||||||
if (this.position === undefined || this.position === null) {
|
if (this.position === undefined || this.position === null) {
|
||||||
this.position = 10 + sql.getValue<number>(`SELECT COALESCE(MAX(position), 0)
|
this.position = 10 + sql.getValue<number>(`SELECT COALESCE(MAX(position), 0)
|
||||||
FROM attachments
|
FROM attachments
|
||||||
WHERE ownerId = ?`, [this.noteId]);
|
WHERE ownerId = ?`, [this.noteId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dateModified = dateUtils.localNowDateTime();
|
this.dateModified = dateUtils.localNowDateTime();
|
||||||
|
@ -87,7 +87,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
|
|||||||
if (!childNote.parents.includes(parentNote)) {
|
if (!childNote.parents.includes(parentNote)) {
|
||||||
childNote.parents.push(parentNote);
|
childNote.parents.push(parentNote);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parentNote.children.includes(childNote)) {
|
if (!parentNote.children.includes(childNote)) {
|
||||||
parentNote.children.push(childNote);
|
parentNote.children.push(childNote);
|
||||||
}
|
}
|
||||||
@ -122,23 +122,23 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Branch is weak when its existence should not hinder deletion of its note.
|
* Branch is weak when its existence should not hinder deletion of its note.
|
||||||
* As a result, note with only weak branches should be immediately deleted.
|
* As a result, note with only weak branches should be immediately deleted.
|
||||||
* An example is shared or bookmarked clones - they are created automatically and exist for technical reasons,
|
* An example is shared or bookmarked clones - they are created automatically and exist for technical reasons,
|
||||||
* not as user-intended actions. From user perspective, they don't count as real clones and for the purpose
|
* not as user-intended actions. From user perspective, they don't count as real clones and for the purpose
|
||||||
* of deletion should not act as a clone.
|
* of deletion should not act as a clone.
|
||||||
*/
|
*/
|
||||||
get isWeak() {
|
get isWeak() {
|
||||||
return ['_share', '_lbBookmarks'].includes(this.parentNoteId);
|
return ['_share', '_lbBookmarks'].includes(this.parentNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a branch. If this is a last note's branch, delete the note as well.
|
* Delete a branch. If this is a last note's branch, delete the note as well.
|
||||||
*
|
*
|
||||||
* @param deleteId - optional delete identified
|
* @param deleteId - optional delete identified
|
||||||
*
|
*
|
||||||
* @returns true if note has been deleted, false otherwise
|
* @returns true if note has been deleted, false otherwise
|
||||||
*/
|
*/
|
||||||
deleteBranch(deleteId?: string, taskContext?: TaskContext): boolean {
|
deleteBranch(deleteId?: string, taskContext?: TaskContext): boolean {
|
||||||
if (!deleteId) {
|
if (!deleteId) {
|
||||||
deleteId = utils.randomString(10);
|
deleteId = utils.randomString(10);
|
||||||
|
@ -178,15 +178,15 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns <i>strong</i> (as opposed to <i>weak</i>) parent branches. See isWeak for details.
|
* Returns <i>strong</i> (as opposed to <i>weak</i>) parent branches. See isWeak for details.
|
||||||
*/
|
*/
|
||||||
getStrongParentBranches() {
|
getStrongParentBranches() {
|
||||||
return this.getParentBranches().filter(branch => !branch.isWeak);
|
return this.getParentBranches().filter(branch => !branch.isWeak);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use getParentBranches() instead
|
* @deprecated use getParentBranches() instead
|
||||||
*/
|
*/
|
||||||
getBranches() {
|
getBranches() {
|
||||||
return this.parentBranches;
|
return this.parentBranches;
|
||||||
}
|
}
|
||||||
@ -209,20 +209,20 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note content has quite special handling - it's not a separate entity, but a lazily loaded
|
* Note content has quite special handling - it's not a separate entity, but a lazily loaded
|
||||||
* part of Note entity with its own sync. Reasons behind this hybrid design has been:
|
* part of Note entity with its own sync. Reasons behind this hybrid design has been:
|
||||||
*
|
*
|
||||||
* - content can be quite large, and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search
|
* - content can be quite large, and it's not necessary to load it / fill memory for any note access even if we don't need a content, especially for bulk operations like search
|
||||||
* - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
|
* - changes in the note metadata or title should not trigger note content sync (so we keep separate utcDateModified and entity changes records)
|
||||||
* - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity)
|
* - but to the user note content and title changes are one and the same - single dateModified (so all changes must go through Note and content is not a separate entity)
|
||||||
*/
|
*/
|
||||||
getContent() {
|
getContent() {
|
||||||
return this._getContent();
|
return this._getContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Error in case of invalid JSON
|
* @throws Error in case of invalid JSON
|
||||||
*/
|
*/
|
||||||
getJsonContent(): any | null {
|
getJsonContent(): any | null {
|
||||||
const content = this.getContent();
|
const content = this.getContent();
|
||||||
|
|
||||||
@ -327,13 +327,13 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beware that the method must not create a copy of the array, but actually returns its internal array
|
* Beware that the method must not create a copy of the array, but actually returns its internal array
|
||||||
* (for performance reasons)
|
* (for performance reasons)
|
||||||
*
|
*
|
||||||
* @param type - (optional) attribute type to filter
|
* @param type - (optional) attribute type to filter
|
||||||
* @param name - (optional) attribute name to filter
|
* @param name - (optional) attribute name to filter
|
||||||
* @returns all note's attributes, including inherited ones
|
* @returns all note's attributes, including inherited ones
|
||||||
*/
|
*/
|
||||||
getAttributes(type?: string, name?: string): BAttribute[] {
|
getAttributes(type?: string, name?: string): BAttribute[] {
|
||||||
this.__validateTypeName(type, name);
|
this.__validateTypeName(type, name);
|
||||||
this.__ensureAttributeCacheIsAvailable();
|
this.__ensureAttributeCacheIsAvailable();
|
||||||
@ -468,18 +468,18 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @param value - label value
|
* @param value - label value
|
||||||
* @returns true if label exists (including inherited)
|
* @returns true if label exists (including inherited)
|
||||||
*/
|
*/
|
||||||
hasLabel(name: string, value?: string): boolean {
|
hasLabel(name: string, value?: string): boolean {
|
||||||
return this.hasAttribute(LABEL, name, value);
|
return this.hasAttribute(LABEL, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @returns true if label exists (including inherited) and does not have "false" value.
|
* @returns true if label exists (including inherited) and does not have "false" value.
|
||||||
*/
|
*/
|
||||||
isLabelTruthy(name: string): boolean {
|
isLabelTruthy(name: string): boolean {
|
||||||
const label = this.getLabel(name);
|
const label = this.getLabel(name);
|
||||||
|
|
||||||
@ -491,112 +491,112 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @param value - label value
|
* @param value - label value
|
||||||
* @returns true if label exists (excluding inherited)
|
* @returns true if label exists (excluding inherited)
|
||||||
*/
|
*/
|
||||||
hasOwnedLabel(name: string, value?: string): boolean {
|
hasOwnedLabel(name: string, value?: string): boolean {
|
||||||
return this.hasOwnedAttribute(LABEL, name, value);
|
return this.hasOwnedAttribute(LABEL, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @param value - relation value
|
* @param value - relation value
|
||||||
* @returns true if relation exists (including inherited)
|
* @returns true if relation exists (including inherited)
|
||||||
*/
|
*/
|
||||||
hasRelation(name: string, value?: string): boolean {
|
hasRelation(name: string, value?: string): boolean {
|
||||||
return this.hasAttribute(RELATION, name, value);
|
return this.hasAttribute(RELATION, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @param value - relation value
|
* @param value - relation value
|
||||||
* @returns true if relation exists (excluding inherited)
|
* @returns true if relation exists (excluding inherited)
|
||||||
*/
|
*/
|
||||||
hasOwnedRelation(name: string, value?: string): boolean {
|
hasOwnedRelation(name: string, value?: string): boolean {
|
||||||
return this.hasOwnedAttribute(RELATION, name, value);
|
return this.hasOwnedAttribute(RELATION, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @returns label if it exists, null otherwise
|
* @returns label if it exists, null otherwise
|
||||||
*/
|
*/
|
||||||
getLabel(name: string): BAttribute | null {
|
getLabel(name: string): BAttribute | null {
|
||||||
return this.getAttribute(LABEL, name);
|
return this.getAttribute(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @returns label if it exists, null otherwise
|
* @returns label if it exists, null otherwise
|
||||||
*/
|
*/
|
||||||
getOwnedLabel(name: string): BAttribute | null {
|
getOwnedLabel(name: string): BAttribute | null {
|
||||||
return this.getOwnedAttribute(LABEL, name);
|
return this.getOwnedAttribute(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @returns relation if it exists, null otherwise
|
* @returns relation if it exists, null otherwise
|
||||||
*/
|
*/
|
||||||
getRelation(name: string): BAttribute | null {
|
getRelation(name: string): BAttribute | null {
|
||||||
return this.getAttribute(RELATION, name);
|
return this.getAttribute(RELATION, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @returns relation if it exists, null otherwise
|
* @returns relation if it exists, null otherwise
|
||||||
*/
|
*/
|
||||||
getOwnedRelation(name: string): BAttribute | null {
|
getOwnedRelation(name: string): BAttribute | null {
|
||||||
return this.getOwnedAttribute(RELATION, name);
|
return this.getOwnedAttribute(RELATION, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @returns label value if label exists, null otherwise
|
* @returns label value if label exists, null otherwise
|
||||||
*/
|
*/
|
||||||
getLabelValue(name: string): string | null {
|
getLabelValue(name: string): string | null {
|
||||||
return this.getAttributeValue(LABEL, name);
|
return this.getAttributeValue(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @returns label value if label exists, null otherwise
|
* @returns label value if label exists, null otherwise
|
||||||
*/
|
*/
|
||||||
getOwnedLabelValue(name: string): string | null {
|
getOwnedLabelValue(name: string): string | null {
|
||||||
return this.getOwnedAttributeValue(LABEL, name);
|
return this.getOwnedAttributeValue(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @returns relation value if relation exists, null otherwise
|
* @returns relation value if relation exists, null otherwise
|
||||||
*/
|
*/
|
||||||
getRelationValue(name: string): string | null {
|
getRelationValue(name: string): string | null {
|
||||||
return this.getAttributeValue(RELATION, name);
|
return this.getAttributeValue(RELATION, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @returns relation value if relation exists, null otherwise
|
* @returns relation value if relation exists, null otherwise
|
||||||
*/
|
*/
|
||||||
getOwnedRelationValue(name: string): string | null {
|
getOwnedRelationValue(name: string): string | null {
|
||||||
return this.getOwnedAttributeValue(RELATION, name);
|
return this.getOwnedAttributeValue(RELATION, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param attribute type (label, relation, etc.)
|
* @param attribute type (label, relation, etc.)
|
||||||
* @param name - attribute name
|
* @param name - attribute name
|
||||||
* @param value - attribute value
|
* @param value - attribute value
|
||||||
* @returns true if note has an attribute with given type and name (excluding inherited)
|
* @returns true if note has an attribute with given type and name (excluding inherited)
|
||||||
*/
|
*/
|
||||||
hasOwnedAttribute(type: string, name: string, value?: string): boolean {
|
hasOwnedAttribute(type: string, name: string, value?: string): boolean {
|
||||||
return !!this.getOwnedAttribute(type, name, value);
|
return !!this.getOwnedAttribute(type, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param type - attribute type (label, relation, etc.)
|
* @param type - attribute type (label, relation, etc.)
|
||||||
* @param name - attribute name
|
* @param name - attribute name
|
||||||
* @returns attribute of the given type and name. If there are more such attributes, first is returned.
|
* @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.
|
* Returns null if there's no such attribute belonging to this note.
|
||||||
*/
|
*/
|
||||||
getAttribute(type: string, name: string): BAttribute | null {
|
getAttribute(type: string, name: string): BAttribute | null {
|
||||||
const attributes = this.getAttributes();
|
const attributes = this.getAttributes();
|
||||||
|
|
||||||
@ -604,10 +604,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param type - attribute type (label, relation, etc.)
|
* @param type - attribute type (label, relation, etc.)
|
||||||
* @param name - attribute name
|
* @param name - attribute name
|
||||||
* @returns attribute value of given type and name or null if no such attribute exists.
|
* @returns attribute value of given type and name or null if no such attribute exists.
|
||||||
*/
|
*/
|
||||||
getAttributeValue(type: string, name: string): string | null {
|
getAttributeValue(type: string, name: string): string | null {
|
||||||
const attr = this.getAttribute(type, name);
|
const attr = this.getAttribute(type, name);
|
||||||
|
|
||||||
@ -615,10 +615,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param type - attribute type (label, relation, etc.)
|
* @param type - attribute type (label, relation, etc.)
|
||||||
* @param name - attribute name
|
* @param name - attribute name
|
||||||
* @returns attribute value of given type and name or null if no such attribute exists.
|
* @returns attribute value of given type and name or null if no such attribute exists.
|
||||||
*/
|
*/
|
||||||
getOwnedAttributeValue(type: string, name: string): string | null {
|
getOwnedAttributeValue(type: string, name: string): string | null {
|
||||||
const attr = this.getOwnedAttribute(type, name);
|
const attr = this.getOwnedAttribute(type, name);
|
||||||
|
|
||||||
@ -626,62 +626,62 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name to filter
|
* @param name - label name to filter
|
||||||
* @returns all note's labels (attributes with type label), including inherited ones
|
* @returns all note's labels (attributes with type label), including inherited ones
|
||||||
*/
|
*/
|
||||||
getLabels(name?: string): BAttribute[] {
|
getLabels(name?: string): BAttribute[] {
|
||||||
return this.getAttributes(LABEL, name);
|
return this.getAttributes(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name to filter
|
* @param name - label name to filter
|
||||||
* @returns all note's label values, including inherited ones
|
* @returns all note's label values, including inherited ones
|
||||||
*/
|
*/
|
||||||
getLabelValues(name: string): string[] {
|
getLabelValues(name: string): string[] {
|
||||||
return this.getLabels(name).map(l => l.value);
|
return this.getLabels(name).map(l => l.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name to filter
|
* @param name - label name to filter
|
||||||
* @returns all note's labels (attributes with type label), excluding inherited ones
|
* @returns all note's labels (attributes with type label), excluding inherited ones
|
||||||
*/
|
*/
|
||||||
getOwnedLabels(name: string): BAttribute[] {
|
getOwnedLabels(name: string): BAttribute[] {
|
||||||
return this.getOwnedAttributes(LABEL, name);
|
return this.getOwnedAttributes(LABEL, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - label name to filter
|
* @param name - label name to filter
|
||||||
* @returns all note's label values, excluding inherited ones
|
* @returns all note's label values, excluding inherited ones
|
||||||
*/
|
*/
|
||||||
getOwnedLabelValues(name: string): string[] {
|
getOwnedLabelValues(name: string): string[] {
|
||||||
return this.getOwnedAttributes(LABEL, name).map(l => l.value);
|
return this.getOwnedAttributes(LABEL, name).map(l => l.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name to filter
|
* @param name - relation name to filter
|
||||||
* @returns all note's relations (attributes with type relation), including inherited ones
|
* @returns all note's relations (attributes with type relation), including inherited ones
|
||||||
*/
|
*/
|
||||||
getRelations(name?: string): BAttribute[] {
|
getRelations(name?: string): BAttribute[] {
|
||||||
return this.getAttributes(RELATION, name);
|
return this.getAttributes(RELATION, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param name - relation name to filter
|
* @param name - relation name to filter
|
||||||
* @returns all note's relations (attributes with type relation), excluding inherited ones
|
* @returns all note's relations (attributes with type relation), excluding inherited ones
|
||||||
*/
|
*/
|
||||||
getOwnedRelations(name?: string | null): BAttribute[] {
|
getOwnedRelations(name?: string | null): BAttribute[] {
|
||||||
return this.getOwnedAttributes(RELATION, name);
|
return this.getOwnedAttributes(RELATION, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beware that the method must not create a copy of the array, but actually returns its internal array
|
* Beware that the method must not create a copy of the array, but actually returns its internal array
|
||||||
* (for performance reasons)
|
* (for performance reasons)
|
||||||
*
|
*
|
||||||
* @param type - (optional) attribute type to filter
|
* @param type - (optional) attribute type to filter
|
||||||
* @param name - (optional) attribute name to filter
|
* @param name - (optional) attribute name to filter
|
||||||
* @param value - (optional) attribute value to filter
|
* @param value - (optional) attribute value to filter
|
||||||
* @returns note's "owned" attributes - excluding inherited ones
|
* @returns note's "owned" attributes - excluding inherited ones
|
||||||
*/
|
*/
|
||||||
getOwnedAttributes(type: string | null = null, name: string | null = null, value: string | null = null) {
|
getOwnedAttributes(type: string | null = null, name: string | null = null, value: string | null = null) {
|
||||||
this.__validateTypeName(type, name);
|
this.__validateTypeName(type, name);
|
||||||
|
|
||||||
@ -703,10 +703,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns attribute belonging to this specific note (excludes inherited attributes)
|
* @returns attribute belonging to this specific note (excludes inherited attributes)
|
||||||
*
|
*
|
||||||
* This method can be significantly faster than the getAttribute()
|
* This method can be significantly faster than the getAttribute()
|
||||||
*/
|
*/
|
||||||
getOwnedAttribute(type: string, name: string, value: string | null = null) {
|
getOwnedAttribute(type: string, name: string, value: string | null = null) {
|
||||||
const attrs = this.getOwnedAttributes(type, name, value);
|
const attrs = this.getOwnedAttributes(type, name, value);
|
||||||
|
|
||||||
@ -776,12 +776,12 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is used for:
|
* This is used for:
|
||||||
* - fast searching
|
* - fast searching
|
||||||
* - note similarity evaluation
|
* - note similarity evaluation
|
||||||
*
|
*
|
||||||
* @returns - returns flattened textual representation of note, prefixes and attributes
|
* @returns - returns flattened textual representation of note, prefixes and attributes
|
||||||
*/
|
*/
|
||||||
getFlatText() {
|
getFlatText() {
|
||||||
if (!this.__flatTextCache) {
|
if (!this.__flatTextCache) {
|
||||||
this.__flatTextCache = `${this.noteId} ${this.type} ${this.mime} `;
|
this.__flatTextCache = `${this.noteId} ${this.type} ${this.mime} `;
|
||||||
@ -1077,7 +1077,7 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @returns returns only notes which are templated, does not include their subtrees
|
/** @returns returns only notes which are templated, does not include their subtrees
|
||||||
* in effect returns notes which are influenced by note's non-inheritable attributes */
|
* in effect returns notes which are influenced by note's non-inheritable attributes */
|
||||||
getInheritingNotes(): BNote[] {
|
getInheritingNotes(): BNote[] {
|
||||||
const arr: BNote[] = [this];
|
const arr: BNote[] = [this];
|
||||||
|
|
||||||
@ -1120,10 +1120,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
|
|
||||||
const query = opts.includeContentLength
|
const query = opts.includeContentLength
|
||||||
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
|
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
|
||||||
FROM attachments
|
FROM attachments
|
||||||
JOIN blobs USING (blobId)
|
JOIN blobs USING (blobId)
|
||||||
WHERE ownerId = ? AND isDeleted = 0
|
WHERE ownerId = ? AND isDeleted = 0
|
||||||
ORDER BY position`
|
ORDER BY position`
|
||||||
: `SELECT * FROM attachments WHERE ownerId = ? AND isDeleted = 0 ORDER BY position`;
|
: `SELECT * FROM attachments WHERE ownerId = ? AND isDeleted = 0 ORDER BY position`;
|
||||||
|
|
||||||
return sql.getRows<AttachmentRow>(query, [this.noteId])
|
return sql.getRows<AttachmentRow>(query, [this.noteId])
|
||||||
@ -1135,9 +1135,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
|
|
||||||
const query = opts.includeContentLength
|
const query = opts.includeContentLength
|
||||||
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
|
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
|
||||||
FROM attachments
|
FROM attachments
|
||||||
JOIN blobs USING (blobId)
|
JOIN blobs USING (blobId)
|
||||||
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
|
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
|
||||||
: `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
|
: `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
|
||||||
|
|
||||||
return sql.getRows<AttachmentRow>(query, [this.noteId, attachmentId])
|
return sql.getRows<AttachmentRow>(query, [this.noteId, attachmentId])
|
||||||
@ -1147,10 +1147,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
getAttachmentsByRole(role: string): BAttachment[] {
|
getAttachmentsByRole(role: string): BAttachment[] {
|
||||||
return sql.getRows<AttachmentRow>(`
|
return sql.getRows<AttachmentRow>(`
|
||||||
SELECT attachments.*
|
SELECT attachments.*
|
||||||
FROM attachments
|
FROM attachments
|
||||||
WHERE ownerId = ?
|
WHERE ownerId = ?
|
||||||
AND role = ?
|
AND role = ?
|
||||||
AND isDeleted = 0
|
AND isDeleted = 0
|
||||||
ORDER BY position`, [this.noteId, role])
|
ORDER BY position`, [this.noteId, role])
|
||||||
.map(row => new BAttachment(row));
|
.map(row => new BAttachment(row));
|
||||||
}
|
}
|
||||||
@ -1161,10 +1161,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
|
* Gives all possible note paths leading to this note. Paths containing search note are ignored (could form cycles)
|
||||||
*
|
*
|
||||||
* @returns array of notePaths (each represented by array of noteIds constituting the particular note path)
|
* @returns array of notePaths (each represented by array of noteIds constituting the particular note path)
|
||||||
*/
|
*/
|
||||||
getAllNotePaths(): string[][] {
|
getAllNotePaths(): string[][] {
|
||||||
if (this.noteId === 'root') {
|
if (this.noteId === 'root') {
|
||||||
return [['root']];
|
return [['root']];
|
||||||
@ -1209,19 +1209,19 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a note path considered to be the "best"
|
* Returns a note path considered to be the "best"
|
||||||
*
|
*
|
||||||
* @return array of noteIds constituting the particular note path
|
* @return array of noteIds constituting the particular note path
|
||||||
*/
|
*/
|
||||||
getBestNotePath(hoistedNoteId: string = 'root'): string[] {
|
getBestNotePath(hoistedNoteId: string = 'root'): string[] {
|
||||||
return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
|
return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a note path considered to be the "best"
|
* Returns a note path considered to be the "best"
|
||||||
*
|
*
|
||||||
* @return serialized note path (e.g. 'root/a1h315/js725h')
|
* @return serialized note path (e.g. 'root/a1h315/js725h')
|
||||||
*/
|
*/
|
||||||
getBestNotePathString(hoistedNoteId: string = 'root'): string {
|
getBestNotePathString(hoistedNoteId: string = 'root'): string {
|
||||||
const notePath = this.getBestNotePath(hoistedNoteId);
|
const notePath = this.getBestNotePath(hoistedNoteId);
|
||||||
|
|
||||||
@ -1229,8 +1229,8 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
|
* @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree
|
||||||
*/
|
*/
|
||||||
isHiddenCompletely() {
|
isHiddenCompletely() {
|
||||||
if (this.noteId === 'root') {
|
if (this.noteId === 'root') {
|
||||||
return false;
|
return false;
|
||||||
@ -1250,8 +1250,8 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns true if ancestorNoteId occurs in at least one of the note's paths
|
* @returns true if ancestorNoteId occurs in at least one of the note's paths
|
||||||
*/
|
*/
|
||||||
isDescendantOfNote(ancestorNoteId: string): boolean {
|
isDescendantOfNote(ancestorNoteId: string): boolean {
|
||||||
const notePaths = this.getAllNotePaths();
|
const notePaths = this.getAllNotePaths();
|
||||||
|
|
||||||
@ -1259,12 +1259,12 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update's given attribute's value or creates it if it doesn't exist
|
* Update's given attribute's value or creates it if it doesn't exist
|
||||||
*
|
*
|
||||||
* @param type - attribute type (label, relation, etc.)
|
* @param type - attribute type (label, relation, etc.)
|
||||||
* @param name - attribute name
|
* @param name - attribute name
|
||||||
* @param value - attribute value (optional)
|
* @param value - attribute value (optional)
|
||||||
*/
|
*/
|
||||||
setAttribute(type: AttributeType, name: string, value?: string) {
|
setAttribute(type: AttributeType, name: string, value?: string) {
|
||||||
const attributes = this.getOwnedAttributes();
|
const attributes = this.getOwnedAttributes();
|
||||||
const attr = attributes.find(attr => attr.type === type && attr.name === name);
|
const attr = attributes.find(attr => attr.type === type && attr.name === name);
|
||||||
@ -1288,12 +1288,12 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes given attribute name-value pair if it exists.
|
* Removes given attribute name-value pair if it exists.
|
||||||
*
|
*
|
||||||
* @param type - attribute type (label, relation, etc.)
|
* @param type - attribute type (label, relation, etc.)
|
||||||
* @param name - attribute name
|
* @param name - attribute name
|
||||||
* @param value - attribute value (optional)
|
* @param value - attribute value (optional)
|
||||||
*/
|
*/
|
||||||
removeAttribute(type: string, name: string, value?: string) {
|
removeAttribute(type: string, name: string, value?: string) {
|
||||||
const attributes = this.getOwnedAttributes();
|
const attributes = this.getOwnedAttributes();
|
||||||
|
|
||||||
@ -1305,13 +1305,13 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new attribute to this note. The attribute is saved and returned.
|
* Adds a new attribute to this note. The attribute is saved and returned.
|
||||||
* See addLabel, addRelation for more specific methods.
|
* See addLabel, addRelation for more specific methods.
|
||||||
*
|
*
|
||||||
* @param type - attribute type (label / relation)
|
* @param type - attribute type (label / relation)
|
||||||
* @param name - name of the attribute, not including the leading ~/#
|
* @param name - name of the attribute, not including the leading ~/#
|
||||||
* @param value - value of the attribute - text for labels, target note ID for relations; optional.
|
* @param value - value of the attribute - text for labels, target note ID for relations; optional.
|
||||||
*/
|
*/
|
||||||
addAttribute(type: AttributeType, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute {
|
addAttribute(type: AttributeType, name: string, value: string = "", isInheritable: boolean = false, position: number | null = null): BAttribute {
|
||||||
return new BAttribute({
|
return new BAttribute({
|
||||||
noteId: this.noteId,
|
noteId: this.noteId,
|
||||||
@ -1324,33 +1324,33 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new label to this note. The label attribute is saved and returned.
|
* Adds a new label to this note. The label attribute is saved and returned.
|
||||||
*
|
*
|
||||||
* @param name - name of the label, not including the leading #
|
* @param name - name of the label, not including the leading #
|
||||||
* @param value - text value of the label; optional
|
* @param value - text value of the label; optional
|
||||||
*/
|
*/
|
||||||
addLabel(name: string, value: string = "", isInheritable: boolean = false): BAttribute {
|
addLabel(name: string, value: string = "", isInheritable: boolean = false): BAttribute {
|
||||||
return this.addAttribute(LABEL, name, value, isInheritable);
|
return this.addAttribute(LABEL, name, value, isInheritable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new relation to this note. The relation attribute is saved and
|
* Adds a new relation to this note. The relation attribute is saved and
|
||||||
* returned.
|
* returned.
|
||||||
*
|
*
|
||||||
* @param name - name of the relation, not including the leading ~
|
* @param name - name of the relation, not including the leading ~
|
||||||
*/
|
*/
|
||||||
addRelation(name: string, targetNoteId: string, isInheritable: boolean = false): BAttribute {
|
addRelation(name: string, targetNoteId: string, isInheritable: boolean = false): BAttribute {
|
||||||
return this.addAttribute(RELATION, name, targetNoteId, isInheritable);
|
return this.addAttribute(RELATION, name, targetNoteId, isInheritable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on enabled, the attribute is either set or removed.
|
* Based on enabled, the attribute is either set or removed.
|
||||||
*
|
*
|
||||||
* @param type - attribute type ('relation', 'label' etc.)
|
* @param type - attribute type ('relation', 'label' etc.)
|
||||||
* @param enabled - toggle On or Off
|
* @param enabled - toggle On or Off
|
||||||
* @param name - attribute name
|
* @param name - attribute name
|
||||||
* @param value - attribute value (optional)
|
* @param value - attribute value (optional)
|
||||||
*/
|
*/
|
||||||
toggleAttribute(type: AttributeType, enabled: boolean, name: string, value?: string) {
|
toggleAttribute(type: AttributeType, enabled: boolean, name: string, value?: string) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
this.setAttribute(type, name, value);
|
this.setAttribute(type, name, value);
|
||||||
@ -1361,63 +1361,63 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on enabled, label is either set or removed.
|
* Based on enabled, label is either set or removed.
|
||||||
*
|
*
|
||||||
* @param enabled - toggle On or Off
|
* @param enabled - toggle On or Off
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @param value - label value (optional)
|
* @param value - label value (optional)
|
||||||
*/
|
*/
|
||||||
toggleLabel(enabled: boolean, name: string, value?: string) {
|
toggleLabel(enabled: boolean, name: string, value?: string) {
|
||||||
return this.toggleAttribute(LABEL, enabled, name, value);
|
return this.toggleAttribute(LABEL, enabled, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on enabled, relation is either set or removed.
|
* Based on enabled, relation is either set or removed.
|
||||||
*
|
*
|
||||||
* @param enabled - toggle On or Off
|
* @param enabled - toggle On or Off
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @param value - relation value (noteId)
|
* @param value - relation value (noteId)
|
||||||
*/
|
*/
|
||||||
toggleRelation(enabled: boolean, name: string, value?: string) {
|
toggleRelation(enabled: boolean, name: string, value?: string) {
|
||||||
return this.toggleAttribute(RELATION, enabled, name, value);
|
return this.toggleAttribute(RELATION, enabled, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update's given label's value or creates it if it doesn't exist
|
* Update's given label's value or creates it if it doesn't exist
|
||||||
*
|
*
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @param value label value
|
* @param value label value
|
||||||
*/
|
*/
|
||||||
setLabel(name: string, value?: string) {
|
setLabel(name: string, value?: string) {
|
||||||
return this.setAttribute(LABEL, name, value);
|
return this.setAttribute(LABEL, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update's given relation's value or creates it if it doesn't exist
|
* Update's given relation's value or creates it if it doesn't exist
|
||||||
*
|
*
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @param value - relation value (noteId)
|
* @param value - relation value (noteId)
|
||||||
*/
|
*/
|
||||||
setRelation(name: string, value?: string) {
|
setRelation(name: string, value?: string) {
|
||||||
return this.setAttribute(RELATION, name, value);
|
return this.setAttribute(RELATION, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove label name-value pair, if it exists.
|
* Remove label name-value pair, if it exists.
|
||||||
*
|
*
|
||||||
* @param name - label name
|
* @param name - label name
|
||||||
* @param value - label value
|
* @param value - label value
|
||||||
*/
|
*/
|
||||||
removeLabel(name: string, value?: string) {
|
removeLabel(name: string, value?: string) {
|
||||||
return this.removeAttribute(LABEL, name, value);
|
return this.removeAttribute(LABEL, name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the relation name-value pair, if it exists.
|
* Remove the relation name-value pair, if it exists.
|
||||||
*
|
*
|
||||||
* @param name - relation name
|
* @param name - relation name
|
||||||
* @param value - relation value (noteId)
|
* @param value - relation value (noteId)
|
||||||
*/
|
*/
|
||||||
removeRelation(name: string, value?: string) {
|
removeRelation(name: string, value?: string) {
|
||||||
return this.removeAttribute(RELATION, name, value);
|
return this.removeAttribute(RELATION, name, value);
|
||||||
}
|
}
|
||||||
@ -1468,20 +1468,20 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some notes are eligible for conversion into an attachment of its parent, note must have these properties:
|
* Some notes are eligible for conversion into an attachment of its parent, note must have these properties:
|
||||||
* - it has exactly one target relation
|
* - it has exactly one target relation
|
||||||
* - it has a relation from its parent note
|
* - it has a relation from its parent note
|
||||||
* - it has no children
|
* - it has no children
|
||||||
* - it has no clones
|
* - it has no clones
|
||||||
* - the parent is of type text
|
* - the parent is of type text
|
||||||
* - both notes are either unprotected or user is in protected session
|
* - both notes are either unprotected or user is in protected session
|
||||||
*
|
*
|
||||||
* Currently, works only for image notes.
|
* Currently, works only for image notes.
|
||||||
*
|
*
|
||||||
* In the future, this functionality might get more generic and some of the requirements relaxed.
|
* In the future, this functionality might get more generic and some of the requirements relaxed.
|
||||||
*
|
*
|
||||||
* @returns null if note is not eligible for conversion
|
* @returns null if note is not eligible for conversion
|
||||||
*/
|
*/
|
||||||
convertToParentAttachment(opts: ConvertOpts = { autoConversion: false }): BAttachment | null {
|
convertToParentAttachment(opts: ConvertOpts = { autoConversion: false }): BAttachment | null {
|
||||||
if (!this.isEligibleForConversionToAttachment(opts)) {
|
if (!this.isEligibleForConversionToAttachment(opts)) {
|
||||||
return null;
|
return null;
|
||||||
@ -1518,10 +1518,10 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (Soft) delete a note and all its descendants.
|
* (Soft) delete a note and all its descendants.
|
||||||
*
|
*
|
||||||
* @param deleteId - optional delete identified
|
* @param deleteId - optional delete identified
|
||||||
*/
|
*/
|
||||||
deleteNote(deleteId: string | null = null, taskContext: TaskContext | null = null) {
|
deleteNote(deleteId: string | null = null, taskContext: TaskContext | null = null) {
|
||||||
if (this.isDeleted) {
|
if (this.isDeleted) {
|
||||||
return;
|
return;
|
||||||
@ -1640,9 +1640,9 @@ class BNote extends AbstractBeccaEntity<BNote> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param matchBy - choose by which property we detect if to update an existing attachment.
|
* @param matchBy - choose by which property we detect if to update an existing attachment.
|
||||||
* Supported values are either 'attachmentId' (default) or 'title'
|
* Supported values are either 'attachmentId' (default) or 'title'
|
||||||
*/
|
*/
|
||||||
saveAttachment({attachmentId, role, mime, title, content, position}: AttachmentRow, matchBy = 'attachmentId') {
|
saveAttachment({attachmentId, role, mime, title, content, position}: AttachmentRow, matchBy = 'attachmentId') {
|
||||||
if (!['attachmentId', 'title'].includes(matchBy)) {
|
if (!['attachmentId', 'title'].includes(matchBy)) {
|
||||||
throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`);
|
throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`);
|
||||||
|
@ -81,19 +81,19 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
|
* Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
|
||||||
* part of Revision entity with its own sync. The reason behind this hybrid design is that
|
* part of Revision entity with its own sync. The reason behind this hybrid design is that
|
||||||
* content can be quite large, and it's not necessary to load it / fill memory for any note access even
|
* content can be quite large, and it's not necessary to load it / fill memory for any note access even
|
||||||
* if we don't need a content, especially for bulk operations like search.
|
* if we don't need a content, especially for bulk operations like search.
|
||||||
*
|
*
|
||||||
* This is the same approach as is used for Note's content.
|
* This is the same approach as is used for Note's content.
|
||||||
*/
|
*/
|
||||||
getContent(): string | Buffer {
|
getContent(): string | Buffer {
|
||||||
return this._getContent();
|
return this._getContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws Error in case of invalid JSON */
|
* @throws Error in case of invalid JSON */
|
||||||
getJsonContent(): {} | null {
|
getJsonContent(): {} | null {
|
||||||
const content = this.getContent();
|
const content = this.getContent();
|
||||||
|
|
||||||
@ -121,9 +121,9 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
|
|||||||
getAttachments(): BAttachment[] {
|
getAttachments(): BAttachment[] {
|
||||||
return sql.getRows<AttachmentRow>(`
|
return sql.getRows<AttachmentRow>(`
|
||||||
SELECT attachments.*
|
SELECT attachments.*
|
||||||
FROM attachments
|
FROM attachments
|
||||||
WHERE ownerId = ?
|
WHERE ownerId = ?
|
||||||
AND isDeleted = 0`, [this.revisionId])
|
AND isDeleted = 0`, [this.revisionId])
|
||||||
.map(row => new BAttachment(row));
|
.map(row => new BAttachment(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,9 +132,9 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
|
|||||||
|
|
||||||
const query = opts.includeContentLength
|
const query = opts.includeContentLength
|
||||||
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
|
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
|
||||||
FROM attachments
|
FROM attachments
|
||||||
JOIN blobs USING (blobId)
|
JOIN blobs USING (blobId)
|
||||||
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
|
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
|
||||||
: `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
|
: `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
|
||||||
|
|
||||||
return sql.getRows<AttachmentRow>(query, [this.revisionId, attachmentId])
|
return sql.getRows<AttachmentRow>(query, [this.revisionId, attachmentId])
|
||||||
@ -144,10 +144,10 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
|
|||||||
getAttachmentsByRole(role: string): BAttachment[] {
|
getAttachmentsByRole(role: string): BAttachment[] {
|
||||||
return sql.getRows<AttachmentRow>(`
|
return sql.getRows<AttachmentRow>(`
|
||||||
SELECT attachments.*
|
SELECT attachments.*
|
||||||
FROM attachments
|
FROM attachments
|
||||||
WHERE ownerId = ?
|
WHERE ownerId = ?
|
||||||
AND role = ?
|
AND role = ?
|
||||||
AND isDeleted = 0
|
AND isDeleted = 0
|
||||||
ORDER BY position`, [this.revisionId, role])
|
ORDER BY position`, [this.revisionId, role])
|
||||||
.map(row => new BAttachment(row));
|
.map(row => new BAttachment(row));
|
||||||
}
|
}
|
||||||
@ -158,8 +158,8 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
|
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
|
||||||
*/
|
*/
|
||||||
eraseRevision() {
|
eraseRevision() {
|
||||||
if (this.revisionId) {
|
if (this.revisionId) {
|
||||||
eraseService.eraseRevisions([this.revisionId]);
|
eraseService.eraseRevisions([this.revisionId]);
|
||||||
|
@ -40,7 +40,7 @@ export interface RecentNoteRow {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Database representation of an option.
|
* Database representation of an option.
|
||||||
*
|
*
|
||||||
* Options are key-value pairs that are used to store information such as user preferences (for example
|
* Options are key-value pairs that are used to store information such as user preferences (for example
|
||||||
* the current theme, sync server information), but also information about the state of the application).
|
* the current theme, sync server information), but also information about the state of the application).
|
||||||
*/
|
*/
|
||||||
|
@ -369,12 +369,12 @@ async function findSimilarNotes(noteId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We want to improve the standing of notes which have been created in similar time to each other since
|
* We want to improve the standing of notes which have been created in similar time to each other since
|
||||||
* there's a good chance they are related.
|
* there's a good chance they are related.
|
||||||
*
|
*
|
||||||
* But there's an exception - if they were created really close to each other (within few seconds) then
|
* But there's an exception - if they were created really close to each other (within few seconds) then
|
||||||
* they are probably part of the import and not created by hand - these OTOH should not benefit.
|
* they are probably part of the import and not created by hand - these OTOH should not benefit.
|
||||||
*/
|
*/
|
||||||
const {utcDateCreated} = candidateNote;
|
const {utcDateCreated} = candidateNote;
|
||||||
|
|
||||||
if (utcDateCreated < dateLimits.minExcludedDate || utcDateCreated > dateLimits.maxExcludedDate) {
|
if (utcDateCreated < dateLimits.minExcludedDate || utcDateCreated > dateLimits.maxExcludedDate) {
|
||||||
|
@ -6,4 +6,4 @@ class NotFoundError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotFoundError;
|
export default NotFoundError;
|
||||||
|
@ -6,4 +6,4 @@ class ValidationError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ValidationError;
|
export default ValidationError;
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
export type ValidatorFunc = (obj: unknown) => (string | undefined);
|
export type ValidatorFunc = (obj: unknown) => (string | undefined);
|
||||||
|
|
||||||
export type ValidatorMap = Record<string, ValidatorFunc[]>;
|
export type ValidatorMap = Record<string, ValidatorFunc[]>;
|
||||||
|
4
src/express.d.ts
vendored
4
src/express.d.ts
vendored
@ -8,7 +8,7 @@ export declare module "express-serve-static-core" {
|
|||||||
headers: {
|
headers: {
|
||||||
"x-local-date"?: string;
|
"x-local-date"?: string;
|
||||||
"x-labels"?: string;
|
"x-labels"?: string;
|
||||||
|
|
||||||
"authorization"?: string;
|
"authorization"?: string;
|
||||||
"trilium-cred"?: string;
|
"trilium-cred"?: string;
|
||||||
"x-csrf-token"?: string;
|
"x-csrf-token"?: string;
|
||||||
@ -18,4 +18,4 @@ export declare module "express-serve-static-core" {
|
|||||||
"trilium-hoisted-note-id"?: string;
|
"trilium-hoisted-note-id"?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,4 @@ async function startApplication() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await initializeTranslations();
|
await initializeTranslations();
|
||||||
await startApplication();
|
await startApplication();
|
||||||
|
@ -93,4 +93,4 @@ export default class Shaca {
|
|||||||
|
|
||||||
return (this as any)[camelCaseEntityName][entityId];
|
return (this as any)[camelCaseEntityName][entityId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ function load() {
|
|||||||
SELECT ?
|
SELECT ?
|
||||||
UNION
|
UNION
|
||||||
SELECT branches.noteId FROM branches
|
SELECT branches.noteId FROM branches
|
||||||
JOIN tree ON branches.parentNoteId = tree.noteId
|
JOIN tree ON branches.parentNoteId = tree.noteId
|
||||||
WHERE branches.isDeleted = 0
|
WHERE branches.isDeleted = 0
|
||||||
)
|
)
|
||||||
SELECT noteId FROM tree`, [shareRoot.SHARE_ROOT_NOTE_ID]);
|
SELECT noteId FROM tree`, [shareRoot.SHARE_ROOT_NOTE_ID]);
|
||||||
@ -38,19 +38,19 @@ function load() {
|
|||||||
|
|
||||||
const rawNoteRows = sql.getRawRows<SNoteRow>(`
|
const rawNoteRows = sql.getRawRows<SNoteRow>(`
|
||||||
SELECT noteId, title, type, mime, blobId, utcDateModified, isProtected
|
SELECT noteId, title, type, mime, blobId, utcDateModified, isProtected
|
||||||
FROM notes
|
FROM notes
|
||||||
WHERE isDeleted = 0
|
WHERE isDeleted = 0
|
||||||
AND noteId IN (${noteIdStr})`);
|
AND noteId IN (${noteIdStr})`);
|
||||||
|
|
||||||
for (const row of rawNoteRows) {
|
for (const row of rawNoteRows) {
|
||||||
new SNote(row);
|
new SNote(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawBranchRows = sql.getRawRows<SBranchRow>(`
|
const rawBranchRows = sql.getRawRows<SBranchRow>(`
|
||||||
SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified
|
SELECT branchId, noteId, parentNoteId, prefix, isExpanded, utcDateModified
|
||||||
FROM branches
|
FROM branches
|
||||||
WHERE isDeleted = 0
|
WHERE isDeleted = 0
|
||||||
AND parentNoteId IN (${noteIdStr})
|
AND parentNoteId IN (${noteIdStr})
|
||||||
ORDER BY notePosition`);
|
ORDER BY notePosition`);
|
||||||
|
|
||||||
for (const row of rawBranchRows) {
|
for (const row of rawBranchRows) {
|
||||||
@ -58,20 +58,20 @@ function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rawAttributeRows = sql.getRawRows<SAttributeRow>(`
|
const rawAttributeRows = sql.getRawRows<SAttributeRow>(`
|
||||||
SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified
|
SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified
|
||||||
FROM attributes
|
FROM attributes
|
||||||
WHERE isDeleted = 0
|
WHERE isDeleted = 0
|
||||||
AND noteId IN (${noteIdStr})`);
|
AND noteId IN (${noteIdStr})`);
|
||||||
|
|
||||||
for (const row of rawAttributeRows) {
|
for (const row of rawAttributeRows) {
|
||||||
new SAttribute(row);
|
new SAttribute(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawAttachmentRows = sql.getRawRows<SAttachmentRow>(`
|
const rawAttachmentRows = sql.getRawRows<SAttachmentRow>(`
|
||||||
SELECT attachmentId, ownerId, role, mime, title, blobId, utcDateModified
|
SELECT attachmentId, ownerId, role, mime, title, blobId, utcDateModified
|
||||||
FROM attachments
|
FROM attachments
|
||||||
WHERE isDeleted = 0
|
WHERE isDeleted = 0
|
||||||
AND ownerId IN (${noteIdStr})`);
|
AND ownerId IN (${noteIdStr})`);
|
||||||
|
|
||||||
for (const row of rawAttachmentRows) {
|
for (const row of rawAttachmentRows) {
|
||||||
new SAttachment(row);
|
new SAttachment(row);
|
||||||
|
@ -8,7 +8,7 @@ let dbConnection!: Database.Database;
|
|||||||
|
|
||||||
sql_init.dbReady.then(() => {
|
sql_init.dbReady.then(() => {
|
||||||
dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true });
|
dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true });
|
||||||
|
|
||||||
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
|
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
|
||||||
process.on(eventType, () => {
|
process.on(eventType, () => {
|
||||||
if (dbConnection) {
|
if (dbConnection) {
|
||||||
|
2
src/types.d.ts
vendored
2
src/types.d.ts
vendored
@ -29,4 +29,4 @@ declare module 'joplin-turndown-plugin-gfm' {
|
|||||||
declare module 'is-animated' {
|
declare module 'is-animated' {
|
||||||
function isAnimated(buffer: Buffer): boolean;
|
function isAnimated(buffer: Buffer): boolean;
|
||||||
export default isAnimated;
|
export default isAnimated;
|
||||||
}
|
}
|
||||||
|
32
src/www.ts
32
src/www.ts
@ -42,21 +42,21 @@ startTrilium();
|
|||||||
|
|
||||||
async function startTrilium() {
|
async function startTrilium() {
|
||||||
/**
|
/**
|
||||||
* The intended behavior is to detect when a second instance is running, in that case open the old instance
|
* The intended behavior is to detect when a second instance is running, in that case open the old instance
|
||||||
* instead of the new one. This is complicated by the fact that it is possible to run multiple instances of Trilium
|
* instead of the new one. This is complicated by the fact that it is possible to run multiple instances of Trilium
|
||||||
* if port and data dir are configured separately. This complication is the source of the following weird usage.
|
* if port and data dir are configured separately. This complication is the source of the following weird usage.
|
||||||
*
|
*
|
||||||
* The line below makes sure that the "second-instance" (process in window.ts) is fired. Normally it returns a boolean
|
* The line below makes sure that the "second-instance" (process in window.ts) is fired. Normally it returns a boolean
|
||||||
* indicating whether another instance is running or not, but we ignore that and kill the app only based on the port conflict.
|
* indicating whether another instance is running or not, but we ignore that and kill the app only based on the port conflict.
|
||||||
*
|
*
|
||||||
* A bit weird is that "second-instance" is triggered also on the valid usecases (different port/data dir) and
|
* A bit weird is that "second-instance" is triggered also on the valid usecases (different port/data dir) and
|
||||||
* focuses the existing window. But the new process is start as well and will steal the focus too, it will win, because
|
* focuses the existing window. But the new process is start as well and will steal the focus too, it will win, because
|
||||||
* its startup is slower than focusing the existing process/window. So in the end, it works out without having
|
* its startup is slower than focusing the existing process/window. So in the end, it works out without having
|
||||||
* to do a complex evaluation.
|
* to do a complex evaluation.
|
||||||
*/
|
*/
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
(await import('electron')).app.requestSingleInstanceLock();
|
(await import('electron')).app.requestSingleInstanceLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(JSON.stringify(appInfo, null, 2));
|
log.info(JSON.stringify(appInfo, null, 2));
|
||||||
|
|
||||||
@ -116,8 +116,8 @@ function startHttpServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen on provided port, on all network interfaces.
|
* Listen on provided port, on all network interfaces.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
httpServer.keepAliveTimeout = 120000 * 5;
|
httpServer.keepAliveTimeout = 120000 * 5;
|
||||||
const listenOnTcp = port !== 0;
|
const listenOnTcp = port !== 0;
|
||||||
@ -149,7 +149,7 @@ function startHttpServer() {
|
|||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
import("electron").then(({ app, dialog }) => {
|
import("electron").then(({ app, dialog }) => {
|
||||||
// Not all situations require showing an error dialog. When Trilium is already open,
|
// Not all situations require showing an error dialog. When Trilium is already open,
|
||||||
// clicking the shortcut, the software icon, or the taskbar icon, or when creating a new window,
|
// clicking the shortcut, the software icon, or the taskbar icon, or when creating a new window,
|
||||||
// should simply focus on the existing window or open a new one, without displaying an error message.
|
// should simply focus on the existing window or open a new one, without displaying an error message.
|
||||||
if ("code" in error && error.code == 'EADDRINUSE') {
|
if ("code" in error && error.code == 'EADDRINUSE') {
|
||||||
if (process.argv.includes('--new-window') || !app.requestSingleInstanceLock()) {
|
if (process.argv.includes('--new-window') || !app.requestSingleInstanceLock()) {
|
||||||
|
@ -21,7 +21,7 @@ test.describe('New Todo', () => {
|
|||||||
|
|
||||||
// Make sure the list only has one todo item.
|
// Make sure the list only has one todo item.
|
||||||
await expect(page.getByTestId('todo-title')).toHaveText([
|
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||||
TODO_ITEMS[0]
|
TODO_ITEMS[0]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Create 2nd todo.
|
// Create 2nd todo.
|
||||||
@ -30,8 +30,8 @@ test.describe('New Todo', () => {
|
|||||||
|
|
||||||
// Make sure the list now has two todo items.
|
// Make sure the list now has two todo items.
|
||||||
await expect(page.getByTestId('todo-title')).toHaveText([
|
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||||
TODO_ITEMS[0],
|
TODO_ITEMS[0],
|
||||||
TODO_ITEMS[1]
|
TODO_ITEMS[1]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await checkNumberOfTodosInLocalStorage(page, 2);
|
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||||
@ -56,7 +56,7 @@ test.describe('New Todo', () => {
|
|||||||
|
|
||||||
// create a todo count locator
|
// create a todo count locator
|
||||||
const todoCount = page.getByTestId('todo-count')
|
const todoCount = page.getByTestId('todo-count')
|
||||||
|
|
||||||
// Check test using different methods.
|
// Check test using different methods.
|
||||||
await expect(page.getByText('3 items left')).toBeVisible();
|
await expect(page.getByText('3 items left')).toBeVisible();
|
||||||
await expect(todoCount).toHaveText('3 items left');
|
await expect(todoCount).toHaveText('3 items left');
|
||||||
@ -127,8 +127,8 @@ test.describe('Item', () => {
|
|||||||
|
|
||||||
// Create two items.
|
// Create two items.
|
||||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
await newTodo.fill(item);
|
await newTodo.fill(item);
|
||||||
await newTodo.press('Enter');
|
await newTodo.press('Enter');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check first item.
|
// Check first item.
|
||||||
@ -152,8 +152,8 @@ test.describe('Item', () => {
|
|||||||
|
|
||||||
// Create two items.
|
// Create two items.
|
||||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
await newTodo.fill(item);
|
await newTodo.fill(item);
|
||||||
await newTodo.press('Enter');
|
await newTodo.press('Enter');
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||||
@ -183,9 +183,9 @@ test.describe('Item', () => {
|
|||||||
|
|
||||||
// Explicitly assert the new text value.
|
// Explicitly assert the new text value.
|
||||||
await expect(todoItems).toHaveText([
|
await expect(todoItems).toHaveText([
|
||||||
TODO_ITEMS[0],
|
TODO_ITEMS[0],
|
||||||
'buy some sausages',
|
'buy some sausages',
|
||||||
TODO_ITEMS[2]
|
TODO_ITEMS[2]
|
||||||
]);
|
]);
|
||||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
});
|
});
|
||||||
@ -202,7 +202,7 @@ test.describe('Editing', () => {
|
|||||||
await todoItem.dblclick();
|
await todoItem.dblclick();
|
||||||
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
|
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
|
||||||
await expect(todoItem.locator('label', {
|
await expect(todoItem.locator('label', {
|
||||||
hasText: TODO_ITEMS[1],
|
hasText: TODO_ITEMS[1],
|
||||||
})).not.toBeVisible();
|
})).not.toBeVisible();
|
||||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||||
});
|
});
|
||||||
@ -214,9 +214,9 @@ test.describe('Editing', () => {
|
|||||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
|
||||||
|
|
||||||
await expect(todoItems).toHaveText([
|
await expect(todoItems).toHaveText([
|
||||||
TODO_ITEMS[0],
|
TODO_ITEMS[0],
|
||||||
'buy some sausages',
|
'buy some sausages',
|
||||||
TODO_ITEMS[2],
|
TODO_ITEMS[2],
|
||||||
]);
|
]);
|
||||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
});
|
});
|
||||||
@ -228,9 +228,9 @@ test.describe('Editing', () => {
|
|||||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
await expect(todoItems).toHaveText([
|
await expect(todoItems).toHaveText([
|
||||||
TODO_ITEMS[0],
|
TODO_ITEMS[0],
|
||||||
'buy some sausages',
|
'buy some sausages',
|
||||||
TODO_ITEMS[2],
|
TODO_ITEMS[2],
|
||||||
]);
|
]);
|
||||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||||
});
|
});
|
||||||
@ -242,8 +242,8 @@ test.describe('Editing', () => {
|
|||||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||||
|
|
||||||
await expect(todoItems).toHaveText([
|
await expect(todoItems).toHaveText([
|
||||||
TODO_ITEMS[0],
|
TODO_ITEMS[0],
|
||||||
TODO_ITEMS[2],
|
TODO_ITEMS[2],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -260,7 +260,7 @@ test.describe('Counter', () => {
|
|||||||
test('should display the current number of todo items', async ({ page }) => {
|
test('should display the current number of todo items', async ({ page }) => {
|
||||||
// create a new todo locator
|
// create a new todo locator
|
||||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
// create a todo count locator
|
// create a todo count locator
|
||||||
const todoCount = page.getByTestId('todo-count')
|
const todoCount = page.getByTestId('todo-count')
|
||||||
|
|
||||||
@ -308,8 +308,8 @@ test.describe('Persistence', () => {
|
|||||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||||
|
|
||||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||||
await newTodo.fill(item);
|
await newTodo.fill(item);
|
||||||
await newTodo.press('Enter');
|
await newTodo.press('Enter');
|
||||||
}
|
}
|
||||||
|
|
||||||
const todoItems = page.getByTestId('todo-item');
|
const todoItems = page.getByTestId('todo-item');
|
||||||
@ -350,22 +350,22 @@ test.describe('Routing', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should respect the back button', async ({ page }) => {
|
test('should respect the back button', async ({ page }) => {
|
||||||
const todoItem = page.getByTestId('todo-item');
|
const todoItem = page.getByTestId('todo-item');
|
||||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||||
|
|
||||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||||
|
|
||||||
await test.step('Showing all items', async () => {
|
await test.step('Showing all items', async () => {
|
||||||
await page.getByRole('link', { name: 'All' }).click();
|
await page.getByRole('link', { name: 'All' }).click();
|
||||||
await expect(todoItem).toHaveCount(3);
|
await expect(todoItem).toHaveCount(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Showing active items', async () => {
|
await test.step('Showing active items', async () => {
|
||||||
await page.getByRole('link', { name: 'Active' }).click();
|
await page.getByRole('link', { name: 'Active' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step('Showing completed items', async () => {
|
await test.step('Showing completed items', async () => {
|
||||||
await page.getByRole('link', { name: 'Completed' }).click();
|
await page.getByRole('link', { name: 'Completed' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(todoItem).toHaveCount(1);
|
await expect(todoItem).toHaveCount(1);
|
||||||
@ -393,7 +393,7 @@ test.describe('Routing', () => {
|
|||||||
|
|
||||||
test('should highlight the currently applied filter', async ({ page }) => {
|
test('should highlight the currently applied filter', async ({ page }) => {
|
||||||
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
|
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
|
||||||
|
|
||||||
//create locators for active and completed links
|
//create locators for active and completed links
|
||||||
const activeLink = page.getByRole('link', { name: 'Active' });
|
const activeLink = page.getByRole('link', { name: 'Active' });
|
||||||
const completedLink = page.getByRole('link', { name: 'Completed' });
|
const completedLink = page.getByRole('link', { name: 'Completed' });
|
||||||
|
Loading…
x
Reference in New Issue
Block a user