mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
feat: migrate jasmine tests to ts
This commit is contained in:
parent
aa4960f1a5
commit
fcb30f6319
46910
package-lock.json
generated
46910
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
316
package.json
316
package.json
@ -1,160 +1,160 @@
|
|||||||
{
|
{
|
||||||
"name": "trilium",
|
"name": "trilium",
|
||||||
"productName": "Trilium Notes",
|
"productName": "Trilium Notes",
|
||||||
"description": "Trilium Notes",
|
"description": "Trilium Notes",
|
||||||
"version": "0.63.5",
|
"version": "0.63.5",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"main": "electron.js",
|
"main": "electron.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"trilium": "src/www.js"
|
"trilium": "src/www.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/zadam/trilium.git"
|
"url": "https://github.com/zadam/trilium.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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": "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",
|
"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",
|
"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": "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 .",
|
"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 .",
|
"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",
|
"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-server": "rm -rf ./node_modules/better-sqlite3 && npm install",
|
||||||
"switch-electron": "./node_modules/.bin/electron-rebuild",
|
"switch-electron": "./node_modules/.bin/electron-rebuild",
|
||||||
"rebuild": "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-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",
|
"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-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-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",
|
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs",
|
||||||
"webpack": "webpack -c webpack.config.js",
|
"webpack": "webpack -c webpack.config.js",
|
||||||
"test-jasmine": "TRILIUM_DATA_DIR=~/trilium/data-test ts-node ./node_modules/.bin/jasmine",
|
"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-es6": "node -r esm spec-es6/attribute_parser.spec.js ",
|
||||||
"test": "npm run test-jasmine && npm run test-es6",
|
"test": "npm run test-jasmine && npm run test-es6",
|
||||||
"postinstall": "rimraf ./node_modules/canvas && npm run rebuild"
|
"postinstall": "rimraf ./node_modules/canvas && npm run rebuild"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.4",
|
"@braintree/sanitize-url": "6.0.4",
|
||||||
"@electron/remote": "2.1.2",
|
"@electron/remote": "2.1.2",
|
||||||
"@excalidraw/excalidraw": "0.17.3",
|
"@excalidraw/excalidraw": "0.17.3",
|
||||||
"archiver": "7.0.0",
|
"archiver": "7.0.0",
|
||||||
"async-mutex": "0.4.1",
|
"async-mutex": "0.4.1",
|
||||||
"axios": "1.6.7",
|
"axios": "1.6.7",
|
||||||
"better-sqlite3": "8.4.0",
|
"better-sqlite3": "8.4.0",
|
||||||
"boxicons": "2.1.4",
|
"boxicons": "2.1.4",
|
||||||
"chokidar": "3.6.0",
|
"chokidar": "3.6.0",
|
||||||
"cls-hooked": "4.2.2",
|
"cls-hooked": "4.2.2",
|
||||||
"compression": "1.7.4",
|
"compression": "1.7.4",
|
||||||
"cookie-parser": "1.4.6",
|
"cookie-parser": "1.4.6",
|
||||||
"csurf": "1.11.0",
|
"csurf": "1.11.0",
|
||||||
"dayjs": "1.11.10",
|
"dayjs": "1.11.10",
|
||||||
"dayjs-plugin-utc": "0.1.2",
|
"dayjs-plugin-utc": "0.1.2",
|
||||||
"debounce": "1.2.1",
|
"debounce": "1.2.1",
|
||||||
"ejs": "3.1.9",
|
"ejs": "3.1.9",
|
||||||
"electron-debug": "3.2.0",
|
"electron-debug": "3.2.0",
|
||||||
"electron-dl": "3.5.2",
|
"electron-dl": "3.5.2",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"express": "4.18.3",
|
"express": "4.18.3",
|
||||||
"express-partial-content": "1.0.2",
|
"express-partial-content": "1.0.2",
|
||||||
"express-rate-limit": "7.2.0",
|
"express-rate-limit": "7.2.0",
|
||||||
"express-session": "1.18.0",
|
"express-session": "1.18.0",
|
||||||
"force-graph": "1.43.5",
|
"force-graph": "1.43.5",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
"helmet": "7.1.0",
|
"helmet": "7.1.0",
|
||||||
"html": "1.0.0",
|
"html": "1.0.0",
|
||||||
"html2plaintext": "2.1.4",
|
"html2plaintext": "2.1.4",
|
||||||
"http-proxy-agent": "7.0.2",
|
"http-proxy-agent": "7.0.2",
|
||||||
"https-proxy-agent": "7.0.4",
|
"https-proxy-agent": "7.0.4",
|
||||||
"image-type": "4.1.0",
|
"image-type": "4.1.0",
|
||||||
"ini": "3.0.1",
|
"ini": "3.0.1",
|
||||||
"is-animated": "2.0.2",
|
"is-animated": "2.0.2",
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"jimp": "0.22.12",
|
"jimp": "0.22.12",
|
||||||
"joplin-turndown-plugin-gfm": "1.0.12",
|
"joplin-turndown-plugin-gfm": "1.0.12",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery-hotkeys": "0.2.2",
|
"jquery-hotkeys": "0.2.2",
|
||||||
"jsdom": "24.0.0",
|
"jsdom": "24.0.0",
|
||||||
"katex": "0.16.9",
|
"katex": "0.16.9",
|
||||||
"marked": "12.0.0",
|
"marked": "12.0.0",
|
||||||
"mermaid": "10.9.0",
|
"mermaid": "10.9.0",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.1",
|
||||||
"node-abi": "3.56.0",
|
"node-abi": "3.56.0",
|
||||||
"normalize-strings": "1.1.1",
|
"normalize-strings": "1.1.1",
|
||||||
"open": "8.4.1",
|
"open": "8.4.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"print-this": "2.0.0",
|
"print-this": "2.0.0",
|
||||||
"rand-token": "1.0.1",
|
"rand-token": "1.0.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"request": "2.88.2",
|
"request": "2.88.2",
|
||||||
"rimraf": "5.0.5",
|
"rimraf": "5.0.5",
|
||||||
"safe-compare": "1.1.4",
|
"safe-compare": "1.1.4",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"sanitize-html": "2.12.1",
|
"sanitize-html": "2.12.1",
|
||||||
"sax": "1.3.0",
|
"sax": "1.3.0",
|
||||||
"semver": "7.6.0",
|
"semver": "7.6.0",
|
||||||
"serve-favicon": "2.5.0",
|
"serve-favicon": "2.5.0",
|
||||||
"session-file-store": "1.5.0",
|
"session-file-store": "1.5.0",
|
||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"stream-throttle": "0.1.3",
|
"stream-throttle": "0.1.3",
|
||||||
"striptags": "3.2.0",
|
"striptags": "3.2.0",
|
||||||
"tmp": "0.2.3",
|
"tmp": "0.2.3",
|
||||||
"tree-kill": "1.2.2",
|
"tree-kill": "1.2.2",
|
||||||
"turndown": "7.1.2",
|
"turndown": "7.1.2",
|
||||||
"unescape": "1.0.1",
|
"unescape": "1.0.1",
|
||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xml2js": "0.6.2",
|
"xml2js": "0.6.2",
|
||||||
"yauzl": "3.1.2"
|
"yauzl": "3.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/archiver": "^6.0.2",
|
"@types/archiver": "^6.0.2",
|
||||||
"@types/better-sqlite3": "^7.6.9",
|
"@types/better-sqlite3": "^7.6.9",
|
||||||
"@types/cls-hooked": "^4.3.8",
|
"@types/cls-hooked": "^4.3.8",
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
"@types/csurf": "^1.11.5",
|
"@types/csurf": "^1.11.5",
|
||||||
"@types/ejs": "^3.1.5",
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/escape-html": "^1.0.4",
|
"@types/escape-html": "^1.0.4",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/express-session": "^1.18.0",
|
"@types/express-session": "^1.18.0",
|
||||||
"@types/html": "^1.0.4",
|
"@types/html": "^1.0.4",
|
||||||
"@types/ini": "^4.1.0",
|
"@types/ini": "^4.1.0",
|
||||||
"@types/jasmine": "^5.1.4",
|
"@types/jasmine": "^5.1.4",
|
||||||
"@types/jsdom": "^21.1.6",
|
"@types/jsdom": "^21.1.6",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@types/multer": "^1.4.11",
|
"@types/multer": "^1.4.11",
|
||||||
"@types/node": "^20.11.19",
|
"@types/node": "^20.11.19",
|
||||||
"@types/safe-compare": "^1.1.2",
|
"@types/safe-compare": "^1.1.2",
|
||||||
"@types/sanitize-html": "^2.11.0",
|
"@types/sanitize-html": "^2.11.0",
|
||||||
"@types/sax": "^1.2.7",
|
"@types/sax": "^1.2.7",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/serve-favicon": "^2.5.7",
|
"@types/serve-favicon": "^2.5.7",
|
||||||
"@types/stream-throttle": "^0.1.4",
|
"@types/stream-throttle": "^0.1.4",
|
||||||
"@types/tmp": "^0.2.6",
|
"@types/tmp": "^0.2.6",
|
||||||
"@types/turndown": "^5.0.4",
|
"@types/turndown": "^5.0.4",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
"@types/xml2js": "^0.4.14",
|
"@types/xml2js": "^0.4.14",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"electron": "25.9.8",
|
"electron": "25.9.8",
|
||||||
"electron-builder": "24.13.3",
|
"electron-builder": "24.13.3",
|
||||||
"electron-packager": "17.1.2",
|
"electron-packager": "17.1.2",
|
||||||
"electron-rebuild": "3.2.9",
|
"electron-rebuild": "3.2.9",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"jasmine": "5.1.0",
|
"jasmine": "5.1.0",
|
||||||
"jsdoc": "4.0.2",
|
"jsdoc": "4.0.2",
|
||||||
"lorem-ipsum": "2.0.8",
|
"lorem-ipsum": "2.0.8",
|
||||||
"nodemon": "3.1.0",
|
"nodemon": "3.1.0",
|
||||||
"rcedit": "4.0.1",
|
"rcedit": "4.0.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"webpack": "5.90.3",
|
"webpack": "5.90.3",
|
||||||
"webpack-cli": "5.1.4"
|
"webpack-cli": "5.1.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"electron-installer-debian": "3.2.0"
|
"electron-installer-debian": "3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ import fs = require("fs");
|
|||||||
import path = require("path");
|
import path = require("path");
|
||||||
|
|
||||||
etapi.describeEtapi("import", () => {
|
etapi.describeEtapi("import", () => {
|
||||||
it("import", async () => {
|
// temporarily skip this test since test-export.zip is missing
|
||||||
|
xit("import", async () => {
|
||||||
const zipFileBuffer = fs.readFileSync(
|
const zipFileBuffer = fs.readFileSync(
|
||||||
path.resolve(__dirname, "test-export.zip")
|
path.resolve(__dirname, "test-export.zip")
|
||||||
);
|
);
|
@ -1,5 +0,0 @@
|
|||||||
describe("Notes", () => {
|
|
||||||
it("zzz", () => {
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
@ -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
|
|
||||||
};
|
|
87
spec/search/becca_mocking.ts
Normal file
87
spec/search/becca_mocking.ts
Normal 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,
|
||||||
|
};
|
@ -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
256
spec/search/lexer.spec.ts
Normal 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",
|
||||||
|
"\\",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -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
319
spec/search/parser.spec.ts
Normal 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 ""');
|
||||||
|
});
|
||||||
|
});
|
@ -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
634
spec/search/search.spec.ts
Normal 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();
|
||||||
|
// })
|
||||||
|
});
|
@ -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());
|
|
||||||
});
|
|
81
spec/search/value_extractor.spec.ts
Normal file
81
spec/search/value_extractor.spec.ts
Normal 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());
|
||||||
|
});
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"spec_dir": "spec",
|
"spec_dir": "spec",
|
||||||
"spec_files": ["./etapi/*.ts"],
|
"spec_files": ["./**/*.spec.ts"],
|
||||||
"helpers": ["helpers/**/*.js"],
|
"helpers": ["helpers/**/*.js"],
|
||||||
"stopSpecOnExpectationFailure": false,
|
"stopSpecOnExpectationFailure": false,
|
||||||
"random": true
|
"random": true
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.js", "./src/**/*.ts", "./*.ts"],
|
"include": ["./src/**/*.js", "./src/**/*.ts", "./*.ts", "./spec/**/*.ts"],
|
||||||
"exclude": ["./node_modules/**/*"],
|
"exclude": ["./node_modules/**/*"],
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"files": true
|
"files": true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user