feat: migrate jasmine tests to ts

This commit is contained in:
Alex 2024-05-08 23:59:11 +02:00
parent aa4960f1a5
commit fcb30f6319
20 changed files with 24998 additions and 24936 deletions

46910
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,160 +1,160 @@
{
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.63.5",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
"trilium": "src/www.js"
},
"repository": {
"type": "git",
"url": "https://github.com/zadam/trilium.git"
},
"scripts": {
"start-server": "cross-env TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/www.ts",
"start-server-no-dir": "cross-env TRILIUM_SAFE_MODE=1 TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/www.ts",
"qstart-server": "npm run qswitch-server && TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/www.ts",
"start-electron": "rimraf ./dist && tsc && ts-node ./bin/copy-dist.ts && cross-env TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron.js --inspect=5858 .",
"start-electron-no-dir": "cross-env TRILIUM_SAFE_MODE=1 TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 electron --inspect=5858 .",
"qstart-electron": "npm run qswitch-electron && TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron --inspect=5858 .",
"start-test-server": "npm run qswitch-server; rm -rf ./data-test; cross-env TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 ts-node src/www.ts",
"switch-server": "rm -rf ./node_modules/better-sqlite3 && npm install",
"switch-electron": "./node_modules/.bin/electron-rebuild",
"rebuild": "electron-rebuild",
"qswitch-server": "rm -rf ./node_modules/better-sqlite3/bin ; mkdir -p ./node_modules/better-sqlite3/build ; cp ./bin/better-sqlite3/linux-server-better_sqlite3.node ./node_modules/better-sqlite3/build/better_sqlite3.node",
"qswitch-electron": "rm -rf ./node_modules/better-sqlite3/bin ; mkdir -p ./node_modules/better-sqlite3/build ; cp ./bin/better-sqlite3/linux-desktop-better_sqlite3.node ./node_modules/better-sqlite3/build/better_sqlite3.node",
"build-backend-docs": "rm -rf ./docs/backend_api && ./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/becca/entities/*.js src/services/backend_script_api.js src/services/sql.js",
"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.js",
"test-jasmine": "TRILIUM_DATA_DIR=~/trilium/data-test ts-node ./node_modules/.bin/jasmine",
"test-es6": "node -r esm spec-es6/attribute_parser.spec.js ",
"test": "npm run test-jasmine && npm run test-es6",
"postinstall": "rimraf ./node_modules/canvas && npm run rebuild"
},
"dependencies": {
"@braintree/sanitize-url": "6.0.4",
"@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.17.3",
"archiver": "7.0.0",
"async-mutex": "0.4.1",
"axios": "1.6.7",
"better-sqlite3": "8.4.0",
"boxicons": "2.1.4",
"chokidar": "3.6.0",
"cls-hooked": "4.2.2",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"csurf": "1.11.0",
"dayjs": "1.11.10",
"dayjs-plugin-utc": "0.1.2",
"debounce": "1.2.1",
"ejs": "3.1.9",
"electron-debug": "3.2.0",
"electron-dl": "3.5.2",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"express": "4.18.3",
"express-partial-content": "1.0.2",
"express-rate-limit": "7.2.0",
"express-session": "1.18.0",
"force-graph": "1.43.5",
"fs-extra": "11.2.0",
"helmet": "7.1.0",
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.4",
"image-type": "4.1.0",
"ini": "3.0.1",
"is-animated": "2.0.2",
"is-svg": "4.3.2",
"jimp": "0.22.12",
"joplin-turndown-plugin-gfm": "1.0.12",
"jquery": "3.7.1",
"jquery-hotkeys": "0.2.2",
"jsdom": "24.0.0",
"katex": "0.16.9",
"marked": "12.0.0",
"mermaid": "10.9.0",
"mime-types": "2.1.35",
"multer": "1.4.5-lts.1",
"node-abi": "3.56.0",
"normalize-strings": "1.1.1",
"open": "8.4.1",
"panzoom": "9.4.3",
"print-this": "2.0.0",
"rand-token": "1.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"request": "2.88.2",
"rimraf": "5.0.5",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.12.1",
"sax": "1.3.0",
"semver": "7.6.0",
"serve-favicon": "2.5.0",
"session-file-store": "1.5.0",
"split.js": "1.6.5",
"stream-throttle": "0.1.3",
"striptags": "3.2.0",
"tmp": "0.2.3",
"tree-kill": "1.2.2",
"turndown": "7.1.2",
"unescape": "1.0.1",
"ws": "8.16.0",
"xml2js": "0.6.2",
"yauzl": "3.1.2"
},
"devDependencies": {
"@types/archiver": "^6.0.2",
"@types/better-sqlite3": "^7.6.9",
"@types/cls-hooked": "^4.3.8",
"@types/compression": "^1.7.5",
"@types/cookie-parser": "^1.4.7",
"@types/csurf": "^1.11.5",
"@types/ejs": "^3.1.5",
"@types/escape-html": "^1.0.4",
"@types/express": "^4.17.21",
"@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",
"@types/node": "^20.11.19",
"@types/safe-compare": "^1.1.2",
"@types/sanitize-html": "^2.11.0",
"@types/sax": "^1.2.7",
"@types/semver": "^7.5.8",
"@types/serve-favicon": "^2.5.7",
"@types/stream-throttle": "^0.1.4",
"@types/tmp": "^0.2.6",
"@types/turndown": "^5.0.4",
"@types/ws": "^8.5.10",
"@types/xml2js": "^0.4.14",
"cross-env": "7.0.3",
"electron": "25.9.8",
"electron-builder": "24.13.3",
"electron-packager": "17.1.2",
"electron-rebuild": "3.2.9",
"esm": "3.2.25",
"jasmine": "5.1.0",
"jsdoc": "4.0.2",
"lorem-ipsum": "2.0.8",
"nodemon": "3.1.0",
"rcedit": "4.0.1",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"webpack": "5.90.3",
"webpack-cli": "5.1.4"
},
"optionalDependencies": {
"electron-installer-debian": "3.2.0"
}
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.63.5",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
"trilium": "src/www.js"
},
"repository": {
"type": "git",
"url": "https://github.com/zadam/trilium.git"
},
"scripts": {
"start-server": "cross-env TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/www.ts",
"start-server-no-dir": "cross-env TRILIUM_SAFE_MODE=1 TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/www.ts",
"qstart-server": "npm run qswitch-server && TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/www.ts",
"start-electron": "rimraf ./dist && tsc && ts-node ./bin/copy-dist.ts && cross-env TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron.js --inspect=5858 .",
"start-electron-no-dir": "cross-env TRILIUM_SAFE_MODE=1 TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 electron --inspect=5858 .",
"qstart-electron": "npm run qswitch-electron && TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron --inspect=5858 .",
"start-test-server": "npm run qswitch-server; rm -rf ./data-test; cross-env TRILIUM_SAFE_MODE=1 TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 ts-node src/www.ts",
"switch-server": "rm -rf ./node_modules/better-sqlite3 && npm install",
"switch-electron": "./node_modules/.bin/electron-rebuild",
"rebuild": "electron-rebuild",
"qswitch-server": "rm -rf ./node_modules/better-sqlite3/bin ; mkdir -p ./node_modules/better-sqlite3/build ; cp ./bin/better-sqlite3/linux-server-better_sqlite3.node ./node_modules/better-sqlite3/build/better_sqlite3.node",
"qswitch-electron": "rm -rf ./node_modules/better-sqlite3/bin ; mkdir -p ./node_modules/better-sqlite3/build ; cp ./bin/better-sqlite3/linux-desktop-better_sqlite3.node ./node_modules/better-sqlite3/build/better_sqlite3.node",
"build-backend-docs": "rm -rf ./docs/backend_api && ./node_modules/.bin/jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/becca/entities/*.js src/services/backend_script_api.js src/services/sql.js",
"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.js",
"test-jasmine": "TRILIUM_DATA_DIR=./data-test ts-node ./node_modules/.bin/jasmine",
"test-es6": "node -r esm spec-es6/attribute_parser.spec.js ",
"test": "npm run test-jasmine && npm run test-es6",
"postinstall": "rimraf ./node_modules/canvas && npm run rebuild"
},
"dependencies": {
"@braintree/sanitize-url": "6.0.4",
"@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.17.3",
"archiver": "7.0.0",
"async-mutex": "0.4.1",
"axios": "1.6.7",
"better-sqlite3": "8.4.0",
"boxicons": "2.1.4",
"chokidar": "3.6.0",
"cls-hooked": "4.2.2",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"csurf": "1.11.0",
"dayjs": "1.11.10",
"dayjs-plugin-utc": "0.1.2",
"debounce": "1.2.1",
"ejs": "3.1.9",
"electron-debug": "3.2.0",
"electron-dl": "3.5.2",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"express": "4.18.3",
"express-partial-content": "1.0.2",
"express-rate-limit": "7.2.0",
"express-session": "1.18.0",
"force-graph": "1.43.5",
"fs-extra": "11.2.0",
"helmet": "7.1.0",
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.4",
"image-type": "4.1.0",
"ini": "3.0.1",
"is-animated": "2.0.2",
"is-svg": "4.3.2",
"jimp": "0.22.12",
"joplin-turndown-plugin-gfm": "1.0.12",
"jquery": "3.7.1",
"jquery-hotkeys": "0.2.2",
"jsdom": "24.0.0",
"katex": "0.16.9",
"marked": "12.0.0",
"mermaid": "10.9.0",
"mime-types": "2.1.35",
"multer": "1.4.5-lts.1",
"node-abi": "3.56.0",
"normalize-strings": "1.1.1",
"open": "8.4.1",
"panzoom": "9.4.3",
"print-this": "2.0.0",
"rand-token": "1.0.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"request": "2.88.2",
"rimraf": "5.0.5",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.12.1",
"sax": "1.3.0",
"semver": "7.6.0",
"serve-favicon": "2.5.0",
"session-file-store": "1.5.0",
"split.js": "1.6.5",
"stream-throttle": "0.1.3",
"striptags": "3.2.0",
"tmp": "0.2.3",
"tree-kill": "1.2.2",
"turndown": "7.1.2",
"unescape": "1.0.1",
"ws": "8.16.0",
"xml2js": "0.6.2",
"yauzl": "3.1.2"
},
"devDependencies": {
"@types/archiver": "^6.0.2",
"@types/better-sqlite3": "^7.6.9",
"@types/cls-hooked": "^4.3.8",
"@types/compression": "^1.7.5",
"@types/cookie-parser": "^1.4.7",
"@types/csurf": "^1.11.5",
"@types/ejs": "^3.1.5",
"@types/escape-html": "^1.0.4",
"@types/express": "^4.17.21",
"@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",
"@types/node": "^20.11.19",
"@types/safe-compare": "^1.1.2",
"@types/sanitize-html": "^2.11.0",
"@types/sax": "^1.2.7",
"@types/semver": "^7.5.8",
"@types/serve-favicon": "^2.5.7",
"@types/stream-throttle": "^0.1.4",
"@types/tmp": "^0.2.6",
"@types/turndown": "^5.0.4",
"@types/ws": "^8.5.10",
"@types/xml2js": "^0.4.14",
"cross-env": "7.0.3",
"electron": "25.9.8",
"electron-builder": "24.13.3",
"electron-packager": "17.1.2",
"electron-rebuild": "3.2.9",
"esm": "3.2.25",
"jasmine": "5.1.0",
"jsdoc": "4.0.2",
"lorem-ipsum": "2.0.8",
"nodemon": "3.1.0",
"rcedit": "4.0.1",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"webpack": "5.90.3",
"webpack-cli": "5.1.4"
},
"optionalDependencies": {
"electron-installer-debian": "3.2.0"
}
}

View File

@ -3,7 +3,8 @@ import fs = require("fs");
import path = require("path");
etapi.describeEtapi("import", () => {
it("import", async () => {
// temporarily skip this test since test-export.zip is missing
xit("import", async () => {
const zipFileBuffer = fs.readFileSync(
path.resolve(__dirname, "test-export.zip")
);

View File

@ -1,5 +0,0 @@
describe("Notes", () => {
it("zzz", () => {
});
});

View File

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

View File

@ -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<SearchResult>,
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,
};

View File

@ -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 <b(r*t)h>").fulltextTokens.map(t => t.token))
.toEqual(["what's", "u=p", "<b(r*t)h>"]);
});
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", "\\"]);
});
});

256
spec/search/lexer.spec.ts Normal file
View File

@ -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 <b(r*t)h>").fulltextTokens.map((t) => t.token)
).toEqual(["what's", "u=p", "<b(r*t)h>"]);
});
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",
"\\",
]);
});
});

View File

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

319
spec/search/parser.spec.ts Normal file
View File

@ -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<string>, cur = 0): Array<any> {
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 ""');
});
});

View File

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

634
spec/search/search.spec.ts Normal file
View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"spec_dir": "spec",
"spec_files": ["./etapi/*.ts"],
"helpers": ["helpers/**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": true
"spec_dir": "spec",
"spec_files": ["./**/*.spec.ts"],
"helpers": ["helpers/**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": true
}

View File

@ -11,7 +11,7 @@
"downlevelIteration": true,
"skipLibCheck": true
},
"include": ["./src/**/*.js", "./src/**/*.ts", "./*.ts"],
"include": ["./src/**/*.js", "./src/**/*.ts", "./*.ts", "./spec/**/*.ts"],
"exclude": ["./node_modules/**/*"],
"ts-node": {
"files": true