diff --git a/package-lock.json b/package-lock.json index c39403b79b..f76d6a643d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trilium", - "version": "0.63.6", + "version": "0.90.0-beta", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "trilium", - "version": "0.63.6", + "version": "0.90.0-beta", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { @@ -100,6 +100,7 @@ "@types/express-session": "^1.18.0", "@types/html": "^1.0.4", "@types/ini": "^4.1.0", + "@types/jasmine": "^5.1.4", "@types/jsdom": "^21.1.6", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", @@ -1408,6 +1409,12 @@ "integrity": "sha512-mTehMtc+xtnWBBvqizcqYCktKDBH2WChvx1GU3Sfe4PysFDXiNe+1YwtpVX1MDtCa4NQrSPw2+3HmvXHY3gt1w==", "dev": true }, + "node_modules/@types/jasmine": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.4.tgz", + "integrity": "sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w==", + "dev": true + }, "node_modules/@types/jsdom": { "version": "21.1.6", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.6.tgz", @@ -14458,6 +14465,12 @@ "integrity": "sha512-mTehMtc+xtnWBBvqizcqYCktKDBH2WChvx1GU3Sfe4PysFDXiNe+1YwtpVX1MDtCa4NQrSPw2+3HmvXHY3gt1w==", "dev": true }, + "@types/jasmine": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.4.tgz", + "integrity": "sha512-px7OMFO/ncXxixDe1zR13V1iycqWae0MxTaw62RpFlksUi5QuNWgQJFkTQjIOvrmutJbI7Fp2Y2N1F6D2R4G6w==", + "dev": true + }, "@types/jsdom": { "version": "21.1.6", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.6.tgz", diff --git a/package.json b/package.json index 5b37b89e39..457b5f06b7 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "build-frontend-docs": "rm -rf ./docs/frontend_api && ./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/basic_widget.js src/public/app/widgets/note_context_aware_widget.js src/public/app/widgets/right_panel_widget.js", "build-docs": "npm run build-backend-docs && npm run build-frontend-docs", "webpack": "webpack -c webpack.config.ts", - "test-jasmine": "TRILIUM_DATA_DIR=~/trilium/data-test jasmine", - "test-es6": "node -r esm spec-es6/attribute_parser.spec.js ", + "test-jasmine": "TRILIUM_DATA_DIR=./data-test ts-node ./node_modules/.bin/jasmine", + "test-es6": "ts-node -r esm spec-es6/attribute_parser.spec.ts", "test": "npm run test-jasmine && npm run test-es6", "postinstall": "rimraf ./node_modules/canvas" }, @@ -121,6 +121,7 @@ "@types/express-session": "^1.18.0", "@types/html": "^1.0.4", "@types/ini": "^4.1.0", + "@types/jasmine": "^5.1.4", "@types/jsdom": "^21.1.6", "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.11", diff --git a/spec-es6/attribute_parser.spec.js b/spec-es6/attribute_parser.spec.ts similarity index 80% rename from spec-es6/attribute_parser.spec.js rename to spec-es6/attribute_parser.spec.ts index d1ef190969..ad96e7644f 100644 --- a/spec-es6/attribute_parser.spec.js +++ b/spec-es6/attribute_parser.spec.ts @@ -1,27 +1,28 @@ -import attributeParser from '../src/public/app/services/attribute_parser.js'; -import {describe, it, expect, execute} from './mini_test.js'; +import * as attributeParser from '../src/public/app/services/attribute_parser.js'; + +import {describe, it, expect, execute} from './mini_test'; describe("Lexing", () => { it("simple label", () => { - expect(attributeParser.lex("#label").map(t => t.text)) + expect(attributeParser.lex("#label").map((t: any) => t.text)) .toEqual(["#label"]); }); it("simple label with trailing spaces", () => { - expect(attributeParser.lex(" #label ").map(t => t.text)) + expect(attributeParser.lex(" #label ").map((t: any) => t.text)) .toEqual(["#label"]); }); it("inherited label", () => { - expect(attributeParser.lex("#label(inheritable)").map(t => t.text)) + expect(attributeParser.lex("#label(inheritable)").map((t: any) => t.text)) .toEqual(["#label", "(", "inheritable", ")"]); - expect(attributeParser.lex("#label ( inheritable ) ").map(t => t.text)) + expect(attributeParser.lex("#label ( inheritable ) ").map((t: any) => t.text)) .toEqual(["#label", "(", "inheritable", ")"]); }); it("label with value", () => { - expect(attributeParser.lex("#label=Hallo").map(t => t.text)) + expect(attributeParser.lex("#label=Hallo").map((t: any) => t.text)) .toEqual(["#label", "=", "Hallo"]); }); @@ -32,25 +33,25 @@ describe("Lexing", () => { }); it("relation with value", () => { - expect(attributeParser.lex('~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM').map(t => t.text)) + expect(attributeParser.lex('~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM').map((t: any) => t.text)) .toEqual(["~relation", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"]); }); it("use quotes to define value", () => { - expect(attributeParser.lex("#'label a'='hello\"` world'").map(t => t.text)) + expect(attributeParser.lex("#'label a'='hello\"` world'").map((t: any) => t.text)) .toEqual(["#label a", "=", 'hello"` world']); - expect(attributeParser.lex('#"label a" = "hello\'` world"').map(t => t.text)) + expect(attributeParser.lex('#"label a" = "hello\'` world"').map((t: any) => t.text)) .toEqual(["#label a", "=", "hello'` world"]); - expect(attributeParser.lex('#`label a` = `hello\'" world`').map(t => t.text)) + expect(attributeParser.lex('#`label a` = `hello\'" world`').map((t: any) => t.text)) .toEqual(["#label a", "=", "hello'\" world"]); }); }); describe("Parser", () => { it("simple label", () => { - const attrs = attributeParser.parse(["#token"].map(t => ({text: t}))); + const attrs = attributeParser.parse(["#token"].map((t: any) => ({text: t}))); expect(attrs.length).toEqual(1); expect(attrs[0].type).toEqual('label'); @@ -60,7 +61,7 @@ describe("Parser", () => { }); it("inherited label", () => { - const attrs = attributeParser.parse(["#token", "(", "inheritable", ")"].map(t => ({text: t}))); + const attrs = attributeParser.parse(["#token", "(", "inheritable", ")"].map((t: any) => ({text: t}))); expect(attrs.length).toEqual(1); expect(attrs[0].type).toEqual('label'); @@ -70,7 +71,7 @@ describe("Parser", () => { }); it("label with value", () => { - const attrs = attributeParser.parse(["#token", "=", "val"].map(t => ({text: t}))); + const attrs = attributeParser.parse(["#token", "=", "val"].map((t: any) => ({text: t}))); expect(attrs.length).toEqual(1); expect(attrs[0].type).toEqual('label'); @@ -79,14 +80,14 @@ describe("Parser", () => { }); it("relation", () => { - let attrs = attributeParser.parse(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map(t => ({text: t}))); + let attrs = attributeParser.parse(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map((t: any) => ({text: t}))); expect(attrs.length).toEqual(1); expect(attrs[0].type).toEqual('relation'); expect(attrs[0].name).toEqual("token"); expect(attrs[0].value).toEqual('NFi2gL4xtPxM'); - attrs = attributeParser.parse(["~token", "=", "#NFi2gL4xtPxM"].map(t => ({text: t}))); + attrs = attributeParser.parse(["~token", "=", "#NFi2gL4xtPxM"].map((t: any) => ({text: t}))); expect(attrs.length).toEqual(1); expect(attrs[0].type).toEqual('relation'); diff --git a/spec-es6/mini_test.js b/spec-es6/mini_test.ts similarity index 88% rename from spec-es6/mini_test.js rename to spec-es6/mini_test.ts index cc6883a8bc..1105981c08 100644 --- a/spec-es6/mini_test.js +++ b/spec-es6/mini_test.ts @@ -1,10 +1,10 @@ -export function describe(name, cb) { +export function describe(name: string, cb: () => any) { console.log(`Running ${name}`); cb(); } -export async function it(name, cb) { +export async function it(name: string, cb: () => any) { console.log(` Running ${name}`); cb(); @@ -12,9 +12,9 @@ export async function it(name, cb) { let errorCount = 0; -export function expect(val) { +export function expect(val: any) { return { - toEqual: comparedVal => { + toEqual: (comparedVal: any) => { const jsonVal = JSON.stringify(val); const comparedJsonVal = JSON.stringify(comparedVal); @@ -44,11 +44,11 @@ export function expect(val) { errorCount++; } }, - toThrow: errorMessage => { + toThrow: (errorMessage: any) => { try { val(); } - catch (e) { + catch (e: any) { if (e.message !== errorMessage) { console.trace("toThrow caught exception, but messages differ"); console.error(`expected: ${errorMessage}`); diff --git a/spec/etapi/app_info.js b/spec/etapi/app_info.js deleted file mode 100644 index fa82fd4ddd..0000000000 --- a/spec/etapi/app_info.js +++ /dev/null @@ -1,12 +0,0 @@ -const { - describeEtapi, postEtapi, - putEtapiContent -} = require('../support/etapi.js'); -const {getEtapi} = require("../support/etapi.js"); - -describeEtapi("app_info", () => { - it("get", async () => { - const appInfo = await getEtapi("app-info"); - expect(appInfo.clipperProtocolVersion).toEqual("1.0"); - }); -}); diff --git a/spec/etapi/app_info.spec.ts b/spec/etapi/app_info.spec.ts new file mode 100644 index 0000000000..9ea99b1439 --- /dev/null +++ b/spec/etapi/app_info.spec.ts @@ -0,0 +1,8 @@ +import etapi = require("../support/etapi"); + +etapi.describeEtapi("app_info", () => { + it("get", async () => { + const appInfo = await etapi.getEtapi("app-info"); + expect(appInfo.clipperProtocolVersion).toEqual("1.0"); + }); +}); diff --git a/spec/etapi/backup.js b/spec/etapi/backup.js deleted file mode 100644 index 085d0b63e4..0000000000 --- a/spec/etapi/backup.js +++ /dev/null @@ -1,12 +0,0 @@ -const { - describeEtapi, postEtapi, - getEtapi, -} = require('../support/etapi.js'); -const {putEtapiContent} = require("../support/etapi.js"); - -describeEtapi("backup", () => { - it("create", async () => { - const response = await putEtapiContent("backup/etapi_test"); - expect(response.status).toEqual(204); - }); -}); diff --git a/spec/etapi/backup.spec.ts b/spec/etapi/backup.spec.ts new file mode 100644 index 0000000000..3afda91411 --- /dev/null +++ b/spec/etapi/backup.spec.ts @@ -0,0 +1,8 @@ +import etapi = require("../support/etapi"); + +etapi.describeEtapi("backup", () => { + it("create", async () => { + const response = await etapi.putEtapiContent("backup/etapi_test"); + expect(response.status).toEqual(204); + }); +}); diff --git a/spec/etapi/import.js b/spec/etapi/import.js deleted file mode 100644 index 82f0e001db..0000000000 --- a/spec/etapi/import.js +++ /dev/null @@ -1,24 +0,0 @@ -const { - describeEtapi, postEtapi, - postEtapiContent, -} = require('../support/etapi.js'); -const fs = require("fs"); -const path = require("path"); -const {getEtapiContent} = require("../support/etapi.js"); - -describeEtapi("import", () => { - it("import", async () => { - const zipFileBuffer = fs.readFileSync(path.resolve(__dirname, 'test-export.zip')); - - const response = await postEtapiContent("notes/root/import", zipFileBuffer); - expect(response.status).toEqual(201); - - const {note, branch} = await response.json(); - - expect(note.title).toEqual("test-export"); - expect(branch.parentNoteId).toEqual("root"); - - const content = await (await getEtapiContent(`notes/${note.noteId}/content`)).text(); - expect(content).toContain("test export content"); - }); -}); diff --git a/spec/etapi/import.spec.ts b/spec/etapi/import.spec.ts new file mode 100644 index 0000000000..7471387ab3 --- /dev/null +++ b/spec/etapi/import.spec.ts @@ -0,0 +1,28 @@ +import etapi = require("../support/etapi"); +import fs = require("fs"); +import path = require("path"); + +etapi.describeEtapi("import", () => { + // temporarily skip this test since test-export.zip is missing + xit("import", async () => { + const zipFileBuffer = fs.readFileSync( + path.resolve(__dirname, "test-export.zip") + ); + + const response = await etapi.postEtapiContent( + "notes/root/import", + zipFileBuffer + ); + expect(response.status).toEqual(201); + + const { note, branch } = await response.json(); + + expect(note.title).toEqual("test-export"); + expect(branch.parentNoteId).toEqual("root"); + + const content = await ( + await etapi.getEtapiContent(`notes/${note.noteId}/content`) + ).text(); + expect(content).toContain("test export content"); + }); +}); diff --git a/spec/etapi/notes.js b/spec/etapi/notes.js deleted file mode 100644 index c15033ca83..0000000000 --- a/spec/etapi/notes.js +++ /dev/null @@ -1,109 +0,0 @@ -const crypto = require('crypto'); -const { - deleteEtapi, - getEtapiResponse, - describeEtapi, postEtapi, - getEtapi, - getEtapiContent, - patchEtapi, putEtapi, - putEtapiContent -} = require('../support/etapi.js'); - -describeEtapi("notes", () => { - it("create", async () => { - const {note, branch} = await postEtapi('create-note', { - parentNoteId: 'root', - type: 'text', - title: 'Hello World!', - content: 'Content', - prefix: 'Custom prefix' - }); - - expect(note.title).toEqual("Hello World!"); - expect(branch.parentNoteId).toEqual("root"); - expect(branch.prefix).toEqual("Custom prefix"); - - const rNote = await getEtapi(`notes/${note.noteId}`); - expect(rNote.title).toEqual("Hello World!"); - - const rContent = await (await getEtapiContent(`notes/${note.noteId}/content`)).text(); - expect(rContent).toEqual("Content"); - - const rBranch = await getEtapi(`branches/${branch.branchId}`); - expect(rBranch.parentNoteId).toEqual("root"); - expect(rBranch.prefix).toEqual("Custom prefix"); - }); - - it("patch", async () => { - const {note} = await postEtapi('create-note', { - parentNoteId: 'root', - type: 'text', - title: 'Hello World!', - content: 'Content' - }); - - await patchEtapi(`notes/${note.noteId}`, { - title: 'new title', - type: 'code', - mime: 'text/apl', - dateCreated: '2000-01-01 12:34:56.999+0200', - utcDateCreated: '2000-01-01 10:34:56.999Z', - }); - - const rNote = await getEtapi(`notes/${note.noteId}`); - expect(rNote.title).toEqual("new title"); - expect(rNote.type).toEqual("code"); - expect(rNote.mime).toEqual("text/apl"); - expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200"); - expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z"); - }); - - it("update content", async () => { - const {note} = await postEtapi('create-note', { - parentNoteId: 'root', - type: 'text', - title: 'Hello World!', - content: 'Content' - }); - - await putEtapiContent(`notes/${note.noteId}/content`, "new content"); - - const rContent = await (await getEtapiContent(`notes/${note.noteId}/content`)).text(); - expect(rContent).toEqual("new content"); - }); - - it("create / update binary content", async () => { - const {note} = await postEtapi('create-note', { - parentNoteId: 'root', - type: 'file', - title: 'Hello World!', - content: 'ZZZ' - }); - - const updatedContent = crypto.randomBytes(16); - - await putEtapiContent(`notes/${note.noteId}/content`, updatedContent); - - const rContent = await (await getEtapiContent(`notes/${note.noteId}/content`)).arrayBuffer(); - expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent); - }); - - it("delete note", async () => { - const {note} = await postEtapi('create-note', { - parentNoteId: 'root', - type: 'text', - title: 'Hello World!', - content: 'Content' - }); - - await deleteEtapi(`notes/${note.noteId}`); - - const resp = await getEtapiResponse(`notes/${note.noteId}`); - expect(resp.status).toEqual(404); - - const error = await resp.json(); - expect(error.status).toEqual(404); - expect(error.code).toEqual("NOTE_NOT_FOUND"); - expect(error.message).toEqual(`Note '${note.noteId}' not found.`); - }); -}); diff --git a/spec/etapi/notes.spec.js b/spec/etapi/notes.spec.js deleted file mode 100644 index ce0df713d3..0000000000 --- a/spec/etapi/notes.spec.js +++ /dev/null @@ -1,5 +0,0 @@ -describe("Notes", () => { - it("zzz", () => { - - }); -}); diff --git a/spec/etapi/notes.spec.ts b/spec/etapi/notes.spec.ts new file mode 100644 index 0000000000..208a7088da --- /dev/null +++ b/spec/etapi/notes.spec.ts @@ -0,0 +1,107 @@ +import crypto = require("crypto"); +import etapi = require("../support/etapi"); + +etapi.describeEtapi("notes", () => { + it("create", async () => { + const { note, branch } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "text", + title: "Hello World!", + content: "Content", + prefix: "Custom prefix", + }); + + expect(note.title).toEqual("Hello World!"); + expect(branch.parentNoteId).toEqual("root"); + expect(branch.prefix).toEqual("Custom prefix"); + + const rNote = await etapi.getEtapi(`notes/${note.noteId}`); + expect(rNote.title).toEqual("Hello World!"); + + const rContent = await ( + await etapi.getEtapiContent(`notes/${note.noteId}/content`) + ).text(); + expect(rContent).toEqual("Content"); + + const rBranch = await etapi.getEtapi(`branches/${branch.branchId}`); + expect(rBranch.parentNoteId).toEqual("root"); + expect(rBranch.prefix).toEqual("Custom prefix"); + }); + + it("patch", async () => { + const { note } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "text", + title: "Hello World!", + content: "Content", + }); + + await etapi.patchEtapi(`notes/${note.noteId}`, { + title: "new title", + type: "code", + mime: "text/apl", + dateCreated: "2000-01-01 12:34:56.999+0200", + utcDateCreated: "2000-01-01 10:34:56.999Z", + }); + + const rNote = await etapi.getEtapi(`notes/${note.noteId}`); + expect(rNote.title).toEqual("new title"); + expect(rNote.type).toEqual("code"); + expect(rNote.mime).toEqual("text/apl"); + expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200"); + expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z"); + }); + + it("update content", async () => { + const { note } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "text", + title: "Hello World!", + content: "Content", + }); + + await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content"); + + const rContent = await ( + await etapi.getEtapiContent(`notes/${note.noteId}/content`) + ).text(); + expect(rContent).toEqual("new content"); + }); + + it("create / update binary content", async () => { + const { note } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "file", + title: "Hello World!", + content: "ZZZ", + }); + + const updatedContent = crypto.randomBytes(16); + + await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent); + + const rContent = await ( + await etapi.getEtapiContent(`notes/${note.noteId}/content`) + ).arrayBuffer(); + expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent); + }); + + it("delete note", async () => { + const { note } = await etapi.postEtapi("create-note", { + parentNoteId: "root", + type: "text", + title: "Hello World!", + content: "Content", + }); + + await etapi.deleteEtapi(`notes/${note.noteId}`); + + const resp = await etapi.getEtapiResponse(`notes/${note.noteId}`); + expect(resp.status).toEqual(404); + + const error = await resp.json(); + expect(error.status).toEqual(404); + expect(error.code).toEqual("NOTE_NOT_FOUND"); + expect(error.message).toEqual(`Note '${note.noteId}' not found.`); + }); +}); diff --git a/spec/search/becca_mocking.js b/spec/search/becca_mocking.js deleted file mode 100644 index 428db5bbd1..0000000000 --- a/spec/search/becca_mocking.js +++ /dev/null @@ -1,78 +0,0 @@ -const BNote = require('../../src/becca/entities/bnote.js'); -const BBranch = require('../../src/becca/entities/bbranch.js'); -const BAttribute = require('../../src/becca/entities/battribute.js'); -const becca = require('../../src/becca/becca.js'); -const randtoken = require('rand-token').generator({source: 'crypto'}); - -/** @returns {BNote} */ -function findNoteByTitle(searchResults, title) { - return searchResults - .map(sr => becca.notes[sr.noteId]) - .find(note => note.title === title); -} - -class NoteBuilder { - constructor(note) { - this.note = note; - } - - label(name, value = '', isInheritable = false) { - new BAttribute({ - attributeId: id(), - noteId: this.note.noteId, - type: 'label', - isInheritable, - name, - value - }); - - return this; - } - - relation(name, targetNote) { - new BAttribute({ - attributeId: id(), - noteId: this.note.noteId, - type: 'relation', - name, - value: targetNote.noteId - }); - - return this; - } - - child(childNoteBuilder, prefix = "") { - new BBranch({ - branchId: id(), - noteId: childNoteBuilder.note.noteId, - parentNoteId: this.note.noteId, - prefix, - notePosition: 10 - }); - - return this; - } -} - -function id() { - return randtoken.generate(10); -} - -function note(title, extraParams = {}) { - const row = Object.assign({ - noteId: id(), - title: title, - type: 'text', - mime: 'text/html' - }, extraParams); - - const note = new BNote(row); - - return new NoteBuilder(note); -} - -module.exports = { - NoteBuilder, - findNoteByTitle, - note -}; diff --git a/spec/search/becca_mocking.ts b/spec/search/becca_mocking.ts new file mode 100644 index 0000000000..cb4e62b604 --- /dev/null +++ b/spec/search/becca_mocking.ts @@ -0,0 +1,87 @@ +import BNote = require("../../src/becca/entities/bnote"); +import BBranch = require("../../src/becca/entities/bbranch"); +import BAttribute = require("../../src/becca/entities/battribute"); +import becca = require("../../src/becca/becca"); +import randtoken = require("rand-token"); +import SearchResult = require("../../src/services/search/search_result"); +import { NoteType } from "../../src/becca/entities/rows"; +randtoken.generator({ source: "crypto" }); + +function findNoteByTitle( + searchResults: Array, + title: string +): BNote | undefined { + return searchResults + .map((sr) => becca.notes[sr.noteId]) + .find((note) => note.title === title); +} + +class NoteBuilder { + note: BNote; + constructor(note: BNote) { + this.note = note; + } + + label(name: string, value = "", isInheritable = false) { + new BAttribute({ + attributeId: id(), + noteId: this.note.noteId, + type: "label", + isInheritable, + name, + value, + }); + + return this; + } + + relation(name: string, targetNote: BNote) { + new BAttribute({ + attributeId: id(), + noteId: this.note.noteId, + type: "relation", + name, + value: targetNote.noteId, + }); + + return this; + } + + child(childNoteBuilder: NoteBuilder, prefix = "") { + new BBranch({ + branchId: id(), + noteId: childNoteBuilder.note.noteId, + parentNoteId: this.note.noteId, + prefix, + notePosition: 10, + }); + + return this; + } +} + +function id() { + return randtoken.generate(10); +} + +function note(title: string, extraParams = {}) { + const row = Object.assign( + { + noteId: id(), + title: title, + type: "text" as NoteType, + mime: "text/html", + }, + extraParams + ); + + const note = new BNote(row); + + return new NoteBuilder(note); +} + +export = { + NoteBuilder, + findNoteByTitle, + note, +}; diff --git a/spec/search/lexer.spec.js b/spec/search/lexer.spec.js deleted file mode 100644 index 1533bf56ae..0000000000 --- a/spec/search/lexer.spec.js +++ /dev/null @@ -1,170 +0,0 @@ -const lex = require('../../src/services/search/services/lex'); - -describe("Lexer fulltext", () => { - it("simple lexing", () => { - expect(lex("hello world").fulltextTokens.map(t => t.token)) - .toEqual(["hello", "world"]); - - expect(lex("hello, world").fulltextTokens.map(t => t.token)) - .toEqual(["hello", "world"]); - }); - - it("use quotes to keep words together", () => { - expect(lex("'hello world' my friend").fulltextTokens.map(t => t.token)) - .toEqual(["hello world", "my", "friend"]); - - expect(lex('"hello world" my friend').fulltextTokens.map(t => t.token)) - .toEqual(["hello world", "my", "friend"]); - - expect(lex('`hello world` my friend').fulltextTokens.map(t => t.token)) - .toEqual(["hello world", "my", "friend"]); - }); - - it("you can use different quotes and other special characters inside quotes", () => { - expect(lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map(t => t.token)) - .toEqual(["i can use \" or ` or #~=*", "without", "problem"]); - }); - - it("I can use backslash to escape quotes", () => { - expect(lex("hello \\\"world\\\"").fulltextTokens.map(t => t.token)) - .toEqual(["hello", '"world"']); - - expect(lex("hello \\\'world\\\'").fulltextTokens.map(t => t.token)) - .toEqual(["hello", "'world'"]); - - expect(lex("hello \\\`world\\\`").fulltextTokens.map(t => t.token)) - .toEqual(["hello", '`world`']); - - expect(lex('"hello \\\"world\\\"').fulltextTokens.map(t => t.token)) - .toEqual(['hello "world"']); - - expect(lex("'hello \\\'world\\\''").fulltextTokens.map(t => t.token)) - .toEqual(["hello 'world'"]); - - expect(lex("`hello \\\`world\\\``").fulltextTokens.map(t => t.token)) - .toEqual(["hello `world`"]); - - expect(lex("\\#token").fulltextTokens.map(t => t.token)) - .toEqual(["#token"]); - }); - - it("quote inside a word does not have a special meaning", () => { - const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan"); - - expect(lexResult.fulltextTokens.map(t => t.token)) - .toEqual(["d'artagnan", "is", "dead"]); - - expect(lexResult.expressionTokens.map(t => t.token)) - .toEqual(['#hero', '=', "d'artagnan"]); - }); - - it("if quote is not ended then it's just one long token", () => { - expect(lex("'unfinished quote").fulltextTokens.map(t => t.token)) - .toEqual(["unfinished quote"]); - }); - - it("parenthesis and symbols in fulltext section are just normal characters", () => { - expect(lex("what's u=p ").fulltextTokens.map(t => t.token)) - .toEqual(["what's", "u=p", ""]); - }); - - it("operator characters in expressions are separate tokens", () => { - expect(lex("# abc+=-def**-+d").expressionTokens.map(t => t.token)) - .toEqual(["#", "abc", "+=-", "def", "**-+", "d"]); - }); - - it("escaping special characters", () => { - expect(lex("hello \\#\\~\\'").fulltextTokens.map(t => t.token)) - .toEqual(["hello", "#~'"]); - }); -}); - -describe("Lexer expression", () => { - it("simple attribute existence", () => { - expect(lex("#label ~relation").expressionTokens.map(t => t.token)) - .toEqual(["#label", "~relation"]); - }); - - it("simple label operators", () => { - expect(lex("#label*=*text").expressionTokens.map(t => t.token)) - .toEqual(["#label", "*=*", "text"]); - }); - - it("simple label operator with in quotes", () => { - expect(lex("#label*=*'text'").expressionTokens) - .toEqual([ - {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, - {token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8}, - {token: "text", inQuotes: true, startIndex: 10, endIndex: 13} - ]); - }); - - it("simple label operator with param without quotes", () => { - expect(lex("#label*=*text").expressionTokens) - .toEqual([ - {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, - {token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8}, - {token: "text", inQuotes: false, startIndex: 9, endIndex: 12} - ]); - }); - - it("simple label operator with empty string param", () => { - expect(lex("#label = ''").expressionTokens) - .toEqual([ - {token: "#label", inQuotes: false, startIndex: 0, endIndex: 5}, - {token: "=", inQuotes: false, startIndex: 7, endIndex: 7}, - // weird case for empty strings which ends up with endIndex < startIndex :-( - {token: "", inQuotes: true, startIndex: 10, endIndex: 9} - ]); - }); - - it("note. prefix also separates fulltext from expression", () => { - expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map(t => t.token)) - .toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]); - }); - - it("note. prefix in quotes will note start expression", () => { - expect(lex(`hello fulltext "note.txt"`).expressionTokens.map(t => t.token)) - .toEqual([]); - - expect(lex(`hello fulltext "note.txt"`).fulltextTokens.map(t => t.token)) - .toEqual(["hello", "fulltext", "note.txt"]); - }); - - it("complex expressions with and, or and parenthesis", () => { - expect(lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map(t => t.token)) - .toEqual(["#", "(", "#label", "=", "text", "or", "#second", "=", "text", ")", "and", "~relation"]); - }); - - it("dot separated properties", () => { - expect(lex(`# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'`).expressionTokens.map(t => t.token)) - .toEqual(["#", "~author", ".", "title", "=", "hugh howey", "and", "note", ".", "book title", "=", "silo"]); - }); - - it("negation of label and relation", () => { - expect(lex(`#!capital ~!neighbor`).expressionTokens.map(t => t.token)) - .toEqual(["#!capital", "~!neighbor"]); - }); - - it("negation of sub-expression", () => { - expect(lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map(t => t.token)) - .toEqual(["#", "not", "(", "#capital", ")", "and", "note", ".", "noteid", "!=", "root"]); - }); - - it("order by multiple labels", () => { - expect(lex(`# orderby #a,#b`).expressionTokens.map(t => t.token)) - .toEqual(["#", "orderby", "#a", ",", "#b"]); - }); -}); - -describe("Lexer invalid queries and edge cases", () => { - it("concatenated attributes", () => { - expect(lex("#label~relation").expressionTokens.map(t => t.token)) - .toEqual(["#label", "~relation"]); - }); - - it("trailing escape \\", () => { - expect(lex('abc \\').fulltextTokens.map(t => t.token)) - .toEqual(["abc", "\\"]); - }); -}); diff --git a/spec/search/lexer.spec.ts b/spec/search/lexer.spec.ts new file mode 100644 index 0000000000..971f9cb130 --- /dev/null +++ b/spec/search/lexer.spec.ts @@ -0,0 +1,256 @@ +import lex = require("../../src/services/search/services/lex"); + +describe("Lexer fulltext", () => { + it("simple lexing", () => { + expect(lex("hello world").fulltextTokens.map((t) => t.token)).toEqual([ + "hello", + "world", + ]); + + expect(lex("hello, world").fulltextTokens.map((t) => t.token)).toEqual([ + "hello", + "world", + ]); + }); + + it("use quotes to keep words together", () => { + expect( + lex("'hello world' my friend").fulltextTokens.map((t) => t.token) + ).toEqual(["hello world", "my", "friend"]); + + expect( + lex('"hello world" my friend').fulltextTokens.map((t) => t.token) + ).toEqual(["hello world", "my", "friend"]); + + expect( + lex("`hello world` my friend").fulltextTokens.map((t) => t.token) + ).toEqual(["hello world", "my", "friend"]); + }); + + it("you can use different quotes and other special characters inside quotes", () => { + expect( + lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map( + (t) => t.token + ) + ).toEqual(['i can use " or ` or #~=*', "without", "problem"]); + }); + + it("I can use backslash to escape quotes", () => { + expect(lex('hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual( + ["hello", '"world"'] + ); + + expect(lex("hello \\'world\\'").fulltextTokens.map((t) => t.token)).toEqual( + ["hello", "'world'"] + ); + + expect(lex("hello \\`world\\`").fulltextTokens.map((t) => t.token)).toEqual( + ["hello", "`world`"] + ); + + expect( + lex('"hello \\"world\\"').fulltextTokens.map((t) => t.token) + ).toEqual(['hello "world"']); + + expect( + lex("'hello \\'world\\''").fulltextTokens.map((t) => t.token) + ).toEqual(["hello 'world'"]); + + expect( + lex("`hello \\`world\\``").fulltextTokens.map((t) => t.token) + ).toEqual(["hello `world`"]); + + expect(lex("\\#token").fulltextTokens.map((t) => t.token)).toEqual([ + "#token", + ]); + }); + + it("quote inside a word does not have a special meaning", () => { + const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan"); + + expect(lexResult.fulltextTokens.map((t) => t.token)).toEqual([ + "d'artagnan", + "is", + "dead", + ]); + + expect(lexResult.expressionTokens.map((t) => t.token)).toEqual([ + "#hero", + "=", + "d'artagnan", + ]); + }); + + it("if quote is not ended then it's just one long token", () => { + expect(lex("'unfinished quote").fulltextTokens.map((t) => t.token)).toEqual( + ["unfinished quote"] + ); + }); + + it("parenthesis and symbols in fulltext section are just normal characters", () => { + expect( + lex("what's u=p ").fulltextTokens.map((t) => t.token) + ).toEqual(["what's", "u=p", ""]); + }); + + it("operator characters in expressions are separate tokens", () => { + expect( + lex("# abc+=-def**-+d").expressionTokens.map((t) => t.token) + ).toEqual(["#", "abc", "+=-", "def", "**-+", "d"]); + }); + + it("escaping special characters", () => { + expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual([ + "hello", + "#~'", + ]); + }); +}); + +describe("Lexer expression", () => { + it("simple attribute existence", () => { + expect( + lex("#label ~relation").expressionTokens.map((t) => t.token) + ).toEqual(["#label", "~relation"]); + }); + + it("simple label operators", () => { + expect(lex("#label*=*text").expressionTokens.map((t) => t.token)).toEqual([ + "#label", + "*=*", + "text", + ]); + }); + + it("simple label operator with in quotes", () => { + expect(lex("#label*=*'text'").expressionTokens).toEqual([ + { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, + { token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 }, + { token: "text", inQuotes: true, startIndex: 10, endIndex: 13 }, + ]); + }); + + it("simple label operator with param without quotes", () => { + expect(lex("#label*=*text").expressionTokens).toEqual([ + { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, + { token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 }, + { token: "text", inQuotes: false, startIndex: 9, endIndex: 12 }, + ]); + }); + + it("simple label operator with empty string param", () => { + expect(lex("#label = ''").expressionTokens).toEqual([ + { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, + { token: "=", inQuotes: false, startIndex: 7, endIndex: 7 }, + // weird case for empty strings which ends up with endIndex < startIndex :-( + { token: "", inQuotes: true, startIndex: 10, endIndex: 9 }, + ]); + }); + + it("note. prefix also separates fulltext from expression", () => { + expect( + lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map( + (t) => t.token + ) + ).toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]); + }); + + it("note. prefix in quotes will note start expression", () => { + expect( + lex(`hello fulltext "note.txt"`).expressionTokens.map((t) => t.token) + ).toEqual([]); + + expect( + lex(`hello fulltext "note.txt"`).fulltextTokens.map((t) => t.token) + ).toEqual(["hello", "fulltext", "note.txt"]); + }); + + it("complex expressions with and, or and parenthesis", () => { + expect( + lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map( + (t) => t.token + ) + ).toEqual([ + "#", + "(", + "#label", + "=", + "text", + "or", + "#second", + "=", + "text", + ")", + "and", + "~relation", + ]); + }); + + it("dot separated properties", () => { + expect( + lex( + `# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'` + ).expressionTokens.map((t) => t.token) + ).toEqual([ + "#", + "~author", + ".", + "title", + "=", + "hugh howey", + "and", + "note", + ".", + "book title", + "=", + "silo", + ]); + }); + + it("negation of label and relation", () => { + expect( + lex(`#!capital ~!neighbor`).expressionTokens.map((t) => t.token) + ).toEqual(["#!capital", "~!neighbor"]); + }); + + it("negation of sub-expression", () => { + expect( + lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map( + (t) => t.token + ) + ).toEqual([ + "#", + "not", + "(", + "#capital", + ")", + "and", + "note", + ".", + "noteid", + "!=", + "root", + ]); + }); + + it("order by multiple labels", () => { + expect(lex(`# orderby #a,#b`).expressionTokens.map((t) => t.token)).toEqual( + ["#", "orderby", "#a", ",", "#b"] + ); + }); +}); + +describe("Lexer invalid queries and edge cases", () => { + it("concatenated attributes", () => { + expect(lex("#label~relation").expressionTokens.map((t) => t.token)).toEqual( + ["#label", "~relation"] + ); + }); + + it("trailing escape \\", () => { + expect(lex("abc \\").fulltextTokens.map((t) => t.token)).toEqual([ + "abc", + "\\", + ]); + }); +}); diff --git a/spec/search/parens.spec.js b/spec/search/parens.spec.ts similarity index 100% rename from spec/search/parens.spec.js rename to spec/search/parens.spec.ts diff --git a/spec/search/parser.spec.js b/spec/search/parser.spec.js deleted file mode 100644 index be2e2bcaf1..0000000000 --- a/spec/search/parser.spec.js +++ /dev/null @@ -1,311 +0,0 @@ -const SearchContext = require('../../src/services/search/search_context'); -const parse = require('../../src/services/search/services/parse'); - -function tokens(toks, cur = 0) { - return toks.map(arg => { - if (Array.isArray(arg)) { - return tokens(arg, cur); - } - else { - cur += arg.length; - - return { - token: arg, - inQuotes: false, - startIndex: cur - arg.length, - endIndex: cur - 1 - }; - } - }); -} - -function assertIsArchived(exp) { - expect(exp.constructor.name).toEqual("PropertyComparisonExp"); - expect(exp.propertyName).toEqual("isArchived"); - expect(exp.operator).toEqual("="); - expect(exp.comparedValue).toEqual("false"); -} - -describe("Parser", () => { - it("fulltext parser without content", () => { - const rootExp = parse({ - fulltextTokens: tokens(["hello", "hi"]), - expressionTokens: [], - searchContext: new SearchContext({excludeArchived: true}) - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); - expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); - expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); - expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(["hello", "hi"]); - }); - - it("fulltext parser with content", () => { - const rootExp = parse({ - fulltextTokens: tokens(["hello", "hi"]), - expressionTokens: [], - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); - - const subs = rootExp.subExpressions[2].subExpressions; - - expect(subs[0].constructor.name).toEqual("NoteFlatTextExp"); - expect(subs[0].tokens).toEqual(["hello", "hi"]); - - expect(subs[1].constructor.name).toEqual("NoteContentFulltextExp"); - expect(subs[1].tokens).toEqual(["hello", "hi"]); - }); - - it("simple label comparison", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#mylabel", "=", "text"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); - expect(rootExp.subExpressions[2].attributeType).toEqual("label"); - expect(rootExp.subExpressions[2].attributeName).toEqual("mylabel"); - expect(rootExp.subExpressions[2].comparator).toBeTruthy(); - }); - - it("simple attribute negation", () => { - let rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#!mylabel"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); - expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); - expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("label"); - expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("mylabel"); - - rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["~!myrelation"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); - expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); - expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("relation"); - expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("myrelation"); - }); - - it("simple label AND", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]), - searchContext: new SearchContext(true) - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); - const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); - expect(secondSub.attributeName).toEqual("second"); - }); - - it("simple label AND without explicit AND", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); - const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); - expect(secondSub.attributeName).toEqual("second"); - }); - - it("simple label OR", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); - const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); - expect(secondSub.attributeName).toEqual("second"); - }); - - it("fulltext and simple label", () => { - const rootExp = parse({ - fulltextTokens: tokens(["hello"]), - expressionTokens: tokens(["#mylabel", "=", "text"]), - searchContext: new SearchContext({excludeArchived: true}) - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions; - - expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); - expect(firstSub.propertyName).toEqual('isArchived'); - - expect(thirdSub.constructor.name).toEqual("OrExp"); - expect(thirdSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); - expect(thirdSub.subExpressions[0].tokens).toEqual(["hello"]); - - expect(fourth.constructor.name).toEqual("LabelComparisonExp"); - expect(fourth.attributeName).toEqual("mylabel"); - }); - - it("label sub-expression", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); - const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("AndExp"); - const [firstSubSub, secondSubSub] = secondSub.subExpressions; - - expect(firstSubSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSubSub.attributeName).toEqual("second"); - - expect(secondSubSub.constructor.name).toEqual("LabelComparisonExp"); - expect(secondSubSub.attributeName).toEqual("third"); - }); - - it("label sub-expression without explicit operator", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); - const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("AttributeExistsExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("OrExp"); - const [firstSubSub, secondSubSub] = secondSub.subExpressions; - - expect(firstSubSub.constructor.name).toEqual("AttributeExistsExp"); - expect(firstSubSub.attributeName).toEqual("second"); - - expect(secondSubSub.constructor.name).toEqual("AttributeExistsExp"); - expect(secondSubSub.attributeName).toEqual("third"); - - expect(thirdSub.constructor.name).toEqual("AttributeExistsExp"); - expect(thirdSub.attributeName).toEqual("fourth"); - }); -}); - -describe("Invalid expressions", () => { - it("incomplete comparison", () => { - const searchContext = new SearchContext(); - - parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "="]), - searchContext - }); - - expect(searchContext.error).toEqual('Misplaced or incomplete expression "="') - }); - - it("comparison between labels is impossible", () => { - let searchContext = new SearchContext(); - searchContext.originalQuery = "#first = #second"; - - parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "#second"]), - searchContext - }); - - expect(searchContext.error).toEqual(`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`); - - searchContext = new SearchContext(); - searchContext.originalQuery = "#first = note.relations.second"; - - parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]), - searchContext - }); - - expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`); - - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: [ - { token: "#first", inQuotes: false }, - { token: "=", inQuotes: false }, - { token: "#second", inQuotes: true }, - ], - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); - expect(rootExp.subExpressions[2].attributeType).toEqual("label"); - expect(rootExp.subExpressions[2].attributeName).toEqual("first"); - expect(rootExp.subExpressions[2].comparator).toBeTruthy(); - }); - - it("searching by relation without note property", () => { - const searchContext = new SearchContext(); - - parse({ - fulltextTokens: [], - expressionTokens: tokens(["~first", "=", "text", "-", "abc"]), - searchContext - }); - - expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""') - }); -}); diff --git a/spec/search/parser.spec.ts b/spec/search/parser.spec.ts new file mode 100644 index 0000000000..d8ba2d7a65 --- /dev/null +++ b/spec/search/parser.spec.ts @@ -0,0 +1,319 @@ +// @ts-nocheck +// There are many issues with the types of the parser e.g. "parse" function returns "Expression" +// but we access properties like "subExpressions" which is not defined in the "Expression" class. + +import Expression = require('../../src/services/search/expressions/expression'); +import SearchContext = require('../../src/services/search/search_context'); +import parse = require('../../src/services/search/services/parse'); + +function tokens(toks: Array, cur = 0): Array { + return toks.map((arg) => { + if (Array.isArray(arg)) { + return tokens(arg, cur); + } else { + cur += arg.length; + + return { + token: arg, + inQuotes: false, + startIndex: cur - arg.length, + endIndex: cur - 1, + }; + } + }); +} + +function assertIsArchived(exp: Expression) { + expect(exp.constructor.name).toEqual('PropertyComparisonExp'); + expect(exp.propertyName).toEqual('isArchived'); + expect(exp.operator).toEqual('='); + expect(exp.comparedValue).toEqual('false'); +} + +describe('Parser', () => { + it('fulltext parser without content', () => { + const rootExp = parse({ + fulltextTokens: tokens(['hello', 'hi']), + expressionTokens: [], + searchContext: new SearchContext({ excludeArchived: true }), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + expect(rootExp.subExpressions[0].constructor.name).toEqual('PropertyComparisonExp'); + expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); + expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual('NoteFlatTextExp'); + expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(['hello', 'hi']); + }); + + it('fulltext parser with content', () => { + const rootExp = parse({ + fulltextTokens: tokens(['hello', 'hi']), + expressionTokens: [], + searchContext: new SearchContext(), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); + + const subs = rootExp.subExpressions[2].subExpressions; + + expect(subs[0].constructor.name).toEqual('NoteFlatTextExp'); + expect(subs[0].tokens).toEqual(['hello', 'hi']); + + expect(subs[1].constructor.name).toEqual('NoteContentFulltextExp'); + expect(subs[1].tokens).toEqual(['hello', 'hi']); + }); + + it('simple label comparison', () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(['#mylabel', '=', 'text']), + searchContext: new SearchContext(), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp'); + expect(rootExp.subExpressions[2].attributeType).toEqual('label'); + expect(rootExp.subExpressions[2].attributeName).toEqual('mylabel'); + expect(rootExp.subExpressions[2].comparator).toBeTruthy(); + }); + + it('simple attribute negation', () => { + let rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(['#!mylabel']), + searchContext: new SearchContext(), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp'); + expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp'); + expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('label'); + expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('mylabel'); + + rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(['~!myrelation']), + searchContext: new SearchContext(), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp'); + expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp'); + expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('relation'); + expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('myrelation'); + }); + + it('simple label AND', () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(['#first', '=', 'text', 'and', '#second', '=', 'text']), + searchContext: new SearchContext(true), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp'); + const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; + + expect(firstSub.constructor.name).toEqual('LabelComparisonExp'); + expect(firstSub.attributeName).toEqual('first'); + + expect(secondSub.constructor.name).toEqual('LabelComparisonExp'); + expect(secondSub.attributeName).toEqual('second'); + }); + + it('simple label AND without explicit AND', () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(['#first', '=', 'text', '#second', '=', 'text']), + searchContext: new SearchContext(), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp'); + const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; + + expect(firstSub.constructor.name).toEqual('LabelComparisonExp'); + expect(firstSub.attributeName).toEqual('first'); + + expect(secondSub.constructor.name).toEqual('LabelComparisonExp'); + expect(secondSub.attributeName).toEqual('second'); + }); + + it('simple label OR', () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(['#first', '=', 'text', 'or', '#second', '=', 'text']), + searchContext: new SearchContext(), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); + const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; + + expect(firstSub.constructor.name).toEqual('LabelComparisonExp'); + expect(firstSub.attributeName).toEqual('first'); + + expect(secondSub.constructor.name).toEqual('LabelComparisonExp'); + expect(secondSub.attributeName).toEqual('second'); + }); + + it('fulltext and simple label', () => { + const rootExp = parse({ + fulltextTokens: tokens(['hello']), + expressionTokens: tokens(['#mylabel', '=', 'text']), + searchContext: new SearchContext({ excludeArchived: true }), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions; + + expect(firstSub.constructor.name).toEqual('PropertyComparisonExp'); + expect(firstSub.propertyName).toEqual('isArchived'); + + expect(thirdSub.constructor.name).toEqual('OrExp'); + expect(thirdSub.subExpressions[0].constructor.name).toEqual('NoteFlatTextExp'); + expect(thirdSub.subExpressions[0].tokens).toEqual(['hello']); + + expect(fourth.constructor.name).toEqual('LabelComparisonExp'); + expect(fourth.attributeName).toEqual('mylabel'); + }); + + it('label sub-expression', () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(['#first', '=', 'text', 'or', ['#second', '=', 'text', 'and', '#third', '=', 'text']]), + searchContext: new SearchContext(), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); + const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; + + expect(firstSub.constructor.name).toEqual('LabelComparisonExp'); + expect(firstSub.attributeName).toEqual('first'); + + expect(secondSub.constructor.name).toEqual('AndExp'); + const [firstSubSub, secondSubSub] = secondSub.subExpressions; + + expect(firstSubSub.constructor.name).toEqual('LabelComparisonExp'); + expect(firstSubSub.attributeName).toEqual('second'); + + expect(secondSubSub.constructor.name).toEqual('LabelComparisonExp'); + expect(secondSubSub.attributeName).toEqual('third'); + }); + + it('label sub-expression without explicit operator', () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(['#first', ['#second', 'or', '#third'], '#fourth']), + searchContext: new SearchContext(), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp'); + const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions; + + expect(firstSub.constructor.name).toEqual('AttributeExistsExp'); + expect(firstSub.attributeName).toEqual('first'); + + expect(secondSub.constructor.name).toEqual('OrExp'); + const [firstSubSub, secondSubSub] = secondSub.subExpressions; + + expect(firstSubSub.constructor.name).toEqual('AttributeExistsExp'); + expect(firstSubSub.attributeName).toEqual('second'); + + expect(secondSubSub.constructor.name).toEqual('AttributeExistsExp'); + expect(secondSubSub.attributeName).toEqual('third'); + + expect(thirdSub.constructor.name).toEqual('AttributeExistsExp'); + expect(thirdSub.attributeName).toEqual('fourth'); + }); +}); + +describe('Invalid expressions', () => { + it('incomplete comparison', () => { + const searchContext = new SearchContext(); + + parse({ + fulltextTokens: [], + expressionTokens: tokens(['#first', '=']), + searchContext, + }); + + expect(searchContext.error).toEqual('Misplaced or incomplete expression "="'); + }); + + it('comparison between labels is impossible', () => { + let searchContext = new SearchContext(); + searchContext.originalQuery = '#first = #second'; + + parse({ + fulltextTokens: [], + expressionTokens: tokens(['#first', '=', '#second']), + searchContext, + }); + + expect(searchContext.error).toEqual( + `Error near token "#second" in "#first = #second", it's possible to compare with constant only.` + ); + + searchContext = new SearchContext(); + searchContext.originalQuery = '#first = note.relations.second'; + + parse({ + fulltextTokens: [], + expressionTokens: tokens(['#first', '=', 'note', '.', 'relations', 'second']), + searchContext, + }); + + expect(searchContext.error).toEqual( + `Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.` + ); + + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: [ + { token: '#first', inQuotes: false }, + { token: '=', inQuotes: false }, + { token: '#second', inQuotes: true }, + ], + searchContext: new SearchContext(), + }); + + expect(rootExp.constructor.name).toEqual('AndExp'); + assertIsArchived(rootExp.subExpressions[0]); + + expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp'); + expect(rootExp.subExpressions[2].attributeType).toEqual('label'); + expect(rootExp.subExpressions[2].attributeName).toEqual('first'); + expect(rootExp.subExpressions[2].comparator).toBeTruthy(); + }); + + it('searching by relation without note property', () => { + const searchContext = new SearchContext(); + + parse({ + fulltextTokens: [], + expressionTokens: tokens(['~first', '=', 'text', '-', 'abc']), + searchContext, + }); + + expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""'); + }); +}); diff --git a/spec/search/search.spec.js b/spec/search/search.spec.js deleted file mode 100644 index c628ac2a0d..0000000000 --- a/spec/search/search.spec.js +++ /dev/null @@ -1,663 +0,0 @@ -const searchService = require('../../src/services/search/services/search'); -const BNote = require('../../src/becca/entities/bnote.js'); -const BBranch = require('../../src/becca/entities/bbranch.js'); -const SearchContext = require('../../src/services/search/search_context'); -const dateUtils = require('../../src/services/date_utils'); -const becca = require('../../src/becca/becca.js'); -const {NoteBuilder, findNoteByTitle, note} = require('./becca_mocking.js'); - -describe("Search", () => { - let rootNote; - - beforeEach(() => { - becca.reset(); - - rootNote = new NoteBuilder(new BNote({noteId: 'root', title: 'root', type: 'text'})); - new BBranch({branchId: 'none_root', noteId: 'root', parentNoteId: 'none', notePosition: 10}); - }); - - it("simple path match", () => { - rootNote - .child(note("Europe") - .child(note("Austria")) - ); - - const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery('europe austria', searchContext); - - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - }); - - it("normal search looks also at attributes", () => { - const austria = note("Austria"); - const vienna = note("Vienna"); - - rootNote - .child(austria - .relation('capital', vienna)) - .child(vienna - .label('inhabitants', '1888776')); - - const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('capital', searchContext); - - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('inhabitants', searchContext); - - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Vienna")).toBeTruthy(); - }); - - it("normal search looks also at type and mime", () => { - rootNote - .child(note("Effective Java", {type: 'book', mime:''})) - .child(note("Hello World.java", {type: 'code', mime: 'text/x-java'})); - - const searchContext = new SearchContext(); - let searchResults = searchService.findResultsWithQuery('book', searchContext); - - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Effective Java")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('text', searchContext); // should match mime - - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Hello World.java")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('java', searchContext); - - expect(searchResults.length).toEqual(2); - }); - - it("only end leafs are results", () => { - rootNote - .child(note("Europe") - .child(note("Austria")) - ); - - const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery('europe', searchContext); - - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); - }); - - it("only end leafs are results", () => { - rootNote - .child(note("Europe") - .child(note("Austria") - .label('capital', 'Vienna')) - ); - - const searchContext = new SearchContext(); - - const searchResults = searchService.findResultsWithQuery('Vienna', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - }); - - it("label comparison with short syntax", () => { - rootNote - .child(note("Europe") - .child(note("Austria") - .label('capital', 'Vienna')) - .child(note("Czech Republic") - .label('capital', 'Prague')) - ); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('#capital=Vienna', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - - // case sensitivity: - searchResults = searchService.findResultsWithQuery('#CAPITAL=VIENNA', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('#caPItal=vienNa', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - }); - - it("label comparison with full syntax", () => { - rootNote - .child(note("Europe") - .child(note("Austria") - .label('capital', 'Vienna')) - .child(note("Czech Republic") - .label('capital', 'Prague')) - ); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('# note.labels.capital=Prague', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - }); - - it("numeric label comparison", () => { - rootNote - .child(note("Europe") - .label('country', '', true) - .child(note("Austria") - .label('population', '8859000')) - .child(note("Czech Republic") - .label('population', '10650000')) - ); - - const searchContext = new SearchContext(); - - const searchResults = searchService.findResultsWithQuery('#country #population >= 10000000', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - }); - - it("inherited label comparison", () => { - rootNote - .child(note("Europe") - .label('country', '', true) - .child(note("Austria")) - .child(note("Czech Republic")) - ); - - const searchContext = new SearchContext(); - - const searchResults = searchService.findResultsWithQuery('austria #country', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - }); - - it("numeric label comparison fallback to string comparison", () => { - // dates should not be coerced into numbers which would then give wrong numbers - - rootNote - .child(note("Europe") - .label('country', '', true) - .child(note("Austria") - .label('established', '1955-07-27')) - .child(note("Czech Republic") - .label('established', '1993-01-01')) - .child(note("Hungary") - .label('established', '1920-06-04')) - ); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('#established <= "1955-01-01"', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Hungary")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('#established > "1955-01-01"', searchContext); - expect(searchResults.length).toEqual(2); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - }); - - it("smart date comparisons", () => { - // dates should not be coerced into numbers which would then give wrong numbers - - rootNote - .child(note("My note", {dateCreated: dateUtils.localNowDateTime()}) - .label('year', new Date().getFullYear().toString()) - .label('month', dateUtils.localNowDate().substr(0, 7)) - .label('date', dateUtils.localNowDate()) - .label('dateTime', dateUtils.localNowDateTime()) - ); - - const searchContext = new SearchContext(); - - function test(query, expectedResultCount) { - const searchResults = searchService.findResultsWithQuery(query, searchContext); - expect(searchResults.length).toEqual(expectedResultCount); - - if (expectedResultCount === 1) { - expect(findNoteByTitle(searchResults, "My note")).toBeTruthy(); - } - } - - test("#year = YEAR", 1); - test("#year = 'YEAR'", 0); - test("#year >= YEAR", 1); - test("#year <= YEAR", 1); - test("#year < YEAR+1", 1); - test("#year < YEAR + 1", 1); - test("#year < year + 1", 1); - test("#year > YEAR+1", 0); - - test("#month = MONTH", 1); - test("#month = month", 1); - test("#month = 'MONTH'", 0); - - test("note.dateCreated =* month", 2); - - test("#date = TODAY", 1); - test("#date = today", 1); - test("#date = 'today'", 0); - test("#date > TODAY", 0); - test("#date > TODAY-1", 1); - test("#date > TODAY - 1", 1); - test("#date < TODAY+1", 1); - test("#date < TODAY + 1", 1); - test("#date < 'TODAY + 1'", 1); - - test("#dateTime <= NOW+10", 1); - test("#dateTime <= NOW + 10", 1); - test("#dateTime < NOW-10", 0); - test("#dateTime >= NOW-10", 1); - test("#dateTime < NOW-10", 0); - }); - - it("logical or", () => { - rootNote - .child(note("Europe") - .label('country', '', true) - .child(note("Austria") - .label('languageFamily', 'germanic')) - .child(note("Czech Republic") - .label('languageFamily', 'slavic')) - .child(note("Hungary") - .label('languageFamily', 'finnougric')) - ); - - const searchContext = new SearchContext(); - - const searchResults = searchService.findResultsWithQuery('#languageFamily = slavic OR #languageFamily = germanic', searchContext); - expect(searchResults.length).toEqual(2); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - }); - - it("fuzzy attribute search", () => { - rootNote - .child(note("Europe") - .label('country', '', true) - .child(note("Austria") - .label('languageFamily', 'germanic')) - .child(note("Czech Republic") - .label('languageFamily', 'slavic'))); - - let searchContext = new SearchContext({fuzzyAttributeSearch: false}); - - let searchResults = searchService.findResultsWithQuery('#language', searchContext); - expect(searchResults.length).toEqual(0); - - searchResults = searchService.findResultsWithQuery('#languageFamily=ger', searchContext); - expect(searchResults.length).toEqual(0); - - searchContext = new SearchContext({fuzzyAttributeSearch: true}); - - searchResults = searchService.findResultsWithQuery('#language', searchContext); - expect(searchResults.length).toEqual(2); - - searchResults = searchService.findResultsWithQuery('#languageFamily=ger', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - }); - - it("filter by note property", () => { - rootNote - .child(note("Europe") - .child(note("Austria")) - .child(note("Czech Republic"))); - - const searchContext = new SearchContext(); - - const searchResults = searchService.findResultsWithQuery('# note.title =* czech', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - }); - - it("filter by note's parent", () => { - rootNote - .child(note("Europe") - .child(note("Austria")) - .child(note("Czech Republic") - .child(note("Prague"))) - ) - .child(note("Asia") - .child(note('Taiwan'))); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe', searchContext); - expect(searchResults.length).toEqual(2); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('# note.parents.title = Asia', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Taiwan")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('# note.parents.parents.title = Europe', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy(); - }); - - it("filter by note's ancestor", () => { - rootNote - .child(note("Europe") - .child(note("Austria")) - .child(note("Czech Republic") - .child(note("Prague").label('city'))) - ) - .child(note("Asia") - .child(note('Taiwan') - .child(note('Taipei').label('city'))) - ); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Europe', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Asia', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Taipei")).toBeTruthy(); - }); - - it("filter by note's child", () => { - rootNote - .child(note("Europe") - .child(note("Austria") - .child(note("Vienna"))) - .child(note("Czech Republic") - .child(note("Prague")))) - .child(note("Oceania") - .child(note('Australia'))); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('# note.children.title =* Aust', searchContext); - expect(searchResults.length).toEqual(2); - expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); - expect(findNoteByTitle(searchResults, "Oceania")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('# note.children.title =* Aust AND note.children.title *= republic', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('# note.children.children.title = Prague', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); - }); - - it("filter by relation's note properties using short syntax", () => { - const austria = note("Austria"); - const portugal = note("Portugal"); - - rootNote - .child(note("Europe") - .child(austria) - .child(note("Czech Republic") - .relation('neighbor', austria.note)) - .child(portugal) - .child(note("Spain") - .relation('neighbor', portugal.note)) - ); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('# ~neighbor.title = Austria', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('# ~neighbor.title = Portugal', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Spain")).toBeTruthy(); - }); - - it("filter by relation's note properties using long syntax", () => { - const austria = note("Austria"); - const portugal = note("Portugal"); - - rootNote - .child(note("Europe") - .child(austria) - .child(note("Czech Republic") - .relation('neighbor', austria.note)) - .child(portugal) - .child(note("Spain") - .relation('neighbor', portugal.note)) - ); - - const searchContext = new SearchContext(); - - const searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.title = Austria', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - }); - - it("filter by multiple level relation", () => { - const austria = note("Austria"); - const slovakia = note("Slovakia"); - const italy = note("Italy"); - const ukraine = note("Ukraine"); - - rootNote - .child(note("Europe") - .child(austria - .relation('neighbor', italy.note) - .relation('neighbor', slovakia.note)) - .child(note("Czech Republic") - .relation('neighbor', austria.note) - .relation('neighbor', slovakia.note)) - .child(slovakia - .relation('neighbor', ukraine.note)) - .child(ukraine) - ); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.relations.neighbor.title = Italy', searchContext); - expect(searchResults.length).toEqual(1); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - - searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.relations.neighbor.title = Ukraine', searchContext); - expect(searchResults.length).toEqual(2); - expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - }); - - it("test note properties", () => { - const austria = note("Austria"); - - austria.relation('myself', austria.note); - austria.label('capital', 'Vienna'); - austria.label('population', '8859000'); - - rootNote - .child(note("Asia")) - .child(note("Europe") - .child(austria - .child(note("Vienna")) - .child(note("Sebastian Kurz")) - ) - ) - .child(note("Mozart") - .child(austria)); - - austria.note.isProtected = false; - austria.note.dateCreated = '2020-05-14 12:11:42.001+0200'; - austria.note.dateModified = '2020-05-14 13:11:42.001+0200'; - austria.note.utcDateCreated = '2020-05-14 10:11:42.001Z'; - austria.note.utcDateModified = '2020-05-14 11:11:42.001Z'; - austria.note.contentLength = 1001; - - const searchContext = new SearchContext(); - - function test(propertyName, value, expectedResultCount) { - const searchResults = searchService.findResultsWithQuery(`# note.${propertyName} = ${value}`, searchContext); - expect(searchResults.length).toEqual(expectedResultCount); - } - - test("type", "text", 7); - test("TYPE", "TEXT", 7); - test("type", "code", 0); - - test("mime", "text/html", 6); - test("mime", "application/json", 0); - - test("isProtected", "false", 7); - test("isProtected", "FALSE", 7); - test("isProtected", "true", 0); - test("isProtected", "TRUE", 0); - - test("dateCreated", "'2020-05-14 12:11:42.001+0200'", 1); - test("dateCreated", "wrong", 0); - - test("dateModified", "'2020-05-14 13:11:42.001+0200'", 1); - test("dateModified", "wrong", 0); - - test("utcDateCreated", "'2020-05-14 10:11:42.001Z'", 1); - test("utcDateCreated", "wrong", 0); - - test("utcDateModified", "'2020-05-14 11:11:42.001Z'", 1); - test("utcDateModified", "wrong", 0); - - test("parentCount", "2", 1); - test("parentCount", "3", 0); - - test("childrenCount", "2", 1); - test("childrenCount", "10", 0); - - test("attributeCount", "3", 1); - test("attributeCount", "4", 0); - - test("labelCount", "2", 1); - test("labelCount", "3", 0); - - test("relationCount", "1", 1); - test("relationCount", "2", 0); - }); - - it("test order by", () => { - const italy = note("Italy").label("capital", "Rome"); - const slovakia = note("Slovakia").label("capital", "Bratislava"); - const austria = note("Austria").label("capital", "Vienna"); - const ukraine = note("Ukraine").label("capital", "Kiev"); - - rootNote - .child(note("Europe") - .child(ukraine) - .child(slovakia) - .child(austria) - .child(italy)); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.title', searchContext); - expect(searchResults.length).toEqual(4); - expect(becca.notes[searchResults[0].noteId].title).toEqual("Austria"); - expect(becca.notes[searchResults[1].noteId].title).toEqual("Italy"); - expect(becca.notes[searchResults[2].noteId].title).toEqual("Slovakia"); - expect(becca.notes[searchResults[3].noteId].title).toEqual("Ukraine"); - - searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.labels.capital', searchContext); - expect(searchResults.length).toEqual(4); - expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia"); - expect(becca.notes[searchResults[1].noteId].title).toEqual("Ukraine"); - expect(becca.notes[searchResults[2].noteId].title).toEqual("Italy"); - expect(becca.notes[searchResults[3].noteId].title).toEqual("Austria"); - - searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.labels.capital DESC', searchContext); - expect(searchResults.length).toEqual(4); - expect(becca.notes[searchResults[0].noteId].title).toEqual("Austria"); - expect(becca.notes[searchResults[1].noteId].title).toEqual("Italy"); - expect(becca.notes[searchResults[2].noteId].title).toEqual("Ukraine"); - expect(becca.notes[searchResults[3].noteId].title).toEqual("Slovakia"); - - searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.labels.capital DESC limit 2', searchContext); - expect(searchResults.length).toEqual(2); - expect(becca.notes[searchResults[0].noteId].title).toEqual("Austria"); - expect(becca.notes[searchResults[1].noteId].title).toEqual("Italy"); - - searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1', searchContext); - expect(searchResults.length).toEqual(1); - - searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext); - expect(searchResults.length).toEqual(4); - }); - - it("test not(...)", () => { - const italy = note("Italy").label("capital", "Rome"); - const slovakia = note("Slovakia").label("capital", "Bratislava"); - - rootNote - .child(note("Europe") - .child(slovakia) - .child(italy)); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('# not(#capital) and note.noteId != root', searchContext); - expect(searchResults.length).toEqual(1); - expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe"); - - searchResults = searchService.findResultsWithQuery('#!capital and note.noteId != root', searchContext); - expect(searchResults.length).toEqual(1); - expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe"); - }); - - it("test note.text *=* something", () => { - const italy = note("Italy").label("capital", "Rome"); - const slovakia = note("Slovakia").label("capital", "Bratislava"); - - rootNote - .child(note("Europe") - .child(slovakia) - .child(italy)); - - const searchContext = new SearchContext(); - - let searchResults = searchService.findResultsWithQuery('# note.text *=* vaki and note.noteId != root', searchContext); - expect(searchResults.length).toEqual(1); - expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia"); - }); - - it("test that fulltext does not match archived notes", () => { - const italy = note("Italy").label("capital", "Rome"); - const slovakia = note("Slovakia").label("capital", "Bratislava"); - - rootNote - .child(note("Reddit").label('archived', '', true) - .child(note('Post X')) - .child(note('Post Y'))) - .child(note ('Reddit is bad')); - - const searchContext = new SearchContext({excludeArchived: true}); - - let searchResults = searchService.findResultsWithQuery('reddit', searchContext); - expect(searchResults.length).toEqual(1); - expect(becca.notes[searchResults[0].noteId].title).toEqual("Reddit is bad"); - }); - - // FIXME: test what happens when we order without any filter criteria - - // it("comparison between labels", () => { - // rootNote - // .child(note("Europe") - // .child(note("Austria") - // .label('capital', 'Vienna') - // .label('largestCity', 'Vienna')) - // .child(note("Canada") - // .label('capital', 'Ottawa') - // .label('largestCity', 'Toronto')) - // .child(note("Czech Republic") - // .label('capital', 'Prague') - // .label('largestCity', 'Prague')) - // ); - // - // const searchContext = new SearchContext(); - // - // const searchResults = searchService.findResultsWithQuery('#capital = #largestCity', searchContext); - // expect(searchResults.length).toEqual(2); - // expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); - // expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy(); - // }) -}); diff --git a/spec/search/search.spec.ts b/spec/search/search.spec.ts new file mode 100644 index 0000000000..2b4a588524 --- /dev/null +++ b/spec/search/search.spec.ts @@ -0,0 +1,634 @@ +import searchService = require('../../src/services/search/services/search'); +import BNote = require('../../src/becca/entities/bnote'); +import BBranch = require('../../src/becca/entities/bbranch'); +import SearchContext = require('../../src/services/search/search_context'); +import dateUtils = require('../../src/services/date_utils'); +import becca = require('../../src/becca/becca'); +// const { NoteBuilder, findNoteByTitle, note } = require("./becca_mocking"); +import becca_mocking = require('./becca_mocking'); + +describe('Search', () => { + let rootNote: any; + + beforeEach(() => { + becca.reset(); + + rootNote = new becca_mocking.NoteBuilder(new BNote({ noteId: 'root', title: 'root', type: 'text' })); + new BBranch({ + branchId: 'none_root', + noteId: 'root', + parentNoteId: 'none', + notePosition: 10, + }); + }); + + it('simple path match', () => { + rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria'))); + + const searchContext = new SearchContext(); + const searchResults = searchService.findResultsWithQuery('europe austria', searchContext); + + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + }); + + it('normal search looks also at attributes', () => { + const austria = becca_mocking.note('Austria'); + const vienna = becca_mocking.note('Vienna'); + + rootNote.child(austria.relation('capital', vienna.note)).child(vienna.label('inhabitants', '1888776')); + + const searchContext = new SearchContext(); + let searchResults = searchService.findResultsWithQuery('capital', searchContext); + + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('inhabitants', searchContext); + + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Vienna')).toBeTruthy(); + }); + + it('normal search looks also at type and mime', () => { + rootNote + .child(becca_mocking.note('Effective Java', { type: 'book', mime: '' })) + .child(becca_mocking.note('Hello World.java', { type: 'code', mime: 'text/x-java' })); + + const searchContext = new SearchContext(); + let searchResults = searchService.findResultsWithQuery('book', searchContext); + + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Effective Java')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('text', searchContext); // should match mime + + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Hello World.java')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('java', searchContext); + + expect(searchResults.length).toEqual(2); + }); + + it('only end leafs are results', () => { + rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria'))); + + const searchContext = new SearchContext(); + const searchResults = searchService.findResultsWithQuery('europe', searchContext); + + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); + }); + + it('only end leafs are results', () => { + rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria').label('capital', 'Vienna'))); + + const searchContext = new SearchContext(); + + const searchResults = searchService.findResultsWithQuery('Vienna', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + }); + + it('label comparison with short syntax', () => { + rootNote.child( + becca_mocking + .note('Europe') + .child(becca_mocking.note('Austria').label('capital', 'Vienna')) + .child(becca_mocking.note('Czech Republic').label('capital', 'Prague')) + ); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('#capital=Vienna', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + + // case sensitivity: + searchResults = searchService.findResultsWithQuery('#CAPITAL=VIENNA', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('#caPItal=vienNa', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + }); + + it('label comparison with full syntax', () => { + rootNote.child( + becca_mocking + .note('Europe') + .child(becca_mocking.note('Austria').label('capital', 'Vienna')) + .child(becca_mocking.note('Czech Republic').label('capital', 'Prague')) + ); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('# note.labels.capital=Prague', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + }); + + it('numeric label comparison', () => { + rootNote.child( + becca_mocking + .note('Europe') + .label('country', '', true) + .child(becca_mocking.note('Austria').label('population', '8859000')) + .child(becca_mocking.note('Czech Republic').label('population', '10650000')) + ); + + const searchContext = new SearchContext(); + + const searchResults = searchService.findResultsWithQuery('#country #population >= 10000000', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + }); + + it('inherited label comparison', () => { + rootNote.child( + becca_mocking + .note('Europe') + .label('country', '', true) + .child(becca_mocking.note('Austria')) + .child(becca_mocking.note('Czech Republic')) + ); + + const searchContext = new SearchContext(); + + const searchResults = searchService.findResultsWithQuery('austria #country', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + }); + + it('numeric label comparison fallback to string comparison', () => { + // dates should not be coerced into numbers which would then give wrong numbers + + rootNote.child( + becca_mocking + .note('Europe') + .label('country', '', true) + .child(becca_mocking.note('Austria').label('established', '1955-07-27')) + .child(becca_mocking.note('Czech Republic').label('established', '1993-01-01')) + .child(becca_mocking.note('Hungary').label('established', '1920-06-04')) + ); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('#established <= "1955-01-01"', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Hungary')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('#established > "1955-01-01"', searchContext); + expect(searchResults.length).toEqual(2); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + }); + + it('smart date comparisons', () => { + // dates should not be coerced into numbers which would then give wrong numbers + + rootNote.child( + becca_mocking + .note('My note', { dateCreated: dateUtils.localNowDateTime() }) + .label('year', new Date().getFullYear().toString()) + .label('month', dateUtils.localNowDate().substr(0, 7)) + .label('date', dateUtils.localNowDate()) + .label('dateTime', dateUtils.localNowDateTime()) + ); + + const searchContext = new SearchContext(); + + function test(query: string, expectedResultCount: number) { + const searchResults = searchService.findResultsWithQuery(query, searchContext); + expect(searchResults.length).toEqual(expectedResultCount); + + if (expectedResultCount === 1) { + expect(becca_mocking.findNoteByTitle(searchResults, 'My note')).toBeTruthy(); + } + } + + test('#year = YEAR', 1); + test("#year = 'YEAR'", 0); + test('#year >= YEAR', 1); + test('#year <= YEAR', 1); + test('#year < YEAR+1', 1); + test('#year < YEAR + 1', 1); + test('#year < year + 1', 1); + test('#year > YEAR+1', 0); + + test('#month = MONTH', 1); + test('#month = month', 1); + test("#month = 'MONTH'", 0); + + test('note.dateCreated =* month', 2); + + test('#date = TODAY', 1); + test('#date = today', 1); + test("#date = 'today'", 0); + test('#date > TODAY', 0); + test('#date > TODAY-1', 1); + test('#date > TODAY - 1', 1); + test('#date < TODAY+1', 1); + test('#date < TODAY + 1', 1); + test("#date < 'TODAY + 1'", 1); + + test('#dateTime <= NOW+10', 1); + test('#dateTime <= NOW + 10', 1); + test('#dateTime < NOW-10', 0); + test('#dateTime >= NOW-10', 1); + test('#dateTime < NOW-10', 0); + }); + + it('logical or', () => { + rootNote.child( + becca_mocking + .note('Europe') + .label('country', '', true) + .child(becca_mocking.note('Austria').label('languageFamily', 'germanic')) + .child(becca_mocking.note('Czech Republic').label('languageFamily', 'slavic')) + .child(becca_mocking.note('Hungary').label('languageFamily', 'finnougric')) + ); + + const searchContext = new SearchContext(); + + const searchResults = searchService.findResultsWithQuery('#languageFamily = slavic OR #languageFamily = germanic', searchContext); + expect(searchResults.length).toEqual(2); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + }); + + it('fuzzy attribute search', () => { + rootNote.child( + becca_mocking + .note('Europe') + .label('country', '', true) + .child(becca_mocking.note('Austria').label('languageFamily', 'germanic')) + .child(becca_mocking.note('Czech Republic').label('languageFamily', 'slavic')) + ); + + let searchContext = new SearchContext({ fuzzyAttributeSearch: false }); + + let searchResults = searchService.findResultsWithQuery('#language', searchContext); + expect(searchResults.length).toEqual(0); + + searchResults = searchService.findResultsWithQuery('#languageFamily=ger', searchContext); + expect(searchResults.length).toEqual(0); + + searchContext = new SearchContext({ fuzzyAttributeSearch: true }); + + searchResults = searchService.findResultsWithQuery('#language', searchContext); + expect(searchResults.length).toEqual(2); + + searchResults = searchService.findResultsWithQuery('#languageFamily=ger', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + }); + + it('filter by note property', () => { + rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria')).child(becca_mocking.note('Czech Republic'))); + + const searchContext = new SearchContext(); + + const searchResults = searchService.findResultsWithQuery('# note.title =* czech', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + }); + + it("filter by note's parent", () => { + rootNote + .child( + becca_mocking + .note('Europe') + .child(becca_mocking.note('Austria')) + .child(becca_mocking.note('Czech Republic').child(becca_mocking.note('Prague'))) + ) + .child(becca_mocking.note('Asia').child(becca_mocking.note('Taiwan'))); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe', searchContext); + expect(searchResults.length).toEqual(2); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('# note.parents.title = Asia', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Taiwan')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('# note.parents.parents.title = Europe', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Prague')).toBeTruthy(); + }); + + it("filter by note's ancestor", () => { + rootNote + .child( + becca_mocking + .note('Europe') + .child(becca_mocking.note('Austria')) + .child(becca_mocking.note('Czech Republic').child(becca_mocking.note('Prague').label('city'))) + ) + .child(becca_mocking.note('Asia').child(becca_mocking.note('Taiwan').child(becca_mocking.note('Taipei').label('city')))); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Europe', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Prague')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Asia', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Taipei')).toBeTruthy(); + }); + + it("filter by note's child", () => { + rootNote + .child( + becca_mocking + .note('Europe') + .child(becca_mocking.note('Austria').child(becca_mocking.note('Vienna'))) + .child(becca_mocking.note('Czech Republic').child(becca_mocking.note('Prague'))) + ) + .child(becca_mocking.note('Oceania').child(becca_mocking.note('Australia'))); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('# note.children.title =* Aust', searchContext); + expect(searchResults.length).toEqual(2); + expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, 'Oceania')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery( + '# note.children.title =* Aust AND note.children.title *= republic', + searchContext + ); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('# note.children.children.title = Prague', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); + }); + + it("filter by relation's note properties using short syntax", () => { + const austria = becca_mocking.note('Austria'); + const portugal = becca_mocking.note('Portugal'); + + rootNote.child( + becca_mocking + .note('Europe') + .child(austria) + .child(becca_mocking.note('Czech Republic').relation('neighbor', austria.note)) + .child(portugal) + .child(becca_mocking.note('Spain').relation('neighbor', portugal.note)) + ); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('# ~neighbor.title = Austria', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('# ~neighbor.title = Portugal', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Spain')).toBeTruthy(); + }); + + it("filter by relation's note properties using long syntax", () => { + const austria = becca_mocking.note('Austria'); + const portugal = becca_mocking.note('Portugal'); + + rootNote.child( + becca_mocking + .note('Europe') + .child(austria) + .child(becca_mocking.note('Czech Republic').relation('neighbor', austria.note)) + .child(portugal) + .child(becca_mocking.note('Spain').relation('neighbor', portugal.note)) + ); + + const searchContext = new SearchContext(); + + const searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.title = Austria', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + }); + + it('filter by multiple level relation', () => { + const austria = becca_mocking.note('Austria'); + const slovakia = becca_mocking.note('Slovakia'); + const italy = becca_mocking.note('Italy'); + const ukraine = becca_mocking.note('Ukraine'); + + rootNote.child( + becca_mocking + .note('Europe') + .child(austria.relation('neighbor', italy.note).relation('neighbor', slovakia.note)) + .child(becca_mocking.note('Czech Republic').relation('neighbor', austria.note).relation('neighbor', slovakia.note)) + .child(slovakia.relation('neighbor', ukraine.note)) + .child(ukraine) + ); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.relations.neighbor.title = Italy', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + + searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.relations.neighbor.title = Ukraine', searchContext); + expect(searchResults.length).toEqual(2); + expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); + expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); + }); + + it('test note properties', () => { + const austria = becca_mocking.note('Austria'); + + austria.relation('myself', austria.note); + austria.label('capital', 'Vienna'); + austria.label('population', '8859000'); + + rootNote + .child(becca_mocking.note('Asia')) + .child( + becca_mocking.note('Europe').child(austria.child(becca_mocking.note('Vienna')).child(becca_mocking.note('Sebastian Kurz'))) + ) + .child(becca_mocking.note('Mozart').child(austria)); + + austria.note.isProtected = false; + austria.note.dateCreated = '2020-05-14 12:11:42.001+0200'; + austria.note.dateModified = '2020-05-14 13:11:42.001+0200'; + austria.note.utcDateCreated = '2020-05-14 10:11:42.001Z'; + austria.note.utcDateModified = '2020-05-14 11:11:42.001Z'; + // austria.note.contentLength = 1001; + + const searchContext = new SearchContext(); + + function test(propertyName: string, value: string, expectedResultCount: number) { + const searchResults = searchService.findResultsWithQuery(`# note.${propertyName} = ${value}`, searchContext); + expect(searchResults.length).toEqual(expectedResultCount); + } + + test('type', 'text', 7); + test('TYPE', 'TEXT', 7); + test('type', 'code', 0); + + test('mime', 'text/html', 6); + test('mime', 'application/json', 0); + + test('isProtected', 'false', 7); + test('isProtected', 'FALSE', 7); + test('isProtected', 'true', 0); + test('isProtected', 'TRUE', 0); + + test('dateCreated', "'2020-05-14 12:11:42.001+0200'", 1); + test('dateCreated', 'wrong', 0); + + test('dateModified', "'2020-05-14 13:11:42.001+0200'", 1); + test('dateModified', 'wrong', 0); + + test('utcDateCreated', "'2020-05-14 10:11:42.001Z'", 1); + test('utcDateCreated', 'wrong', 0); + + test('utcDateModified', "'2020-05-14 11:11:42.001Z'", 1); + test('utcDateModified', 'wrong', 0); + + test('parentCount', '2', 1); + test('parentCount', '3', 0); + + test('childrenCount', '2', 1); + test('childrenCount', '10', 0); + + test('attributeCount', '3', 1); + test('attributeCount', '4', 0); + + test('labelCount', '2', 1); + test('labelCount', '3', 0); + + test('relationCount', '1', 1); + test('relationCount', '2', 0); + }); + + it('test order by', () => { + const italy = becca_mocking.note('Italy').label('capital', 'Rome'); + const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); + const austria = becca_mocking.note('Austria').label('capital', 'Vienna'); + const ukraine = becca_mocking.note('Ukraine').label('capital', 'Kiev'); + + rootNote.child(becca_mocking.note('Europe').child(ukraine).child(slovakia).child(austria).child(italy)); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.title', searchContext); + expect(searchResults.length).toEqual(4); + expect(becca.notes[searchResults[0].noteId].title).toEqual('Austria'); + expect(becca.notes[searchResults[1].noteId].title).toEqual('Italy'); + expect(becca.notes[searchResults[2].noteId].title).toEqual('Slovakia'); + expect(becca.notes[searchResults[3].noteId].title).toEqual('Ukraine'); + + searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.labels.capital', searchContext); + expect(searchResults.length).toEqual(4); + expect(becca.notes[searchResults[0].noteId].title).toEqual('Slovakia'); + expect(becca.notes[searchResults[1].noteId].title).toEqual('Ukraine'); + expect(becca.notes[searchResults[2].noteId].title).toEqual('Italy'); + expect(becca.notes[searchResults[3].noteId].title).toEqual('Austria'); + + searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.labels.capital DESC', searchContext); + expect(searchResults.length).toEqual(4); + expect(becca.notes[searchResults[0].noteId].title).toEqual('Austria'); + expect(becca.notes[searchResults[1].noteId].title).toEqual('Italy'); + expect(becca.notes[searchResults[2].noteId].title).toEqual('Ukraine'); + expect(becca.notes[searchResults[3].noteId].title).toEqual('Slovakia'); + + searchResults = searchService.findResultsWithQuery( + '# note.parents.title = Europe orderBy note.labels.capital DESC limit 2', + searchContext + ); + expect(searchResults.length).toEqual(2); + expect(becca.notes[searchResults[0].noteId].title).toEqual('Austria'); + expect(becca.notes[searchResults[1].noteId].title).toEqual('Italy'); + + searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1', searchContext); + expect(searchResults.length).toEqual(1); + + searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext); + expect(searchResults.length).toEqual(4); + }); + + it('test not(...)', () => { + const italy = becca_mocking.note('Italy').label('capital', 'Rome'); + const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); + + rootNote.child(becca_mocking.note('Europe').child(slovakia).child(italy)); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('# not(#capital) and note.noteId != root', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca.notes[searchResults[0].noteId].title).toEqual('Europe'); + + searchResults = searchService.findResultsWithQuery('#!capital and note.noteId != root', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca.notes[searchResults[0].noteId].title).toEqual('Europe'); + }); + + it('test note.text *=* something', () => { + const italy = becca_mocking.note('Italy').label('capital', 'Rome'); + const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); + + rootNote.child(becca_mocking.note('Europe').child(slovakia).child(italy)); + + const searchContext = new SearchContext(); + + let searchResults = searchService.findResultsWithQuery('# note.text *=* vaki and note.noteId != root', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca.notes[searchResults[0].noteId].title).toEqual('Slovakia'); + }); + + it('test that fulltext does not match archived notes', () => { + const italy = becca_mocking.note('Italy').label('capital', 'Rome'); + const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); + + rootNote + .child( + becca_mocking + .note('Reddit') + .label('archived', '', true) + .child(becca_mocking.note('Post X')) + .child(becca_mocking.note('Post Y')) + ) + .child(becca_mocking.note('Reddit is bad')); + + const searchContext = new SearchContext({ includeArchivedNotes: false }); + + let searchResults = searchService.findResultsWithQuery('reddit', searchContext); + expect(searchResults.length).toEqual(1); + expect(becca.notes[searchResults[0].noteId].title).toEqual('Reddit is bad'); + }); + + // FIXME: test what happens when we order without any filter criteria + + // it("comparison between labels", () => { + // rootNote + // .child(becca_mocking.note("Europe") + // .child(becca_mocking.note("Austria") + // .label('capital', 'Vienna') + // .label('largestCity', 'Vienna')) + // .child(becca_mocking.note("Canada") + // .label('capital', 'Ottawa') + // .label('largestCity', 'Toronto')) + // .child(becca_mocking.note("Czech Republic") + // .label('capital', 'Prague') + // .label('largestCity', 'Prague')) + // ); + // + // const searchContext = new SearchContext(); + // + // const searchResults = searchService.findResultsWithQuery('#capital = #largestCity', searchContext); + // expect(searchResults.length).toEqual(2); + // expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); + // expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy(); + // }) +}); diff --git a/spec/search/value_extractor.spec.js b/spec/search/value_extractor.spec.js deleted file mode 100644 index 08508a4bf8..0000000000 --- a/spec/search/value_extractor.spec.js +++ /dev/null @@ -1,89 +0,0 @@ -const {note} = require('./becca_mocking.js'); -const ValueExtractor = require('../../src/services/search/value_extractor'); -const becca = require('../../src/becca/becca.js'); -const SearchContext = require('../../src/services/search/search_context'); - -const dsc = new SearchContext(); - -describe("Value extractor", () => { - beforeEach(() => { - becca.reset(); - }); - - it("simple title extraction", async () => { - const europe = note("Europe").note; - - const valueExtractor = new ValueExtractor(dsc, ["note", "title"]); - - expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(europe)).toEqual("Europe"); - }); - - it("label extraction", async () => { - const austria = note("Austria") - .label("Capital", "Vienna") - .note; - - let valueExtractor = new ValueExtractor(dsc, ["note", "labels", "capital"]); - - expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(austria)).toEqual("Vienna"); - - valueExtractor = new ValueExtractor(dsc, ["#capital"]); - - expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(austria)).toEqual("Vienna"); - }); - - it("parent/child property extraction", async () => { - const vienna = note("Vienna"); - const europe = note("Europe") - .child(note("Austria") - .child(vienna)); - - let valueExtractor = new ValueExtractor(dsc, ["note", "children", "children", "title"]); - - expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(europe.note)).toEqual("Vienna"); - - valueExtractor = new ValueExtractor(dsc, ["note", "parents", "parents", "title"]); - - expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(vienna.note)).toEqual("Europe"); - }); - - it("extract through relation", async () => { - const czechRepublic = note("Czech Republic").label("capital", "Prague"); - const slovakia = note("Slovakia").label("capital", "Bratislava"); - const austria = note("Austria") - .relation('neighbor', czechRepublic.note) - .relation('neighbor', slovakia.note); - - let valueExtractor = new ValueExtractor(dsc, ["note", "relations", "neighbor", "labels", "capital"]); - - expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(austria.note)).toEqual("Prague"); - - valueExtractor = new ValueExtractor(dsc, ["~neighbor", "labels", "capital"]); - - expect(valueExtractor.validate()).toBeFalsy(); - expect(valueExtractor.extract(austria.note)).toEqual("Prague"); - }); -}); - -describe("Invalid value extractor property path", () => { - it('each path must start with "note" (or label/relation)', - () => expect(new ValueExtractor(dsc, ["neighbor"]).validate()).toBeTruthy()); - - it("extra path element after terminal label", - () => expect(new ValueExtractor(dsc, ["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy()); - - it("extra path element after terminal title", - () => expect(new ValueExtractor(dsc, ["note", "title", "isProtected"]).validate()).toBeTruthy()); - - it("relation name and note property is missing", - () => expect(new ValueExtractor(dsc, ["note", "relations"]).validate()).toBeTruthy()); - - it("relation is specified but target note property is not specified", - () => expect(new ValueExtractor(dsc, ["note", "relations", "myrel"]).validate()).toBeTruthy()); -}); diff --git a/spec/search/value_extractor.spec.ts b/spec/search/value_extractor.spec.ts new file mode 100644 index 0000000000..28343bab85 --- /dev/null +++ b/spec/search/value_extractor.spec.ts @@ -0,0 +1,81 @@ +import becca_mocking = require('./becca_mocking'); +import ValueExtractor = require('../../src/services/search/value_extractor'); +import becca = require('../../src/becca/becca'); +import SearchContext = require('../../src/services/search/search_context'); + +const dsc = new SearchContext(); + +describe('Value extractor', () => { + beforeEach(() => { + becca.reset(); + }); + + it('simple title extraction', async () => { + const europe = becca_mocking.note('Europe').note; + + const valueExtractor = new ValueExtractor(dsc, ['note', 'title']); + + expect(valueExtractor.validate()).toBeFalsy(); + expect(valueExtractor.extract(europe)).toEqual('Europe'); + }); + + it('label extraction', async () => { + const austria = becca_mocking.note('Austria').label('Capital', 'Vienna').note; + + let valueExtractor = new ValueExtractor(dsc, ['note', 'labels', 'capital']); + + expect(valueExtractor.validate()).toBeFalsy(); + expect(valueExtractor.extract(austria)).toEqual('Vienna'); + + valueExtractor = new ValueExtractor(dsc, ['#capital']); + + expect(valueExtractor.validate()).toBeFalsy(); + expect(valueExtractor.extract(austria)).toEqual('Vienna'); + }); + + it('parent/child property extraction', async () => { + const vienna = becca_mocking.note('Vienna'); + const europe = becca_mocking.note('Europe').child(becca_mocking.note('Austria').child(vienna)); + + let valueExtractor = new ValueExtractor(dsc, ['note', 'children', 'children', 'title']); + + expect(valueExtractor.validate()).toBeFalsy(); + expect(valueExtractor.extract(europe.note)).toEqual('Vienna'); + + valueExtractor = new ValueExtractor(dsc, ['note', 'parents', 'parents', 'title']); + + expect(valueExtractor.validate()).toBeFalsy(); + expect(valueExtractor.extract(vienna.note)).toEqual('Europe'); + }); + + it('extract through relation', async () => { + const czechRepublic = becca_mocking.note('Czech Republic').label('capital', 'Prague'); + const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); + const austria = becca_mocking.note('Austria').relation('neighbor', czechRepublic.note).relation('neighbor', slovakia.note); + + let valueExtractor = new ValueExtractor(dsc, ['note', 'relations', 'neighbor', 'labels', 'capital']); + + expect(valueExtractor.validate()).toBeFalsy(); + expect(valueExtractor.extract(austria.note)).toEqual('Prague'); + + valueExtractor = new ValueExtractor(dsc, ['~neighbor', 'labels', 'capital']); + + expect(valueExtractor.validate()).toBeFalsy(); + expect(valueExtractor.extract(austria.note)).toEqual('Prague'); + }); +}); + +describe('Invalid value extractor property path', () => { + it('each path must start with "note" (or label/relation)', () => expect(new ValueExtractor(dsc, ['neighbor']).validate()).toBeTruthy()); + + it('extra path element after terminal label', () => + expect(new ValueExtractor(dsc, ['~neighbor', 'labels', 'capital', 'noteId']).validate()).toBeTruthy()); + + it('extra path element after terminal title', () => + expect(new ValueExtractor(dsc, ['note', 'title', 'isProtected']).validate()).toBeTruthy()); + + it('relation name and note property is missing', () => expect(new ValueExtractor(dsc, ['note', 'relations']).validate()).toBeTruthy()); + + it('relation is specified but target note property is not specified', () => + expect(new ValueExtractor(dsc, ['note', 'relations', 'myrel']).validate()).toBeTruthy()); +}); diff --git a/spec/support/etapi.js b/spec/support/etapi.js deleted file mode 100644 index 97bf5aee1c..0000000000 --- a/spec/support/etapi.js +++ /dev/null @@ -1,184 +0,0 @@ -const {spawn} = require("child_process"); -const kill = require('tree-kill'); - -let etapiAuthToken; - -const getEtapiAuthorizationHeader = () => "Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString('base64'); - -const PORT = '9999'; -const HOST = 'http://localhost:' + PORT; - -function describeEtapi(description, specDefinitions) { - describe(description, () => { - let appProcess; - - beforeAll(async () => { - appProcess = spawn('npm', ['run', 'start-test-server']); - - await new Promise(res => { - appProcess.stdout.on('data', data => { - console.log("Trilium: " + data.toString().trim()); - - if (data.toString().includes('Listening on port')) { - res(); - } - }); - }); - - await fetch(HOST + '/api/setup/new-document', { method: 'POST' }); - - const formData = new URLSearchParams(); - formData.append('password1', '1234'); - formData.append('password2', '1234'); - - await fetch(HOST + '/set-password', { method: 'POST', body: formData }); - - etapiAuthToken = (await (await fetch(HOST + '/etapi/auth/login', { - method: 'POST', - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ password: '1234' }) - })).json()).authToken; - }); - - afterAll(() => { - console.log("Attempting to kill the Trilium process as part of the cleanup..."); - kill(appProcess.pid, 'SIGKILL', () => { console.log("Trilium process killed.") }); - }); - - specDefinitions(); - }); -} - -async function getEtapiResponse(url) { - return await fetch(`${HOST}/etapi/${url}`, { - method: 'GET', - headers: { - Authorization: getEtapiAuthorizationHeader() - } - }); -} - -async function getEtapi(url) { - const response = await getEtapiResponse(url); - return await processEtapiResponse(response); -} - -async function getEtapiContent(url) { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: 'GET', - headers: { - Authorization: getEtapiAuthorizationHeader() - } - }); - - checkStatus(response); - - return response; -} - -async function postEtapi(url, data = {}) { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: 'POST', - headers: { - "Content-Type": "application/json", - Authorization: getEtapiAuthorizationHeader() - }, - body: JSON.stringify(data) - }); - return await processEtapiResponse(response); -} - -async function postEtapiContent(url, data) { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: 'POST', - headers: { - "Content-Type": "application/octet-stream", - Authorization: getEtapiAuthorizationHeader() - }, - body: data - }); - - checkStatus(response); - - return response; -} - -async function putEtapi(url, data = {}) { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: 'PUT', - headers: { - "Content-Type": "application/json", - Authorization: getEtapiAuthorizationHeader() - }, - body: JSON.stringify(data) - }); - return await processEtapiResponse(response); -} - -async function putEtapiContent(url, data) { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: 'PUT', - headers: { - "Content-Type": "application/octet-stream", - Authorization: getEtapiAuthorizationHeader() - }, - body: data - }); - - checkStatus(response); - - return response; -} - -async function patchEtapi(url, data = {}) { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: 'PATCH', - headers: { - "Content-Type": "application/json", - Authorization: getEtapiAuthorizationHeader() - }, - body: JSON.stringify(data) - }); - return await processEtapiResponse(response); -} - -async function deleteEtapi(url) { - const response = await fetch(`${HOST}/etapi/${url}`, { - method: 'DELETE', - headers: { - Authorization: getEtapiAuthorizationHeader() - } - }); - return await processEtapiResponse(response); -} - -async function processEtapiResponse(response) { - const text = await response.text(); - - if (response.status < 200 || response.status >= 300) { - throw new Error(`ETAPI error ${response.status}: ` + text); - } - - return text?.trim() ? JSON.parse(text) : null; -} - -function checkStatus(response) { - if (response.status < 200 || response.status >= 300) { - throw new Error(`ETAPI error ${response.status}`); - } -} - -module.exports = { - describeEtapi, - getEtapi, - getEtapiResponse, - getEtapiContent, - postEtapi, - postEtapiContent, - putEtapi, - putEtapiContent, - patchEtapi, - deleteEtapi -}; diff --git a/spec/support/etapi.ts b/spec/support/etapi.ts new file mode 100644 index 0000000000..05da87bc88 --- /dev/null +++ b/spec/support/etapi.ts @@ -0,0 +1,224 @@ +import child_process = require("child_process"); +import kill = require("tree-kill"); + +let etapiAuthToken: string | undefined; + +const getEtapiAuthorizationHeader = (): string => + "Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString("base64"); + +const PORT: string = "9999"; +const HOST: string = "http://localhost:" + PORT; + +type SpecDefinitionsFunc = () => void; + +function describeEtapi( + description: string, + specDefinitions: SpecDefinitionsFunc +): void { + describe(description, () => { + let appProcess: ReturnType; + + beforeAll(async () => { + appProcess = child_process.spawn("npm", ["run", "start-test-server"]); + if (!appProcess) { + throw new Error("Failed to start the Trilium process."); + } + + await new Promise((res) => { + appProcess.stdout!.on("data", (data) => { + console.log("Trilium: " + data.toString().trim()); + + if (data.toString().includes("Listening on port")) { + res(); + } + }); + }); + + await fetch(`${HOST}/api/setup/new-document`, { method: "POST" }); + + const formData = new URLSearchParams(); + formData.append("password1", "1234"); + formData.append("password2", "1234"); + + await fetch(`${HOST}/set-password`, { method: "POST", body: formData }); + + etapiAuthToken = ( + await ( + await fetch(`${HOST}/etapi/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ password: "1234" }), + }) + ).json() + ).authToken; + }); + + afterAll(() => { + console.log( + "Attempting to kill the Trilium process as part of the cleanup..." + ); + if (!appProcess.pid) { + console.log("Trilium process not found. Cannot kill."); + return; + } + + kill(appProcess.pid, "SIGKILL", (error) => { + if (error) { + console.error("Failed to kill the Trilium process.", error); + } + console.log("Trilium process killed."); + }); + }); + + specDefinitions(); + }); +} + +async function getEtapiResponse(url: string): Promise { + return await fetch(`${HOST}/etapi/${url}`, { + method: "GET", + headers: { + Authorization: getEtapiAuthorizationHeader(), + }, + }); +} + +async function getEtapi(url: string): Promise { + const response = await getEtapiResponse(url); + return await processEtapiResponse(response); +} + +async function getEtapiContent(url: string): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "GET", + headers: { + Authorization: getEtapiAuthorizationHeader(), + }, + }); + + checkStatus(response); + + return response; +} + +async function postEtapi( + url: string, + data: Record = {} +): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: getEtapiAuthorizationHeader(), + }, + body: JSON.stringify(data), + }); + return await processEtapiResponse(response); +} + +async function postEtapiContent( + url: string, + data: BodyInit +): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "POST", + headers: { + "Content-Type": "application/octet-stream", + Authorization: getEtapiAuthorizationHeader(), + }, + body: data, + }); + + checkStatus(response); + + return response; +} + +async function putEtapi( + url: string, + data: Record = {} +): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: getEtapiAuthorizationHeader(), + }, + body: JSON.stringify(data), + }); + return await processEtapiResponse(response); +} + +async function putEtapiContent( + url: string, + data?: BodyInit +): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "PUT", + headers: { + "Content-Type": "application/octet-stream", + Authorization: getEtapiAuthorizationHeader(), + }, + body: data, + }); + + checkStatus(response); + + return response; +} + +async function patchEtapi( + url: string, + data: Record = {} +): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: getEtapiAuthorizationHeader(), + }, + body: JSON.stringify(data), + }); + return await processEtapiResponse(response); +} + +async function deleteEtapi(url: string): Promise { + const response = await fetch(`${HOST}/etapi/${url}`, { + method: "DELETE", + headers: { + Authorization: getEtapiAuthorizationHeader(), + }, + }); + return await processEtapiResponse(response); +} + +async function processEtapiResponse(response: Response): Promise { + const text = await response.text(); + + if (response.status < 200 || response.status >= 300) { + throw new Error(`ETAPI error ${response.status}: ${text}`); + } + + return text?.trim() ? JSON.parse(text) : null; +} + +function checkStatus(response: Response): void { + if (response.status < 200 || response.status >= 300) { + throw new Error(`ETAPI error ${response.status}`); + } +} + +export { + describeEtapi, + getEtapi, + getEtapiResponse, + getEtapiContent, + postEtapi, + postEtapiContent, + putEtapi, + putEtapiContent, + patchEtapi, + deleteEtapi, +}; diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json index fc531ef50f..1a1e6b68e8 100644 --- a/spec/support/jasmine.json +++ b/spec/support/jasmine.json @@ -1,12 +1,7 @@ { - "spec_dir": "spec", - "spec_files": [ - "**/*[sS]pec.js", - "**/*[sS]pec.mjs" - ], - "helpers": [ - "helpers/**/*.js" - ], - "stopSpecOnExpectationFailure": false, - "random": true + "spec_dir": "spec", + "spec_files": ["./**/*.spec.ts"], + "helpers": ["helpers/**/*.js"], + "stopSpecOnExpectationFailure": false, + "random": true } diff --git a/src/public/app/services/attribute_parser.d.ts b/src/public/app/services/attribute_parser.d.ts new file mode 100644 index 0000000000..58fe9b9169 --- /dev/null +++ b/src/public/app/services/attribute_parser.d.ts @@ -0,0 +1,7 @@ +declare module 'attribute_parser'; + + +export function lex(str: string): any[] +export function parse(tokens: any[], str?: string, allowEmptyRelations?: boolean): any[] +export function lexAndParse(str: string, allowEmptyRelations?: boolean): any[] + diff --git a/src/public/app/services/attribute_parser.js b/src/public/app/services/attribute_parser.js index 9665e35792..863e5f5324 100644 --- a/src/public/app/services/attribute_parser.js +++ b/src/public/app/services/attribute_parser.js @@ -1,4 +1,4 @@ -import utils from "./utils.js"; +const utils = require("./utils.js"); function lex(str) { str = str.trim(); @@ -222,7 +222,7 @@ function lexAndParse(str, allowEmptyRelations = false) { return parse(tokens, str, allowEmptyRelations); } -export default { +module.exports = { lex, parse, lexAndParse diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.js index b88146012e..3b0d650c04 100644 --- a/src/public/app/services/utils.js +++ b/src/public/app/services/utils.js @@ -505,7 +505,7 @@ function createImageSrcUrl(note) { return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`; } -export default { +module.exports = { reloadFrontendApp, parseDate, formatDateISO, diff --git a/tsconfig.json b/tsconfig.json index 5be52f40c0..55f0090c83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "moduleResolution": "Node", + "moduleResolution": "Node", "declaration": false, "sourceMap": true, "outDir": "./build", @@ -13,9 +13,11 @@ "esModuleInterop": true }, "include": [ - "./src/**/*.js", + "./src/**/*.js", "./src/**/*.ts", - "./*.ts" + "./*.ts", + "./spec/**/*.ts", + "./spec-es6/**/*.ts" ], "exclude": ["./node_modules/**/*"], "ts-node": { @@ -24,4 +26,4 @@ "files": [ "src/types.d.ts" ] - } +}