mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	chore(code): fix more js & ts files
This commit is contained in:
		| @@ -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' }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user