chore(code): fix more js & ts files

This commit is contained in:
Elian Doran 2024-12-22 15:45:54 +02:00
parent b321d99076
commit 7a2b5e731e
No known key found for this signature in database
44 changed files with 574 additions and 574 deletions

View File

@ -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

View File

@ -48,8 +48,8 @@ const copy = async () => {
} }
/** /**
* 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) {

View File

@ -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();

View File

@ -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. */

View File

@ -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");
}); });

View File

@ -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}`);

View File

@ -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
); );

View File

@ -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",
"\\", "\\",
]); ]);
}); });
}); });

View File

@ -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);

View File

@ -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])

View File

@ -99,8 +99,8 @@ 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;
@ -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]);
@ -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`);

View File

@ -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();

View File

@ -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);

View File

@ -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])
@ -1149,8 +1149,8 @@ class BNote extends AbstractBeccaEntity<BNote> {
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'.`);

View File

@ -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();
@ -123,7 +123,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
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])
@ -146,8 +146,8 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
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]);

View File

@ -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) {

View File

@ -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]);
@ -40,7 +40,7 @@ function load() {
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);
@ -50,7 +50,7 @@ function load() {
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) {
@ -61,7 +61,7 @@ function load() {
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);
@ -71,7 +71,7 @@ function load() {
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);

View File

@ -42,18 +42,18 @@ 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();
} }
@ -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;

View File

@ -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);
@ -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],
]); ]);
}); });
@ -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');
@ -356,16 +356,16 @@ test.describe('Routing', () => {
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);