Merge branch 'develop' of https://github.com/TriliumNext/Notes into style/next/forms

This commit is contained in:
Adorian Doran
2025-01-12 23:45:03 +02:00
612 changed files with 138078 additions and 26087 deletions

View File

@@ -1,9 +1,9 @@
name: Playwright Tests name: Playwright Tests
on: on:
push: push:
branches: [ main, master ] branches: [ develop ]
pull_request: pull_request:
branches: [ main, master ] branches: [ develop ]
jobs: jobs:
test: test:
timeout-minutes: 60 timeout-minutes: 60
@@ -20,7 +20,7 @@ jobs:
- name: Run Playwright tests - name: Run Playwright tests
run: npx playwright test run: npx playwright test
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: ${{ !cancelled() }}
with: with:
name: playwright-report name: playwright-report
path: playwright-report/ path: playwright-report/

View File

@@ -1,53 +1,53 @@
name: Publish Docker image name: Publish Docker image
on: on:
push: push:
tags: [v*] tags: [v*]
jobs: jobs:
push_to_registries: push_to_registries:
name: Push Docker image to multiple registries name: Push Docker image to multiple registries
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
packages: write packages: write
contents: read contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v3
with: with:
images: | images: |
zadam/trilium zadam/trilium
ghcr.io/zadam/trilium ghcr.io/zadam/trilium
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}-latest type=semver,pattern={{major}}.{{minor}}-latest
type=match,pattern=(\d+.\d+).\d+\-beta,enable=${{ endsWith(github.ref, 'beta') }},group=1,suffix=-latest type=match,pattern=(\d+.\d+).\d+\-beta,enable=${{ endsWith(github.ref, 'beta') }},group=1,suffix=-latest
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
with: with:
install: true install: true
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to GitHub Docker Registry - name: Log in to GitHub Docker Registry
uses: docker/login-action@v1 uses: docker/login-action@v1
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Create server-package.json - name: Create server-package.json
run: cat package.json | grep -v electron > server-package.json run: cat package.json | grep -v electron > server-package.json
- name: Build and Push - name: Build and Push
uses: docker/build-push-action@v2.7.0 uses: docker/build-push-action@v2.7.0
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
push: true push: true
cache-from: type=registry,ref=zadam/trilium:buildcache cache-from: type=registry,ref=zadam/trilium:buildcache
cache-to: type=registry,ref=zadam/trilium:buildcache,mode=max cache-to: type=registry,ref=zadam/trilium:buildcache,mode=max
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}

2
.gitignore vendored
View File

@@ -9,12 +9,12 @@ po-*/
*.db *.db
!integration-tests/db/document.db !integration-tests/db/document.db
!integration-tests/db/config.ini
integration-tests/db/log integration-tests/db/log
integration-tests/db/sessions integration-tests/db/sessions
integration-tests/db/backup integration-tests/db/backup
integration-tests/db/session_secret.txt integration-tests/db/session_secret.txt
config.ini
cert.key cert.key
cert.crt cert.crt
server-package.json server-package.json

View File

@@ -3,12 +3,20 @@
"tabWidth": 4, "tabWidth": 4,
"useTabs": false, "useTabs": false,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": false,
"quoteProps": "as-needed", "quoteProps": "as-needed",
"trailingComma": "none", "trailingComma": "none",
"bracketSpacing": false, "bracketSpacing": true,
"arrowParens": "always", "arrowParens": "always",
"proseWrap": "preserve", "proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css", "htmlWhitespaceSensitivity": "css",
"endOfLine": "lf" "endOfLine": "lf",
"overrides": [
{
"files": ["*.json"],
"options": {
"tabWidth": 2
}
}
]
} }

View File

@@ -1,6 +1,3 @@
{ {
"recommendations": [ "recommendations": ["lokalise.i18n-ally", "editorconfig.editorconfig"]
"lokalise.i18n-ally",
"editorconfig.editorconfig"
]
} }

42
.vscode/launch.json vendored
View File

@@ -1,24 +1,22 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
// nodemon should be installed globally, use npm i -g nodemon // nodemon should be installed globally, use npm i -g nodemon
{ {
"console": "integratedTerminal", "console": "integratedTerminal",
"internalConsoleOptions": "neverOpen", "internalConsoleOptions": "neverOpen",
"name": "nodemon start-server", "name": "nodemon start-server",
"program": "${workspaceFolder}/src/www", "program": "${workspaceFolder}/src/www",
"request": "launch", "request": "launch",
"restart": true, "restart": true,
"runtimeExecutable": "nodemon", "runtimeExecutable": "nodemon",
"env": { "env": {
"TRILIUM_ENV": "dev", "TRILIUM_ENV": "dev",
"TRILIUM_DATA_DIR": "./data" "TRILIUM_DATA_DIR": "./data"
}, },
"skipFiles": [ "skipFiles": ["<node_internals>/**"],
"<node_internals>/**" "type": "node",
], "outputCapture": "std"
"type": "node", }
"outputCapture": "std", ]
},
]
} }

45
.vscode/settings.json vendored
View File

@@ -1,27 +1,22 @@
{ {
"editor.formatOnSave": false, "editor.formatOnSave": false,
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"files.eol": "\n", "files.eol": "\n",
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"i18n-ally.sourceLanguage": "en", "i18n-ally.sourceLanguage": "en",
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": ["./src/public/translations", "./translations"],
"./src/public/translations", "[jsonc]": {
"./translations" "editor.defaultFormatter": "vscode.json-language-features"
], },
"[jsonc]": { "[javascript]": {
"editor.defaultFormatter": "vscode.json-language-features" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"[javascript]": { "[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"[typescript]": { "github-actions.workflows.pinned.workflows": [".github/workflows/nightly.yml"],
"editor.defaultFormatter": "vscode.typescript-language-features" "[css]": {
}, "editor.defaultFormatter": "vscode.css-language-features"
"github-actions.workflows.pinned.workflows": [ }
".github/workflows/nightly.yml"
],
"[css]": {
"editor.defaultFormatter": "vscode.css-language-features"
},
} }

View File

@@ -1,26 +1,24 @@
{ {
// Place your Notes workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and // Place your Notes workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are: // used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected. // Placeholders with the same ids are connected.
// Example: // Example:
// "Print to console": { // "Print to console": {
// "scope": "javascript,typescript", // "scope": "javascript,typescript",
// "prefix": "log", // "prefix": "log",
// "body": [ // "body": [
// "console.log('$1');", // "console.log('$1');",
// "$2" // "$2"
// ], // ],
// "description": "Log output to console" // "description": "Log output to console"
// } // }
"JQuery HTMLElement field": { "JQuery HTMLElement field": {
"scope": "typescript", "scope": "typescript",
"prefix": "jqf", "prefix": "jqf",
"body": [ "body": ["private $${1:name}!: JQuery<HTMLElement>;"]
"private $${1:name}!: JQuery<HTMLElement>;" }
]
}
} }

View File

@@ -1,5 +1,5 @@
# Build stage # Build stage
FROM node:22.12.0-bullseye-slim AS builder FROM node:22.13.0-bullseye-slim AS builder
# Configure build dependencies in a single layer # Configure build dependencies in a single layer
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -23,7 +23,6 @@ COPY server-package.json package.json
# Build and cleanup in a single layer # Build and cleanup in a single layer
RUN cp -R build/src/* src/. && \ RUN cp -R build/src/* src/. && \
cp build/docker_healthcheck.js . && \ cp build/docker_healthcheck.js . && \
rm -r build && \
rm docker_healthcheck.ts && \ rm docker_healthcheck.ts && \
npm install && \ npm install && \
npm run webpack && \ npm run webpack && \
@@ -31,11 +30,14 @@ RUN cp -R build/src/* src/. && \
npm cache clean --force && \ npm cache clean --force && \
cp src/public/app/share.js src/public/app-dist/. && \ cp src/public/app/share.js src/public/app-dist/. && \
cp -r src/public/app/doc_notes src/public/app-dist/. && \ cp -r src/public/app/doc_notes src/public/app-dist/. && \
rm -rf src/public/app && \ rm -rf src/public/app/* && \
rm src/services/asset_path.ts mkdir -p src/public/app/services && \
cp -r build/src/public/app/services/mime_type_definitions.js src/public/app/services/mime_type_definitions.js && \
rm src/services/asset_path.ts && \
rm -r build
# Runtime stage # Runtime stage
FROM node:22.12.0-bullseye-slim FROM node:22.13.0-bullseye-slim
# Install only runtime dependencies # Install only runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \

View File

@@ -1,5 +1,5 @@
# Build stage # Build stage
FROM node:22.12.0-alpine AS builder FROM node:22.13.0-alpine AS builder
# Configure build dependencies # Configure build dependencies
RUN apk add --no-cache --virtual .build-dependencies \ RUN apk add --no-cache --virtual .build-dependencies \
@@ -22,7 +22,6 @@ COPY server-package.json package.json
# Build and cleanup in a single layer # Build and cleanup in a single layer
RUN cp -R build/src/* src/. && \ RUN cp -R build/src/* src/. && \
cp build/docker_healthcheck.js . && \ cp build/docker_healthcheck.js . && \
rm -r build && \
rm docker_healthcheck.ts && \ rm docker_healthcheck.ts && \
npm install && \ npm install && \
npm run webpack && \ npm run webpack && \
@@ -31,10 +30,13 @@ RUN cp -R build/src/* src/. && \
cp src/public/app/share.js src/public/app-dist/. && \ cp src/public/app/share.js src/public/app-dist/. && \
cp -r src/public/app/doc_notes src/public/app-dist/. && \ cp -r src/public/app/doc_notes src/public/app-dist/. && \
rm -rf src/public/app && \ rm -rf src/public/app && \
rm src/services/asset_path.ts mkdir -p src/public/app/services && \
cp -r build/src/public/app/services/mime_type_definitions.js src/public/app/services/mime_type_definitions.js && \
rm src/services/asset_path.ts && \
rm -r build
# Runtime stage # Runtime stage
FROM node:22.12.0-alpine FROM node:22.13.0-alpine
# Install runtime dependencies # Install runtime dependencies
RUN apk add --no-cache su-exec shadow RUN apk add --no-cache su-exec shadow

View File

@@ -8,108 +8,107 @@ const DEST_DIR_NODE_MODULES = path.join(DEST_DIR, "node_modules");
const VERBOSE = process.env.VERBOSE; const VERBOSE = process.env.VERBOSE;
function log(...args) { function log(...args) {
if (VERBOSE) { if (VERBOSE) {
console.log(args); console.log(args);
} }
} }
async function copyNodeModuleFileOrFolder(source: string) { async function copyNodeModuleFileOrFolder(source: string) {
const adjustedSource = source.substring(13); const adjustedSource = source.substring(13);
const destination = path.join(DEST_DIR_NODE_MODULES, adjustedSource); const destination = path.join(DEST_DIR_NODE_MODULES, adjustedSource);
log(`Copying ${source} to ${destination}`); log(`Copying ${source} to ${destination}`);
await fs.ensureDir(path.dirname(destination)); await fs.ensureDir(path.dirname(destination));
await fs.copy(source, destination); await fs.copy(source, destination);
} }
const copy = async () => { const copy = async () => {
for (const srcFile of fs.readdirSync("build")) { for (const srcFile of fs.readdirSync("build")) {
const destFile = path.join(DEST_DIR, path.basename(srcFile)); const destFile = path.join(DEST_DIR, path.basename(srcFile));
log(`Copying source ${srcFile} -> ${destFile}.`); log(`Copying source ${srcFile} -> ${destFile}.`);
fs.copySync(path.join("build", srcFile), destFile, { recursive: true }); fs.copySync(path.join("build", srcFile), destFile, { recursive: true });
} }
const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json"]; const filesToCopy = ["config-sample.ini", "tsconfig.webpack.json"];
for (const file of filesToCopy) { for (const file of filesToCopy) {
log(`Copying ${file}`); log(`Copying ${file}`);
await fs.copy(file, path.join(DEST_DIR, file)); await fs.copy(file, path.join(DEST_DIR, file));
} }
const dirsToCopy = ["images", "libraries", "translations", "db"]; const dirsToCopy = ["images", "libraries", "translations", "db"];
for (const dir of dirsToCopy) { for (const dir of dirsToCopy) {
log(`Copying ${dir}`); log(`Copying ${dir}`);
await fs.copy(dir, path.join(DEST_DIR, dir)); await fs.copy(dir, path.join(DEST_DIR, dir));
} }
const srcDirsToCopy = ["./src/public", "./src/views", "./build"]; const srcDirsToCopy = ["./src/public", "./src/views", "./build"];
for (const dir of srcDirsToCopy) { for (const dir of srcDirsToCopy) {
log(`Copying ${dir}`); log(`Copying ${dir}`);
await fs.copy(dir, path.join(DEST_DIR_SRC, path.basename(dir))); await fs.copy(dir, path.join(DEST_DIR_SRC, path.basename(dir)));
} }
/** /**
* Directories to be copied relative to the project root into <resource_dir>/src/public/app-dist. * Directories to be copied relative to the project root into <resource_dir>/src/public/app-dist.
*/ */
const publicDirsToCopy = [ "./src/public/app/doc_notes" ]; const publicDirsToCopy = ["./src/public/app/doc_notes"];
const PUBLIC_DIR = path.join(DEST_DIR, "src", "public", "app-dist"); const PUBLIC_DIR = path.join(DEST_DIR, "src", "public", "app-dist");
for (const dir of publicDirsToCopy) { for (const dir of publicDirsToCopy) {
await fs.copy(dir, path.join(PUBLIC_DIR, path.basename(dir))); await fs.copy(dir, path.join(PUBLIC_DIR, path.basename(dir)));
} }
const nodeModulesFile = [ const nodeModulesFile = [
"node_modules/react/umd/react.production.min.js", "node_modules/react/umd/react.production.min.js",
"node_modules/react/umd/react.development.js", "node_modules/react/umd/react.development.js",
"node_modules/react-dom/umd/react-dom.production.min.js", "node_modules/react-dom/umd/react-dom.production.min.js",
"node_modules/react-dom/umd/react-dom.development.js", "node_modules/react-dom/umd/react-dom.development.js",
"node_modules/katex/dist/katex.min.js", "node_modules/katex/dist/katex.min.js",
"node_modules/katex/dist/contrib/mhchem.min.js", "node_modules/katex/dist/contrib/mhchem.min.js",
"node_modules/katex/dist/contrib/auto-render.min.js", "node_modules/katex/dist/contrib/auto-render.min.js",
"node_modules/@highlightjs/cdn-assets/highlight.min.js", "node_modules/@highlightjs/cdn-assets/highlight.min.js",
"node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs", "node_modules/@mind-elixir/node-menu/dist/node-menu.umd.cjs"
]; ];
for (const file of nodeModulesFile) { for (const file of nodeModulesFile) {
await copyNodeModuleFileOrFolder(file); await copyNodeModuleFileOrFolder(file);
} }
const nodeModulesFolder = [ const nodeModulesFolder = [
"node_modules/@excalidraw/excalidraw/dist/", "node_modules/@excalidraw/excalidraw/dist/",
"node_modules/katex/dist/", "node_modules/katex/dist/",
"node_modules/dayjs/", "node_modules/dayjs/",
"node_modules/force-graph/dist/", "node_modules/force-graph/dist/",
"node_modules/boxicons/css/", "node_modules/boxicons/css/",
"node_modules/boxicons/fonts/", "node_modules/boxicons/fonts/",
"node_modules/mermaid/dist/", "node_modules/mermaid/dist/",
"node_modules/jquery/dist/", "node_modules/jquery/dist/",
"node_modules/jquery-hotkeys/", "node_modules/jquery-hotkeys/",
"node_modules/print-this/", "node_modules/print-this/",
"node_modules/split.js/dist/", "node_modules/split.js/dist/",
"node_modules/panzoom/dist/", "node_modules/panzoom/dist/",
"node_modules/i18next/", "node_modules/i18next/",
"node_modules/i18next-http-backend/", "node_modules/i18next-http-backend/",
"node_modules/eslint/bin/", "node_modules/jsplumb/dist/",
"node_modules/jsplumb/dist/", "node_modules/vanilla-js-wheel-zoom/dist/",
"node_modules/vanilla-js-wheel-zoom/dist/", "node_modules/mark.js/dist/",
"node_modules/mark.js/dist/", "node_modules/knockout/build/output/",
"node_modules/knockout/build/output/", "node_modules/normalize.css/",
"node_modules/normalize.css/", "node_modules/jquery.fancytree/dist/",
"node_modules/jquery.fancytree/dist/", "node_modules/bootstrap/dist/",
"node_modules/bootstrap/dist/", "node_modules/autocomplete.js/dist/",
"node_modules/autocomplete.js/dist/", "node_modules/codemirror/lib/",
"node_modules/codemirror/lib/", "node_modules/codemirror/addon/",
"node_modules/codemirror/addon/", "node_modules/codemirror/mode/",
"node_modules/codemirror/mode/", "node_modules/codemirror/keymap/",
"node_modules/codemirror/keymap/", "node_modules/mind-elixir/dist/",
"node_modules/mind-elixir/dist/", "node_modules/@highlightjs/cdn-assets/languages",
"node_modules/@highlightjs/cdn-assets/languages", "node_modules/@highlightjs/cdn-assets/styles"
"node_modules/@highlightjs/cdn-assets/styles" ];
];
for (const folder of nodeModulesFolder) { for (const folder of nodeModulesFolder) {
await copyNodeModuleFileOrFolder(folder); await copyNodeModuleFileOrFolder(folder);
} }
}; };
copy() copy()
.then(() => console.log("Copying complete!")) .then(() => console.log("Copying complete!"))
.catch((err) => console.error("Error during copy:", err)); .catch((err) => console.error("Error during copy:", err));

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import anonymizationService from '../src/services/anonymization.js'; import anonymizationService from "../src/services/anonymization.js";
import fs from 'fs'; import fs from "fs";
import path from 'path'; import path from "path";
fs.writeFileSync(path.resolve(__dirname, 'tpl', 'anonymize-database.sql'), anonymizationService.getFullAnonymizationScript()); fs.writeFileSync(path.resolve(__dirname, "tpl", "anonymize-database.sql"), anonymizationService.getFullAnonymizationScript());

View File

@@ -13,11 +13,15 @@ async function fetchNote(noteId = null) {
return await resp.json(); return await resp.json();
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener(
const toggleMenuButton = document.getElementById('toggleMenuButton'); "DOMContentLoaded",
const layout = document.getElementById('layout'); () => {
const toggleMenuButton = document.getElementById("toggleMenuButton");
const layout = document.getElementById("layout");
if (toggleMenuButton && layout) { if (toggleMenuButton && layout) {
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu')); toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu"));
} }
}, false); },
false
);

View File

@@ -1,6 +1,8 @@
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */ /* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */ .printed-content .ck-widget__selection-handle,
.printed-content .ck-widget__type-around {
/* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
display: none; display: none;
} }
@@ -59,7 +61,7 @@
.ck-content .table table td, .ck-content .table table td,
.ck-content .table table th { .ck-content .table table th {
min-width: 2em; min-width: 2em;
padding: .4em; padding: 0.4em;
border: 1px solid hsl(0, 0%, 75%); border: 1px solid hsl(0, 0%, 75%);
} }
/* @ckeditor/ckeditor5-table/theme/table.css */ /* @ckeditor/ckeditor5-table/theme/table.css */
@@ -83,8 +85,8 @@
text-align: center; text-align: center;
color: var(--ck-color-selector-caption-text); color: var(--ck-color-selector-caption-text);
background-color: var(--ck-color-selector-caption-background); background-color: var(--ck-color-selector-caption-background);
padding: .6em; padding: 0.6em;
font-size: .75em; font-size: 0.75em;
outline-offset: -1px; outline-offset: -1px;
} }
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
@@ -98,7 +100,7 @@
} }
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after { .ck-content .page-break::after {
content: ''; content: "";
position: absolute; position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%); border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%; width: 100%;
@@ -107,7 +109,7 @@
.ck-content .page-break__label { .ck-content .page-break__label {
position: relative; position: relative;
z-index: 1; z-index: 1;
padding: .3em .6em; padding: 0.3em 0.6em;
display: block; display: block;
text-transform: uppercase; text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%); border: 1px solid hsl(0, 0%, 77%);
@@ -158,7 +160,7 @@
margin-left: 0; margin-left: 0;
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content[dir=rtl] .todo-list .todo-list__label > input { .ck-content[dir="rtl"] .todo-list .todo-list__label > input {
left: 0; left: 0;
margin-right: 0; margin-right: 0;
right: -25px; right: -25px;
@@ -169,7 +171,7 @@
display: block; display: block;
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;
content: ''; content: "";
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 1px solid hsl(0, 0%, 20%); border: 1px solid hsl(0, 0%, 20%);
@@ -182,14 +184,14 @@
position: absolute; position: absolute;
box-sizing: content-box; box-sizing: content-box;
pointer-events: none; pointer-events: none;
content: ''; content: "";
left: calc( var(--ck-todo-list-checkmark-size) / 3 ); left: calc(var(--ck-todo-list-checkmark-size) / 3);
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); top: calc(var(--ck-todo-list-checkmark-size) / 5.3);
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); width: calc(var(--ck-todo-list-checkmark-size) / 5.3);
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); height: calc(var(--ck-todo-list-checkmark-size) / 2.6);
border-style: solid; border-style: solid;
border-color: transparent; border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; border-width: 0 calc(var(--ck-todo-list-checkmark-size) / 8) calc(var(--ck-todo-list-checkmark-size) / 8) 0;
transform: rotate(45deg); transform: rotate(45deg);
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
@@ -206,20 +208,21 @@
vertical-align: middle; vertical-align: middle;
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { .ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type="checkbox"] {
position: absolute; position: absolute;
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input, .ck-editor__editable.ck-content .todo-list .todo-list__label > input,
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input {
cursor: pointer; cursor: pointer;
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before { .ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before,
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input:hover::before {
box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1);
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input {
-webkit-appearance: none; -webkit-appearance: none;
display: inline-block; display: inline-block;
position: relative; position: relative;
@@ -233,18 +236,18 @@
margin-left: 0; margin-left: 0;
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input { .ck-editor__editable.ck-content[dir="rtl"] .todo-list .todo-list__label > span[contenteditable="false"] > input {
left: 0; left: 0;
margin-right: 0; margin-right: 0;
right: -25px; right: -25px;
margin-left: -15px; margin-left: -15px;
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before { .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input::before {
display: block; display: block;
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;
content: ''; content: "";
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 1px solid hsl(0, 0%, 20%); border: 1px solid hsl(0, 0%, 20%);
@@ -252,32 +255,32 @@
transition: 250ms ease-in-out box-shadow; transition: 250ms ease-in-out box-shadow;
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after { .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input::after {
display: block; display: block;
position: absolute; position: absolute;
box-sizing: content-box; box-sizing: content-box;
pointer-events: none; pointer-events: none;
content: ''; content: "";
left: calc( var(--ck-todo-list-checkmark-size) / 3 ); left: calc(var(--ck-todo-list-checkmark-size) / 3);
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); top: calc(var(--ck-todo-list-checkmark-size) / 5.3);
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); width: calc(var(--ck-todo-list-checkmark-size) / 5.3);
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); height: calc(var(--ck-todo-list-checkmark-size) / 2.6);
border-style: solid; border-style: solid;
border-color: transparent; border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; border-width: 0 calc(var(--ck-todo-list-checkmark-size) / 8) calc(var(--ck-todo-list-checkmark-size) / 8) 0;
transform: rotate(45deg); transform: rotate(45deg);
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before { .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::before {
background: hsl(126, 64%, 41%); background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%); border-color: hsl(126, 64%, 41%);
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after { .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::after {
border-color: hsl(0, 0%, 100%); border-color: hsl(0, 0%, 100%);
} }
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { .ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type="checkbox"] {
position: absolute; position: absolute;
} }
/* @ckeditor/ckeditor5-list/theme/list.css */ /* @ckeditor/ckeditor5-list/theme/list.css */
@@ -379,8 +382,8 @@
word-break: break-word; word-break: break-word;
color: var(--ck-color-image-caption-text); color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background); background-color: var(--ck-color-image-caption-background);
padding: .6em; padding: 0.6em;
font-size: .75em; font-size: 0.75em;
outline-offset: -1px; outline-offset: -1px;
} }
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ /* @ckeditor/ckeditor5-image/theme/imagestyle.css */
@@ -488,16 +491,16 @@
/* @ckeditor/ckeditor5-basic-styles/theme/code.css */ /* @ckeditor/ckeditor5-basic-styles/theme/code.css */
.ck-content code { .ck-content code {
background-color: hsla(0, 0%, 78%, 0.3); background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em; padding: 0.15em;
border-radius: 2px; border-radius: 2px;
} }
/* @ckeditor/ckeditor5-font/theme/fontsize.css */ /* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny { .ck-content .text-tiny {
font-size: .7em; font-size: 0.7em;
} }
/* @ckeditor/ckeditor5-font/theme/fontsize.css */ /* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-small { .ck-content .text-small {
font-size: .85em; font-size: 0.85em;
} }
/* @ckeditor/ckeditor5-font/theme/fontsize.css */ /* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-big { .ck-content .text-big {

View File

@@ -1,2 +1,148 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none} /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
main {
display: block;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
pre {
font-family: monospace, monospace;
font-size: 1em;
}
a {
background-color: transparent;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
img {
border-style: none;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
[type="button"],
[type="reset"],
[type="submit"],
button {
-webkit-appearance: button;
}
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner,
button::-moz-focus-inner {
border-style: none;
padding: 0;
}
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring,
button:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
padding: 0.35em 0.75em 0.625em;
}
legend {
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
progress {
vertical-align: baseline;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
details {
display: block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none;
}
/*# sourceMappingURL=normalize.min.css.map */ /*# sourceMappingURL=normalize.min.css.map */

View File

@@ -1,5 +1,5 @@
body { body {
font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; font-family: "Lucida Grande", "Lucida Sans Unicode", arial, sans-serif;
line-height: 1.5; line-height: 1.5;
} }

View File

@@ -4,15 +4,11 @@ import fs from "fs";
function getBuildDate() { function getBuildDate() {
const now = new Date(); const now = new Date();
now.setMilliseconds(0); now.setMilliseconds(0);
return now return now.toISOString().replace(".000", "");
.toISOString()
.replace(".000", "");
} }
function getGitRevision() { function getGitRevision() {
return child_process.execSync('git log -1 --format="%H"') return child_process.execSync('git log -1 --format="%H"').toString("utf-8").trimEnd();
.toString("utf-8")
.trimEnd();
} }
const output = `\ const output = `\

View File

@@ -20,11 +20,7 @@ function processVersion(version) {
version = version.replace("-beta", ""); version = version.replace("-beta", "");
// Add the nightly suffix, plus the date. // Add the nightly suffix, plus the date.
const referenceDate = new Date() const referenceDate = new Date().toISOString().substring(2, 19).replace(/[-:]*/g, "").replace("T", "-");
.toISOString()
.substring(2, 19)
.replace(/[-:]*/g, "")
.replace("T", "-");
version = `${version}-test-${referenceDate}`; version = `${version}-test-${referenceDate}`;
return version; return version;

View File

@@ -12,7 +12,5 @@ function onFileChanged(sourceFile: string) {
const sourceDir = "src/public"; const sourceDir = "src/public";
chokidar chokidar.watch(sourceDir).on("change", onFileChanged);
.watch(sourceDir)
.on("change", onFileChanged);
console.log(`Watching for changes to ${sourceDir}...`); console.log(`Watching for changes to ${sourceDir}...`);

View File

@@ -1,6 +1,6 @@
module.exports = () => { module.exports = () => {
const sql = require('../../src/services/sql'); const sql = require("../../src/services/sql");
const utils = require('../../src/services/utils'); const utils = require("../../src/services/utils");
const existingBlobIds = new Set(); const existingBlobIds = new Set();
@@ -11,7 +11,7 @@ module.exports = () => {
if (!existingBlobIds.has(blobId)) { if (!existingBlobIds.has(blobId)) {
existingBlobIds.add(blobId); existingBlobIds.add(blobId);
sql.insert('blobs', { sql.insert("blobs", {
blobId, blobId,
content: row.content, content: row.content,
dateModified: row.dateModified, dateModified: row.dateModified,
@@ -24,7 +24,7 @@ module.exports = () => {
sql.execute("DELETE FROM entity_changes WHERE entityName = 'note_contents' AND entityId = ?", [row.noteId]); sql.execute("DELETE FROM entity_changes WHERE entityName = 'note_contents' AND entityId = ?", [row.noteId]);
} }
sql.execute('UPDATE notes SET blobId = ? WHERE noteId = ?', [blobId, row.noteId]); sql.execute("UPDATE notes SET blobId = ? WHERE noteId = ?", [blobId, row.noteId]);
} }
for (const noteRevisionId of sql.getColumn(`SELECT noteRevisionId FROM note_revision_contents`)) { for (const noteRevisionId of sql.getColumn(`SELECT noteRevisionId FROM note_revision_contents`)) {
@@ -34,7 +34,7 @@ module.exports = () => {
if (!existingBlobIds.has(blobId)) { if (!existingBlobIds.has(blobId)) {
existingBlobIds.add(blobId); existingBlobIds.add(blobId);
sql.insert('blobs', { sql.insert("blobs", {
blobId, blobId,
content: row.content, content: row.content,
dateModified: row.utcDateModified, dateModified: row.utcDateModified,
@@ -47,7 +47,7 @@ module.exports = () => {
sql.execute("DELETE FROM entity_changes WHERE entityName = 'note_revision_contents' AND entityId = ?", [row.noteId]); sql.execute("DELETE FROM entity_changes WHERE entityName = 'note_revision_contents' AND entityId = ?", [row.noteId]);
} }
sql.execute('UPDATE note_revisions SET blobId = ? WHERE noteRevisionId = ?', [blobId, row.noteRevisionId]); sql.execute("UPDATE note_revisions SET blobId = ? WHERE noteRevisionId = ?", [blobId, row.noteRevisionId]);
} }
const notesWithoutBlobIds = sql.getColumn("SELECT noteId FROM notes WHERE blobId IS NULL"); const notesWithoutBlobIds = sql.getColumn("SELECT noteId FROM notes WHERE blobId IS NULL");

View File

@@ -1,9 +1,9 @@
module.exports = () => { module.exports = () => {
const beccaLoader = require('../../src/becca/becca_loader'); const beccaLoader = require("../../src/becca/becca_loader");
const becca = require('../../src/becca/becca'); const becca = require("../../src/becca/becca");
const cls = require('../../src/services/cls'); const cls = require("../../src/services/cls");
const log = require('../../src/services/log'); const log = require("../../src/services/log");
const sql = require('../../src/services/sql'); const sql = require("../../src/services/sql");
cls.init(() => { cls.init(() => {
// emergency disabling of image compression since it appears to make problems in migration to 0.61 // emergency disabling of image compression since it appears to make problems in migration to 0.61
@@ -18,8 +18,7 @@ module.exports = () => {
if (attachment) { if (attachment) {
log.info(`Auto-converted note '${note.noteId}' into attachment '${attachment.attachmentId}'.`); log.info(`Auto-converted note '${note.noteId}' into attachment '${attachment.attachmentId}'.`);
} }
} } catch (e) {
catch (e) {
log.error(`Cannot convert note '${note.noteId}' to attachment: ${e.message} ${e.stack}`); log.error(`Cannot convert note '${note.noteId}' to attachment: ${e.message} ${e.stack}`);
} }
} }

View File

@@ -1,8 +1,8 @@
import http from "http"; import http from "http";
import ini from "ini"; import ini from "ini";
import fs from "fs"; import fs from "fs";
import dataDir from './src/services/data_dir.js'; import dataDir from "./src/services/data_dir.js";
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, 'utf-8')); const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
if (config.Network.https) { if (config.Network.https) {
// built-in TLS (terminated by trilium) is not supported yet, PRs are welcome // built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
@@ -10,12 +10,12 @@ if (config.Network.https) {
process.exit(0); process.exit(0);
} }
import port from './src/services/port.js'; import port from "./src/services/port.js";
import host from './src/services/host.js'; import host from "./src/services/host.js";
const options: http.RequestOptions = { timeout: 2000 }; const options: http.RequestOptions = { timeout: 2000 };
const callback: (res: http.IncomingMessage) => void = res => { const callback: (res: http.IncomingMessage) => void = (res) => {
console.log(`STATUS: ${res.statusCode}`); console.log(`STATUS: ${res.statusCode}`);
if (res.statusCode === 200) { if (res.statusCode === 200) {
process.exit(0); process.exit(0);
@@ -26,16 +26,18 @@ const callback: (res: http.IncomingMessage) => void = res => {
let request; let request;
if (port !== 0) { // TCP socket. if (port !== 0) {
// TCP socket.
const url = `http://${host}:${port}/api/health-check`; const url = `http://${host}:${port}/api/health-check`;
request = http.request(url, options, callback); request = http.request(url, options, callback);
} else { // Unix socket. } else {
// Unix socket.
options.socketPath = host; options.socketPath = host;
options.path = '/api/health-check'; options.path = "/api/health-check";
request = http.request(options, callback); request = http.request(options, callback);
} }
request.on("error", err => { request.on("error", (err) => {
console.log("ERROR"); console.log("ERROR");
process.exit(1); process.exit(1);
}); });

View File

@@ -1,33 +1,37 @@
#!/usr/bin/env node #!/usr/bin/env node
import yargs from 'yargs'; import yargs from "yargs";
import { hideBin } from 'yargs/helpers'; import { hideBin } from "yargs/helpers";
import dumpService from './inc/dump.js'; import dumpService from "./inc/dump.js";
yargs(hideBin(process.argv)) yargs(hideBin(process.argv))
.command('$0 <path_to_document> <target_directory>', 'dump the contents of document.db into the target directory', (yargs) => { .command(
return yargs "$0 <path_to_document> <target_directory>",
.option('path_to_document', { alias: 'p', describe: 'path to the document.db', type: 'string', demandOption: true }) "dump the contents of document.db into the target directory",
.option('target_directory', { alias: 't', describe: 'path of the directory into which the notes should be dumped', type: 'string', demandOption: true }); (yargs) => {
}, (argv) => { return yargs
try { .option("path_to_document", { alias: "p", describe: "path to the document.db", type: "string", demandOption: true })
dumpService.dumpDocument(argv.path_to_document, argv.target_directory, { .option("target_directory", { alias: "t", describe: "path of the directory into which the notes should be dumped", type: "string", demandOption: true });
includeDeleted: argv.includeDeleted, },
password: argv.password (argv) => {
}); try {
} dumpService.dumpDocument(argv.path_to_document, argv.target_directory, {
catch (e) { includeDeleted: argv.includeDeleted,
console.error(`Unrecoverable error:`, e); password: argv.password
process.exit(1); });
} catch (e) {
console.error(`Unrecoverable error:`, e);
process.exit(1);
}
} }
)
.option("password", {
type: "string",
description: "Set password to be able to decrypt protected notes."
}) })
.option('password', { .option("include-deleted", {
type: 'string', type: "boolean",
description: 'Set password to be able to decrypt protected notes.'
})
.option('include-deleted', {
type: 'boolean',
default: false, default: false,
description: 'If set to true, dump also deleted notes.' description: "If set to true, dump also deleted notes."
}) })
.parse(); .parse();

View File

@@ -1,6 +1,6 @@
import crypto from 'crypto'; import crypto from "crypto";
import sql from './sql.js'; import sql from "./sql.js";
import decryptService from './decrypt.js'; import decryptService from "./decrypt.js";
function getDataKey(password: any) { function getDataKey(password: any) {
if (!password) { if (!password) {
@@ -10,26 +10,24 @@ function getDataKey(password: any) {
try { try {
const passwordDerivedKey = getPasswordDerivedKey(password); const passwordDerivedKey = getPasswordDerivedKey(password);
const encryptedDataKey = getOption('encryptedDataKey'); const encryptedDataKey = getOption("encryptedDataKey");
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16); const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey, 16);
return decryptedDataKey; return decryptedDataKey;
} } catch (e: any) {
catch (e: any) {
throw new Error(`Cannot read data key, the entered password might be wrong. The underlying error: '${e.message}', stack:\n${e.stack}`); throw new Error(`Cannot read data key, the entered password might be wrong. The underlying error: '${e.message}', stack:\n${e.stack}`);
} }
} }
function getPasswordDerivedKey(password: any) { function getPasswordDerivedKey(password: any) {
const salt = getOption('passwordDerivedKeySalt'); const salt = getOption("passwordDerivedKeySalt");
return getScryptHash(password, salt); return getScryptHash(password, salt);
} }
function getScryptHash(password: any, salt: any) { function getScryptHash(password: any, salt: any) {
const hashed = crypto.scryptSync(password, salt, 32, const hashed = crypto.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
{ N: 16384, r: 8, p: 1 });
return hashed; return hashed;
} }

View File

@@ -1,4 +1,4 @@
import crypto from 'crypto'; import crypto from "crypto";
function decryptString(dataKey: any, cipherText: any) { function decryptString(dataKey: any, cipherText: any) {
const buffer = decrypt(dataKey, cipherText); const buffer = decrypt(dataKey, cipherText);
@@ -7,9 +7,9 @@ function decryptString(dataKey: any, cipherText: any) {
return null; return null;
} }
const str = buffer.toString('utf-8'); const str = buffer.toString("utf-8");
if (str === 'false') { if (str === "false") {
throw new Error("Could not decrypt string."); throw new Error("Could not decrypt string.");
} }
@@ -26,12 +26,12 @@ function decrypt(key: any, cipherText: any, ivLength = 13) {
} }
try { try {
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), 'base64'); const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
const iv = cipherTextBufferWithIv.slice(0, ivLength); const iv = cipherTextBufferWithIv.slice(0, ivLength);
const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength); const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength);
const decipher = crypto.createDecipheriv('aes-128-cbc', pad(key), pad(iv)); const decipher = crypto.createDecipheriv("aes-128-cbc", pad(key), pad(iv));
const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]); const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]);
@@ -45,14 +45,12 @@ function decrypt(key: any, cipherText: any, ivLength = 13) {
} }
return payload; return payload;
} } catch (e: any) {
catch (e: any) {
// recovery from https://github.com/zadam/trilium/issues/510 // recovery from https://github.com/zadam/trilium/issues/510
if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) { if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) {
console.log("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead"); console.log("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
return cipherText; return cipherText;
} } else {
else {
throw e; throw e;
} }
} }
@@ -61,8 +59,7 @@ function decrypt(key: any, cipherText: any, ivLength = 13) {
function pad(data: any) { function pad(data: any) {
if (data.length > 16) { if (data.length > 16) {
data = data.slice(0, 16); data = data.slice(0, 16);
} } else if (data.length < 16) {
else if (data.length < 16) {
const zeros = Array(16 - data.length).fill(0); const zeros = Array(16 - data.length).fill(0);
data = Buffer.concat([data, Buffer.from(zeros)]); data = Buffer.concat([data, Buffer.from(zeros)]);
@@ -82,7 +79,7 @@ function arraysIdentical(a: any, b: any) {
function shaArray(content: any) { function shaArray(content: any) {
// we use this as simple checksum and don't rely on its security so SHA-1 is good enough // we use this as simple checksum and don't rely on its security so SHA-1 is good enough
return crypto.createHash('sha1').update(content).digest(); return crypto.createHash("sha1").update(content).digest();
} }
export default { export default {

View File

@@ -1,11 +1,11 @@
import fs from 'fs'; import fs from "fs";
import sanitize from 'sanitize-filename'; import sanitize from "sanitize-filename";
import sql from './sql.js'; import sql from "./sql.js";
import decryptService from './decrypt.js'; import decryptService from "./decrypt.js";
import dataKeyService from './data_key.js'; import dataKeyService from "./data_key.js";
import extensionService from './extension.js'; import extensionService from "./extension.js";
function dumpDocument(documentPath: string, targetPath: string, options: { password: any; includeDeleted: any; }) { function dumpDocument(documentPath: string, targetPath: string, options: { password: any; includeDeleted: any }) {
const stats = { const stats = {
succeeded: 0, succeeded: 0,
failed: 0, failed: 0,
@@ -22,7 +22,7 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw
const existingPaths: Record<string, any> = {}; const existingPaths: Record<string, any> = {};
const noteIdToPath: Record<string, any> = {}; const noteIdToPath: Record<string, any> = {};
dumpNote(targetPath, 'root'); dumpNote(targetPath, "root");
printDumpResults(stats, options); printDumpResults(stats, options);
@@ -56,10 +56,10 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw
safeTitle = safeTitle.substring(0, 20); safeTitle = safeTitle.substring(0, 20);
} }
childTargetPath = targetPath + '/' + safeTitle; childTargetPath = targetPath + "/" + safeTitle;
for (let i = 1; i < 100000 && childTargetPath in existingPaths; i++) { for (let i = 1; i < 100000 && childTargetPath in existingPaths; i++) {
childTargetPath = targetPath + '/' + safeTitle + '_' + i; childTargetPath = targetPath + "/" + safeTitle + "_" + i;
} }
existingPaths[childTargetPath] = true; existingPaths[childTargetPath] = true;
@@ -93,8 +93,7 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw
} }
noteIdToPath[noteId] = childTargetPath; noteIdToPath[noteId] = childTargetPath;
} } catch (e: any) {
catch (e: any) {
console.error(`DUMPERROR: Writing '${noteId}' failed with error '${e.message}':\n${e.stack}`); console.error(`DUMPERROR: Writing '${noteId}' failed with error '${e.message}':\n${e.stack}`);
stats.failed++; stats.failed++;
@@ -104,13 +103,12 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw
if (childNoteIds.length > 0) { if (childNoteIds.length > 0) {
if (childTargetPath === fileNameWithPath) { if (childTargetPath === fileNameWithPath) {
childTargetPath += '_dir'; childTargetPath += "_dir";
} }
try { try {
fs.mkdirSync(childTargetPath as string, { recursive: true }); fs.mkdirSync(childTargetPath as string, { recursive: true });
} } catch (e: any) {
catch (e: any) {
console.error(`DUMPERROR: Creating directory ${childTargetPath} failed with error '${e.message}'`); console.error(`DUMPERROR: Creating directory ${childTargetPath} failed with error '${e.message}'`);
} }
@@ -122,12 +120,12 @@ function dumpDocument(documentPath: string, targetPath: string, options: { passw
} }
function printDumpResults(stats: any, options: any) { function printDumpResults(stats: any, options: any) {
console.log('\n----------------------- STATS -----------------------'); console.log("\n----------------------- STATS -----------------------");
console.log('Successfully dumpted notes: ', stats.succeeded.toString().padStart(5, ' ')); console.log("Successfully dumpted notes: ", stats.succeeded.toString().padStart(5, " "));
console.log('Protected notes: ', stats.protected.toString().padStart(5, ' '), options.password ? '' : '(skipped)'); console.log("Protected notes: ", stats.protected.toString().padStart(5, " "), options.password ? "" : "(skipped)");
console.log('Failed notes: ', stats.failed.toString().padStart(5, ' ')); console.log("Failed notes: ", stats.failed.toString().padStart(5, " "));
console.log('Deleted notes: ', stats.deleted.toString().padStart(5, ' '), options.includeDeleted ? "(dumped)" : "(at least, skipped)"); console.log("Deleted notes: ", stats.deleted.toString().padStart(5, " "), options.includeDeleted ? "(dumped)" : "(at least, skipped)");
console.log('-----------------------------------------------------'); console.log("-----------------------------------------------------");
if (!options.password && stats.protected > 0) { if (!options.password && stats.protected > 0) {
console.log("\nWARNING: protected notes are present in the document but no password has been provided. Protected notes have not been dumped."); console.log("\nWARNING: protected notes are present in the document but no password has been provided. Protected notes have not been dumped.");
@@ -140,12 +138,10 @@ function isContentEmpty(content: any) {
} }
if (typeof content === "string") { if (typeof content === "string") {
return !content.trim() || content.trim() === '<p></p>'; return !content.trim() || content.trim() === "<p></p>";
} } else if (Buffer.isBuffer(content)) {
else if (Buffer.isBuffer(content)) {
return content.length === 0; return content.length === 0;
} } else {
else {
return false; return false;
} }
} }

View File

@@ -5,15 +5,17 @@ function getFileName(note: any, childTargetPath: string, safeTitle: string) {
let existingExtension = path.extname(safeTitle).toLowerCase(); let existingExtension = path.extname(safeTitle).toLowerCase();
let newExtension; let newExtension;
if (note.type === 'text') { if (note.type === "text") {
newExtension = 'html'; newExtension = "html";
} else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') { } else if (note.mime === "application/x-javascript" || note.mime === "text/javascript") {
newExtension = 'js'; newExtension = "js";
} else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it } else if (existingExtension.length > 0) {
// if the page already has an extension, then we'll just keep it
newExtension = null; newExtension = null;
} else { } else {
if (note.mime?.toLowerCase()?.trim() === "image/jpg") { // image/jpg is invalid but pretty common if (note.mime?.toLowerCase()?.trim() === "image/jpg") {
newExtension = 'jpg'; // image/jpg is invalid but pretty common
newExtension = "jpg";
} else { } else {
newExtension = mimeTypes.extension(note.mime) || "dat"; newExtension = mimeTypes.extension(note.mime) || "dat";
} }

View File

@@ -2,7 +2,9 @@ import Database, { Database as DatabaseType } from "better-sqlite3";
let dbConnection: DatabaseType; let dbConnection: DatabaseType;
const openDatabase = (documentPath: string) => { dbConnection = new Database(documentPath, { readonly: true }) }; const openDatabase = (documentPath: string) => {
dbConnection = new Database(documentPath, { readonly: true });
};
const getRow = (query: string, params: string[] = []): Record<string, any> => dbConnection.prepare(query).get(params) as Record<string, any>; const getRow = (query: string, params: string[] = []): Record<string, any> => dbConnection.prepare(query).get(params) as Record<string, any>;
const getRows = (query: string, params = []) => dbConnection.prepare(query).all(params); const getRows = (query: string, params = []) => dbConnection.prepare(query).all(params);

View File

@@ -1,10 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"target": "ES6", "target": "ES6",
"strict": true "strict": true
} }
} }

51
e2e/i18n.spec.ts Normal file
View File

@@ -0,0 +1,51 @@
import { test, expect, Page } from "@playwright/test";
import App from "./support/app";
test("Displays translation on desktop", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await expect(page.locator("#left-pane .quick-search input"))
.toHaveAttribute("placeholder", "Quick search");
});
test("Displays translation on mobile", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ isMobile: true });
await expect(page.locator("#mobile-sidebar-wrapper .quick-search input"))
.toHaveAttribute("placeholder", "Quick search");
});
test("Displays translations in Settings", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToSettings();
await app.noteTree.getByText("Appearance").click();
await expect(app.currentNoteSplit).toContainText("Localization");
await expect(app.currentNoteSplit).toContainText("Language");
});
test("User can change language from settings", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToSettings();
await app.noteTree.getByText("Appearance").click();
// Check that the default value (English) is set.
await expect(app.currentNoteSplit).toContainText("Theme");
const languageCombobox = await app.currentNoteSplit.getByRole("combobox").first();
await expect(languageCombobox).toHaveValue("en");
// Select Chinese and ensure the translation is set.
await languageCombobox.selectOption("cn");
await expect(app.currentNoteSplit).toContainText("主题");
// Select English again.
await languageCombobox.selectOption("en");
await expect(app.currentNoteSplit).toContainText("Language");
});

View File

@@ -0,0 +1,59 @@
import { test, expect } from "@playwright/test";
import App from "../support/app";
const NOTE_TITLE = "Trilium Integration Test DB";
test("Can drag tabs around", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
// [1]: Trilium Integration Test DB note
await app.closeAllTabs();
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
await expect(app.getActiveTab()).toContainText(NOTE_TITLE);
// [1] [2] [3]
await app.addNewTab();
await app.addNewTab();
let tab = app.getTab(0);
// Drag the first tab at the end
await tab.dragTo(app.getTab(2), { targetPosition: { x: 50, y: 0 }});
tab = app.getTab(2);
await expect(tab).toContainText(NOTE_TITLE);
// Drag the tab to the left
await tab.dragTo(app.getTab(0), { targetPosition: { x: 50, y: 0 }});
await expect(app.getTab(0)).toContainText(NOTE_TITLE);
});
test("Can drag tab to new window", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
const tab = app.getTab(0);
await expect(tab).toContainText(NOTE_TITLE);
const popupPromise = page.waitForEvent("popup");
const tabPos = await tab.boundingBox();
if (tabPos) {
const x = tabPos.x + tabPos.width / 2;
const y = tabPos.y + tabPos.height / 2;
await page.mouse.move(x, y);
await page.mouse.down();
await page.mouse.move(x, y + tabPos.height + 100, { steps: 5 });
await page.mouse.up();
} else {
test.fail(true, "Unable to determine tab position");
}
// Wait for the popup to show
const popup = await popupPromise;
const popupApp = new App(popup, context);
await expect(popupApp.getActiveTab()).toHaveText(NOTE_TITLE);
});

View File

@@ -0,0 +1,45 @@
import { test, expect, Page } from "@playwright/test";
import App from "../support/app";
test("Displays lint warnings for backend script", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToNoteInNewTab("Backend script with lint warnings");
const codeEditor = app.currentNoteSplit.locator(".CodeMirror");
// Expect two warning signs in the gutter.
expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2);
// Hover over hello
await codeEditor.getByText("hello").first().hover();
await expectTooltip(page, "'hello' is defined but never used.");
// Hover over world
await codeEditor.getByText("world").first().hover();
await expectTooltip(page, "'world' is defined but never used.");
});
test("Displays lint errors for backend script", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToNoteInNewTab("Backend script with lint errors");
const codeEditor = app.currentNoteSplit.locator(".CodeMirror");
// Expect two warning signs in the gutter.
const errorMarker = codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-error");
await expect(errorMarker).toHaveCount(1);
// Hover over hello
await errorMarker.hover();
await expectTooltip(page, "Parsing error: Unexpected token world");
});
async function expectTooltip(page: Page, tooltip: string) {
await expect(page.locator(".CodeMirror-lint-tooltip:visible", {
"hasText": tooltip
})).toBeVisible();
}

View File

@@ -0,0 +1,22 @@
import { test, expect, Page } from "@playwright/test";
import App from "../support/app";
test("displays simple map", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Sample mindmap");
expect(app.currentNoteSplit).toContainText("Hello world");
expect(app.currentNoteSplit).toContainText("1");
expect(app.currentNoteSplit).toContainText("1a");
});
test("displays note settings", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.goToNoteInNewTab("Sample mindmap");
await app.currentNoteSplit.getByText("Hello world").click({ force: true });
const nodeMenu = app.currentNoteSplit.locator(".node-menu");
expect(nodeMenu).toBeVisible();
});

View File

@@ -0,0 +1,51 @@
import { test, expect, Page } from "@playwright/test";
import App from "../support/app";
test("Table of contents is displayed", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToNoteInNewTab("Table of contents");
await expect(app.sidebar).toContainText("Table of Contents");
const rootList = app.sidebar.locator(".toc-widget > span > ol");
// Heading 1.1
// Heading 1.1
// Heading 1.2
// Heading 2
// Heading 2.1
// Heading 2.2
// Heading 2.2.1
// Heading 2.2.1.1
// Heading 2.2.11.1
await expect(rootList.locator("> li")).toHaveCount(2);
await expect(rootList.locator("> li").first()).toHaveText("Heading 1");
await expect(rootList.locator("> ol").first().locator("> li").first()).toHaveText("Heading 1.1");
await expect(rootList.locator("> ol").first().locator("> li").nth(1)).toHaveText("Heading 1.2");
// Heading 2 has a Katex equation, check if it's rendered.
await expect(rootList.locator("> li").nth(1)).toContainText("Heading 2");
await expect(rootList.locator("> li").nth(1).locator(".katex")).toBeAttached();
await expect(rootList.locator("> ol")).toHaveCount(2);
await expect(rootList.locator("> ol").nth(1).locator("> li")).toHaveCount(2);
await expect(rootList.locator("> ol").nth(1).locator("> ol")).toHaveCount(1);
await expect(rootList.locator("> ol").nth(1).locator("> ol > ol")).toHaveCount(1);
await expect(rootList.locator("> ol").nth(1).locator("> ol > ol > ol")).toHaveCount(1);
});
test("Highlights list is displayed", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
await app.goToNoteInNewTab("Highlights list");
await expect(app.sidebar).toContainText("Highlights List");
const rootList = app.sidebar.locator(".highlights-list ol");
let index=0;
for (const highlightedEl of [ "Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2" ]) {
await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl);
}
});

78
e2e/support/app.ts Normal file
View File

@@ -0,0 +1,78 @@
import { expect, Locator, Page } from "@playwright/test";
import type { BrowserContext } from "@playwright/test";
interface GotoOpts {
isMobile?: boolean;
}
export default class App {
readonly page: Page;
readonly context: BrowserContext;
readonly tabBar: Locator;
readonly noteTree: Locator;
readonly currentNoteSplit: Locator;
readonly sidebar: Locator;
constructor(page: Page, context: BrowserContext) {
this.page = page;
this.context = context;
this.tabBar = page.locator(".tab-row-widget-container");
this.noteTree = page.locator(".tree-wrapper");
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)")
this.sidebar = page.locator("#right-pane");
}
async goto(opts: GotoOpts = {}) {
await this.context.addCookies([
{
url: "http://127.0.0.1:8082",
name: "trilium-device",
value: opts.isMobile ? "mobile" : "desktop"
}
]);
await this.page.goto("/", { waitUntil: "networkidle" });
// Wait for the page to load.
await expect(this.page.locator(".tree"))
.toContainText("Trilium Integration Test");
await this.closeAllTabs();
}
async goToNoteInNewTab(noteTitle: string) {
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
await autocomplete.fill(noteTitle);
await autocomplete.press("ArrowDown");
await autocomplete.press("Enter");
}
async goToSettings() {
await this.page.locator(".launcher-button.bx-cog").click();
}
getTab(tabIndex: number) {
return this.tabBar.locator(".note-tab-wrapper").nth(tabIndex);
}
getActiveTab() {
return this.tabBar.locator(".note-tab[active]");
}
async closeAllTabs() {
await this.getTab(0).click({ button: "right" });
await this.page.waitForTimeout(500); // TODO: context menu won't dismiss otherwise
await this.page.getByText("Close all tabs").click({ force: true });
await this.page.waitForTimeout(500); // TODO: context menu won't dismiss otherwise
}
async addNewTab() {
await this.page.locator('[data-trigger-command="openNewTab"]').click();
}
async clickNoteOnNoteTreeByTitle(title: string) {
this.noteTree.getByText(title).click();
}
}

View File

@@ -1,4 +1,4 @@
import { initializeTranslations } from "./src/services/i18n.js"; import { initializeTranslations } from "./src/services/i18n.js";
await initializeTranslations(); await initializeTranslations();
await import("./electron.js") await import("./electron.js");

View File

@@ -12,8 +12,8 @@ import sourceMapSupport from "source-map-support";
sourceMapSupport.install(); sourceMapSupport.install();
// Prevent Trilium starting twice on first install and on uninstall for the Windows installer. // Prevent Trilium starting twice on first install and on uninstall for the Windows installer.
if ((await import('electron-squirrel-startup')).default) { if ((await import("electron-squirrel-startup")).default) {
process.exit(0); process.exit(0);
} }
// Adds debug features like hotkeys for triggering dev tools and reload // Adds debug features like hotkeys for triggering dev tools and reload
@@ -24,50 +24,48 @@ appIconService.installLocalAppIcon();
electronDl({ saveAs: true }); electronDl({ saveAs: true });
// needed for excalidraw export https://github.com/zadam/trilium/issues/4271 // needed for excalidraw export https://github.com/zadam/trilium/issues/4271
electron.app.commandLine.appendSwitch( electron.app.commandLine.appendSwitch("enable-experimental-web-platform-features");
"enable-experimental-web-platform-features"
);
// Quit when all windows are closed, except on macOS. There, it's common // Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q. // explicitly with Cmd + Q.
electron.app.on("window-all-closed", () => { electron.app.on("window-all-closed", () => {
if (process.platform !== "darwin") { if (process.platform !== "darwin") {
electron.app.quit(); electron.app.quit();
} }
}); });
electron.app.on("ready", async () => { electron.app.on("ready", async () => {
// electron.app.setAppUserModelId('com.github.zadam.trilium'); // electron.app.setAppUserModelId('com.github.zadam.trilium');
// if db is not initialized -> setup process // if db is not initialized -> setup process
// if db is initialized, then we need to wait until the migration process is finished // if db is initialized, then we need to wait until the migration process is finished
if (sqlInit.isDbInitialized()) { if (sqlInit.isDbInitialized()) {
await sqlInit.dbReady; await sqlInit.dbReady;
await windowService.createMainWindow(electron.app);
if (process.platform === "darwin") {
electron.app.on("activate", async () => {
if (electron.BrowserWindow.getAllWindows().length === 0) {
await windowService.createMainWindow(electron.app); await windowService.createMainWindow(electron.app);
if (process.platform === "darwin") {
electron.app.on("activate", async () => {
if (electron.BrowserWindow.getAllWindows().length === 0) {
await windowService.createMainWindow(electron.app);
}
});
} }
});
tray.createTray();
} else {
await windowService.createSetupWindow();
} }
tray.createTray(); await windowService.registerGlobalShortcuts();
} else {
await windowService.createSetupWindow();
}
await windowService.registerGlobalShortcuts();
}); });
electron.app.on("will-quit", () => { electron.app.on("will-quit", () => {
electron.globalShortcut.unregisterAll(); electron.globalShortcut.unregisterAll();
}); });
// this is to disable electron warning spam in the dev console (local development only) // this is to disable electron warning spam in the dev console (local development only)
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true"; process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
await import('./src/main.js'); await import("./src/main.js");

View File

@@ -1,117 +1,115 @@
const path = require('path'); const path = require("path");
const fs = require('fs-extra'); const fs = require("fs-extra");
const APP_NAME = "TriliumNext Notes"; const APP_NAME = "TriliumNext Notes";
module.exports = { module.exports = {
packagerConfig: { packagerConfig: {
executableName: "trilium", executableName: "trilium",
name: APP_NAME, name: APP_NAME,
overwrite: true, overwrite: true,
asar: true, asar: true,
icon: "./images/app-icons/icon", icon: "./images/app-icons/icon",
extraResource: [ extraResource: [
// Moved to root // Moved to root
...getExtraResourcesForPlatform(), ...getExtraResourcesForPlatform(),
// Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS) // Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
"translations/", "translations/",
"node_modules/@highlightjs/cdn-assets/styles" "node_modules/@highlightjs/cdn-assets/styles"
],
afterComplete: [
(buildPath, _electronVersion, platform, _arch, callback) => {
const extraResources = getExtraResourcesForPlatform();
for (const resource of extraResources) {
const baseName = path.basename(resource);
let sourcePath;
if (platform === "darwin") {
sourcePath = path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName);
} else {
sourcePath = path.join(buildPath, "resources", baseName);
}
let destPath;
if (baseName !== "256x256.png") {
destPath = path.join(buildPath, baseName);
} else {
destPath = path.join(buildPath, "icon.png");
}
// Copy files from resources folder to root
fs.move(sourcePath, destPath)
.then(() => callback())
.catch((err) => callback(err));
}
}
]
},
rebuildConfig: {
force: true
},
makers: [
{
name: "@electron-forge/maker-deb",
config: {
options: {
icon: "./images/app-icons/png/128x128.png",
desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs")
}
}
},
{
name: "@electron-forge/maker-squirrel",
config: {
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico",
setupIcon: "./images/app-icons/icon.ico",
loadingGif: "./images/app-icons/win/setup-banner.gif"
}
},
{
name: "@electron-forge/maker-dmg",
config: {
icon: "./images/app-icons/icon.icns"
}
},
{
name: "@electron-forge/maker-zip",
config: {
options: {
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico",
icon: "./images/app-icons/icon.ico"
}
}
}
], ],
afterComplete: [(buildPath, _electronVersion, platform, _arch, callback) => { plugins: [
const extraResources = getExtraResourcesForPlatform(); {
for (const resource of extraResources) { name: "@electron-forge/plugin-auto-unpack-natives",
const baseName = path.basename(resource); config: {}
let sourcePath;
if (platform === 'darwin') {
sourcePath = path.join(buildPath, `${APP_NAME}.app`, 'Contents', 'Resources', baseName);
} else {
sourcePath = path.join(buildPath, 'resources', baseName);
} }
let destPath; ]
if (baseName !== "256x256.png") {
destPath = path.join(buildPath, baseName);
} else {
destPath = path.join(buildPath, "icon.png");
}
// Copy files from resources folder to root
fs.move(sourcePath, destPath)
.then(() => callback())
.catch(err => callback(err));
}
}]
},
rebuildConfig: {
force: true
},
makers: [
{
name: '@electron-forge/maker-deb',
config: {
options: {
icon: "./images/app-icons/png/128x128.png",
desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs")
}
}
},
{
name: '@electron-forge/maker-squirrel',
config: {
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico",
setupIcon: "./images/app-icons/icon.ico",
loadingGif: "./images/app-icons/win/setup-banner.gif"
}
},
{
name: '@electron-forge/maker-dmg',
config: {
icon: "./images/app-icons/icon.icns",
}
},
{
name: '@electron-forge/maker-zip',
config: {
options: {
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Notes/develop/images/app-icons/icon.ico",
icon: "./images/app-icons/icon.ico",
}
}
}
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
],
}; };
function getExtraResourcesForPlatform() { function getExtraResourcesForPlatform() {
let resources = [ let resources = ["dump-db/", "./bin/tpl/anonymize-database.sql"];
'dump-db/', const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"];
'./bin/tpl/anonymize-database.sql' switch (process.platform) {
]; case "win32":
const scripts = ['trilium-portable', 'trilium-safe-mode', 'trilium-no-cert-check'] for (const script of scripts) {
switch (process.platform) { resources.push(`./bin/tpl/${script}.bat`);
case 'win32': }
for (const script of scripts) { break;
resources.push(`./bin/tpl/${script}.bat`) case "darwin":
} break;
break; case "linux":
case 'darwin': resources.push("images/app-icons/png/256x256.png");
break; for (const script of scripts) {
case 'linux': resources.push(`./bin/tpl/${script}.sh`);
resources.push("images/app-icons/png/256x256.png") }
for (const script of scripts) { break;
resources.push(`./bin/tpl/${script}.sh`) default:
} break;
break; }
default:
break;
}
return resources; return resources;
} }

View File

@@ -1,6 +1,6 @@
import { test as setup, expect } from '@playwright/test'; import { test as setup, expect } from "@playwright/test";
const authFile = 'playwright/.auth/user.json'; const authFile = "playwright/.auth/user.json";
const ROOT_URL = "http://localhost:8082"; const ROOT_URL = "http://localhost:8082";
const LOGIN_PASSWORD = "demo1234"; const LOGIN_PASSWORD = "demo1234";
@@ -12,6 +12,6 @@ setup("authenticate", async ({ page }) => {
await expect(page).toHaveURL(`${ROOT_URL}/login`); await expect(page).toHaveURL(`${ROOT_URL}/login`);
await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD); await page.getByRole("textbox", { name: "Password" }).fill(LOGIN_PASSWORD);
await page.getByRole("button", { name: "Login"}).click(); await page.getByRole("button", { name: "Login" }).click();
await page.context().storageState({ path: authFile }); await page.context().storageState({ path: authFile });
}); });

View File

@@ -0,0 +1,29 @@
[General]
# Instance name can be used to distinguish between different instances using backend api.getInstanceName()
instanceName=
# set to true to allow using Trilium without authentication (makes sense for server build only, desktop build doesn't need password)
noAuthentication=true
# set to true to disable backups (e.g. because of limited space on server)
noBackup=false
# Disable automatically generating desktop icon
# noDesktopIcon=true
[Network]
# host setting is relevant only for web deployments - set the host on which the server will listen
# host=0.0.0.0
# port setting is relevant only for web deployments, desktop builds run on a fixed port (changeable with TRILIUM_PORT environment variable)
port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (insecure).
https=false
# path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
certPath=
keyPath=
# setting to give trust to reverse proxies, a comma-separated list of trusted rev. proxy IPs can be specified (CIDR notation is permitted),
# alternatively 'true' will make use of the leftmost IP in X-Forwarded-For, ultimately an integer can be used to tell about the number of hops between
# Trilium (which is hop 0) and the first trusted rev. proxy.
# once set, expressjs will use the X-Forwarded-For header set by the rev. proxy to determinate the real IPs of clients.
# expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
trustedReverseProxy=false

Binary file not shown.

View File

@@ -1,9 +1,9 @@
import { test, expect } from '@playwright/test'; import { test, expect } from "@playwright/test";
test("Can duplicate note with broken links", async ({ page }) => { test("Can duplicate note with broken links", async ({ page }) => {
await page.goto(`http://localhost:8082/#2VammGGdG6Ie`); await page.goto(`http://localhost:8082/#2VammGGdG6Ie`);
await page.locator('.tree-wrapper .fancytree-active').getByText('Note map').click({ button: 'right' }); await page.locator(".tree-wrapper .fancytree-active").getByText("Note map").click({ button: "right" });
await page.getByText('Duplicate subtree').click(); await page.getByText("Duplicate subtree").click();
await expect(page.locator(".toast-body")).toBeHidden(); await expect(page.locator(".toast-body")).toBeHidden();
await expect(page.locator('.tree-wrapper').getByText('Note map (dup)')).toBeVisible(); await expect(page.locator(".tree-wrapper").getByText("Note map (dup)")).toBeVisible();
}); });

View File

@@ -1,18 +1,18 @@
import { test, expect } from '@playwright/test'; import { test, expect } from "@playwright/test";
test('has title', async ({ page }) => { test("has title", async ({ page }) => {
await page.goto('https://playwright.dev/'); await page.goto("https://playwright.dev/");
// Expect a title "to contain" a substring. // Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/); await expect(page).toHaveTitle(/Playwright/);
}); });
test('get started link', async ({ page }) => { test("get started link", async ({ page }) => {
await page.goto('https://playwright.dev/'); await page.goto("https://playwright.dev/");
// Click the get started link. // Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click(); await page.getByRole("link", { name: "Get started" }).click();
// Expects page to have a heading with the name of Installation. // Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible();
}); });

View File

@@ -1,23 +1,23 @@
import test, { expect } from "@playwright/test"; import test, { expect } from "@playwright/test";
test('Help popup', async ({ page }) => { test("Help popup", async ({ page }) => {
await page.goto('http://localhost:8082'); await page.goto("http://localhost:8082");
await page.getByText('Trilium Integration Test DB').click(); await page.getByText("Trilium Integration Test DB").click();
await page.locator('body').press('F1'); await page.locator("body").press("F1");
await page.getByRole('link', { name: 'online↗' }).click(); await page.getByRole("link", { name: "online↗" }).click();
expect((await page.waitForEvent('popup')).url()).toBe("https://triliumnext.github.io/Docs/") expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/");
}); });
test('Complete help in search', async ({ page }) => { test("Complete help in search", async ({ page }) => {
await page.goto('http://localhost:8082'); await page.goto("http://localhost:8082");
// Clear all tabs // Clear all tabs
await page.locator('.note-tab:first-of-type').locator("div").nth(1).click({ button: 'right' }); await page.locator(".note-tab:first-of-type").locator("div").nth(1).click({ button: "right" });
await page.getByText('Close all tabs').click(); await page.getByText("Close all tabs").click();
await page.locator('#launcher-container').getByRole('button', { name: '' }).first().click(); await page.locator("#launcher-container").getByRole("button", { name: "" }).first().click();
await page.getByRole('cell', { name: ' ' }).locator('span').first().click(); await page.getByRole("cell", { name: " " }).locator("span").first().click();
await page.getByRole('button', { name: 'complete help on search syntax' }).click(); await page.getByRole("button", { name: "complete help on search syntax" }).click();
expect((await page.waitForEvent('popup')).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html"); expect((await page.waitForEvent("popup")).url()).toBe("https://triliumnext.github.io/Docs/Wiki/search.html");
}); });

View File

@@ -1,43 +0,0 @@
import test, { expect } from "@playwright/test";
test("User can change language from settings", async ({ page }) => {
await page.goto('http://localhost:8082');
// Clear all tabs
await page.locator('.note-tab:first-of-type').locator("div").nth(1).click({ button: 'right' });
await page.getByText('Close all tabs').click();
// Go to options -> Appearance
await page.locator('#launcher-pane div').filter({ hasText: 'Options Open New Window' }).getByRole('button').click();
await page.locator('#launcher-pane').getByText('Options').click();
await page.locator('#center-pane').getByText('Appearance').click();
// Check that the default value (English) is set.
await expect(page.locator('#center-pane')).toContainText('Theme');
const languageCombobox = await page.getByRole('combobox').first();
await expect(languageCombobox).toHaveValue("en");
// Select Chinese and ensure the translation is set.
languageCombobox.selectOption("cn");
await expect(page.locator('#center-pane')).toContainText('主题');
// Select English again.
languageCombobox.selectOption("en");
});
test("Restores language on start-up on desktop", async ({ page, context }) => {
await page.goto('http://localhost:8082');
await expect(page.locator('#launcher-pane').first()).toContainText("Open New Window");
});
test("Restores language on start-up on mobile", async ({ page, context }) => {
await context.addCookies([
{
url: "http://localhost:8082",
name: "trilium-device",
value: "mobile"
}
]);
await page.goto('http://localhost:8082');
await expect(page.locator('#launcher-pane div').first()).toContainText("Open New Window");
});

View File

@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test'; import { test, expect } from "@playwright/test";
const ROOT_URL = "http://localhost:8080"; const ROOT_URL = "http://localhost:8080";
const LOGIN_PASSWORD = "eliandoran"; const LOGIN_PASSWORD = "eliandoran";
@@ -12,7 +12,6 @@ test("Can insert equations", async ({ page }) => {
// .click(); // .click();
const activeNote = page.locator(".component.note-split:visible"); const activeNote = page.locator(".component.note-split:visible");
const noteContent = activeNote const noteContent = activeNote.locator(".note-detail-editable-text-editor");
.locator(".note-detail-editable-text-editor")
await noteContent.press("Ctrl+M"); await noteContent.press("Ctrl+M");
}); });

View File

@@ -1,21 +1,21 @@
import test, { expect } from "@playwright/test"; import test, { expect } from "@playwright/test";
test("Native Title Bar not displayed on web", async ({ page }) => { test("Native Title Bar not displayed on web", async ({ page }) => {
await page.goto('http://localhost:8082/#root/_hidden/_options/_optionsAppearance'); await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsAppearance");
await expect(page.getByRole('heading', { name: 'Theme' })).toBeVisible(); await expect(page.getByRole("heading", { name: "Theme" })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Native Title Bar (requires' })).toBeHidden(); await expect(page.getByRole("heading", { name: "Native Title Bar (requires" })).toBeHidden();
}); });
test("Tray settings not displayed on web", async ({ page }) => { test("Tray settings not displayed on web", async ({ page }) => {
await page.goto('http://localhost:8082/#root/_hidden/_options/_optionsOther'); await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsOther");
await expect(page.getByRole('heading', { name: 'Note Erasure Timeout' })).toBeVisible(); await expect(page.getByRole("heading", { name: "Note Erasure Timeout" })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Tray' })).toBeHidden(); await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
}); });
test("Spellcheck settings not displayed on web", async ({ page }) => { test("Spellcheck settings not displayed on web", async ({ page }) => {
await page.goto('http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck'); await page.goto("http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck");
await expect(page.getByRole('heading', { name: 'Spell Check' })).toBeVisible(); await expect(page.getByRole("heading", { name: "Spell Check" })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Tray' })).toBeHidden(); await expect(page.getByRole("heading", { name: "Tray" })).toBeHidden();
await expect(page.getByText('These options apply only for desktop builds')).toBeVisible(); await expect(page.getByText("These options apply only for desktop builds")).toBeVisible();
await expect(page.getByText('Enable spellcheck')).toBeHidden(); await expect(page.getByText("Enable spellcheck")).toBeHidden();
}); });

View File

@@ -1,8 +1,8 @@
import test, { expect } from "@playwright/test"; import test, { expect } from "@playwright/test";
test("Renders on desktop", async ({ page, context }) => { test("Renders on desktop", async ({ page, context }) => {
await page.goto('http://localhost:8082'); await page.goto("http://localhost:8082");
await expect(page.locator('.tree')).toContainText('Trilium Integration Test'); await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
}); });
test("Renders on mobile", async ({ page, context }) => { test("Renders on mobile", async ({ page, context }) => {
@@ -13,6 +13,6 @@ test("Renders on mobile", async ({ page, context }) => {
value: "mobile" value: "mobile"
} }
]); ]);
await page.goto('http://localhost:8082'); await page.goto("http://localhost:8082");
await expect(page.locator('.tree')).toContainText('Trilium Integration Test'); await expect(page.locator(".tree")).toContainText("Trilium Integration Test");
}); });

View File

@@ -1,12 +1,12 @@
import { test, expect } from '@playwright/test'; import { test, expect } from "@playwright/test";
const expectedVersion = "0.90.3"; const expectedVersion = "0.90.3";
test("Displays update badge when there is a version available", async ({ page }) => { test("Displays update badge when there is a version available", async ({ page }) => {
await page.goto("http://localhost:8080"); await page.goto("http://localhost:8080");
await page.getByRole('button', { name: '' }).click(); await page.getByRole("button", { name: "" }).click();
await page.getByText(`Version ${expectedVersion} is available,`).click(); await page.getByText(`Version ${expectedVersion} is available,`).click();
const page1 = await page.waitForEvent('popup'); const page1 = await page.waitForEvent("popup");
expect(page1.url()).toBe(`https://github.com/TriliumNext/Notes/releases/tag/v${expectedVersion}`); expect(page1.url()).toBe(`https://github.com/TriliumNext/Notes/releases/tag/v${expectedVersion}`);
}); });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,100 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
async function validatorHtml(text, options) {
const result = /<script[^>]*>([\s\S]+)<\/script>/ig.exec(text);
if (result !== null) {
// preceding code is copied over but any (non-newline) character is replaced with space
// this will preserve line numbers etc.
const prefix = text.substr(0, result.index).replace(/./g, " ");
const js = prefix + result[1];
return await validatorJavaScript(js, options);
}
return [];
}
async function validatorJavaScript(text, options) {
if (glob.isMobile()
|| glob.getActiveContextNote() == null
|| glob.getActiveContextNote().mime === 'application/json') {
// eslint doesn't seem to validate pure JSON well
return [];
}
await glob.requireLibrary(glob.ESLINT);
if (text.length > 20000) {
console.log("Skipping linting because of large size: ", text.length);
return [];
}
const errors = new eslint().verify(text, {
root: true,
parserOptions: {
ecmaVersion: "latest"
},
extends: ['eslint:recommended', 'airbnb-base'],
env: {
'browser': true,
'node': true
},
rules: {
'import/no-unresolved': 'off',
'func-names': 'off',
'comma-dangle': ['warn'],
'padded-blocks': 'off',
'linebreak-style': 'off',
'class-methods-use-this': 'off',
'no-unused-vars': ['warn', { vars: 'local', args: 'after-used' }],
'no-nested-ternary': 'off',
'no-underscore-dangle': ['error', {'allow': ['_super', '_lookupFactory']}]
},
globals: {
"api": "readonly"
}
});
console.log(errors);
const result = [];
if (errors) {
parseErrors(errors, result);
}
return result;
}
CodeMirror.registerHelper("lint", "javascript", validatorJavaScript);
CodeMirror.registerHelper("lint", "html", validatorHtml);
function parseErrors(errors, output) {
for (const error of errors) {
const startLine = error.line - 1;
const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine;
const startCol = error.column - 1;
const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1;
output.push({
message: error.message,
severity: error.severity === 1 ? "warning" : "error",
from: CodeMirror.Pos(startLine, startCol),
to: CodeMirror.Pos(endLine, endCol)
});
}
}
});

112883
libraries/eslint/eslint.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,6 @@
// Then probably can change webpack comand to // Then probably can change webpack comand to
// "webpack": "cross-env NODE_OPTIONS=--import=ts-node/esm webpack -c webpack.config.ts", // "webpack": "cross-env NODE_OPTIONS=--import=ts-node/esm webpack -c webpack.config.ts",
import { register } from 'node:module'; import { register } from "node:module";
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from "node:url";
register('ts-node/esm', pathToFileURL('./')); register("ts-node/esm", pathToFileURL("./"));

View File

@@ -1,15 +1,12 @@
{ {
"restartable": "rs", "restartable": "rs",
"ignore": [".git", "node_modules/**/node_modules", "src/public/"], "ignore": [".git", "node_modules/**/node_modules", "src/public/"],
"verbose": false, "verbose": false,
"exec": "tsx", "exec": "tsx",
"watch": [ "watch": ["src/", "translations/"],
"src/", "signal": "SIGTERM",
"translations/" "env": {
], "NODE_ENV": "development"
"signal": "SIGTERM", },
"env": { "ext": "ts,js,json"
"NODE_ENV": "development"
},
"ext": "ts,js,json"
} }

796
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@
"prepare-dist": "rimraf ./dist && tsc && tsx ./bin/copy-dist.ts", "prepare-dist": "rimraf ./dist && tsc && tsx ./bin/copy-dist.ts",
"watch-dist": "tsx ./bin/watch-dist.ts", "watch-dist": "tsx ./bin/watch-dist.ts",
"update-build-info": "tsx bin/update-build-info.ts", "update-build-info": "tsx bin/update-build-info.ts",
"integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts", "integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts", "integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts", "integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"generate-document": "cross-env nodemon src/tools/generate_document.ts 1000", "generate-document": "cross-env nodemon src/tools/generate_document.ts 1000",
@@ -78,13 +78,13 @@
"dayjs": "1.11.13", "dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2", "dayjs-plugin-utc": "0.1.2",
"debounce": "2.2.0", "debounce": "2.2.0",
"draggabilly": "3.0.0",
"ejs": "3.1.10", "ejs": "3.1.10",
"electron-debug": "4.1.0", "electron-debug": "4.1.0",
"electron-dl": "4.0.0", "electron-dl": "4.0.0",
"electron-squirrel-startup": "1.0.1", "electron-squirrel-startup": "1.0.1",
"electron-window-state": "5.0.3", "electron-window-state": "5.0.3",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"eslint": "9.17.0",
"express": "4.21.2", "express": "4.21.2",
"express-rate-limit": "7.5.0", "express-rate-limit": "7.5.0",
"express-session": "1.18.1", "express-session": "1.18.1",
@@ -95,7 +95,7 @@
"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.6", "https-proxy-agent": "7.0.6",
"i18next": "24.2.0", "i18next": "24.2.1",
"i18next-fs-backend": "2.6.0", "i18next-fs-backend": "2.6.0",
"i18next-http-backend": "3.0.1", "i18next-http-backend": "3.0.1",
"image-type": "5.2.0", "image-type": "5.2.0",
@@ -107,12 +107,12 @@
"jquery": "3.7.1", "jquery": "3.7.1",
"jquery-hotkeys": "0.2.2", "jquery-hotkeys": "0.2.2",
"jquery.fancytree": "2.38.4", "jquery.fancytree": "2.38.4",
"jsdom": "25.0.1", "jsdom": "26.0.0",
"jsplumb": "2.15.6", "jsplumb": "2.15.6",
"katex": "0.16.19", "katex": "0.16.19",
"knockout": "3.5.1", "knockout": "3.5.1",
"mark.js": "8.11.1", "mark.js": "8.11.1",
"marked": "15.0.5", "marked": "15.0.6",
"mermaid": "11.4.1", "mermaid": "11.4.1",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"mind-elixir": "4.3.5", "mind-elixir": "4.3.5",
@@ -189,7 +189,7 @@
"@types/xml2js": "0.4.14", "@types/xml2js": "0.4.14",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "33.2.1", "electron": "33.3.1",
"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",
@@ -205,7 +205,7 @@
"tslib": "2.8.1", "tslib": "2.8.1",
"tsx": "4.19.2", "tsx": "4.19.2",
"typedoc": "0.27.6", "typedoc": "0.27.6",
"typescript": "5.7.2", "typescript": "5.7.3",
"webpack": "5.97.1", "webpack": "5.97.1",
"webpack-cli": "6.0.1", "webpack-cli": "6.0.1",
"webpack-dev-middleware": "7.4.2" "webpack-dev-middleware": "7.4.2"

View File

@@ -1,17 +1,20 @@
import { defineConfig, devices } from '@playwright/test'; import { defineConfig, devices } from '@playwright/test';
const SERVER_URL = 'http://127.0.0.1:8082';
/** /**
* Read environment variables from file. * Read environment variables from file.
* https://github.com/motdotla/dotenv * https://github.com/motdotla/dotenv
*/ */
// import dotenv from 'dotenv'; // import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') }); // dotenv.config({ path: path.resolve(__dirname, '.env') });
/** /**
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
export default defineConfig({ export default defineConfig({
testDir: './integration-tests', testDir: './e2e',
/* Run tests in files in parallel */ /* Run tests in files in parallel */
fullyParallel: true, fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -25,36 +28,29 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000', baseURL: SERVER_URL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
}, },
webServer: {
command: "npm run integration-mem-db",
url: "http://127.0.0.1:8082",
reuseExistingServer: true,
stdout: "ignore",
stderr: "pipe"
},
/* Configure projects for major browsers */ /* Configure projects for major browsers */
projects: [ projects: [
{ {
name: "setup", name: 'chromium',
testMatch: /.*\.setup\.ts/ use: { ...devices['Desktop Chrome'] },
}, },
{ {
name: "firefox", name: 'firefox',
use: { use: { ...devices['Desktop Firefox'] },
...devices[ "Desktop Firefox" ],
storageState: "playwright/.auth/user.json"
},
dependencies: [ "setup" ]
}, },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */ /* Test against mobile viewports. */
// { // {
// name: 'Mobile Chrome', // name: 'Mobile Chrome',
@@ -77,9 +73,9 @@ export default defineConfig({
], ],
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
// webServer: { webServer: {
// command: 'npm run start', command: 'npm run integration-mem-db-dev',
// url: 'http://127.0.0.1:3000', url: SERVER_URL,
// reuseExistingServer: !process.env.CI, // reuseExistingServer: !process.env.CI,
// }, },
}); });

View File

@@ -1,29 +1,24 @@
import * as attributeParser from '../src/public/app/services/attribute_parser.js'; import * as attributeParser from "../src/public/app/services/attribute_parser.js";
import {describe, it, expect, execute} from './mini_test.js'; import { describe, it, expect, execute } from "./mini_test.js";
describe("Lexing", () => { describe("Lexing", () => {
it("simple label", () => { it("simple label", () => {
expect(attributeParser.lex("#label").map((t: any) => t.text)) expect(attributeParser.lex("#label").map((t: any) => t.text)).toEqual(["#label"]);
.toEqual(["#label"]);
}); });
it("simple label with trailing spaces", () => { it("simple label with trailing spaces", () => {
expect(attributeParser.lex(" #label ").map((t: any) => t.text)) expect(attributeParser.lex(" #label ").map((t: any) => t.text)).toEqual(["#label"]);
.toEqual(["#label"]);
}); });
it("inherited label", () => { it("inherited label", () => {
expect(attributeParser.lex("#label(inheritable)").map((t: any) => t.text)) expect(attributeParser.lex("#label(inheritable)").map((t: any) => t.text)).toEqual(["#label", "(", "inheritable", ")"]);
.toEqual(["#label", "(", "inheritable", ")"]);
expect(attributeParser.lex("#label ( inheritable ) ").map((t: any) => t.text)) expect(attributeParser.lex("#label ( inheritable ) ").map((t: any) => t.text)).toEqual(["#label", "(", "inheritable", ")"]);
.toEqual(["#label", "(", "inheritable", ")"]);
}); });
it("label with value", () => { it("label with value", () => {
expect(attributeParser.lex("#label=Hallo").map((t: any) => t.text)) expect(attributeParser.lex("#label=Hallo").map((t: any) => t.text)).toEqual(["#label", "=", "Hallo"]);
.toEqual(["#label", "=", "Hallo"]);
}); });
it("label with value", () => { it("label with value", () => {
@@ -33,79 +28,72 @@ describe("Lexing", () => {
}); });
it("relation with value", () => { it("relation with value", () => {
expect(attributeParser.lex('~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM').map((t: any) => t.text)) expect(attributeParser.lex("~relation=#root/RclIpMauTOKS/NFi2gL4xtPxM").map((t: any) => t.text)).toEqual(["~relation", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"]);
.toEqual(["~relation", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"]);
}); });
it("use quotes to define value", () => { it("use quotes to define value", () => {
expect(attributeParser.lex("#'label a'='hello\"` world'").map((t: any) => t.text)) expect(attributeParser.lex("#'label a'='hello\"` world'").map((t: any) => t.text)).toEqual(["#label a", "=", 'hello"` world']);
.toEqual(["#label a", "=", 'hello"` world']);
expect(attributeParser.lex('#"label a" = "hello\'` world"').map((t: any) => t.text)) expect(attributeParser.lex('#"label a" = "hello\'` world"').map((t: any) => t.text)).toEqual(["#label a", "=", "hello'` world"]);
.toEqual(["#label a", "=", "hello'` world"]);
expect(attributeParser.lex('#`label a` = `hello\'" world`').map((t: any) => t.text)) expect(attributeParser.lex("#`label a` = `hello'\" world`").map((t: any) => t.text)).toEqual(["#label a", "=", "hello'\" world"]);
.toEqual(["#label a", "=", "hello'\" world"]);
}); });
}); });
describe("Parser", () => { describe("Parser", () => {
it("simple label", () => { it("simple label", () => {
const attrs = attributeParser.parse(["#token"].map((t: any) => ({text: t}))); const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t })));
expect(attrs.length).toEqual(1); expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('label'); expect(attrs[0].type).toEqual("label");
expect(attrs[0].name).toEqual('token'); expect(attrs[0].name).toEqual("token");
expect(attrs[0].isInheritable).toBeFalsy(); expect(attrs[0].isInheritable).toBeFalsy();
expect(attrs[0].value).toBeFalsy(); expect(attrs[0].value).toBeFalsy();
}); });
it("inherited label", () => { it("inherited label", () => {
const attrs = attributeParser.parse(["#token", "(", "inheritable", ")"].map((t: any) => ({text: t}))); const attrs = attributeParser.parse(["#token", "(", "inheritable", ")"].map((t: any) => ({ text: t })));
expect(attrs.length).toEqual(1); expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('label'); expect(attrs[0].type).toEqual("label");
expect(attrs[0].name).toEqual('token'); expect(attrs[0].name).toEqual("token");
expect(attrs[0].isInheritable).toBeTruthy(); expect(attrs[0].isInheritable).toBeTruthy();
expect(attrs[0].value).toBeFalsy(); expect(attrs[0].value).toBeFalsy();
}); });
it("label with value", () => { it("label with value", () => {
const attrs = attributeParser.parse(["#token", "=", "val"].map((t: any) => ({text: t}))); const attrs = attributeParser.parse(["#token", "=", "val"].map((t: any) => ({ text: t })));
expect(attrs.length).toEqual(1); expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('label'); expect(attrs[0].type).toEqual("label");
expect(attrs[0].name).toEqual('token'); expect(attrs[0].name).toEqual("token");
expect(attrs[0].value).toEqual("val"); expect(attrs[0].value).toEqual("val");
}); });
it("relation", () => { it("relation", () => {
let attrs = attributeParser.parse(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map((t: any) => ({text: t}))); let attrs = attributeParser.parse(["~token", "=", "#root/RclIpMauTOKS/NFi2gL4xtPxM"].map((t: any) => ({ text: t })));
expect(attrs.length).toEqual(1); expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('relation'); expect(attrs[0].type).toEqual("relation");
expect(attrs[0].name).toEqual("token"); expect(attrs[0].name).toEqual("token");
expect(attrs[0].value).toEqual('NFi2gL4xtPxM'); expect(attrs[0].value).toEqual("NFi2gL4xtPxM");
attrs = attributeParser.parse(["~token", "=", "#NFi2gL4xtPxM"].map((t: any) => ({text: t}))); attrs = attributeParser.parse(["~token", "=", "#NFi2gL4xtPxM"].map((t: any) => ({ text: t })));
expect(attrs.length).toEqual(1); expect(attrs.length).toEqual(1);
expect(attrs[0].type).toEqual('relation'); expect(attrs[0].type).toEqual("relation");
expect(attrs[0].name).toEqual("token"); expect(attrs[0].name).toEqual("token");
expect(attrs[0].value).toEqual('NFi2gL4xtPxM'); expect(attrs[0].value).toEqual("NFi2gL4xtPxM");
}); });
}); });
describe("error cases", () => { describe("error cases", () => {
it("error cases", () => { it("error cases", () => {
expect(() => attributeParser.lexAndParse('~token')) expect(() => attributeParser.lexAndParse("~token")).toThrow('Relation "~token" in "~token" should point to a note.');
.toThrow('Relation "~token" in "~token" should point to a note.');
expect(() => attributeParser.lexAndParse("#a&b/s")) expect(() => attributeParser.lexAndParse("#a&b/s")).toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`);
.toThrow(`Attribute name "a&b/s" contains disallowed characters, only alphanumeric characters, colon and underscore are allowed.`);
expect(() => attributeParser.lexAndParse("#")) expect(() => attributeParser.lexAndParse("#")).toThrow(`Attribute name is empty, please fill the name.`);
.toThrow(`Attribute name is empty, please fill the name.`);
}); });
}); });

View File

@@ -47,8 +47,7 @@ export function expect(val: any) {
toThrow: (errorMessage: any) => { toThrow: (errorMessage: any) => {
try { try {
val(); val();
} } catch (e: any) {
catch (e: any) {
if (e.message !== errorMessage) { if (e.message !== errorMessage) {
console.trace("toThrow caught exception, but messages differ"); console.trace("toThrow caught exception, but messages differ");
console.error(`expected: ${errorMessage}`); console.error(`expected: ${errorMessage}`);
@@ -66,7 +65,7 @@ export function expect(val: any) {
console.error(`got: [none]`); console.error(`got: [none]`);
errorCount++; errorCount++;
} }
} };
} }
export function execute() { export function execute() {
@@ -74,8 +73,7 @@ export function execute() {
if (errorCount) { if (errorCount) {
console.log(`!!!${errorCount} tests failed!!!`); console.log(`!!!${errorCount} tests failed!!!`);
} } else {
else {
console.log("All tests passed!"); console.log("All tests passed!");
} }
} }

View File

@@ -1,43 +1,39 @@
import sanitizeAttributeName from "../src/services/sanitize_attribute_name" import sanitizeAttributeName from "../src/services/sanitize_attribute_name";
import { describe, it, execute, expect } from "./mini_test"; import { describe, it, execute, expect } from "./mini_test";
// fn value, expected value // fn value, expected value
const testCases: [fnValue: string, expectedValue: string][] = [ const testCases: [fnValue: string, expectedValue: string][] = [
["testName", "testName"], ["testName", "testName"],
["test_name", "test_name"], ["test_name", "test_name"],
["test with space", "test_with_space"], ["test with space", "test_with_space"],
["test:with:colon", "test:with:colon"], ["test:with:colon", "test:with:colon"],
// numbers // numbers
["123456", "123456"], ["123456", "123456"],
["123:456", "123:456"], ["123:456", "123:456"],
["123456 abc", "123456_abc"], ["123456 abc", "123456_abc"],
// non-latin characters // non-latin characters
["ε", "ε"], ["ε", "ε"],
["attribute ε", "attribute_ε"], ["attribute ε", "attribute_ε"],
// special characters
["test/name", "test_name"],
["test%name", "test_name"],
["\/", "_"],
// empty string
["", "unnamed"],
]
// special characters
["test/name", "test_name"],
["test%name", "test_name"],
["\/", "_"],
// empty string
["", "unnamed"]
];
describe("sanitizeAttributeName unit tests", () => { describe("sanitizeAttributeName unit tests", () => {
testCases.forEach((testCase) => {
return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => {
const [value, expected] = testCase;
const actual = sanitizeAttributeName(value);
expect(actual).toEqual(expected);
});
});
});
testCases.forEach(testCase => { execute();
return it(`'${testCase[0]}' should return '${testCase[1]}'`, () => {
const [value, expected] = testCase;
const actual = sanitizeAttributeName(value);
expect(actual).toEqual(expected);
})
})
})
execute()

View File

@@ -2,128 +2,62 @@ import { formatDownloadTitle } from "../../src/services/utils.ts";
import { describe, it, execute, expect } from "../mini_test.ts"; import { describe, it, execute, expect } from "../mini_test.ts";
const testCases: [fnValue: Parameters<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [ const testCases: [fnValue: Parameters<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [
// empty fileName tests // empty fileName tests
[ [["", "text", ""], "untitled.html"],
["", "text", ""],
"untitled.html"
],
[ [["", "canvas", ""], "untitled.json"],
["", "canvas", ""],
"untitled.json"
],
[ [["", null, ""], "untitled"],
["", null, ""],
"untitled"
],
// json extension from type tests // json extension from type tests
[ [["test_file", "canvas", ""], "test_file.json"],
["test_file", "canvas", ""],
"test_file.json"
],
[ [["test_file", "relationMap", ""], "test_file.json"],
["test_file", "relationMap", ""],
"test_file.json"
],
[ [["test_file", "search", ""], "test_file.json"],
["test_file", "search", ""],
"test_file.json"
],
// extension based on mime type // extension based on mime type
[ [["test_file", null, "text/csv"], "test_file.csv"],
["test_file", null, "text/csv"],
"test_file.csv"
],
[ [["test_file_wo_ext", "image", "image/svg+xml"], "test_file_wo_ext.svg"],
["test_file_wo_ext", "image", "image/svg+xml"],
"test_file_wo_ext.svg"
],
[ [["test_file_wo_ext", "file", "application/json"], "test_file_wo_ext.json"],
["test_file_wo_ext", "file", "application/json"],
"test_file_wo_ext.json"
],
[ [["test_file_w_fake_ext.ext", "image", "image/svg+xml"], "test_file_w_fake_ext.ext.svg"],
["test_file_w_fake_ext.ext", "image", "image/svg+xml"],
"test_file_w_fake_ext.ext.svg"
],
[ [["test_file_w_correct_ext.svg", "image", "image/svg+xml"], "test_file_w_correct_ext.svg"],
["test_file_w_correct_ext.svg", "image", "image/svg+xml"],
"test_file_w_correct_ext.svg"
],
[ [["test_file_w_correct_ext.svgz", "image", "image/svg+xml"], "test_file_w_correct_ext.svgz"],
["test_file_w_correct_ext.svgz", "image", "image/svg+xml"],
"test_file_w_correct_ext.svgz"
],
[ [["test_file.zip", "file", "application/zip"], "test_file.zip"],
["test_file.zip", "file", "application/zip"],
"test_file.zip"
],
[ [["test_file", "file", "application/zip"], "test_file.zip"],
["test_file", "file", "application/zip"],
"test_file.zip"
],
// application/octet-stream tests // application/octet-stream tests
[ [["test_file", "file", "application/octet-stream"], "test_file"],
["test_file", "file", "application/octet-stream"],
"test_file"
],
[ [["test_file.zip", "file", "application/octet-stream"], "test_file.zip"],
["test_file.zip", "file", "application/octet-stream"],
"test_file.zip"
],
[ [["test_file.unknown", null, "application/octet-stream"], "test_file.unknown"],
["test_file.unknown", null, "application/octet-stream"],
"test_file.unknown"
],
// sanitized filename tests // sanitized filename tests
[ [["test/file", null, "application/octet-stream"], "testfile"],
["test/file", null, "application/octet-stream"],
"testfile"
],
[ [["test:file.zip", "file", "application/zip"], "testfile.zip"],
["test:file.zip", "file", "application/zip"],
"testfile.zip"
],
[ [[":::", "file", "application/zip"], ".zip"],
[":::", "file", "application/zip"],
".zip"
],
[
[":::a", "file", "application/zip"],
"a.zip"
],
]
[[":::a", "file", "application/zip"], "a.zip"]
];
describe("utils/formatDownloadTitle unit tests", () => { describe("utils/formatDownloadTitle unit tests", () => {
testCases.forEach((testCase) => {
return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => {
const [value, expected] = testCase;
const actual = formatDownloadTitle(...value);
expect(actual).toEqual(expected);
});
});
});
testCases.forEach(testCase => { execute();
return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => {
const [value, expected] = testCase;
const actual = formatDownloadTitle(...value);
expect(actual).toEqual(expected);
})
})
})
execute()

View File

@@ -1,8 +1,8 @@
import etapi from "../support/etapi.js"; import etapi from "../support/etapi.js";
etapi.describeEtapi("app_info", () => { etapi.describeEtapi("app_info", () => {
it("get", async () => { it("get", async () => {
const appInfo = await etapi.getEtapi("app-info"); const appInfo = await etapi.getEtapi("app-info");
expect(appInfo.clipperProtocolVersion).toEqual("1.0"); expect(appInfo.clipperProtocolVersion).toEqual("1.0");
}); });
}); });

View File

@@ -1,8 +1,8 @@
import etapi from "../support/etapi.js"; import etapi from "../support/etapi.js";
etapi.describeEtapi("backup", () => { etapi.describeEtapi("backup", () => {
it("create", async () => { it("create", async () => {
const response = await etapi.putEtapiContent("backup/etapi_test"); const response = await etapi.putEtapiContent("backup/etapi_test");
expect(response.status).toEqual(204); expect(response.status).toEqual(204);
}); });
}); });

View File

@@ -4,28 +4,21 @@ import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
etapi.describeEtapi("import", () => { etapi.describeEtapi("import", () => {
// temporarily skip this test since test-export.zip is missing // temporarily skip this test since test-export.zip is missing
xit("import", async () => { xit("import", async () => {
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const zipFileBuffer = fs.readFileSync( const zipFileBuffer = fs.readFileSync(path.resolve(scriptDir, "test-export.zip"));
path.resolve(scriptDir, "test-export.zip")
);
const response = await etapi.postEtapiContent( const response = await etapi.postEtapiContent("notes/root/import", zipFileBuffer);
"notes/root/import", expect(response.status).toEqual(201);
zipFileBuffer
);
expect(response.status).toEqual(201);
const { note, branch } = await response.json(); const { note, branch } = await response.json();
expect(note.title).toEqual("test-export"); expect(note.title).toEqual("test-export");
expect(branch.parentNoteId).toEqual("root"); expect(branch.parentNoteId).toEqual("root");
const content = await ( const content = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
await etapi.getEtapiContent(`notes/${note.noteId}/content`) expect(content).toContain("test export content");
).text(); });
expect(content).toContain("test export content");
});
}); });

View File

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

View File

@@ -2,106 +2,100 @@ import crypto from "crypto";
import etapi from "../support/etapi.js"; import etapi from "../support/etapi.js";
etapi.describeEtapi("notes", () => { etapi.describeEtapi("notes", () => {
it("create", async () => { it("create", async () => {
const { note, branch } = await etapi.postEtapi("create-note", { const { note, branch } = await etapi.postEtapi("create-note", {
parentNoteId: "root", parentNoteId: "root",
type: "text", type: "text",
title: "Hello World!", title: "Hello World!",
content: "Content", content: "Content",
prefix: "Custom prefix", prefix: "Custom prefix"
});
expect(note.title).toEqual("Hello World!");
expect(branch.parentNoteId).toEqual("root");
expect(branch.prefix).toEqual("Custom prefix");
const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
expect(rNote.title).toEqual("Hello World!");
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
expect(rContent).toEqual("Content");
const rBranch = await etapi.getEtapi(`branches/${branch.branchId}`);
expect(rBranch.parentNoteId).toEqual("root");
expect(rBranch.prefix).toEqual("Custom prefix");
}); });
expect(note.title).toEqual("Hello World!"); it("patch", async () => {
expect(branch.parentNoteId).toEqual("root"); const { note } = await etapi.postEtapi("create-note", {
expect(branch.prefix).toEqual("Custom prefix"); parentNoteId: "root",
type: "text",
title: "Hello World!",
content: "Content"
});
const rNote = await etapi.getEtapi(`notes/${note.noteId}`); await etapi.patchEtapi(`notes/${note.noteId}`, {
expect(rNote.title).toEqual("Hello World!"); title: "new title",
type: "code",
mime: "text/apl",
dateCreated: "2000-01-01 12:34:56.999+0200",
utcDateCreated: "2000-01-01 10:34:56.999Z"
});
const rContent = await ( const rNote = await etapi.getEtapi(`notes/${note.noteId}`);
await etapi.getEtapiContent(`notes/${note.noteId}/content`) expect(rNote.title).toEqual("new title");
).text(); expect(rNote.type).toEqual("code");
expect(rContent).toEqual("Content"); expect(rNote.mime).toEqual("text/apl");
expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200");
const rBranch = await etapi.getEtapi(`branches/${branch.branchId}`); expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z");
expect(rBranch.parentNoteId).toEqual("root");
expect(rBranch.prefix).toEqual("Custom prefix");
});
it("patch", async () => {
const { note } = await etapi.postEtapi("create-note", {
parentNoteId: "root",
type: "text",
title: "Hello World!",
content: "Content",
}); });
await etapi.patchEtapi(`notes/${note.noteId}`, { it("update content", async () => {
title: "new title", const { note } = await etapi.postEtapi("create-note", {
type: "code", parentNoteId: "root",
mime: "text/apl", type: "text",
dateCreated: "2000-01-01 12:34:56.999+0200", title: "Hello World!",
utcDateCreated: "2000-01-01 10:34:56.999Z", content: "Content"
});
await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content");
const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).text();
expect(rContent).toEqual("new content");
}); });
const rNote = await etapi.getEtapi(`notes/${note.noteId}`); it("create / update binary content", async () => {
expect(rNote.title).toEqual("new title"); const { note } = await etapi.postEtapi("create-note", {
expect(rNote.type).toEqual("code"); parentNoteId: "root",
expect(rNote.mime).toEqual("text/apl"); type: "file",
expect(rNote.dateCreated).toEqual("2000-01-01 12:34:56.999+0200"); title: "Hello World!",
expect(rNote.utcDateCreated).toEqual("2000-01-01 10:34:56.999Z"); content: "ZZZ"
}); });
it("update content", async () => { const updatedContent = crypto.randomBytes(16);
const { note } = await etapi.postEtapi("create-note", {
parentNoteId: "root", await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent);
type: "text",
title: "Hello World!", const rContent = await (await etapi.getEtapiContent(`notes/${note.noteId}/content`)).arrayBuffer();
content: "Content", expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent);
}); });
await etapi.putEtapiContent(`notes/${note.noteId}/content`, "new content"); it("delete note", async () => {
const { note } = await etapi.postEtapi("create-note", {
parentNoteId: "root",
type: "text",
title: "Hello World!",
content: "Content"
});
const rContent = await ( await etapi.deleteEtapi(`notes/${note.noteId}`);
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
).text();
expect(rContent).toEqual("new content");
});
it("create / update binary content", async () => { const resp = await etapi.getEtapiResponse(`notes/${note.noteId}`);
const { note } = await etapi.postEtapi("create-note", { expect(resp.status).toEqual(404);
parentNoteId: "root",
type: "file", const error = await resp.json();
title: "Hello World!", expect(error.status).toEqual(404);
content: "ZZZ", expect(error.code).toEqual("NOTE_NOT_FOUND");
expect(error.message).toEqual(`Note '${note.noteId}' not found.`);
}); });
const updatedContent = crypto.randomBytes(16);
await etapi.putEtapiContent(`notes/${note.noteId}/content`, updatedContent);
const rContent = await (
await etapi.getEtapiContent(`notes/${note.noteId}/content`)
).arrayBuffer();
expect(Buffer.from(new Uint8Array(rContent))).toEqual(updatedContent);
});
it("delete note", async () => {
const { note } = await etapi.postEtapi("create-note", {
parentNoteId: "root",
type: "text",
title: "Hello World!",
content: "Content",
});
await etapi.deleteEtapi(`notes/${note.noteId}`);
const resp = await etapi.getEtapiResponse(`notes/${note.noteId}`);
expect(resp.status).toEqual(404);
const error = await resp.json();
expect(error.status).toEqual(404);
expect(error.code).toEqual("NOTE_NOT_FOUND");
expect(error.message).toEqual(`Note '${note.noteId}' not found.`);
});
}); });

View File

@@ -4,84 +4,79 @@ import BAttribute from "../../src/becca/entities/battribute.js";
import becca from "../../src/becca/becca.js"; import becca from "../../src/becca/becca.js";
import randtoken from "rand-token"; import randtoken from "rand-token";
import SearchResult from "../../src/services/search/search_result.js"; import SearchResult from "../../src/services/search/search_result.js";
import { NoteType } from "../../src/becca/entities/rows.js"; import type { NoteType } from "../../src/becca/entities/rows.js";
randtoken.generator({ source: "crypto" }); randtoken.generator({ source: "crypto" });
function findNoteByTitle( function findNoteByTitle(searchResults: Array<SearchResult>, title: string): BNote | undefined {
searchResults: Array<SearchResult>, return searchResults.map((sr) => becca.notes[sr.noteId]).find((note) => note.title === title);
title: string
): BNote | undefined {
return searchResults
.map((sr) => becca.notes[sr.noteId])
.find((note) => note.title === title);
} }
class NoteBuilder { class NoteBuilder {
note: BNote; note: BNote;
constructor(note: BNote) { constructor(note: BNote) {
this.note = note; this.note = note;
} }
label(name: string, value = "", isInheritable = false) { label(name: string, value = "", isInheritable = false) {
new BAttribute({ new BAttribute({
attributeId: id(), attributeId: id(),
noteId: this.note.noteId, noteId: this.note.noteId,
type: "label", type: "label",
isInheritable, isInheritable,
name, name,
value, value
}); });
return this; return this;
} }
relation(name: string, targetNote: BNote) { relation(name: string, targetNote: BNote) {
new BAttribute({ new BAttribute({
attributeId: id(), attributeId: id(),
noteId: this.note.noteId, noteId: this.note.noteId,
type: "relation", type: "relation",
name, name,
value: targetNote.noteId, value: targetNote.noteId
}); });
return this; return this;
} }
child(childNoteBuilder: NoteBuilder, prefix = "") { child(childNoteBuilder: NoteBuilder, prefix = "") {
new BBranch({ new BBranch({
branchId: id(), branchId: id(),
noteId: childNoteBuilder.note.noteId, noteId: childNoteBuilder.note.noteId,
parentNoteId: this.note.noteId, parentNoteId: this.note.noteId,
prefix, prefix,
notePosition: 10, notePosition: 10
}); });
return this; return this;
} }
} }
function id() { function id() {
return randtoken.generate(10); return randtoken.generate(10);
} }
function note(title: string, extraParams = {}) { function note(title: string, extraParams = {}) {
const row = Object.assign( const row = Object.assign(
{ {
noteId: id(), noteId: id(),
title: title, title: title,
type: "text" as NoteType, type: "text" as NoteType,
mime: "text/html", mime: "text/html"
}, },
extraParams extraParams
); );
const note = new BNote(row); const note = new BNote(row);
return new NoteBuilder(note); return new NoteBuilder(note);
} }
export default { export default {
NoteBuilder, NoteBuilder,
findNoteByTitle, findNoteByTitle,
note, note
}; };

View File

@@ -1,256 +1,162 @@
import lex from "../../src/services/search/services/lex.js"; import lex from "../../src/services/search/services/lex.js";
describe("Lexer fulltext", () => { describe("Lexer fulltext", () => {
it("simple lexing", () => { it("simple lexing", () => {
expect(lex("hello world").fulltextTokens.map((t) => t.token)).toEqual([ expect(lex("hello world").fulltextTokens.map((t) => t.token)).toEqual(["hello", "world"]);
"hello",
"world",
]);
expect(lex("hello, world").fulltextTokens.map((t) => t.token)).toEqual([ expect(lex("hello, world").fulltextTokens.map((t) => t.token)).toEqual(["hello", "world"]);
"hello", });
"world",
]);
});
it("use quotes to keep words together", () => { it("use quotes to keep words together", () => {
expect( expect(lex("'hello world' my friend").fulltextTokens.map((t) => t.token)).toEqual(["hello world", "my", "friend"]);
lex("'hello world' my friend").fulltextTokens.map((t) => t.token)
).toEqual(["hello world", "my", "friend"]);
expect( expect(lex('"hello world" my friend').fulltextTokens.map((t) => t.token)).toEqual(["hello world", "my", "friend"]);
lex('"hello world" my friend').fulltextTokens.map((t) => t.token)
).toEqual(["hello world", "my", "friend"]);
expect( expect(lex("`hello world` my friend").fulltextTokens.map((t) => t.token)).toEqual(["hello world", "my", "friend"]);
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", () => { it("you can use different quotes and other special characters inside quotes", () => {
expect( expect(lex("'i can use \" or ` or #~=*' without problem").fulltextTokens.map((t) => t.token)).toEqual(['i can use " or ` or #~=*', "without", "problem"]);
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", () => { it("I can use backslash to escape quotes", () => {
expect(lex('hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual( expect(lex('hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual(["hello", '"world"']);
["hello", '"world"']
);
expect(lex("hello \\'world\\'").fulltextTokens.map((t) => t.token)).toEqual( expect(lex("hello \\'world\\'").fulltextTokens.map((t) => t.token)).toEqual(["hello", "'world'"]);
["hello", "'world'"]
);
expect(lex("hello \\`world\\`").fulltextTokens.map((t) => t.token)).toEqual( expect(lex("hello \\`world\\`").fulltextTokens.map((t) => t.token)).toEqual(["hello", "`world`"]);
["hello", "`world`"]
);
expect( expect(lex('"hello \\"world\\"').fulltextTokens.map((t) => t.token)).toEqual(['hello "world"']);
lex('"hello \\"world\\"').fulltextTokens.map((t) => t.token)
).toEqual(['hello "world"']);
expect( expect(lex("'hello \\'world\\''").fulltextTokens.map((t) => t.token)).toEqual(["hello 'world'"]);
lex("'hello \\'world\\''").fulltextTokens.map((t) => t.token)
).toEqual(["hello 'world'"]);
expect( expect(lex("`hello \\`world\\``").fulltextTokens.map((t) => t.token)).toEqual(["hello `world`"]);
lex("`hello \\`world\\``").fulltextTokens.map((t) => t.token)
).toEqual(["hello `world`"]);
expect(lex("\\#token").fulltextTokens.map((t) => t.token)).toEqual([ expect(lex("\\#token").fulltextTokens.map((t) => t.token)).toEqual(["#token"]);
"#token", });
]);
});
it("quote inside a word does not have a special meaning", () => { it("quote inside a word does not have a special meaning", () => {
const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan"); const lexResult = lex("d'Artagnan is dead #hero = d'Artagnan");
expect(lexResult.fulltextTokens.map((t) => t.token)).toEqual([ expect(lexResult.fulltextTokens.map((t) => t.token)).toEqual(["d'artagnan", "is", "dead"]);
"d'artagnan",
"is",
"dead",
]);
expect(lexResult.expressionTokens.map((t) => t.token)).toEqual([ expect(lexResult.expressionTokens.map((t) => t.token)).toEqual(["#hero", "=", "d'artagnan"]);
"#hero", });
"=",
"d'artagnan",
]);
});
it("if quote is not ended then it's just one long token", () => { it("if quote is not ended then it's just one long token", () => {
expect(lex("'unfinished quote").fulltextTokens.map((t) => t.token)).toEqual( expect(lex("'unfinished quote").fulltextTokens.map((t) => t.token)).toEqual(["unfinished quote"]);
["unfinished quote"] });
);
});
it("parenthesis and symbols in fulltext section are just normal characters", () => { it("parenthesis and symbols in fulltext section are just normal characters", () => {
expect( expect(lex("what's u=p <b(r*t)h>").fulltextTokens.map((t) => t.token)).toEqual(["what's", "u=p", "<b(r*t)h>"]);
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", () => { it("operator characters in expressions are separate tokens", () => {
expect( expect(lex("# abc+=-def**-+d").expressionTokens.map((t) => t.token)).toEqual(["#", "abc", "+=-", "def", "**-+", "d"]);
lex("# abc+=-def**-+d").expressionTokens.map((t) => t.token) });
).toEqual(["#", "abc", "+=-", "def", "**-+", "d"]);
});
it("escaping special characters", () => { it("escaping special characters", () => {
expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual([ expect(lex("hello \\#\\~\\'").fulltextTokens.map((t) => t.token)).toEqual(["hello", "#~'"]);
"hello", });
"#~'",
]);
});
}); });
describe("Lexer expression", () => { describe("Lexer expression", () => {
it("simple attribute existence", () => { it("simple attribute existence", () => {
expect( expect(lex("#label ~relation").expressionTokens.map((t) => t.token)).toEqual(["#label", "~relation"]);
lex("#label ~relation").expressionTokens.map((t) => t.token) });
).toEqual(["#label", "~relation"]);
});
it("simple label operators", () => { it("simple label operators", () => {
expect(lex("#label*=*text").expressionTokens.map((t) => t.token)).toEqual([ expect(lex("#label*=*text").expressionTokens.map((t) => t.token)).toEqual(["#label", "*=*", "text"]);
"#label", });
"*=*",
"text",
]);
});
it("simple label operator with in quotes", () => { it("simple label operator with in quotes", () => {
expect(lex("#label*=*'text'").expressionTokens).toEqual([ expect(lex("#label*=*'text'").expressionTokens).toEqual([
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 }, { token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
{ token: "text", inQuotes: true, startIndex: 10, endIndex: 13 }, { token: "text", inQuotes: true, startIndex: 10, endIndex: 13 }
]); ]);
}); });
it("simple label operator with param without quotes", () => { it("simple label operator with param without quotes", () => {
expect(lex("#label*=*text").expressionTokens).toEqual([ expect(lex("#label*=*text").expressionTokens).toEqual([
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
{ token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 }, { token: "*=*", inQuotes: false, startIndex: 6, endIndex: 8 },
{ token: "text", inQuotes: false, startIndex: 9, endIndex: 12 }, { token: "text", inQuotes: false, startIndex: 9, endIndex: 12 }
]); ]);
}); });
it("simple label operator with empty string param", () => { it("simple label operator with empty string param", () => {
expect(lex("#label = ''").expressionTokens).toEqual([ expect(lex("#label = ''").expressionTokens).toEqual([
{ token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 }, { token: "#label", inQuotes: false, startIndex: 0, endIndex: 5 },
{ token: "=", inQuotes: false, startIndex: 7, endIndex: 7 }, { token: "=", inQuotes: false, startIndex: 7, endIndex: 7 },
// weird case for empty strings which ends up with endIndex < startIndex :-( // weird case for empty strings which ends up with endIndex < startIndex :-(
{ token: "", inQuotes: true, startIndex: 10, endIndex: 9 }, { token: "", inQuotes: true, startIndex: 10, endIndex: 9 }
]); ]);
}); });
it("note. prefix also separates fulltext from expression", () => { it("note. prefix also separates fulltext from expression", () => {
expect( expect(lex(`hello fulltext note.labels.capital = Prague`).expressionTokens.map((t) => t.token)).toEqual(["note", ".", "labels", ".", "capital", "=", "prague"]);
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", () => { it("note. prefix in quotes will note start expression", () => {
expect( expect(lex(`hello fulltext "note.txt"`).expressionTokens.map((t) => t.token)).toEqual([]);
lex(`hello fulltext "note.txt"`).expressionTokens.map((t) => t.token)
).toEqual([]);
expect( expect(lex(`hello fulltext "note.txt"`).fulltextTokens.map((t) => t.token)).toEqual(["hello", "fulltext", "note.txt"]);
lex(`hello fulltext "note.txt"`).fulltextTokens.map((t) => t.token) });
).toEqual(["hello", "fulltext", "note.txt"]);
});
it("complex expressions with and, or and parenthesis", () => { it("complex expressions with and, or and parenthesis", () => {
expect( expect(lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map((t) => t.token)).toEqual([
lex(`# (#label=text OR #second=text) AND ~relation`).expressionTokens.map( "#",
(t) => t.token "(",
) "#label",
).toEqual([ "=",
"#", "text",
"(", "or",
"#label", "#second",
"=", "=",
"text", "text",
"or", ")",
"#second", "and",
"=", "~relation"
"text", ]);
")", });
"and",
"~relation",
]);
});
it("dot separated properties", () => { it("dot separated properties", () => {
expect( expect(lex(`# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'`).expressionTokens.map((t) => t.token)).toEqual([
lex( "#",
`# ~author.title = 'Hugh Howey' AND note.'book title' = 'Silo'` "~author",
).expressionTokens.map((t) => t.token) ".",
).toEqual([ "title",
"#", "=",
"~author", "hugh howey",
".", "and",
"title", "note",
"=", ".",
"hugh howey", "book title",
"and", "=",
"note", "silo"
".", ]);
"book title", });
"=",
"silo",
]);
});
it("negation of label and relation", () => { it("negation of label and relation", () => {
expect( expect(lex(`#!capital ~!neighbor`).expressionTokens.map((t) => t.token)).toEqual(["#!capital", "~!neighbor"]);
lex(`#!capital ~!neighbor`).expressionTokens.map((t) => t.token) });
).toEqual(["#!capital", "~!neighbor"]);
});
it("negation of sub-expression", () => { it("negation of sub-expression", () => {
expect( expect(lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map((t) => t.token)).toEqual(["#", "not", "(", "#capital", ")", "and", "note", ".", "noteid", "!=", "root"]);
lex(`# not(#capital) and note.noteId != "root"`).expressionTokens.map( });
(t) => t.token
)
).toEqual([
"#",
"not",
"(",
"#capital",
")",
"and",
"note",
".",
"noteid",
"!=",
"root",
]);
});
it("order by multiple labels", () => { it("order by multiple labels", () => {
expect(lex(`# orderby #a,#b`).expressionTokens.map((t) => t.token)).toEqual( expect(lex(`# orderby #a,#b`).expressionTokens.map((t) => t.token)).toEqual(["#", "orderby", "#a", ",", "#b"]);
["#", "orderby", "#a", ",", "#b"] });
);
});
}); });
describe("Lexer invalid queries and edge cases", () => { describe("Lexer invalid queries and edge cases", () => {
it("concatenated attributes", () => { it("concatenated attributes", () => {
expect(lex("#label~relation").expressionTokens.map((t) => t.token)).toEqual( expect(lex("#label~relation").expressionTokens.map((t) => t.token)).toEqual(["#label", "~relation"]);
["#label", "~relation"] });
);
});
it("trailing escape \\", () => { it("trailing escape \\", () => {
expect(lex("abc \\").fulltextTokens.map((t) => t.token)).toEqual([ expect(lex("abc \\").fulltextTokens.map((t) => t.token)).toEqual(["abc", "\\"]);
"abc", });
"\\",
]);
});
}); });

View File

@@ -1,25 +1,11 @@
import handleParens from "../../src/services/search/services/handle_parens.js"; import handleParens from "../../src/services/search/services/handle_parens.js";
import { TokenStructure } from "../../src/services/search/services/types.js"; import type { TokenStructure } from "../../src/services/search/services/types.js";
describe("Parens handler", () => { describe("Parens handler", () => {
it("handles parens", () => { it("handles parens", () => {
const input = ["(", "hello", ")", "and", "(", "(", "pick", "one", ")", "and", "another", ")"] const input = ["(", "hello", ")", "and", "(", "(", "pick", "one", ")", "and", "another", ")"].map((token) => ({ token }));
.map(token => ({token}));
const actual: TokenStructure = [ const actual: TokenStructure = [[{ token: "hello" }], { token: "and" }, [[{ token: "pick" }, { token: "one" }], { token: "and" }, { token: "another" }]];
[
{token: "hello"}
],
{token: "and"},
[
[
{token: "pick"},
{token: "one"}
],
{token: "and"},
{token: "another"}
]
];
expect(handleParens(input)).toEqual(actual); expect(handleParens(input)).toEqual(actual);
}); });

View File

@@ -1,12 +1,274 @@
// @ts-nocheck import AndExp from "../../src/services/search/expressions/and.js";
// There are many issues with the types of the parser e.g. "parse" function returns "Expression" import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js";
// but we access properties like "subExpressions" which is not defined in the "Expression" class.
import Expression from "../../src/services/search/expressions/expression.js"; import Expression from "../../src/services/search/expressions/expression.js";
import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js";
import NotExp from "../../src/services/search/expressions/not.js";
import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js";
import NoteFlatTextExp from "../../src/services/search/expressions/note_flat_text.js";
import OrExp from "../../src/services/search/expressions/or.js";
import OrderByAndLimitExp from "../../src/services/search/expressions/order_by_and_limit.js";
import PropertyComparisonExp from "../../src/services/search/expressions/property_comparison.js";
import SearchContext from "../../src/services/search/search_context.js"; import SearchContext from "../../src/services/search/search_context.js";
import parse from "../../src/services/search/services/parse.js"; import { default as parseInternal, type ParseOpts } from "../../src/services/search/services/parse.js";
function tokens(toks: Array<string>, cur = 0): Array<any> { describe("Parser", () => {
it("fulltext parser without content", () => {
const rootExp = parse({
fulltextTokens: tokens(["hello", "hi"]),
expressionTokens: [],
searchContext: new SearchContext()
}, AndExp);
expectExpression(rootExp.subExpressions[0], PropertyComparisonExp);
const orExp = expectExpression(rootExp.subExpressions[2], OrExp);
const flatTextExp = expectExpression(orExp.subExpressions[0], NoteFlatTextExp);
expect(flatTextExp.tokens).toEqual(["hello", "hi"]);
});
it("fulltext parser with content", () => {
const rootExp = parse({
fulltextTokens: tokens(["hello", "hi"]),
expressionTokens: [],
searchContext: new SearchContext()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
const orExp = expectExpression(rootExp.subExpressions[2], OrExp);
const firstSub = expectExpression(orExp.subExpressions[0], NoteFlatTextExp);
expect(firstSub.tokens).toEqual(["hello", "hi"]);
const secondSub = expectExpression(orExp.subExpressions[1], NoteContentFulltextExp);
expect(secondSub.tokens).toEqual(["hello", "hi"]);
});
it("simple label comparison", () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#mylabel", "=", "text"]),
searchContext: new SearchContext()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
const labelComparisonExp = expectExpression(rootExp.subExpressions[2], LabelComparisonExp);
expect(labelComparisonExp.attributeType).toEqual("label");
expect(labelComparisonExp.attributeName).toEqual("mylabel");
expect(labelComparisonExp.comparator).toBeTruthy();
});
it("simple attribute negation", () => {
let rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#!mylabel"]),
searchContext: new SearchContext()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
let notExp = expectExpression(rootExp.subExpressions[2], NotExp);
let attributeExistsExp = expectExpression(notExp.subExpression, AttributeExistsExp);
expect(attributeExistsExp.attributeType).toEqual("label");
expect(attributeExistsExp.attributeName).toEqual("mylabel");
rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["~!myrelation"]),
searchContext: new SearchContext()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
notExp = expectExpression(rootExp.subExpressions[2], NotExp);
attributeExistsExp = expectExpression(notExp.subExpression, AttributeExistsExp);
expect(attributeExistsExp.attributeType).toEqual("relation");
expect(attributeExistsExp.attributeName).toEqual("myrelation");
});
it("simple label AND", () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]),
searchContext: new SearchContext()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
const andExp = expectExpression(rootExp.subExpressions[2], AndExp);
const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp);
expect(firstSub.attributeName).toEqual("first");
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()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
const andExp = expectExpression(rootExp.subExpressions[2], AndExp);
const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp);
expect(firstSub.attributeName).toEqual("first");
expect(secondSub.attributeName).toEqual("second");
});
it("simple label OR", () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]),
searchContext: new SearchContext()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
const orExp = expectExpression(rootExp.subExpressions[2], OrExp);
const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, LabelComparisonExp);
expect(firstSub.attributeName).toEqual("first");
expect(secondSub.attributeName).toEqual("second");
});
it("fulltext and simple label", () => {
const rootExp = parse({
fulltextTokens: tokens(["hello"]),
expressionTokens: tokens(["#mylabel", "=", "text"]),
searchContext: new SearchContext()
}, AndExp);
const [firstSub, _, thirdSub, fourth] = expectSubexpressions(rootExp, PropertyComparisonExp, undefined, OrExp, LabelComparisonExp);
expect(firstSub.propertyName).toEqual("isArchived");
const noteFlatTextExp = expectExpression(thirdSub.subExpressions[0], NoteFlatTextExp);
expect(noteFlatTextExp.tokens).toEqual(["hello"]);
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()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
const orExp = expectExpression(rootExp.subExpressions[2], OrExp);
const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, AndExp);
expect(firstSub.attributeName).toEqual("first");
const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, LabelComparisonExp, LabelComparisonExp);
expect(firstSubSub.attributeName).toEqual("second");
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()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
const andExp = expectExpression(rootExp.subExpressions[2], AndExp);
const [firstSub, secondSub, thirdSub] = expectSubexpressions(andExp, AttributeExistsExp, OrExp, AttributeExistsExp);
expect(firstSub.attributeName).toEqual("first");
const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, AttributeExistsExp, AttributeExistsExp);
expect(firstSubSub.attributeName).toEqual("second");
expect(secondSubSub.attributeName).toEqual("third");
expect(thirdSub.attributeName).toEqual("fourth");
});
it("parses limit without order by", () => {
const rootExp = parse({
fulltextTokens: tokens(["hello", "hi"]),
expressionTokens: [],
searchContext: new SearchContext({ limit: 2 })
}, OrderByAndLimitExp);
expect(rootExp.limit).toBe(2);
expect(rootExp.subExpression).toBeInstanceOf(AndExp);
});
});
describe("Invalid expressions", () => {
it("incomplete comparison", () => {
const searchContext = new SearchContext();
parseInternal({
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";
parseInternal({
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";
parseInternal({
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()
}, AndExp);
assertIsArchived(rootExp.subExpressions[0]);
const labelComparisonExp = expectExpression(rootExp.subExpressions[2], LabelComparisonExp);
expect(labelComparisonExp.attributeType).toEqual("label");
expect(labelComparisonExp.attributeName).toEqual("first");
expect(labelComparisonExp.comparator).toBeTruthy();
});
it("searching by relation without note property", () => {
const searchContext = new SearchContext();
parseInternal({
fulltextTokens: [],
expressionTokens: tokens(["~first", "=", "text", "-", "abc"]),
searchContext
});
expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""');
});
});
type ClassType<T extends Expression> = new (...args: any[]) => T;
function tokens(toks: (string | string[])[], cur = 0): Array<any> {
return toks.map((arg) => { return toks.map((arg) => {
if (Array.isArray(arg)) { if (Array.isArray(arg)) {
return tokens(arg, cur); return tokens(arg, cur);
@@ -17,303 +279,77 @@ function tokens(toks: Array<string>, cur = 0): Array<any> {
token: arg, token: arg,
inQuotes: false, inQuotes: false,
startIndex: cur - arg.length, startIndex: cur - arg.length,
endIndex: cur - 1, endIndex: cur - 1
}; };
} }
}); });
} }
function assertIsArchived(exp: Expression) { function assertIsArchived(_exp: Expression) {
expect(exp.constructor.name).toEqual('PropertyComparisonExp'); const exp = expectExpression(_exp, PropertyComparisonExp);
expect(exp.propertyName).toEqual('isArchived'); expect(exp.propertyName).toEqual("isArchived");
expect(exp.operator).toEqual('='); expect(exp.operator).toEqual("=");
expect(exp.comparedValue).toEqual('false'); expect(exp.comparedValue).toEqual("false");
} }
describe('Parser', () => { /**
it('fulltext parser without content', () => { * Parses the corresponding {@link Expression} from plain text, while also expecting the resulting expression to be of the given type.
const rootExp = parse({ *
fulltextTokens: tokens(['hello', 'hi']), * @param opts the options for parsing.
expressionTokens: [], * @param type the expected type of the expression.
searchContext: new SearchContext({ excludeArchived: true }), * @returns the expression typecasted to the expected type.
}); */
function parse<T extends Expression>(opts: ParseOpts, type: ClassType<T>) {
return expectExpression(parseInternal(opts), type);
}
expect(rootExp.constructor.name).toEqual('AndExp'); /**
expect(rootExp.subExpressions[0].constructor.name).toEqual('PropertyComparisonExp'); * Expects the given {@link Expression} to be of the given type.
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); *
expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual('NoteFlatTextExp'); * @param exp an instance of an {@link Expression}.
expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(['hello', 'hi']); * @param type a type class such as {@link AndExp}, {@link OrExp}, etc.
}); * @returns the same expression typecasted to the expected type.
*/
function expectExpression<T extends Expression>(exp: Expression, type: ClassType<T>) {
expect(exp).toBeInstanceOf(type);
return exp as T;
}
it('fulltext parser with content', () => { /**
const rootExp = parse({ * For an {@link AndExp}, it goes through all its subexpressions (up to fourth) and checks their type and returns them as a typecasted array.
fulltextTokens: tokens(['hello', 'hi']), * Each subexpression can have their own type.
expressionTokens: [], *
searchContext: new SearchContext(), * @param exp the expression containing one or more subexpressions.
}); * @param firstType the type of the first subexpression.
* @param secondType the type of the second subexpression.
expect(rootExp.constructor.name).toEqual('AndExp'); * @param thirdType the type of the third subexpression.
assertIsArchived(rootExp.subExpressions[0]); * @param fourthType the type of the fourth subexpression.
* @returns an array of all the subexpressions (in order) typecasted to their expected type.
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp'); */
function expectSubexpressions<FirstT extends Expression,
const subs = rootExp.subExpressions[2].subExpressions; SecondT extends Expression,
ThirdT extends Expression,
expect(subs[0].constructor.name).toEqual('NoteFlatTextExp'); FourthT extends Expression>(
expect(subs[0].tokens).toEqual(['hello', 'hi']); exp: AndExp,
firstType: ClassType<FirstT>,
expect(subs[1].constructor.name).toEqual('NoteContentFulltextExp'); secondType?: ClassType<SecondT>,
expect(subs[1].tokens).toEqual(['hello', 'hi']); thirdType?: ClassType<ThirdT>,
}); fourthType?: ClassType<FourthT>): [ FirstT, SecondT, ThirdT, FourthT ]
{
it('simple label comparison', () => { expectExpression(exp.subExpressions[0], firstType);
const rootExp = parse({ if (secondType) {
fulltextTokens: [], expectExpression(exp.subExpressions[1], secondType);
expressionTokens: tokens(['#mylabel', '=', 'text']), }
searchContext: new SearchContext(), if (thirdType) {
}); expectExpression(exp.subExpressions[2], thirdType);
}
expect(rootExp.constructor.name).toEqual('AndExp'); if (fourthType) {
assertIsArchived(rootExp.subExpressions[0]); expectExpression(exp.subExpressions[3], fourthType);
expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp'); }
expect(rootExp.subExpressions[2].attributeType).toEqual('label'); return [
expect(rootExp.subExpressions[2].attributeName).toEqual('mylabel'); exp.subExpressions[0] as FirstT,
expect(rootExp.subExpressions[2].comparator).toBeTruthy(); exp.subExpressions[1] as SecondT,
}); exp.subExpressions[2] as ThirdT,
exp.subExpressions[3] as FourthT
it('simple attribute negation', () => { ]
let rootExp = parse({ }
fulltextTokens: [],
expressionTokens: tokens(['#!mylabel']),
searchContext: new SearchContext(),
});
expect(rootExp.constructor.name).toEqual('AndExp');
assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp');
expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp');
expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('label');
expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('mylabel');
rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(['~!myrelation']),
searchContext: new SearchContext(),
});
expect(rootExp.constructor.name).toEqual('AndExp');
assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp');
expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp');
expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('relation');
expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('myrelation');
});
it('simple label AND', () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(['#first', '=', 'text', 'and', '#second', '=', 'text']),
searchContext: new SearchContext(true),
});
expect(rootExp.constructor.name).toEqual('AndExp');
assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp');
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
expect(firstSub.attributeName).toEqual('first');
expect(secondSub.constructor.name).toEqual('LabelComparisonExp');
expect(secondSub.attributeName).toEqual('second');
});
it('simple label AND without explicit AND', () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(['#first', '=', 'text', '#second', '=', 'text']),
searchContext: new SearchContext(),
});
expect(rootExp.constructor.name).toEqual('AndExp');
assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp');
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
expect(firstSub.attributeName).toEqual('first');
expect(secondSub.constructor.name).toEqual('LabelComparisonExp');
expect(secondSub.attributeName).toEqual('second');
});
it('simple label OR', () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(['#first', '=', 'text', 'or', '#second', '=', 'text']),
searchContext: new SearchContext(),
});
expect(rootExp.constructor.name).toEqual('AndExp');
assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
expect(firstSub.attributeName).toEqual('first');
expect(secondSub.constructor.name).toEqual('LabelComparisonExp');
expect(secondSub.attributeName).toEqual('second');
});
it('fulltext and simple label', () => {
const rootExp = parse({
fulltextTokens: tokens(['hello']),
expressionTokens: tokens(['#mylabel', '=', 'text']),
searchContext: new SearchContext({ excludeArchived: true }),
});
expect(rootExp.constructor.name).toEqual('AndExp');
const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual('PropertyComparisonExp');
expect(firstSub.propertyName).toEqual('isArchived');
expect(thirdSub.constructor.name).toEqual('OrExp');
expect(thirdSub.subExpressions[0].constructor.name).toEqual('NoteFlatTextExp');
expect(thirdSub.subExpressions[0].tokens).toEqual(['hello']);
expect(fourth.constructor.name).toEqual('LabelComparisonExp');
expect(fourth.attributeName).toEqual('mylabel');
});
it('label sub-expression', () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(['#first', '=', 'text', 'or', ['#second', '=', 'text', 'and', '#third', '=', 'text']]),
searchContext: new SearchContext(),
});
expect(rootExp.constructor.name).toEqual('AndExp');
assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
expect(firstSub.attributeName).toEqual('first');
expect(secondSub.constructor.name).toEqual('AndExp');
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
expect(firstSubSub.constructor.name).toEqual('LabelComparisonExp');
expect(firstSubSub.attributeName).toEqual('second');
expect(secondSubSub.constructor.name).toEqual('LabelComparisonExp');
expect(secondSubSub.attributeName).toEqual('third');
});
it('label sub-expression without explicit operator', () => {
const rootExp = parse({
fulltextTokens: [],
expressionTokens: tokens(['#first', ['#second', 'or', '#third'], '#fourth']),
searchContext: new SearchContext(),
});
expect(rootExp.constructor.name).toEqual('AndExp');
assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp');
const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions;
expect(firstSub.constructor.name).toEqual('AttributeExistsExp');
expect(firstSub.attributeName).toEqual('first');
expect(secondSub.constructor.name).toEqual('OrExp');
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
expect(firstSubSub.constructor.name).toEqual('AttributeExistsExp');
expect(firstSubSub.attributeName).toEqual('second');
expect(secondSubSub.constructor.name).toEqual('AttributeExistsExp');
expect(secondSubSub.attributeName).toEqual('third');
expect(thirdSub.constructor.name).toEqual('AttributeExistsExp');
expect(thirdSub.attributeName).toEqual('fourth');
});
});
describe('Invalid expressions', () => {
it('incomplete comparison', () => {
const searchContext = new SearchContext();
parse({
fulltextTokens: [],
expressionTokens: tokens(['#first', '=']),
searchContext,
});
expect(searchContext.error).toEqual('Misplaced or incomplete expression "="');
});
it('comparison between labels is impossible', () => {
let searchContext = new SearchContext();
searchContext.originalQuery = '#first = #second';
parse({
fulltextTokens: [],
expressionTokens: tokens(['#first', '=', '#second']),
searchContext,
});
expect(searchContext.error).toEqual(
`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`
);
searchContext = new SearchContext();
searchContext.originalQuery = '#first = note.relations.second';
parse({
fulltextTokens: [],
expressionTokens: tokens(['#first', '=', 'note', '.', 'relations', 'second']),
searchContext,
});
expect(searchContext.error).toEqual(
`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`
);
const rootExp = parse({
fulltextTokens: [],
expressionTokens: [
{ token: '#first', inQuotes: false },
{ token: '=', inQuotes: false },
{ token: '#second', inQuotes: true },
],
searchContext: new SearchContext(),
});
expect(rootExp.constructor.name).toEqual('AndExp');
assertIsArchived(rootExp.subExpressions[0]);
expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp');
expect(rootExp.subExpressions[2].attributeType).toEqual('label');
expect(rootExp.subExpressions[2].attributeName).toEqual('first');
expect(rootExp.subExpressions[2].comparator).toBeTruthy();
});
it('searching by relation without note property', () => {
const searchContext = new SearchContext();
parse({
fulltextTokens: [],
expressionTokens: tokens(['~first', '=', 'text', '-', 'abc']),
searchContext,
});
expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""');
});
});

View File

@@ -6,195 +6,177 @@ import dateUtils from "../../src/services/date_utils.js";
import becca from "../../src/becca/becca.js"; import becca from "../../src/becca/becca.js";
import becca_mocking from "./becca_mocking.js"; import becca_mocking from "./becca_mocking.js";
describe('Search', () => { describe("Search", () => {
let rootNote: any; let rootNote: any;
beforeEach(() => { beforeEach(() => {
becca.reset(); becca.reset();
rootNote = new becca_mocking.NoteBuilder(new BNote({ noteId: 'root', title: 'root', type: 'text' })); rootNote = new becca_mocking.NoteBuilder(new BNote({ noteId: "root", title: "root", type: "text" }));
new BBranch({ new BBranch({
branchId: 'none_root', branchId: "none_root",
noteId: 'root', noteId: "root",
parentNoteId: 'none', parentNoteId: "none",
notePosition: 10, notePosition: 10
}); });
}); });
xit('simple path match', () => { xit("simple path match", () => {
rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria'))); rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
const searchResults = searchService.findResultsWithQuery('europe austria', searchContext); const searchResults = searchService.findResultsWithQuery("europe austria", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
}); });
xit('normal search looks also at attributes', () => { xit("normal search looks also at attributes", () => {
const austria = becca_mocking.note('Austria'); const austria = becca_mocking.note("Austria");
const vienna = becca_mocking.note('Vienna'); const vienna = becca_mocking.note("Vienna");
rootNote.child(austria.relation('capital', vienna.note)).child(vienna.label('inhabitants', '1888776')); rootNote.child(austria.relation("capital", vienna.note)).child(vienna.label("inhabitants", "1888776"));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('capital', searchContext); let searchResults = searchService.findResultsWithQuery("capital", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('inhabitants', searchContext); searchResults = searchService.findResultsWithQuery("inhabitants", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Vienna')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Vienna")).toBeTruthy();
}); });
xit('normal search looks also at type and mime', () => { xit("normal search looks also at type and mime", () => {
rootNote rootNote.child(becca_mocking.note("Effective Java", { type: "book", mime: "" })).child(becca_mocking.note("Hello World.java", { type: "code", mime: "text/x-java" }));
.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(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('book', searchContext); let searchResults = searchService.findResultsWithQuery("book", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Effective Java')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Effective Java")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('text', searchContext); // should match mime searchResults = searchService.findResultsWithQuery("text", searchContext); // should match mime
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Hello World.java')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Hello World.java")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('java', searchContext); searchResults = searchService.findResultsWithQuery("java", searchContext);
expect(searchResults.length).toEqual(2); expect(searchResults.length).toEqual(2);
}); });
xit('only end leafs are results', () => { xit("only end leafs are results", () => {
rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria'))); rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
const searchResults = searchService.findResultsWithQuery('europe', searchContext); const searchResults = searchService.findResultsWithQuery("europe", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy();
}); });
xit('only end leafs are results', () => { xit("only end leafs are results", () => {
rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria').label('capital', 'Vienna'))); rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna")));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
const searchResults = searchService.findResultsWithQuery('Vienna', searchContext); const searchResults = searchService.findResultsWithQuery("Vienna", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
}); });
it('label comparison with short syntax', () => { it("label comparison with short syntax", () => {
rootNote.child( rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria").label("capital", "Vienna")).child(becca_mocking.note("Czech Republic").label("capital", "Prague")));
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(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('#capital=Vienna', searchContext); let searchResults = searchService.findResultsWithQuery("#capital=Vienna", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
// case sensitivity: // case sensitivity:
searchResults = searchService.findResultsWithQuery('#CAPITAL=VIENNA', searchContext); searchResults = searchService.findResultsWithQuery("#CAPITAL=VIENNA", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('#caPItal=vienNa', searchContext); searchResults = searchService.findResultsWithQuery("#caPItal=vienNa", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
}); });
it('label comparison with full syntax', () => { 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( rootNote.child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.child(becca_mocking.note('Austria').label('capital', 'Vienna')) .label("country", "", true)
.child(becca_mocking.note('Czech Republic').label('capital', 'Prague')) .child(becca_mocking.note("Austria").label("population", "8859000"))
.child(becca_mocking.note("Czech Republic").label("population", "10650000"))
); );
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('# note.labels.capital=Prague', searchContext); const searchResults = searchService.findResultsWithQuery("#country #population >= 10000000", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
}); });
it('numeric label comparison', () => { xit("inherited label comparison", () => {
rootNote.child( rootNote.child(becca_mocking.note("Europe").label("country", "", true).child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic")));
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 searchContext = new SearchContext();
const searchResults = searchService.findResultsWithQuery('#country #population >= 10000000', searchContext); const searchResults = searchService.findResultsWithQuery("austria #country", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
}); });
xit('inherited label comparison', () => { it("numeric label comparison fallback to string 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 // dates should not be coerced into numbers which would then give wrong numbers
rootNote.child( rootNote.child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.label('country', '', true) .label("country", "", true)
.child(becca_mocking.note('Austria').label('established', '1955-07-27')) .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("Czech Republic").label("established", "1993-01-01"))
.child(becca_mocking.note('Hungary').label('established', '1920-06-04')) .child(becca_mocking.note("Hungary").label("established", "1920-06-04"))
); );
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('#established <= "1955-01-01"', searchContext); let searchResults = searchService.findResultsWithQuery('#established <= "1955-01-01"', searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Hungary')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Hungary")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('#established > "1955-01-01"', searchContext); searchResults = searchService.findResultsWithQuery('#established > "1955-01-01"', searchContext);
expect(searchResults.length).toEqual(2); expect(searchResults.length).toEqual(2);
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
}); });
it('smart date comparisons', () => { it("smart date comparisons", () => {
// dates should not be coerced into numbers which would then give wrong numbers // dates should not be coerced into numbers which would then give wrong numbers
rootNote.child( rootNote.child(
becca_mocking becca_mocking
.note('My note', { dateCreated: dateUtils.localNowDateTime() }) .note("My note", { dateCreated: dateUtils.localNowDateTime() })
.label('year', new Date().getFullYear().toString()) .label("year", new Date().getFullYear().toString())
.label('month', dateUtils.localNowDate().substr(0, 7)) .label("month", dateUtils.localNowDate().substr(0, 7))
.label('date', dateUtils.localNowDate()) .label("date", dateUtils.localNowDate())
.label('dateTime', dateUtils.localNowDateTime()) .label("dateTime", dateUtils.localNowDateTime())
); );
const searchContext = new SearchContext(); const searchContext = new SearchContext();
@@ -206,263 +188,258 @@ describe('Search', () => {
.toEqual(expectedResultCount); .toEqual(expectedResultCount);
if (expectedResultCount === 1) { if (expectedResultCount === 1) {
expect(becca_mocking.findNoteByTitle(searchResults, 'My note')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "My note")).toBeTruthy();
} }
} }
test('#year = YEAR', 1); test("#year = YEAR", 1);
test("#year = 'YEAR'", 0); test("#year = 'YEAR'", 0);
test('#year >= YEAR', 1); test("#year >= YEAR", 1);
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", 1);
test('#year < year + 1', 1); test("#year < year + 1", 1);
test('#year > YEAR+1', 0); test("#year > YEAR+1", 0);
test('#month = MONTH', 1); test("#month = MONTH", 1);
test('#month = month', 1); test("#month = month", 1);
test("#month = 'MONTH'", 0); test("#month = 'MONTH'", 0);
test('note.dateCreated =* month', 2); test("note.dateCreated =* month", 2);
test('#date = TODAY', 1); test("#date = TODAY", 1);
test('#date = today', 1); test("#date = today", 1);
test("#date = 'today'", 0); test("#date = 'today'", 0);
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("#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', 1); test("#dateTime <= NOW + 10", 1);
test('#dateTime < NOW-10', 0); test("#dateTime < NOW-10", 0);
test('#dateTime >= NOW-10', 1); test("#dateTime >= NOW-10", 1);
test('#dateTime < NOW-10', 0); test("#dateTime < NOW-10", 0);
}); });
it('logical or', () => { it("logical or", () => {
rootNote.child( rootNote.child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.label('country', '', true) .label("country", "", true)
.child(becca_mocking.note('Austria').label('languageFamily', 'germanic')) .child(becca_mocking.note("Austria").label("languageFamily", "germanic"))
.child(becca_mocking.note('Czech Republic').label('languageFamily', 'slavic')) .child(becca_mocking.note("Czech Republic").label("languageFamily", "slavic"))
.child(becca_mocking.note('Hungary').label('languageFamily', 'finnougric')) .child(becca_mocking.note("Hungary").label("languageFamily", "finnougric"))
); );
const searchContext = new SearchContext(); const searchContext = new SearchContext();
const searchResults = searchService.findResultsWithQuery('#languageFamily = slavic OR #languageFamily = germanic', searchContext); const searchResults = searchService.findResultsWithQuery("#languageFamily = slavic OR #languageFamily = germanic", searchContext);
expect(searchResults.length).toEqual(2); expect(searchResults.length).toEqual(2);
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
}); });
it('fuzzy attribute search', () => { it("fuzzy attribute search", () => {
rootNote.child( rootNote.child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.label('country', '', true) .label("country", "", true)
.child(becca_mocking.note('Austria').label('languageFamily', 'germanic')) .child(becca_mocking.note("Austria").label("languageFamily", "germanic"))
.child(becca_mocking.note('Czech Republic').label('languageFamily', 'slavic')) .child(becca_mocking.note("Czech Republic").label("languageFamily", "slavic"))
); );
let searchContext = new SearchContext({ fuzzyAttributeSearch: false }); let searchContext = new SearchContext({ fuzzyAttributeSearch: false });
let searchResults = searchService.findResultsWithQuery('#language', searchContext); let searchResults = searchService.findResultsWithQuery("#language", searchContext);
expect(searchResults.length).toEqual(0); expect(searchResults.length).toEqual(0);
searchResults = searchService.findResultsWithQuery('#languageFamily=ger', searchContext); searchResults = searchService.findResultsWithQuery("#languageFamily=ger", searchContext);
expect(searchResults.length).toEqual(0); expect(searchResults.length).toEqual(0);
searchContext = new SearchContext({ fuzzyAttributeSearch: true }); searchContext = new SearchContext({ fuzzyAttributeSearch: true });
searchResults = searchService.findResultsWithQuery('#language', searchContext); searchResults = searchService.findResultsWithQuery("#language", searchContext);
expect(searchResults.length).toEqual(2); expect(searchResults.length).toEqual(2);
searchResults = searchService.findResultsWithQuery('#languageFamily=ger', searchContext); searchResults = searchService.findResultsWithQuery("#languageFamily=ger", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
}); });
it('filter by note property', () => { it("filter by note property", () => {
rootNote.child(becca_mocking.note('Europe').child(becca_mocking.note('Austria')).child(becca_mocking.note('Czech Republic'))); rootNote.child(becca_mocking.note("Europe").child(becca_mocking.note("Austria")).child(becca_mocking.note("Czech Republic")));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
const searchResults = searchService.findResultsWithQuery('# note.title =* czech', searchContext); const searchResults = searchService.findResultsWithQuery("# note.title =* czech", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
}); });
it("filter by note's parent", () => { it("filter by note's parent", () => {
rootNote rootNote
.child( .child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.child(becca_mocking.note('Austria')) .child(becca_mocking.note("Austria"))
.child(becca_mocking.note('Czech Republic').child(becca_mocking.note('Prague'))) .child(becca_mocking.note("Czech Republic").child(becca_mocking.note("Prague")))
) )
.child(becca_mocking.note('Asia').child(becca_mocking.note('Taiwan'))); .child(becca_mocking.note("Asia").child(becca_mocking.note("Taiwan")));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe', searchContext); let searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe", searchContext);
expect(searchResults.length).toEqual(2); expect(searchResults.length).toEqual(2);
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('# note.parents.title = Asia', searchContext); searchResults = searchService.findResultsWithQuery("# note.parents.title = Asia", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Taiwan')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Taiwan")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('# note.parents.parents.title = Europe', searchContext); searchResults = searchService.findResultsWithQuery("# note.parents.parents.title = Europe", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Prague')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Prague")).toBeTruthy();
}); });
it("filter by note's ancestor", () => { it("filter by note's ancestor", () => {
rootNote rootNote
.child( .child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.child(becca_mocking.note('Austria')) .child(becca_mocking.note("Austria"))
.child(becca_mocking.note('Czech Republic').child(becca_mocking.note('Prague').label('city'))) .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')))); .child(becca_mocking.note("Asia").child(becca_mocking.note("Taiwan").child(becca_mocking.note("Taipei").label("city"))));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Europe', searchContext); let searchResults = searchService.findResultsWithQuery("#city AND note.ancestors.title = Europe", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Prague')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Prague")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('#city AND note.ancestors.title = Asia', searchContext); searchResults = searchService.findResultsWithQuery("#city AND note.ancestors.title = Asia", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Taipei')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Taipei")).toBeTruthy();
}); });
it("filter by note's child", () => { it("filter by note's child", () => {
rootNote rootNote
.child( .child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.child(becca_mocking.note('Austria').child(becca_mocking.note('Vienna'))) .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("Czech Republic").child(becca_mocking.note("Prague")))
) )
.child(becca_mocking.note('Oceania').child(becca_mocking.note('Australia'))); .child(becca_mocking.note("Oceania").child(becca_mocking.note("Australia")));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('# note.children.title =* Aust', searchContext); let searchResults = searchService.findResultsWithQuery("# note.children.title =* Aust", searchContext);
expect(searchResults.length).toEqual(2); expect(searchResults.length).toEqual(2);
expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy();
expect(becca_mocking.findNoteByTitle(searchResults, 'Oceania')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Oceania")).toBeTruthy();
searchResults = searchService.findResultsWithQuery( searchResults = searchService.findResultsWithQuery("# note.children.title =* Aust AND note.children.title *= republic", searchContext);
'# note.children.title =* Aust AND note.children.title *= republic',
searchContext
);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('# note.children.children.title = Prague', searchContext); searchResults = searchService.findResultsWithQuery("# note.children.children.title = Prague", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Europe')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Europe")).toBeTruthy();
}); });
it("filter by relation's note properties using short syntax", () => { it("filter by relation's note properties using short syntax", () => {
const austria = becca_mocking.note('Austria'); const austria = becca_mocking.note("Austria");
const portugal = becca_mocking.note('Portugal'); const portugal = becca_mocking.note("Portugal");
rootNote.child( rootNote.child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.child(austria) .child(austria)
.child(becca_mocking.note('Czech Republic').relation('neighbor', austria.note)) .child(becca_mocking.note("Czech Republic").relation("neighbor", austria.note))
.child(portugal) .child(portugal)
.child(becca_mocking.note('Spain').relation('neighbor', portugal.note)) .child(becca_mocking.note("Spain").relation("neighbor", portugal.note))
); );
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('# ~neighbor.title = Austria', searchContext); let searchResults = searchService.findResultsWithQuery("# ~neighbor.title = Austria", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('# ~neighbor.title = Portugal', searchContext); searchResults = searchService.findResultsWithQuery("# ~neighbor.title = Portugal", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Spain')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Spain")).toBeTruthy();
}); });
it("filter by relation's note properties using long syntax", () => { it("filter by relation's note properties using long syntax", () => {
const austria = becca_mocking.note('Austria'); const austria = becca_mocking.note("Austria");
const portugal = becca_mocking.note('Portugal'); const portugal = becca_mocking.note("Portugal");
rootNote.child( rootNote.child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.child(austria) .child(austria)
.child(becca_mocking.note('Czech Republic').relation('neighbor', austria.note)) .child(becca_mocking.note("Czech Republic").relation("neighbor", austria.note))
.child(portugal) .child(portugal)
.child(becca_mocking.note('Spain').relation('neighbor', portugal.note)) .child(becca_mocking.note("Spain").relation("neighbor", portugal.note))
); );
const searchContext = new SearchContext(); const searchContext = new SearchContext();
const searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.title = Austria', searchContext); const searchResults = searchService.findResultsWithQuery("# note.relations.neighbor.title = Austria", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
}); });
it('filter by multiple level relation', () => { it("filter by multiple level relation", () => {
const austria = becca_mocking.note('Austria'); const austria = becca_mocking.note("Austria");
const slovakia = becca_mocking.note('Slovakia'); const slovakia = becca_mocking.note("Slovakia");
const italy = becca_mocking.note('Italy'); const italy = becca_mocking.note("Italy");
const ukraine = becca_mocking.note('Ukraine'); const ukraine = becca_mocking.note("Ukraine");
rootNote.child( rootNote.child(
becca_mocking becca_mocking
.note('Europe') .note("Europe")
.child(austria.relation('neighbor', italy.note).relation('neighbor', slovakia.note)) .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(becca_mocking.note("Czech Republic").relation("neighbor", austria.note).relation("neighbor", slovakia.note))
.child(slovakia.relation('neighbor', ukraine.note)) .child(slovakia.relation("neighbor", ukraine.note))
.child(ukraine) .child(ukraine)
); );
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.relations.neighbor.title = Italy', searchContext); let searchResults = searchService.findResultsWithQuery("# note.relations.neighbor.relations.neighbor.title = Italy", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = searchService.findResultsWithQuery('# note.relations.neighbor.relations.neighbor.title = Ukraine', searchContext); searchResults = searchService.findResultsWithQuery("# note.relations.neighbor.relations.neighbor.title = Ukraine", searchContext);
expect(searchResults.length).toEqual(2); expect(searchResults.length).toEqual(2);
expect(becca_mocking.findNoteByTitle(searchResults, 'Czech Republic')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
expect(becca_mocking.findNoteByTitle(searchResults, 'Austria')).toBeTruthy(); expect(becca_mocking.findNoteByTitle(searchResults, "Austria")).toBeTruthy();
}); });
it('test note properties', () => { it("test note properties", () => {
const austria = becca_mocking.note('Austria'); const austria = becca_mocking.note("Austria");
austria.relation('myself', austria.note); austria.relation("myself", austria.note);
austria.label('capital', 'Vienna'); austria.label("capital", "Vienna");
austria.label('population', '8859000'); austria.label("population", "8859000");
rootNote rootNote
.child(becca_mocking.note('Asia')) .child(becca_mocking.note("Asia"))
.child( .child(becca_mocking.note("Europe").child(austria.child(becca_mocking.note("Vienna")).child(becca_mocking.note("Sebastian Kurz"))))
becca_mocking.note('Europe').child(austria.child(becca_mocking.note('Vienna')).child(becca_mocking.note('Sebastian Kurz'))) .child(becca_mocking.note("Mozart").child(austria));
)
.child(becca_mocking.note('Mozart').child(austria));
austria.note.isProtected = false; austria.note.isProtected = false;
austria.note.dateCreated = '2020-05-14 12:11:42.001+0200'; austria.note.dateCreated = "2020-05-14 12:11:42.001+0200";
austria.note.dateModified = '2020-05-14 13: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.utcDateCreated = "2020-05-14 10:11:42.001Z";
austria.note.utcDateModified = '2020-05-14 11:11:42.001Z'; austria.note.utcDateModified = "2020-05-14 11:11:42.001Z";
// austria.note.contentLength = 1001; // austria.note.contentLength = 1001;
const searchContext = new SearchContext(); const searchContext = new SearchContext();
@@ -472,141 +449,130 @@ describe('Search', () => {
expect(searchResults.length).toEqual(expectedResultCount); expect(searchResults.length).toEqual(expectedResultCount);
} }
test('type', 'text', 7); test("type", "text", 7);
test('TYPE', 'TEXT', 7); test("TYPE", "TEXT", 7);
test('type', 'code', 0); test("type", "code", 0);
test('mime', 'text/html', 6); test("mime", "text/html", 6);
test('mime', 'application/json', 0); test("mime", "application/json", 0);
test('isProtected', 'false', 7); test("isProtected", "false", 7);
test('isProtected', 'FALSE', 7); test("isProtected", "FALSE", 7);
test('isProtected', 'true', 0); test("isProtected", "true", 0);
test('isProtected', 'TRUE', 0); test("isProtected", "TRUE", 0);
test('dateCreated', "'2020-05-14 12:11:42.001+0200'", 1); test("dateCreated", "'2020-05-14 12:11:42.001+0200'", 1);
test('dateCreated', 'wrong', 0); test("dateCreated", "wrong", 0);
test('dateModified', "'2020-05-14 13:11:42.001+0200'", 1); test("dateModified", "'2020-05-14 13:11:42.001+0200'", 1);
test('dateModified', 'wrong', 0); test("dateModified", "wrong", 0);
test('utcDateCreated', "'2020-05-14 10:11:42.001Z'", 1); test("utcDateCreated", "'2020-05-14 10:11:42.001Z'", 1);
test('utcDateCreated', 'wrong', 0); test("utcDateCreated", "wrong", 0);
test('utcDateModified', "'2020-05-14 11:11:42.001Z'", 1); test("utcDateModified", "'2020-05-14 11:11:42.001Z'", 1);
test('utcDateModified', 'wrong', 0); test("utcDateModified", "wrong", 0);
test('parentCount', '2', 1); test("parentCount", "2", 1);
test('parentCount', '3', 0); test("parentCount", "3", 0);
test('childrenCount', '2', 1); test("childrenCount", "2", 1);
test('childrenCount', '10', 0); test("childrenCount", "10", 0);
test('attributeCount', '3', 1); test("attributeCount", "3", 1);
test('attributeCount', '4', 0); test("attributeCount", "4", 0);
test('labelCount', '2', 1); test("labelCount", "2", 1);
test('labelCount', '3', 0); test("labelCount", "3", 0);
test('relationCount', '1', 1); test("relationCount", "1", 1);
test('relationCount', '2', 0); test("relationCount", "2", 0);
}); });
it('test order by', () => { it("test order by", () => {
const italy = becca_mocking.note('Italy').label('capital', 'Rome'); const italy = becca_mocking.note("Italy").label("capital", "Rome");
const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
const austria = becca_mocking.note('Austria').label('capital', 'Vienna'); const austria = becca_mocking.note("Austria").label("capital", "Vienna");
const ukraine = becca_mocking.note('Ukraine').label('capital', 'Kiev'); const ukraine = becca_mocking.note("Ukraine").label("capital", "Kiev");
rootNote.child(becca_mocking.note('Europe').child(ukraine).child(slovakia).child(austria).child(italy)); rootNote.child(becca_mocking.note("Europe").child(ukraine).child(slovakia).child(austria).child(italy));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.title', searchContext); let searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy note.title", searchContext);
expect(searchResults.length).toEqual(4); expect(searchResults.length).toEqual(4);
expect(becca.notes[searchResults[0].noteId].title).toEqual('Austria'); expect(becca.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(becca.notes[searchResults[1].noteId].title).toEqual('Italy'); expect(becca.notes[searchResults[1].noteId].title).toEqual("Italy");
expect(becca.notes[searchResults[2].noteId].title).toEqual('Slovakia'); expect(becca.notes[searchResults[2].noteId].title).toEqual("Slovakia");
expect(becca.notes[searchResults[3].noteId].title).toEqual('Ukraine'); expect(becca.notes[searchResults[3].noteId].title).toEqual("Ukraine");
searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.labels.capital', searchContext); searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy note.labels.capital", searchContext);
expect(searchResults.length).toEqual(4); expect(searchResults.length).toEqual(4);
expect(becca.notes[searchResults[0].noteId].title).toEqual('Slovakia'); expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia");
expect(becca.notes[searchResults[1].noteId].title).toEqual('Ukraine'); expect(becca.notes[searchResults[1].noteId].title).toEqual("Ukraine");
expect(becca.notes[searchResults[2].noteId].title).toEqual('Italy'); expect(becca.notes[searchResults[2].noteId].title).toEqual("Italy");
expect(becca.notes[searchResults[3].noteId].title).toEqual('Austria'); expect(becca.notes[searchResults[3].noteId].title).toEqual("Austria");
searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy note.labels.capital DESC', searchContext); searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy note.labels.capital DESC", searchContext);
expect(searchResults.length).toEqual(4); expect(searchResults.length).toEqual(4);
expect(becca.notes[searchResults[0].noteId].title).toEqual('Austria'); expect(becca.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(becca.notes[searchResults[1].noteId].title).toEqual('Italy'); expect(becca.notes[searchResults[1].noteId].title).toEqual("Italy");
expect(becca.notes[searchResults[2].noteId].title).toEqual('Ukraine'); expect(becca.notes[searchResults[2].noteId].title).toEqual("Ukraine");
expect(becca.notes[searchResults[3].noteId].title).toEqual('Slovakia'); expect(becca.notes[searchResults[3].noteId].title).toEqual("Slovakia");
searchResults = searchService.findResultsWithQuery( searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy note.labels.capital DESC limit 2", searchContext);
'# note.parents.title = Europe orderBy note.labels.capital DESC limit 2',
searchContext
);
expect(searchResults.length).toEqual(2); expect(searchResults.length).toEqual(2);
expect(becca.notes[searchResults[0].noteId].title).toEqual('Austria'); expect(becca.notes[searchResults[0].noteId].title).toEqual("Austria");
expect(becca.notes[searchResults[1].noteId].title).toEqual('Italy'); expect(becca.notes[searchResults[1].noteId].title).toEqual("Italy");
searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1', searchContext); searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy #capital DESC limit 1", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
searchResults = searchService.findResultsWithQuery('# note.parents.title = Europe orderBy #capital DESC limit 1000', searchContext); searchResults = searchService.findResultsWithQuery("# note.parents.title = Europe orderBy #capital DESC limit 1000", searchContext);
expect(searchResults.length).toEqual(4); expect(searchResults.length).toEqual(4);
}); });
it('test not(...)', () => { it("test not(...)", () => {
const italy = becca_mocking.note('Italy').label('capital', 'Rome'); const italy = becca_mocking.note("Italy").label("capital", "Rome");
const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
rootNote.child(becca_mocking.note('Europe').child(slovakia).child(italy)); rootNote.child(becca_mocking.note("Europe").child(slovakia).child(italy));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('# not(#capital) and note.noteId != root', searchContext); let searchResults = searchService.findResultsWithQuery("# not(#capital) and note.noteId != root", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca.notes[searchResults[0].noteId].title).toEqual('Europe'); expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe");
searchResults = searchService.findResultsWithQuery('#!capital and note.noteId != root', searchContext); searchResults = searchService.findResultsWithQuery("#!capital and note.noteId != root", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca.notes[searchResults[0].noteId].title).toEqual('Europe'); expect(becca.notes[searchResults[0].noteId].title).toEqual("Europe");
}); });
xit('test note.text *=* something', () => { xit("test note.text *=* something", () => {
const italy = becca_mocking.note('Italy').label('capital', 'Rome'); const italy = becca_mocking.note("Italy").label("capital", "Rome");
const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
rootNote.child(becca_mocking.note('Europe').child(slovakia).child(italy)); rootNote.child(becca_mocking.note("Europe").child(slovakia).child(italy));
const searchContext = new SearchContext(); const searchContext = new SearchContext();
let searchResults = searchService.findResultsWithQuery('# note.text *=* vaki and note.noteId != root', searchContext); let searchResults = searchService.findResultsWithQuery("# note.text *=* vaki and note.noteId != root", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca.notes[searchResults[0].noteId].title).toEqual('Slovakia'); expect(becca.notes[searchResults[0].noteId].title).toEqual("Slovakia");
}); });
xit('test that fulltext does not match archived notes', () => { xit("test that fulltext does not match archived notes", () => {
const italy = becca_mocking.note('Italy').label('capital', 'Rome'); const italy = becca_mocking.note("Italy").label("capital", "Rome");
const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
rootNote 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"));
.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 }); const searchContext = new SearchContext({ includeArchivedNotes: false });
let searchResults = searchService.findResultsWithQuery('reddit', searchContext); let searchResults = searchService.findResultsWithQuery("reddit", searchContext);
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(becca.notes[searchResults[0].noteId].title).toEqual('Reddit is bad'); expect(becca.notes[searchResults[0].noteId].title).toEqual("Reddit is bad");
}); });
// FIXME: test what happens when we order without any filter criteria // FIXME: test what happens when we order without any filter criteria

View File

@@ -5,77 +5,74 @@ import SearchContext from "../../src/services/search/search_context.js";
const dsc = new SearchContext(); const dsc = new SearchContext();
describe('Value extractor', () => { describe("Value extractor", () => {
beforeEach(() => { beforeEach(() => {
becca.reset(); becca.reset();
}); });
it('simple title extraction', async () => { it("simple title extraction", async () => {
const europe = becca_mocking.note('Europe').note; const europe = becca_mocking.note("Europe").note;
const valueExtractor = new ValueExtractor(dsc, ['note', 'title']); const valueExtractor = new ValueExtractor(dsc, ["note", "title"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(europe)).toEqual('Europe'); expect(valueExtractor.extract(europe)).toEqual("Europe");
}); });
it('label extraction', async () => { it("label extraction", async () => {
const austria = becca_mocking.note('Austria').label('Capital', 'Vienna').note; const austria = becca_mocking.note("Austria").label("Capital", "Vienna").note;
let valueExtractor = new ValueExtractor(dsc, ['note', 'labels', 'capital']); let valueExtractor = new ValueExtractor(dsc, ["note", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria)).toEqual('Vienna'); expect(valueExtractor.extract(austria)).toEqual("Vienna");
valueExtractor = new ValueExtractor(dsc, ['#capital']); valueExtractor = new ValueExtractor(dsc, ["#capital"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria)).toEqual('Vienna'); expect(valueExtractor.extract(austria)).toEqual("Vienna");
}); });
it('parent/child property extraction', async () => { it("parent/child property extraction", async () => {
const vienna = becca_mocking.note('Vienna'); const vienna = becca_mocking.note("Vienna");
const europe = becca_mocking.note('Europe').child(becca_mocking.note('Austria').child(vienna)); const europe = becca_mocking.note("Europe").child(becca_mocking.note("Austria").child(vienna));
let valueExtractor = new ValueExtractor(dsc, ['note', 'children', 'children', 'title']); let valueExtractor = new ValueExtractor(dsc, ["note", "children", "children", "title"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(europe.note)).toEqual('Vienna'); expect(valueExtractor.extract(europe.note)).toEqual("Vienna");
valueExtractor = new ValueExtractor(dsc, ['note', 'parents', 'parents', 'title']); valueExtractor = new ValueExtractor(dsc, ["note", "parents", "parents", "title"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(vienna.note)).toEqual('Europe'); expect(valueExtractor.extract(vienna.note)).toEqual("Europe");
}); });
it('extract through relation', async () => { it("extract through relation", async () => {
const czechRepublic = becca_mocking.note('Czech Republic').label('capital', 'Prague'); const czechRepublic = becca_mocking.note("Czech Republic").label("capital", "Prague");
const slovakia = becca_mocking.note('Slovakia').label('capital', 'Bratislava'); const slovakia = becca_mocking.note("Slovakia").label("capital", "Bratislava");
const austria = becca_mocking.note('Austria').relation('neighbor', czechRepublic.note).relation('neighbor', slovakia.note); const austria = becca_mocking.note("Austria").relation("neighbor", czechRepublic.note).relation("neighbor", slovakia.note);
let valueExtractor = new ValueExtractor(dsc, ['note', 'relations', 'neighbor', 'labels', 'capital']); let valueExtractor = new ValueExtractor(dsc, ["note", "relations", "neighbor", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria.note)).toEqual('Prague'); expect(valueExtractor.extract(austria.note)).toEqual("Prague");
valueExtractor = new ValueExtractor(dsc, ['~neighbor', 'labels', 'capital']); valueExtractor = new ValueExtractor(dsc, ["~neighbor", "labels", "capital"]);
expect(valueExtractor.validate()).toBeFalsy(); expect(valueExtractor.validate()).toBeFalsy();
expect(valueExtractor.extract(austria.note)).toEqual('Prague'); expect(valueExtractor.extract(austria.note)).toEqual("Prague");
}); });
}); });
describe('Invalid value extractor property path', () => { describe("Invalid value extractor property path", () => {
it('each path must start with "note" (or label/relation)', () => expect(new ValueExtractor(dsc, ['neighbor']).validate()).toBeTruthy()); it('each path must start with "note" (or label/relation)', () => expect(new ValueExtractor(dsc, ["neighbor"]).validate()).toBeTruthy());
it('extra path element after terminal label', () => it("extra path element after terminal label", () => expect(new ValueExtractor(dsc, ["~neighbor", "labels", "capital", "noteId"]).validate()).toBeTruthy());
expect(new ValueExtractor(dsc, ['~neighbor', 'labels', 'capital', 'noteId']).validate()).toBeTruthy());
it('extra path element after terminal title', () => it("extra path element after terminal title", () => expect(new ValueExtractor(dsc, ["note", "title", "isProtected"]).validate()).toBeTruthy());
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 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', () => it("relation is specified but target note property is not specified", () => expect(new ValueExtractor(dsc, ["note", "relations", "myrel"]).validate()).toBeTruthy());
expect(new ValueExtractor(dsc, ['note', 'relations', 'myrel']).validate()).toBeTruthy());
}); });

View File

@@ -2,176 +2,153 @@ import child_process from "child_process";
let etapiAuthToken: string | undefined; let etapiAuthToken: string | undefined;
const getEtapiAuthorizationHeader = (): string => const getEtapiAuthorizationHeader = (): string => "Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString("base64");
"Basic " + Buffer.from(`etapi:${etapiAuthToken}`).toString("base64");
const PORT: string = "9999"; const PORT: string = "9999";
const HOST: string = "http://localhost:" + PORT; const HOST: string = "http://localhost:" + PORT;
type SpecDefinitionsFunc = () => void; type SpecDefinitionsFunc = () => void;
function describeEtapi( function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
description: string, describe(description, () => {
specDefinitions: SpecDefinitionsFunc let appProcess: ReturnType<typeof child_process.spawn>;
): void {
describe(description, () => {
let appProcess: ReturnType<typeof child_process.spawn>;
beforeAll(async () => { beforeAll(async () => {});
afterAll(() => {});
specDefinitions();
}); });
afterAll(() => {
});
specDefinitions();
});
} }
async function getEtapiResponse(url: string): Promise<Response> { async function getEtapiResponse(url: string): Promise<Response> {
return await fetch(`${HOST}/etapi/${url}`, { return await fetch(`${HOST}/etapi/${url}`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: getEtapiAuthorizationHeader(), Authorization: getEtapiAuthorizationHeader()
}, }
}); });
} }
async function getEtapi(url: string): Promise<any> { async function getEtapi(url: string): Promise<any> {
const response = await getEtapiResponse(url); const response = await getEtapiResponse(url);
return await processEtapiResponse(response); return await processEtapiResponse(response);
} }
async function getEtapiContent(url: string): Promise<Response> { async function getEtapiContent(url: string): Promise<Response> {
const response = await fetch(`${HOST}/etapi/${url}`, { const response = await fetch(`${HOST}/etapi/${url}`, {
method: "GET", method: "GET",
headers: { headers: {
Authorization: getEtapiAuthorizationHeader(), Authorization: getEtapiAuthorizationHeader()
}, }
}); });
checkStatus(response); checkStatus(response);
return response; return response;
} }
async function postEtapi( async function postEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
url: string, const response = await fetch(`${HOST}/etapi/${url}`, {
data: Record<string, unknown> = {} method: "POST",
): Promise<any> { headers: {
const response = await fetch(`${HOST}/etapi/${url}`, { "Content-Type": "application/json",
method: "POST", Authorization: getEtapiAuthorizationHeader()
headers: { },
"Content-Type": "application/json", body: JSON.stringify(data)
Authorization: getEtapiAuthorizationHeader(), });
}, return await processEtapiResponse(response);
body: JSON.stringify(data),
});
return await processEtapiResponse(response);
} }
async function postEtapiContent( async function postEtapiContent(url: string, data: BodyInit): Promise<Response> {
url: string, const response = await fetch(`${HOST}/etapi/${url}`, {
data: BodyInit method: "POST",
): Promise<Response> { headers: {
const response = await fetch(`${HOST}/etapi/${url}`, { "Content-Type": "application/octet-stream",
method: "POST", Authorization: getEtapiAuthorizationHeader()
headers: { },
"Content-Type": "application/octet-stream", body: data
Authorization: getEtapiAuthorizationHeader(), });
},
body: data,
});
checkStatus(response); checkStatus(response);
return response; return response;
} }
async function putEtapi( async function putEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
url: string, const response = await fetch(`${HOST}/etapi/${url}`, {
data: Record<string, unknown> = {} method: "PUT",
): Promise<any> { headers: {
const response = await fetch(`${HOST}/etapi/${url}`, { "Content-Type": "application/json",
method: "PUT", Authorization: getEtapiAuthorizationHeader()
headers: { },
"Content-Type": "application/json", body: JSON.stringify(data)
Authorization: getEtapiAuthorizationHeader(), });
}, return await processEtapiResponse(response);
body: JSON.stringify(data),
});
return await processEtapiResponse(response);
} }
async function putEtapiContent( async function putEtapiContent(url: string, data?: BodyInit): Promise<Response> {
url: string, const response = await fetch(`${HOST}/etapi/${url}`, {
data?: BodyInit method: "PUT",
): Promise<Response> { headers: {
const response = await fetch(`${HOST}/etapi/${url}`, { "Content-Type": "application/octet-stream",
method: "PUT", Authorization: getEtapiAuthorizationHeader()
headers: { },
"Content-Type": "application/octet-stream", body: data
Authorization: getEtapiAuthorizationHeader(), });
},
body: data,
});
checkStatus(response); checkStatus(response);
return response; return response;
} }
async function patchEtapi( async function patchEtapi(url: string, data: Record<string, unknown> = {}): Promise<any> {
url: string, const response = await fetch(`${HOST}/etapi/${url}`, {
data: Record<string, unknown> = {} method: "PATCH",
): Promise<any> { headers: {
const response = await fetch(`${HOST}/etapi/${url}`, { "Content-Type": "application/json",
method: "PATCH", Authorization: getEtapiAuthorizationHeader()
headers: { },
"Content-Type": "application/json", body: JSON.stringify(data)
Authorization: getEtapiAuthorizationHeader(), });
}, return await processEtapiResponse(response);
body: JSON.stringify(data),
});
return await processEtapiResponse(response);
} }
async function deleteEtapi(url: string): Promise<any> { async function deleteEtapi(url: string): Promise<any> {
const response = await fetch(`${HOST}/etapi/${url}`, { const response = await fetch(`${HOST}/etapi/${url}`, {
method: "DELETE", method: "DELETE",
headers: { headers: {
Authorization: getEtapiAuthorizationHeader(), Authorization: getEtapiAuthorizationHeader()
}, }
}); });
return await processEtapiResponse(response); return await processEtapiResponse(response);
} }
async function processEtapiResponse(response: Response): Promise<any> { async function processEtapiResponse(response: Response): Promise<any> {
const text = await response.text(); const text = await response.text();
if (response.status < 200 || response.status >= 300) { if (response.status < 200 || response.status >= 300) {
throw new Error(`ETAPI error ${response.status}: ${text}`); throw new Error(`ETAPI error ${response.status}: ${text}`);
} }
return text?.trim() ? JSON.parse(text) : null; return text?.trim() ? JSON.parse(text) : null;
} }
function checkStatus(response: Response): void { function checkStatus(response: Response): void {
if (response.status < 200 || response.status >= 300) { if (response.status < 200 || response.status >= 300) {
throw new Error(`ETAPI error ${response.status}`); throw new Error(`ETAPI error ${response.status}`);
} }
} }
export default { export default {
describeEtapi, describeEtapi,
getEtapi, getEtapi,
getEtapiResponse, getEtapiResponse,
getEtapiContent, getEtapiContent,
postEtapi, postEtapi,
postEtapiContent, postEtapiContent,
putEtapi, putEtapi,
putEtapiContent, putEtapiContent,
patchEtapi, patchEtapi,
deleteEtapi, deleteEtapi
}; };

View File

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

View File

@@ -5,8 +5,7 @@ describe("Utils", () => {
expect(trimIndentation`\ expect(trimIndentation`\
Hello Hello
world world
123` 123`).toBe(`\
).toBe(`\
Hello Hello
world world
123`); 123`);

View File

@@ -3,16 +3,16 @@ export function trimIndentation(strings: TemplateStringsArray) {
// Count the number of spaces on the first line. // Count the number of spaces on the first line.
let numSpaces = 0; let numSpaces = 0;
while (str.charAt(numSpaces) == ' ' && numSpaces < str.length) { while (str.charAt(numSpaces) == " " && numSpaces < str.length) {
numSpaces++; numSpaces++;
} }
// Trim the indentation of the first line in all the lines. // Trim the indentation of the first line in all the lines.
const lines = str.split("\n"); const lines = str.split("\n");
const output = []; const output = [];
for (let i=0; i<lines.length; i++) { for (let i = 0; i < lines.length; i++) {
let numSpacesLine = 0; let numSpacesLine = 0;
while (str.charAt(numSpacesLine) == ' ' && numSpacesLine < str.length) { while (str.charAt(numSpacesLine) == " " && numSpacesLine < str.length) {
numSpacesLine++; numSpacesLine++;
} }
output.push(lines[i].substring(numSpacesLine)); output.push(lines[i].substring(numSpacesLine));

View File

@@ -6,7 +6,7 @@ sqlInit.dbReady.then(async () => {
try { try {
console.log("Starting anonymization..."); console.log("Starting anonymization...");
const resp = await anonymizationService.createAnonymizedCopy('full'); const resp = await anonymizationService.createAnonymizedCopy("full");
if (resp.success) { if (resp.success) {
console.log(`Anonymized file has been saved to: ${resp.anonymizedFilePath}`); console.log(`Anonymized file has been saved to: ${resp.anonymizedFilePath}`);
@@ -15,8 +15,7 @@ sqlInit.dbReady.then(async () => {
} else { } else {
console.log("Anonymization failed."); console.log("Anonymization failed.");
} }
} } catch (e: any) {
catch (e: any) {
console.error(e.message, e.stack); console.error(e.message, e.stack);
} }

View File

@@ -16,8 +16,8 @@ import { startScheduledCleanup } from "./services/erase.js";
import sql_init from "./services/sql_init.js"; import sql_init from "./services/sql_init.js";
import { t } from "i18next"; import { t } from "i18next";
await import('./services/handlers.js'); await import("./services/handlers.js");
await import('./becca/becca_loader.js'); await import("./becca/becca_loader.js");
const app = express(); const app = express();
@@ -27,8 +27,8 @@ const scriptDir = dirname(fileURLToPath(import.meta.url));
sql_init.initializeDb(); sql_init.initializeDb();
// view engine setup // view engine setup
app.set('views', path.join(scriptDir, 'views')); app.set("views", path.join(scriptDir, "views"));
app.set('view engine', 'ejs'); app.set("view engine", "ejs");
app.use((req, res, next) => { app.use((req, res, next) => {
res.locals.t = t; res.locals.t = t;
@@ -39,21 +39,23 @@ if (!utils.isElectron()) {
app.use(compression()); // HTTP compression app.use(compression()); // HTTP compression
} }
app.use(helmet({ app.use(
hidePoweredBy: false, // errors out in electron helmet({
contentSecurityPolicy: false, hidePoweredBy: false, // errors out in electron
crossOriginEmbedderPolicy: false contentSecurityPolicy: false,
})); crossOriginEmbedderPolicy: false
})
);
app.use(express.text({ limit: '500mb' })); app.use(express.text({ limit: "500mb" }));
app.use(express.json({ limit: '500mb' })); app.use(express.json({ limit: "500mb" }));
app.use(express.raw({ limit: '500mb' })); app.use(express.raw({ limit: "500mb" }));
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(scriptDir, 'public/root'))); app.use(express.static(path.join(scriptDir, "public/root")));
app.use(`/manifest.webmanifest`, express.static(path.join(scriptDir, 'public/manifest.webmanifest'))); app.use(`/manifest.webmanifest`, express.static(path.join(scriptDir, "public/manifest.webmanifest")));
app.use(`/robots.txt`, express.static(path.join(scriptDir, 'public/robots.txt'))); app.use(`/robots.txt`, express.static(path.join(scriptDir, "public/robots.txt")));
app.use(`/icon.png`, express.static(path.join(scriptDir, 'public/icon.png'))); app.use(`/icon.png`, express.static(path.join(scriptDir, "public/icon.png")));
app.use(sessionParser); app.use(sessionParser);
app.use(favicon(`${scriptDir}/../images/app-icons/icon.ico`)); app.use(favicon(`${scriptDir}/../images/app-icons/icon.ico`));
@@ -66,17 +68,17 @@ error_handlers.register(app);
await import("./services/sync.js"); await import("./services/sync.js");
// triggers backup timer // triggers backup timer
await import('./services/backup.js'); await import("./services/backup.js");
// trigger consistency checks timer // trigger consistency checks timer
await import('./services/consistency_checks.js'); await import("./services/consistency_checks.js");
await import('./services/scheduler.js'); await import("./services/scheduler.js");
startScheduledCleanup(); startScheduledCleanup();
if (utils.isElectron()) { if (utils.isElectron()) {
(await import('@electron/remote/main/index.js')).initialize(); (await import("@electron/remote/main/index.js")).initialize();
} }
export default app; export default app;

View File

@@ -8,7 +8,7 @@ import BAttribute from "./entities/battribute.js";
import BBranch from "./entities/bbranch.js"; import BBranch from "./entities/bbranch.js";
import BRevision from "./entities/brevision.js"; import BRevision from "./entities/brevision.js";
import BAttachment from "./entities/battachment.js"; import BAttachment from "./entities/battachment.js";
import { AttachmentRow, BlobRow, RevisionRow } from './entities/rows.js'; import type { AttachmentRow, BlobRow, RevisionRow } from "./entities/rows.js";
import BBlob from "./entities/bblob.js"; import BBlob from "./entities/bblob.js";
import BRecentNote from "./entities/brecent_note.js"; import BRecentNote from "./entities/brecent_note.js";
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
@@ -55,13 +55,13 @@ export default class Becca {
} }
getRoot() { getRoot() {
return this.getNote('root'); return this.getNote("root");
} }
findAttributes(type: string, name: string): BAttribute[] { findAttributes(type: string, name: string): BAttribute[] {
name = name.trim().toLowerCase(); name = name.trim().toLowerCase();
if (name.startsWith('#') || name.startsWith('~')) { if (name.startsWith("#") || name.startsWith("~")) {
name = name.substr(1); name = name.substr(1);
} }
@@ -177,8 +177,7 @@ export default class Becca {
WHERE attachmentId = ? AND isDeleted = 0` WHERE attachmentId = ? AND isDeleted = 0`
: `SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`; : `SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [attachmentId]) return sql.getRows<AttachmentRow>(query, [attachmentId]).map((row) => new BAttachment(row))[0];
.map(row => new BAttachment(row))[0];
} }
getAttachmentOrThrow(attachmentId: string, opts: AttachmentOpts = {}): BAttachment { getAttachmentOrThrow(attachmentId: string, opts: AttachmentOpts = {}): BAttachment {
@@ -190,8 +189,7 @@ export default class Becca {
} }
getAttachments(attachmentIds: string[]): BAttachment[] { getAttachments(attachmentIds: string[]): BAttachment[] {
return sql.getManyRows<AttachmentRow>("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds) return sql.getManyRows<AttachmentRow>("SELECT * FROM attachments WHERE attachmentId IN (???) AND isDeleted = 0", attachmentIds).map((row) => new BAttachment(row));
.map(row => new BAttachment(row));
} }
getBlob(entity: { blobId?: string }): BBlob | null { getBlob(entity: { blobId?: string }): BBlob | null {
@@ -220,18 +218,13 @@ export default class Becca {
return null; return null;
} }
if (entityName === 'revisions') { if (entityName === "revisions") {
return this.getRevision(entityId); return this.getRevision(entityId);
} else if (entityName === 'attachments') { } else if (entityName === "attachments") {
return this.getAttachment(entityId); return this.getAttachment(entityId);
} }
const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g, (group) => group.toUpperCase().replace("_", ""));
group =>
group
.toUpperCase()
.replace('_', '')
);
if (!(camelCaseEntityName in this)) { if (!(camelCaseEntityName in this)) {
throw new Error(`Unknown entity name '${camelCaseEntityName}' (original argument '${entityName}')`); throw new Error(`Unknown entity name '${camelCaseEntityName}' (original argument '${entityName}')`);
@@ -242,12 +235,12 @@ export default class Becca {
getRecentNotesFromQuery(query: string, params: string[] = []): BRecentNote[] { getRecentNotesFromQuery(query: string, params: string[] = []): BRecentNote[] {
const rows = sql.getRows<BRecentNote>(query, params); const rows = sql.getRows<BRecentNote>(query, params);
return rows.map(row => new BRecentNote(row)); return rows.map((row) => new BRecentNote(row));
} }
getRevisionsFromQuery(query: string, params: string[] = []): BRevision[] { getRevisionsFromQuery(query: string, params: string[] = []): BRevision[] {
const rows = sql.getRows<RevisionRow>(query, params); const rows = sql.getRows<RevisionRow>(query, params);
return rows.map(row => new BRevision(row)); return rows.map((row) => new BRevision(row));
} }
/** Should be called when the set of all non-skeleton notes changes (added/removed) */ /** Should be called when the set of all non-skeleton notes changes (added/removed) */

View File

@@ -11,7 +11,7 @@ import BOption from "./entities/boption.js";
import BEtapiToken from "./entities/betapi_token.js"; import BEtapiToken from "./entities/betapi_token.js";
import cls from "../services/cls.js"; import cls from "../services/cls.js";
import entityConstructor from "../becca/entity_constructor.js"; import entityConstructor from "../becca/entity_constructor.js";
import { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from './entities/rows.js'; import type { AttributeRow, BranchRow, EtapiTokenRow, NoteRow, OptionRow } from "./entities/rows.js";
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import ws from "../services/ws.js"; import ws from "../services/ws.js";
@@ -119,13 +119,13 @@ eventService.subscribeBeccaLoader(eventService.ENTITY_CHANGED, ({ entityName, en
* It should be therefore treated as a row. * It should be therefore treated as a row.
*/ */
function postProcessEntityUpdate(entityName: string, entityRow: any) { function postProcessEntityUpdate(entityName: string, entityRow: any) {
if (entityName === 'notes') { if (entityName === "notes") {
noteUpdated(entityRow); noteUpdated(entityRow);
} else if (entityName === 'branches') { } else if (entityName === "branches") {
branchUpdated(entityRow); branchUpdated(entityRow);
} else if (entityName === 'attributes') { } else if (entityName === "attributes") {
attributeUpdated(entityRow); attributeUpdated(entityRow);
} else if (entityName === 'note_reordering') { } else if (entityName === "note_reordering") {
noteReorderingUpdated(entityRow); noteReorderingUpdated(entityRow);
} }
} }
@@ -135,13 +135,13 @@ eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENT
return; return;
} }
if (entityName === 'notes') { if (entityName === "notes") {
noteDeleted(entityId); noteDeleted(entityId);
} else if (entityName === 'branches') { } else if (entityName === "branches") {
branchDeleted(entityId); branchDeleted(entityId);
} else if (entityName === 'attributes') { } else if (entityName === "attributes") {
attributeDeleted(entityId); attributeDeleted(entityId);
} else if (entityName === 'etapi_tokens') { } else if (entityName === "etapi_tokens") {
etapiTokenDeleted(entityId); etapiTokenDeleted(entityId);
} }
}); });
@@ -162,9 +162,8 @@ function branchDeleted(branchId: string) {
const childNote = becca.notes[branch.noteId]; const childNote = becca.notes[branch.noteId];
if (childNote) { if (childNote) {
childNote.parents = childNote.parents.filter(parent => parent.noteId !== branch.parentNoteId); childNote.parents = childNote.parents.filter((parent) => parent.noteId !== branch.parentNoteId);
childNote.parentBranches = childNote.parentBranches childNote.parentBranches = childNote.parentBranches.filter((parentBranch) => parentBranch.branchId !== branch.branchId);
.filter(parentBranch => parentBranch.branchId !== branch.branchId);
if (childNote.parents.length > 0) { if (childNote.parents.length > 0) {
// subtree notes might lose some inherited attributes // subtree notes might lose some inherited attributes
@@ -175,7 +174,7 @@ function branchDeleted(branchId: string) {
const parentNote = becca.notes[branch.parentNoteId]; const parentNote = becca.notes[branch.parentNoteId];
if (parentNote) { if (parentNote) {
parentNote.children = parentNote.children.filter(child => child.noteId !== branch.noteId); parentNote.children = parentNote.children.filter((child) => child.noteId !== branch.noteId);
} }
delete becca.childParentToBranch[`${branch.noteId}-${branch.parentNoteId}`]; delete becca.childParentToBranch[`${branch.noteId}-${branch.parentNoteId}`];
@@ -230,12 +229,12 @@ function attributeDeleted(attributeId: string) {
note.invalidateThisCache(); note.invalidateThisCache();
} }
note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attribute.attributeId); note.ownedAttributes = note.ownedAttributes.filter((attr) => attr.attributeId !== attribute.attributeId);
const targetNote = attribute.targetNote; const targetNote = attribute.targetNote;
if (targetNote) { if (targetNote) {
targetNote.targetRelations = targetNote.targetRelations.filter(rel => rel.attributeId !== attribute.attributeId); targetNote.targetRelations = targetNote.targetRelations.filter((rel) => rel.attributeId !== attribute.attributeId);
} }
} }
@@ -244,7 +243,7 @@ function attributeDeleted(attributeId: string) {
const key = `${attribute.type}-${attribute.name.toLowerCase()}`; const key = `${attribute.type}-${attribute.name.toLowerCase()}`;
if (key in becca.attributeIndex) { if (key in becca.attributeIndex) {
becca.attributeIndex[key] = becca.attributeIndex[key].filter(attr => attr.attributeId !== attribute.attributeId); becca.attributeIndex[key] = becca.attributeIndex[key].filter((attr) => attr.attributeId !== attribute.attributeId);
} }
} }
@@ -282,8 +281,7 @@ function etapiTokenDeleted(etapiTokenId: string) {
eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => { eventService.subscribeBeccaLoader(eventService.ENTER_PROTECTED_SESSION, () => {
try { try {
becca.decryptProtectedNotes(); becca.decryptProtectedNotes();
} } catch (e: any) {
catch (e: any) {
log.error(`Could not decrypt protected notes: ${e.message} ${e.stack}`); log.error(`Could not decrypt protected notes: ${e.message} ${e.stack}`);
} }
}); });

View File

@@ -37,7 +37,7 @@ function getNoteTitle(childNoteId: string, parentNoteId?: string) {
const branch = parentNote ? becca.getBranchFromChildAndParent(childNote.noteId, parentNote.noteId) : null; const branch = parentNote ? becca.getBranchFromChildAndParent(childNote.noteId, parentNote.noteId) : null;
return `${(branch && branch.prefix) ? `${branch.prefix} - ` : ''}${title}`; return `${branch && branch.prefix ? `${branch.prefix} - ` : ""}${title}`;
} }
function getNoteTitleArrayForPath(notePathArray: string[]) { function getNoteTitleArrayForPath(notePathArray: string[]) {
@@ -51,7 +51,7 @@ function getNoteTitleArrayForPath(notePathArray: string[]) {
const titles = []; const titles = [];
let parentNoteId = 'root'; let parentNoteId = "root";
let hoistedNotePassed = false; let hoistedNotePassed = false;
// this is a notePath from outside of hoisted subtree, so the full title path needs to be returned // this is a notePath from outside of hoisted subtree, so the full title path needs to be returned
@@ -79,7 +79,7 @@ function getNoteTitleArrayForPath(notePathArray: string[]) {
function getNoteTitleForPath(notePathArray: string[]) { function getNoteTitleForPath(notePathArray: string[]) {
const titles = getNoteTitleArrayForPath(notePathArray); const titles = getNoteTitleArrayForPath(notePathArray);
return titles.join(' / '); return titles.join(" / ");
} }
export default { export default {

View File

@@ -9,7 +9,7 @@ import cls from "../../services/cls.js";
import log from "../../services/log.js"; import log from "../../services/log.js";
import protectedSessionService from "../../services/protected_session.js"; import protectedSessionService from "../../services/protected_session.js";
import blobService from "../../services/blob.js"; import blobService from "../../services/blob.js";
import Becca, { ConstructorData } from '../becca-interface.js'; import Becca, { type ConstructorData } from "../becca-interface.js";
import becca from "../becca.js"; import becca from "../becca.js";
interface ContentOpts { interface ContentOpts {
@@ -23,7 +23,6 @@ interface ContentOpts {
* @type T the same entity type needed for self-reference in {@link ConstructorData}. * @type T the same entity type needed for self-reference in {@link ConstructorData}.
*/ */
abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> { abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
utcDateModified?: string; utcDateModified?: string;
dateCreated?: string; dateCreated?: string;
dateModified?: string; dateModified?: string;
@@ -35,7 +34,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
blobId?: string; blobId?: string;
protected beforeSaving(opts?: {}) { protected beforeSaving(opts?: {}) {
const constructorData = (this.constructor as unknown as ConstructorData<T>); const constructorData = this.constructor as unknown as ConstructorData<T>;
if (!(this as any)[constructorData.primaryKeyName]) { if (!(this as any)[constructorData.primaryKeyName]) {
(this as any)[constructorData.primaryKeyName] = utils.newEntityId(); (this as any)[constructorData.primaryKeyName] = utils.newEntityId();
} }
@@ -50,19 +49,19 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
} }
protected putEntityChange(isDeleted: boolean) { protected putEntityChange(isDeleted: boolean) {
const constructorData = (this.constructor as unknown as ConstructorData<T>); const constructorData = this.constructor as unknown as ConstructorData<T>;
entityChangesService.putEntityChange({ entityChangesService.putEntityChange({
entityName: constructorData.entityName, entityName: constructorData.entityName,
entityId: (this as any)[constructorData.primaryKeyName], entityId: (this as any)[constructorData.primaryKeyName],
hash: this.generateHash(isDeleted), hash: this.generateHash(isDeleted),
isErased: false, isErased: false,
utcDateChanged: this.getUtcDateChanged(), utcDateChanged: this.getUtcDateChanged(),
isSynced: constructorData.entityName !== 'options' || !!this.isSynced isSynced: constructorData.entityName !== "options" || !!this.isSynced
}); });
} }
generateHash(isDeleted?: boolean): string { generateHash(isDeleted?: boolean): string {
const constructorData = (this.constructor as unknown as ConstructorData<T>); const constructorData = this.constructor as unknown as ConstructorData<T>;
let contentToHash = ""; let contentToHash = "";
for (const propertyName of constructorData.hashedProperties) { for (const propertyName of constructorData.hashedProperties) {
@@ -99,10 +98,10 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
} }
/** /**
* Saves entity - executes SQL, but doesn't commit the transaction on its own * Saves entity - executes SQL, but doesn't commit the transaction on its own
*/ */
save(opts?: {}): this { save(opts?: {}): this {
const constructorData = (this.constructor as unknown as ConstructorData<T>); const constructorData = this.constructor as unknown as ConstructorData<T>;
const entityName = constructorData.entityName; const entityName = constructorData.entityName;
const primaryKeyName = constructorData.primaryKeyName; const primaryKeyName = constructorData.primaryKeyName;
@@ -115,7 +114,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
sql.transactional(() => { sql.transactional(() => {
sql.upsert(entityName, primaryKeyName, pojo); sql.upsert(entityName, primaryKeyName, pojo);
if (entityName === 'recent_notes') { if (entityName === "recent_notes") {
return; return;
} }
@@ -144,7 +143,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
opts.forceFrontendReload = !!opts.forceFrontendReload; opts.forceFrontendReload = !!opts.forceFrontendReload;
if (content === null || content === undefined) { if (content === null || content === undefined) {
const constructorData = (this.constructor as unknown as ConstructorData<T>); const constructorData = this.constructor as unknown as ConstructorData<T>;
throw new Error(`Cannot set null content to ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}'`); throw new Error(`Cannot set null content to ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}'`);
} }
@@ -206,9 +205,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
if (this.isProtected) { if (this.isProtected) {
// a "random" prefix makes sure that the calculated hash/blobId is different for a decrypted/encrypted content // a "random" prefix makes sure that the calculated hash/blobId is different for a decrypted/encrypted content
const encryptedPrefixSuffix = "t$[nvQg7q)&_ENCRYPTED_?M:Bf&j3jr_"; const encryptedPrefixSuffix = "t$[nvQg7q)&_ENCRYPTED_?M:Bf&j3jr_";
return Buffer.isBuffer(unencryptedContent) return Buffer.isBuffer(unencryptedContent) ? Buffer.concat([Buffer.from(encryptedPrefixSuffix), unencryptedContent]) : `${encryptedPrefixSuffix}${unencryptedContent}`;
? Buffer.concat([Buffer.from(encryptedPrefixSuffix), unencryptedContent])
: `${encryptedPrefixSuffix}${unencryptedContent}`;
} else { } else {
return unencryptedContent; return unencryptedContent;
} }
@@ -216,13 +213,13 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
private saveBlob(content: string | Buffer, unencryptedContentForHashCalculation: string | Buffer, opts: ContentOpts = {}) { private saveBlob(content: string | Buffer, unencryptedContentForHashCalculation: string | Buffer, opts: ContentOpts = {}) {
/* /*
* We're using the unencrypted blob for the hash calculation, because otherwise the random IV would * We're using the unencrypted blob for the hash calculation, because otherwise the random IV would
* cause every content blob to be unique which would balloon the database size (esp. with revisioning). * cause every content blob to be unique which would balloon the database size (esp. with revisioning).
* This has minor security implications (it's easy to infer that given content is shared between different * This has minor security implications (it's easy to infer that given content is shared between different
* notes/attachments), but the trade-off comes out clearly positive. * notes/attachments), but the trade-off comes out clearly positive.
*/ */
const newBlobId = utils.hashedBlobId(unencryptedContentForHashCalculation); const newBlobId = utils.hashedBlobId(unencryptedContentForHashCalculation);
const blobNeedsInsert = !sql.getValue('SELECT 1 FROM blobs WHERE blobId = ?', [newBlobId]); const blobNeedsInsert = !sql.getValue("SELECT 1 FROM blobs WHERE blobId = ?", [newBlobId]);
if (!blobNeedsInsert) { if (!blobNeedsInsert) {
return newBlobId; return newBlobId;
@@ -242,7 +239,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
const hash = blobService.calculateContentHash(pojo); const hash = blobService.calculateContentHash(pojo);
entityChangesService.putEntityChange({ entityChangesService.putEntityChange({
entityName: 'blobs', entityName: "blobs",
entityId: newBlobId, entityId: newBlobId,
hash: hash, hash: hash,
isErased: false, isErased: false,
@@ -254,7 +251,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
}); });
eventService.emit(eventService.ENTITY_CHANGED, { eventService.emit(eventService.ENTITY_CHANGED, {
entityName: 'blobs', entityName: "blobs",
entity: this entity: this
}); });
@@ -265,7 +262,7 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
const row = sql.getRow<{ content: string | Buffer }>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]); const row = sql.getRow<{ content: string | Buffer }>(`SELECT content FROM blobs WHERE blobId = ?`, [this.blobId]);
if (!row) { if (!row) {
const constructorData = (this.constructor as unknown as ConstructorData<T>); const constructorData = this.constructor as unknown as ConstructorData<T>;
throw new Error(`Cannot find content for ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}', blobId '${this.blobId}'`); throw new Error(`Cannot find content for ${constructorData.primaryKeyName} '${(this as any)[constructorData.primaryKeyName]}', blobId '${this.blobId}'`);
} }
@@ -273,26 +270,27 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
} }
/** /**
* Mark the entity as (soft) deleted. It will be completely erased later. * Mark the entity as (soft) deleted. It will be completely erased later.
* *
* This is a low-level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead. * This is a low-level method, for notes and branches use `note.deleteNote()` and 'branch.deleteBranch()` instead.
*/ */
markAsDeleted(deleteId: string | null = null) { markAsDeleted(deleteId: string | null = null) {
const constructorData = (this.constructor as unknown as ConstructorData<T>); const constructorData = this.constructor as unknown as ConstructorData<T>;
const entityId = (this as any)[constructorData.primaryKeyName]; const entityId = (this as any)[constructorData.primaryKeyName];
const entityName = constructorData.entityName; const entityName = constructorData.entityName;
this.utcDateModified = dateUtils.utcNowDateTime(); this.utcDateModified = dateUtils.utcNowDateTime();
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ? sql.execute(
`UPDATE ${entityName} SET isDeleted = 1, deleteId = ?, utcDateModified = ?
WHERE ${constructorData.primaryKeyName} = ?`, WHERE ${constructorData.primaryKeyName} = ?`,
[deleteId, this.utcDateModified, entityId]); [deleteId, this.utcDateModified, entityId]
);
if (this.dateModified) { if (this.dateModified) {
this.dateModified = dateUtils.localNowDateTime(); this.dateModified = dateUtils.localNowDateTime();
sql.execute(`UPDATE ${entityName} SET dateModified = ? WHERE ${constructorData.primaryKeyName} = ?`, sql.execute(`UPDATE ${entityName} SET dateModified = ? WHERE ${constructorData.primaryKeyName} = ?`, [this.dateModified, entityId]);
[this.dateModified, entityId]);
} }
log.info(`Marking ${entityName} ${entityId} as deleted`); log.info(`Marking ${entityName} ${entityId} as deleted`);
@@ -303,15 +301,17 @@ abstract class AbstractBeccaEntity<T extends AbstractBeccaEntity<T>> {
} }
markAsDeletedSimple() { markAsDeletedSimple() {
const constructorData = (this.constructor as unknown as ConstructorData<T>); const constructorData = this.constructor as unknown as ConstructorData<T>;
const entityId = (this as any)[constructorData.primaryKeyName]; const entityId = (this as any)[constructorData.primaryKeyName];
const entityName = constructorData.entityName; const entityName = constructorData.entityName;
this.utcDateModified = dateUtils.utcNowDateTime(); this.utcDateModified = dateUtils.utcNowDateTime();
sql.execute(`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ? sql.execute(
`UPDATE ${entityName} SET isDeleted = 1, utcDateModified = ?
WHERE ${constructorData.primaryKeyName} = ?`, WHERE ${constructorData.primaryKeyName} = ?`,
[this.utcDateModified, entityId]); [this.utcDateModified, entityId]
);
log.info(`Marking ${entityName} ${entityId} as deleted`); log.info(`Marking ${entityName} ${entityId} as deleted`);

View File

@@ -6,14 +6,14 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
import sql from "../../services/sql.js"; import sql from "../../services/sql.js";
import protectedSessionService from "../../services/protected_session.js"; import protectedSessionService from "../../services/protected_session.js";
import log from "../../services/log.js"; import log from "../../services/log.js";
import { AttachmentRow } from './rows.js'; import type { AttachmentRow } from "./rows.js";
import BNote from "./bnote.js"; import BNote from "./bnote.js";
import BBranch from "./bbranch.js"; import BBranch from "./bbranch.js";
import noteService from "../../services/notes.js"; import noteService from "../../services/notes.js";
const attachmentRoleToNoteTypeMapping = { const attachmentRoleToNoteTypeMapping = {
'image': 'image', image: "image",
'file': 'file' file: "file"
}; };
interface ContentOpts { interface ContentOpts {
@@ -31,9 +31,15 @@ interface ContentOpts {
* larger amounts of data and generally not accessible to the user. * larger amounts of data and generally not accessible to the user.
*/ */
class BAttachment extends AbstractBeccaEntity<BAttachment> { class BAttachment extends AbstractBeccaEntity<BAttachment> {
static get entityName() { return "attachments"; } static get entityName() {
static get primaryKeyName() { return "attachmentId"; } return "attachments";
static get hashedProperties() { return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"]; } }
static get primaryKeyName() {
return "attachmentId";
}
static get hashedProperties() {
return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"];
}
noteId?: number; noteId?: number;
attachmentId?: string; attachmentId?: string;
@@ -102,13 +108,15 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
} }
isContentAvailable() { isContentAvailable() {
return !this.attachmentId // new attachment which was not encrypted yet return (
|| !this.isProtected !this.attachmentId || // new attachment which was not encrypted yet
|| protectedSessionService.isProtectedSessionAvailable() !this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
} }
getTitleOrProtected() { getTitleOrProtected() {
return this.isContentAvailable() ? this.title : '[protected]'; return this.isContentAvailable() ? this.title : "[protected]";
} }
decrypt() { decrypt() {
@@ -121,8 +129,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
try { try {
this.title = protectedSessionService.decryptString(this.title) || ""; this.title = protectedSessionService.decryptString(this.title) || "";
this.isDecrypted = true; this.isDecrypted = true;
} } catch (e: any) {
catch (e: any) {
log.error(`Could not decrypt attachment ${this.attachmentId}: ${e.message} ${e.stack}`); log.error(`Could not decrypt attachment ${this.attachmentId}: ${e.message} ${e.stack}`);
} }
} }
@@ -136,22 +143,22 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
this._setContent(content, opts); this._setContent(content, opts);
} }
convertToNote(): { note: BNote, branch: BBranch } { convertToNote(): { note: BNote; branch: BBranch } {
// TODO: can this ever be "search"? // TODO: can this ever be "search"?
if (this.type as string === 'search') { if ((this.type as string) === "search") {
throw new Error(`Note of type search cannot have child notes`); throw new Error(`Note of type search cannot have child notes`);
} }
if (!this.getNote()) { if (!this.getNote()) {
throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + throw new Error("Cannot find note of this attachment. It is possible that this is note revision's attachment. " + "Converting note revision's attachments to note is not (yet) supported.");
"Converting note revision's attachments to note is not (yet) supported.");
} }
if (!(this.role in attachmentRoleToNoteTypeMapping)) { if (!(this.role in attachmentRoleToNoteTypeMapping)) {
throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`); throw new Error(`Mapping from attachment role '${this.role}' to note's type is not defined`);
} }
if (!this.isContentAvailable()) { // isProtected is the same for attachment if (!this.isContentAvailable()) {
// isProtected is the same for attachment
throw new Error(`Cannot convert protected attachment outside of protected session`); throw new Error(`Cannot convert protected attachment outside of protected session`);
} }
@@ -168,7 +175,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
const parentNote = this.getNote(); const parentNote = this.getNote();
if (this.role === 'image' && parentNote.type === 'text') { if (this.role === "image" && parentNote.type === "text") {
const origContent = parentNote.getContent(); const origContent = parentNote.getContent();
if (typeof origContent !== "string") { if (typeof origContent !== "string") {
@@ -191,7 +198,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
} }
getFileName() { getFileName() {
const type = this.role === 'image' ? 'image' : 'file'; const type = this.role === "image" ? "image" : "file";
return utils.formatDownloadTitle(this.title, type, this.mime); return utils.formatDownloadTitle(this.title, type, this.mime);
} }
@@ -200,9 +207,14 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
super.beforeSaving(); super.beforeSaving();
if (this.position === undefined || this.position === null) { if (this.position === undefined || this.position === null) {
this.position = 10 + sql.getValue<number>(`SELECT COALESCE(MAX(position), 0) this.position =
10 +
sql.getValue<number>(
`SELECT COALESCE(MAX(position), 0)
FROM attachments FROM attachments
WHERE ownerId = ?`, [this.noteId]); WHERE ownerId = ?`,
[this.noteId]
);
} }
this.dateModified = dateUtils.localNowDateTime(); this.dateModified = dateUtils.localNowDateTime();
@@ -234,8 +246,7 @@ class BAttachment extends AbstractBeccaEntity<BAttachment> {
if (pojo.isProtected) { if (pojo.isProtected) {
if (this.isDecrypted) { if (this.isDecrypted) {
pojo.title = protectedSessionService.encrypt(pojo.title || "") || undefined; pojo.title = protectedSessionService.encrypt(pojo.title || "") || undefined;
} } else {
else {
// updating protected note outside of protected session means we will keep original ciphertexts // updating protected note outside of protected session means we will keep original ciphertexts
delete pojo.title; delete pojo.title;
} }

View File

@@ -5,7 +5,7 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
import dateUtils from "../../services/date_utils.js"; import dateUtils from "../../services/date_utils.js";
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js"; import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
import sanitizeAttributeName from "../../services/sanitize_attribute_name.js"; import sanitizeAttributeName from "../../services/sanitize_attribute_name.js";
import { AttributeRow, AttributeType } from './rows.js'; import type { AttributeRow, AttributeType } from "./rows.js";
interface SavingOpts { interface SavingOpts {
skipValidation?: boolean; skipValidation?: boolean;
@@ -16,9 +16,15 @@ interface SavingOpts {
* and relation (representing named relationship between source and target note) * and relation (representing named relationship between source and target note)
*/ */
class BAttribute extends AbstractBeccaEntity<BAttribute> { class BAttribute extends AbstractBeccaEntity<BAttribute> {
static get entityName() { return "attributes"; } static get entityName() {
static get primaryKeyName() { return "attributeId"; } return "attributes";
static get hashedProperties() { return ["attributeId", "noteId", "type", "name", "value", "isInheritable"]; } }
static get primaryKeyName() {
return "attributeId";
}
static get hashedProperties() {
return ["attributeId", "noteId", "type", "name", "value", "isInheritable"];
}
attributeId!: string; attributeId!: string;
noteId!: string; noteId!: string;
@@ -40,16 +46,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
} }
updateFromRow(row: AttributeRow) { updateFromRow(row: AttributeRow) {
this.update([ this.update([row.attributeId, row.noteId, row.type, row.name, row.value, row.isInheritable, row.position, row.utcDateModified]);
row.attributeId,
row.noteId,
row.type,
row.name,
row.value,
row.isInheritable,
row.position,
row.utcDateModified
]);
} }
update([attributeId, noteId, type, name, value, isInheritable, position, utcDateModified]: any) { update([attributeId, noteId, type, name, value, isInheritable, position, utcDateModified]: any) {
@@ -72,7 +69,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
if (!(this.noteId in this.becca.notes)) { if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync, create skeleton which will be filled later // entities can come out of order in sync, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({noteId: this.noteId})); this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
} }
this.becca.notes[this.noteId].ownedAttributes.push(this); this.becca.notes[this.noteId].ownedAttributes.push(this);
@@ -97,22 +94,22 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
throw new Error(`Invalid empty name in attribute '${this.attributeId}' of note '${this.noteId}'`); throw new Error(`Invalid empty name in attribute '${this.attributeId}' of note '${this.noteId}'`);
} }
if (this.type === 'relation' && !(this.value in this.becca.notes)) { if (this.type === "relation" && !(this.value in this.becca.notes)) {
throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it targets not existing note '${this.value}'.`); throw new Error(`Cannot save relation '${this.name}' of note '${this.noteId}' since it targets not existing note '${this.value}'.`);
} }
} }
get isAffectingSubtree() { get isAffectingSubtree() {
return this.isInheritable return this.isInheritable || (this.type === "relation" && ["template", "inherit"].includes(this.name));
|| (this.type === 'relation' && ['template', 'inherit'].includes(this.name));
} }
get targetNoteId() { // alias get targetNoteId() {
return this.type === 'relation' ? this.value : undefined; // alias
return this.type === "relation" ? this.value : undefined;
} }
isAutoLink() { isAutoLink() {
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name); return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
} }
get note() { get note() {
@@ -120,7 +117,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
} }
get targetNote() { get targetNote() {
if (this.type === 'relation') { if (this.type === "relation") {
return this.becca.notes[this.value]; return this.becca.notes[this.value];
} }
} }
@@ -136,7 +133,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
} }
getTargetNote() { getTargetNote() {
if (this.type !== 'relation') { if (this.type !== "relation") {
throw new Error(`Attribute '${this.attributeId}' is not a relation.`); throw new Error(`Attribute '${this.attributeId}' is not a relation.`);
} }
@@ -148,7 +145,7 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
} }
isDefinition() { isDefinition() {
return this.type === 'label' && (this.name.startsWith('label:') || this.name.startsWith('relation:')); return this.type === "label" && (this.name.startsWith("label:") || this.name.startsWith("relation:"));
} }
getDefinition() { getDefinition() {
@@ -156,9 +153,9 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
} }
getDefinedName() { getDefinedName() {
if (this.type === 'label' && this.name.startsWith('label:')) { if (this.type === "label" && this.name.startsWith("label:")) {
return this.name.substr(6); return this.name.substr(6);
} else if (this.type === 'label' && this.name.startsWith('relation:')) { } else if (this.type === "label" && this.name.startsWith("relation:")) {
return this.name.substr(9); return this.name.substr(9);
} else { } else {
return this.name; return this.name;
@@ -182,7 +179,8 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
} }
if (this.position === undefined || this.position === null) { if (this.position === undefined || this.position === null) {
const maxExistingPosition = this.getNote().getAttributes() const maxExistingPosition = this.getNote()
.getAttributes()
.reduce((maxPosition, attr) => Math.max(maxPosition, attr.position || 0), 0); .reduce((maxPosition, attr) => Math.max(maxPosition, attr.position || 0), 0);
this.position = maxExistingPosition + 10; this.position = maxExistingPosition + 10;

View File

@@ -1,11 +1,17 @@
import AbstractBeccaEntity from "./abstract_becca_entity.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js";
import { BlobRow } from "./rows.js"; import type { BlobRow } from "./rows.js";
// TODO: Why this does not extend the abstract becca? // TODO: Why this does not extend the abstract becca?
class BBlob extends AbstractBeccaEntity<BBlob> { class BBlob extends AbstractBeccaEntity<BBlob> {
static get entityName() { return "blobs"; } static get entityName() {
static get primaryKeyName() { return "blobId"; } return "blobs";
static get hashedProperties() { return ["blobId", "content"]; } }
static get primaryKeyName() {
return "blobId";
}
static get hashedProperties() {
return ["blobId", "content"];
}
content!: string | Buffer; content!: string | Buffer;
contentLength!: number; contentLength!: number;

View File

@@ -7,7 +7,7 @@ import utils from "../../services/utils.js";
import TaskContext from "../../services/task_context.js"; import TaskContext from "../../services/task_context.js";
import cls from "../../services/cls.js"; import cls from "../../services/cls.js";
import log from "../../services/log.js"; import log from "../../services/log.js";
import { BranchRow } from './rows.js'; import type { BranchRow } from "./rows.js";
import handlers from "../../services/handlers.js"; import handlers from "../../services/handlers.js";
/** /**
@@ -18,10 +18,16 @@ import handlers from "../../services/handlers.js";
* Always check noteId instead. * Always check noteId instead.
*/ */
class BBranch extends AbstractBeccaEntity<BBranch> { class BBranch extends AbstractBeccaEntity<BBranch> {
static get entityName() { return "branches"; } static get entityName() {
static get primaryKeyName() { return "branchId"; } return "branches";
}
static get primaryKeyName() {
return "branchId";
}
// notePosition is not part of hash because it would produce a lot of updates in case of reordering // notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "prefix"]; } static get hashedProperties() {
return ["branchId", "noteId", "parentNoteId", "prefix"];
}
branchId?: string; branchId?: string;
noteId!: string; noteId!: string;
@@ -42,15 +48,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
} }
updateFromRow(row: BranchRow) { updateFromRow(row: BranchRow) {
this.update([ this.update([row.branchId, row.noteId, row.parentNoteId, row.prefix, row.notePosition, row.isExpanded, row.utcDateModified]);
row.branchId,
row.noteId,
row.parentNoteId,
row.prefix,
row.notePosition,
row.isExpanded,
row.utcDateModified
]);
} }
update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified]: any) { update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified]: any) {
@@ -78,7 +76,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
childNote.parentBranches.push(this); childNote.parentBranches.push(this);
} }
if (this.noteId === 'root') { if (this.noteId === "root") {
return; return;
} }
@@ -97,7 +95,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
get childNote(): BNote { get childNote(): BNote {
if (!(this.noteId in this.becca.notes)) { if (!(this.noteId in this.becca.notes)) {
// entities can come out of order in sync/import, create skeleton which will be filled later // entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.noteId, new BNote({noteId: this.noteId})); this.becca.addNote(this.noteId, new BNote({ noteId: this.noteId }));
} }
return this.becca.notes[this.noteId]; return this.becca.notes[this.noteId];
@@ -109,43 +107,43 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
/** @returns root branch will have undefined parent, all other branches have to have a parent note */ /** @returns root branch will have undefined parent, all other branches have to have a parent note */
get parentNote(): BNote | undefined { get parentNote(): BNote | undefined {
if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== 'none') { if (!(this.parentNoteId in this.becca.notes) && this.parentNoteId !== "none") {
// entities can come out of order in sync/import, create skeleton which will be filled later // entities can come out of order in sync/import, create skeleton which will be filled later
this.becca.addNote(this.parentNoteId, new BNote({noteId: this.parentNoteId})); this.becca.addNote(this.parentNoteId, new BNote({ noteId: this.parentNoteId }));
} }
return this.becca.notes[this.parentNoteId]; return this.becca.notes[this.parentNoteId];
} }
get isDeleted() { get isDeleted() {
return (this.branchId == undefined || !(this.branchId in this.becca.branches)); return this.branchId == undefined || !(this.branchId in this.becca.branches);
} }
/** /**
* Branch is weak when its existence should not hinder deletion of its note. * Branch is weak when its existence should not hinder deletion of its note.
* As a result, note with only weak branches should be immediately deleted. * As a result, note with only weak branches should be immediately deleted.
* An example is shared or bookmarked clones - they are created automatically and exist for technical reasons, * An example is shared or bookmarked clones - they are created automatically and exist for technical reasons,
* not as user-intended actions. From user perspective, they don't count as real clones and for the purpose * not as user-intended actions. From user perspective, they don't count as real clones and for the purpose
* of deletion should not act as a clone. * of deletion should not act as a clone.
*/ */
get isWeak() { get isWeak() {
return ['_share', '_lbBookmarks'].includes(this.parentNoteId); return ["_share", "_lbBookmarks"].includes(this.parentNoteId);
} }
/** /**
* Delete a branch. If this is a last note's branch, delete the note as well. * Delete a branch. If this is a last note's branch, delete the note as well.
* *
* @param deleteId - optional delete identified * @param deleteId - optional delete identified
* *
* @returns true if note has been deleted, false otherwise * @returns true if note has been deleted, false otherwise
*/ */
deleteBranch(deleteId?: string, taskContext?: TaskContext): boolean { deleteBranch(deleteId?: string, taskContext?: TaskContext): boolean {
if (!deleteId) { if (!deleteId) {
deleteId = utils.randomString(10); deleteId = utils.randomString(10);
} }
if (!taskContext) { if (!taskContext) {
taskContext = new TaskContext('no-progress-reporting'); taskContext = new TaskContext("no-progress-reporting");
} }
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
@@ -157,13 +155,11 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
if (parentBranches.length === 1 && parentBranches[0] === this) { if (parentBranches.length === 1 && parentBranches[0] === this) {
// needs to be run before branches and attributes are deleted and thus attached relations disappear // needs to be run before branches and attributes are deleted and thus attached relations disappear
handlers.runAttachedRelations(note, 'runOnNoteDeletion', note); handlers.runAttachedRelations(note, "runOnNoteDeletion", note);
} }
} }
if (this.noteId === 'root' if (this.noteId === "root" || this.noteId === cls.getHoistedNoteId()) {
|| this.noteId === cls.getHoistedNoteId()) {
throw new Error("Can't delete root or hoisted branch/note"); throw new Error("Can't delete root or hoisted branch/note");
} }
@@ -203,8 +199,7 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
note.markAsDeleted(deleteId); note.markAsDeleted(deleteId);
return true; return true;
} } else {
else {
return false; return false;
} }
} }
@@ -225,8 +220,9 @@ class BBranch extends AbstractBeccaEntity<BBranch> {
continue; continue;
} }
if (maxNotePos < childBranch.notePosition if (
&& childBranch.noteId !== '_hidden' // hidden has a very large notePosition to always stay last maxNotePos < childBranch.notePosition &&
childBranch.noteId !== "_hidden" // hidden has a very large notePosition to always stay last
) { ) {
maxNotePos = childBranch.notePosition; maxNotePos = childBranch.notePosition;
} }

View File

@@ -1,6 +1,6 @@
"use strict"; "use strict";
import { EtapiTokenRow } from "./rows.js"; import type { EtapiTokenRow } from "./rows.js";
import dateUtils from "../../services/date_utils.js"; import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js";
@@ -15,9 +15,15 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
* from tokenHash and token. * from tokenHash and token.
*/ */
class BEtapiToken extends AbstractBeccaEntity<BEtapiToken> { class BEtapiToken extends AbstractBeccaEntity<BEtapiToken> {
static get entityName() { return "etapi_tokens"; } static get entityName() {
static get primaryKeyName() { return "etapiTokenId"; } return "etapi_tokens";
static get hashedProperties() { return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"]; } }
static get primaryKeyName() {
return "etapiTokenId";
}
static get hashedProperties() {
return ["etapiTokenId", "name", "tokenHash", "utcDateCreated", "utcDateModified", "isDeleted"];
}
etapiTokenId?: string; etapiTokenId?: string;
name!: string; name!: string;
@@ -66,7 +72,7 @@ class BEtapiToken extends AbstractBeccaEntity<BEtapiToken> {
utcDateCreated: this.utcDateCreated, utcDateCreated: this.utcDateCreated,
utcDateModified: this.utcDateModified, utcDateModified: this.utcDateModified,
isDeleted: this.isDeleted isDeleted: this.isDeleted
} };
} }
beforeSaving() { beforeSaving() {

File diff suppressed because it is too large Load Diff

View File

@@ -2,15 +2,21 @@
import dateUtils from "../../services/date_utils.js"; import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js";
import { OptionRow } from './rows.js'; import type { OptionRow } from "./rows.js";
/** /**
* Option represents a name-value pair, either directly configurable by the user or some system property. * Option represents a name-value pair, either directly configurable by the user or some system property.
*/ */
class BOption extends AbstractBeccaEntity<BOption> { class BOption extends AbstractBeccaEntity<BOption> {
static get entityName() { return "options"; } static get entityName() {
static get primaryKeyName() { return "name"; } return "options";
static get hashedProperties() { return ["name", "value"]; } }
static get primaryKeyName() {
return "name";
}
static get hashedProperties() {
return ["name", "value"];
}
name!: string; name!: string;
value!: string; value!: string;
@@ -43,7 +49,7 @@ class BOption extends AbstractBeccaEntity<BOption> {
value: this.value, value: this.value,
isSynced: this.isSynced, isSynced: this.isSynced,
utcDateModified: this.utcDateModified utcDateModified: this.utcDateModified
} };
} }
} }

View File

@@ -1,6 +1,6 @@
"use strict"; "use strict";
import { RecentNoteRow } from "./rows.js"; import type { RecentNoteRow } from "./rows.js";
import dateUtils from "../../services/date_utils.js"; import dateUtils from "../../services/date_utils.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js";
@@ -9,9 +9,15 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
* RecentNote represents recently visited note. * RecentNote represents recently visited note.
*/ */
class BRecentNote extends AbstractBeccaEntity<BRecentNote> { class BRecentNote extends AbstractBeccaEntity<BRecentNote> {
static get entityName() { return "recent_notes"; } static get entityName() {
static get primaryKeyName() { return "noteId"; } return "recent_notes";
static get hashedProperties() { return ["noteId", "notePath"]; } }
static get primaryKeyName() {
return "noteId";
}
static get hashedProperties() {
return ["noteId", "notePath"];
}
noteId!: string; noteId!: string;
notePath!: string; notePath!: string;
@@ -33,7 +39,7 @@ class BRecentNote extends AbstractBeccaEntity<BRecentNote> {
noteId: this.noteId, noteId: this.noteId,
notePath: this.notePath, notePath: this.notePath,
utcDateCreated: this.utcDateCreated utcDateCreated: this.utcDateCreated
} };
} }
} }

View File

@@ -7,7 +7,7 @@ import becca from "../becca.js";
import AbstractBeccaEntity from "./abstract_becca_entity.js"; import AbstractBeccaEntity from "./abstract_becca_entity.js";
import sql from "../../services/sql.js"; import sql from "../../services/sql.js";
import BAttachment from "./battachment.js"; import BAttachment from "./battachment.js";
import { AttachmentRow, RevisionRow } from './rows.js'; import type { AttachmentRow, RevisionRow } from "./rows.js";
import eraseService from "../../services/erase.js"; import eraseService from "../../services/erase.js";
interface ContentOpts { interface ContentOpts {
@@ -24,10 +24,15 @@ interface GetByIdOpts {
* It's used for seamless note versioning. * It's used for seamless note versioning.
*/ */
class BRevision extends AbstractBeccaEntity<BRevision> { class BRevision extends AbstractBeccaEntity<BRevision> {
static get entityName() { return "revisions"; } static get entityName() {
static get primaryKeyName() { return "revisionId"; } return "revisions";
static get hashedProperties() { return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", }
"utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"]; } static get primaryKeyName() {
return "revisionId";
}
static get hashedProperties() {
return ["revisionId", "noteId", "title", "isProtected", "dateLastEdited", "dateCreated", "utcDateLastEdited", "utcDateCreated", "utcDateModified", "blobId"];
}
revisionId?: string; revisionId?: string;
noteId!: string; noteId!: string;
@@ -75,25 +80,27 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
} }
isContentAvailable() { isContentAvailable() {
return !this.revisionId // new note which was not encrypted yet return (
|| !this.isProtected !this.revisionId || // new note which was not encrypted yet
|| protectedSessionService.isProtectedSessionAvailable() !this.isProtected ||
protectedSessionService.isProtectedSessionAvailable()
);
} }
/* /*
* Note revision content has quite special handling - it's not a separate entity, but a lazily loaded * Note revision content has quite special handling - it's not a separate entity, but a lazily loaded
* part of Revision entity with its own sync. The reason behind this hybrid design is that * part of Revision entity with its own sync. The reason behind this hybrid design is that
* content can be quite large, and it's not necessary to load it / fill memory for any note access even * content can be quite large, and it's not necessary to load it / fill memory for any note access even
* if we don't need a content, especially for bulk operations like search. * if we don't need a content, especially for bulk operations like search.
* *
* This is the same approach as is used for Note's content. * This is the same approach as is used for Note's content.
*/ */
getContent(): string | Buffer { getContent(): string | Buffer {
return this._getContent(); return this._getContent();
} }
/** /**
* @throws Error in case of invalid JSON */ * @throws Error in case of invalid JSON */
getJsonContent(): {} | null { getJsonContent(): {} | null {
const content = this.getContent(); const content = this.getContent();
@@ -108,8 +115,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
getJsonContentSafely(): {} | null { getJsonContentSafely(): {} | null {
try { try {
return this.getJsonContent(); return this.getJsonContent();
} } catch (e) {
catch (e) {
return null; return null;
} }
} }
@@ -119,12 +125,16 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
} }
getAttachments(): BAttachment[] { getAttachments(): BAttachment[] {
return sql.getRows<AttachmentRow>(` return sql
.getRows<AttachmentRow>(
`
SELECT attachments.* SELECT attachments.*
FROM attachments FROM attachments
WHERE ownerId = ? WHERE ownerId = ?
AND isDeleted = 0`, [this.revisionId]) AND isDeleted = 0`,
.map(row => new BAttachment(row)); [this.revisionId]
)
.map((row) => new BAttachment(row));
} }
getAttachmentById(attachmentId: String, opts: GetByIdOpts = {}): BAttachment | null { getAttachmentById(attachmentId: String, opts: GetByIdOpts = {}): BAttachment | null {
@@ -137,29 +147,32 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0` WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`; : `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [this.revisionId, attachmentId]) return sql.getRows<AttachmentRow>(query, [this.revisionId, attachmentId]).map((row) => new BAttachment(row))[0];
.map(row => new BAttachment(row))[0];
} }
getAttachmentsByRole(role: string): BAttachment[] { getAttachmentsByRole(role: string): BAttachment[] {
return sql.getRows<AttachmentRow>(` return sql
.getRows<AttachmentRow>(
`
SELECT attachments.* SELECT attachments.*
FROM attachments FROM attachments
WHERE ownerId = ? WHERE ownerId = ?
AND role = ? AND role = ?
AND isDeleted = 0 AND isDeleted = 0
ORDER BY position`, [this.revisionId, role]) ORDER BY position`,
.map(row => new BAttachment(row)); [this.revisionId, role]
)
.map((row) => new BAttachment(row));
} }
getAttachmentByTitle(title: string): BAttachment { getAttachmentByTitle(title: string): BAttachment {
// cannot use SQL to filter by title since it can be encrypted // cannot use SQL to filter by title since it can be encrypted
return this.getAttachments().filter(attachment => attachment.title === title)[0]; return this.getAttachments().filter((attachment) => attachment.title === title)[0];
} }
/** /**
* Revisions are not soft-deletable, they are immediately hard-deleted (erased). * Revisions are not soft-deletable, they are immediately hard-deleted (erased).
*/ */
eraseRevision() { eraseRevision() {
if (this.revisionId) { if (this.revisionId) {
eraseService.eraseRevisions([this.revisionId]); eraseService.eraseRevisions([this.revisionId]);
@@ -199,8 +212,7 @@ class BRevision extends AbstractBeccaEntity<BRevision> {
if (pojo.isProtected) { if (pojo.isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) { if (protectedSessionService.isProtectedSessionAvailable()) {
pojo.title = protectedSessionService.encrypt(this.title) || undefined; pojo.title = protectedSessionService.encrypt(this.title) || undefined;
} } else {
else {
// updating protected note outside of protected session means we will keep original ciphertexts // updating protected note outside of protected session means we will keep original ciphertexts
delete pojo.title; delete pojo.title;
} }

View File

@@ -100,8 +100,25 @@ export interface BranchRow {
* end user. Those types should be used only for checking against, they are * end user. Those types should be used only for checking against, they are
* not for direct use. * not for direct use.
*/ */
export const ALLOWED_NOTE_TYPES = [ "file", "image", "search", "noteMap", "launcher", "doc", "contentWidget", "text", "relationMap", "render", "canvas", "mermaid", "book", "webView", "code", "mindMap" ] as const; export const ALLOWED_NOTE_TYPES = [
export type NoteType = typeof ALLOWED_NOTE_TYPES[number]; "file",
"image",
"search",
"noteMap",
"launcher",
"doc",
"contentWidget",
"text",
"relationMap",
"render",
"canvas",
"mermaid",
"book",
"webView",
"code",
"mindMap"
] as const;
export type NoteType = (typeof ALLOWED_NOTE_TYPES)[number];
export interface NoteRow { export interface NoteRow {
noteId: string; noteId: string;

View File

@@ -1,4 +1,4 @@
import { ConstructorData } from './becca-interface.js'; import type { ConstructorData } from "./becca-interface.js";
import AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
import BAttachment from "./entities/battachment.js"; import BAttachment from "./entities/battachment.js";
import BAttribute from "./entities/battribute.js"; import BAttribute from "./entities/battribute.js";
@@ -13,15 +13,15 @@ import BRevision from "./entities/brevision.js";
type EntityClass = new (row?: any) => AbstractBeccaEntity<any>; type EntityClass = new (row?: any) => AbstractBeccaEntity<any>;
const ENTITY_NAME_TO_ENTITY: Record<string, ConstructorData<any> & EntityClass> = { const ENTITY_NAME_TO_ENTITY: Record<string, ConstructorData<any> & EntityClass> = {
"attachments": BAttachment, attachments: BAttachment,
"attributes": BAttribute, attributes: BAttribute,
"blobs": BBlob, blobs: BBlob,
"branches": BBranch, branches: BBranch,
"etapi_tokens": BEtapiToken, etapi_tokens: BEtapiToken,
"notes": BNote, notes: BNote,
"options": BOption, options: BOption,
"recent_notes": BRecentNote, recent_notes: BRecentNote,
"revisions": BRevision revisions: BRevision
}; };
function getEntityFromEntityName(entityName: keyof typeof ENTITY_NAME_TO_ENTITY) { function getEntityFromEntityName(entityName: keyof typeof ENTITY_NAME_TO_ENTITY) {

View File

@@ -7,11 +7,7 @@ import BNote from "./entities/bnote.js";
const DEBUG = false; const DEBUG = false;
const IGNORED_ATTRS = [ const IGNORED_ATTRS = ["datenote", "monthnote", "yearnote"];
"datenote",
"monthnote",
"yearnote"
];
const IGNORED_ATTR_NAMES = [ const IGNORED_ATTR_NAMES = [
"includenotelink", "includenotelink",
@@ -30,7 +26,7 @@ const IGNORED_ATTR_NAMES = [
"similarnoteswidgetdisabled", "similarnoteswidgetdisabled",
"disableinclusion", "disableinclusion",
"rendernote", "rendernote",
"pageurl", "pageurl"
]; ];
interface DateLimits { interface DateLimits {
@@ -42,9 +38,9 @@ interface DateLimits {
function filterUrlValue(value: string) { function filterUrlValue(value: string) {
return value return value
.replace(/https?:\/\//ig, "") .replace(/https?:\/\//gi, "")
.replace(/www.js\./ig, "") .replace(/www.js\./gi, "")
.replace(/(\.net|\.com|\.org|\.info|\.edu)/ig, ""); .replace(/(\.net|\.com|\.org|\.info|\.edu)/gi, "");
} }
function buildRewardMap(note: BNote) { function buildRewardMap(note: BNote) {
@@ -61,8 +57,7 @@ function buildRewardMap(note: BNote) {
const currentReward = map.get(word) || 0; const currentReward = map.get(word) || 0;
// reward grows with the length of matched string // reward grows with the length of matched string
const length = word.length const length = word.length - 0.9; // to penalize specifically very short words - 1 and 2 characters
- 0.9; // to penalize specifically very short words - 1 and 2 characters
map.set(word, currentReward + rewardFactor * Math.pow(length, 0.7)); map.set(word, currentReward + rewardFactor * Math.pow(length, 0.7));
} }
@@ -70,7 +65,7 @@ function buildRewardMap(note: BNote) {
} }
for (const ancestorNote of note.getAncestors()) { for (const ancestorNote of note.getAncestors()) {
if (ancestorNote.noteId === 'root') { if (ancestorNote.noteId === "root") {
continue; continue;
} }
@@ -94,9 +89,7 @@ function buildRewardMap(note: BNote) {
} }
for (const attr of note.getAttributes()) { for (const attr of note.getAttributes()) {
if (attr.name.startsWith('child:') if (attr.name.startsWith("child:") || attr.name.startsWith("relation:") || attr.name.startsWith("label:")) {
|| attr.name.startsWith('relation:')
|| attr.name.startsWith('label:')) {
continue; continue;
} }
@@ -111,13 +104,13 @@ function buildRewardMap(note: BNote) {
addToRewardMap(attr.name, reward); addToRewardMap(attr.name, reward);
} }
if (attr.name === 'cliptype') { if (attr.name === "cliptype") {
reward /= 2; reward /= 2;
} }
let value = attr.value; let value = attr.value;
if (value.startsWith('http')) { if (value.startsWith("http")) {
value = filterUrlValue(value); value = filterUrlValue(value);
// words in URLs are not that valuable // words in URLs are not that valuable
@@ -127,7 +120,7 @@ function buildRewardMap(note: BNote) {
addToRewardMap(value, reward); addToRewardMap(value, reward);
} }
if (note.type === 'text' && note.isDecrypted) { if (note.type === "text" && note.isDecrypted) {
const content = note.getContent(); const content = note.getContent();
const dom = new JSDOM(content); const dom = new JSDOM(content);
@@ -135,7 +128,7 @@ function buildRewardMap(note: BNote) {
for (const el of dom.window.document.querySelectorAll(elName)) { for (const el of dom.window.document.querySelectorAll(elName)) {
addToRewardMap(el.textContent, rewardFactor); addToRewardMap(el.textContent, rewardFactor);
} }
} };
// the title is the top with weight 1 so smaller headings will have lower weight // the title is the top with weight 1 so smaller headings will have lower weight
@@ -154,12 +147,12 @@ function buildRewardMap(note: BNote) {
const mimeCache: Record<string, string> = {}; const mimeCache: Record<string, string> = {};
function trimMime(mime: string) { function trimMime(mime: string) {
if (!mime || mime === 'text/html') { if (!mime || mime === "text/html") {
return; return;
} }
if (!(mime in mimeCache)) { if (!(mime in mimeCache)) {
const chunks = mime.split('/'); const chunks = mime.split("/");
let str = ""; let str = "";
@@ -167,7 +160,7 @@ function trimMime(mime: string) {
// we're not interested in 'text/' or 'application/' prefix // we're not interested in 'text/' or 'application/' prefix
str = chunks[1]; str = chunks[1];
if (str.startsWith('-x')) { if (str.startsWith("-x")) {
str = str.substr(2); str = str.substr(2);
} }
} }
@@ -185,7 +178,7 @@ function buildDateLimits(baseNote: BNote): DateLimits {
minDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs - 3600 * 1000)), minDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs - 3600 * 1000)),
minExcludedDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs - 5 * 1000)), minExcludedDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs - 5 * 1000)),
maxExcludedDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 5 * 1000)), maxExcludedDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 5 * 1000)),
maxDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 3600 * 1000)), maxDate: dateUtils.utcDateTimeStr(new Date(dateCreatedTs + 3600 * 1000))
}; };
} }
@@ -193,9 +186,34 @@ function buildDateLimits(baseNote: BNote): DateLimits {
const wordCache = new Map(); const wordCache = new Map();
const WORD_BLACKLIST = [ const WORD_BLACKLIST = [
"a", "the", "in", "for", "from", "but", "s", "so", "if", "while", "until", "a",
"whether", "after", "before", "because", "since", "when", "where", "how", "the",
"than", "then", "and", "either", "or", "neither", "nor", "both", "also" "in",
"for",
"from",
"but",
"s",
"so",
"if",
"while",
"until",
"whether",
"after",
"before",
"because",
"since",
"when",
"where",
"how",
"than",
"then",
"and",
"either",
"or",
"neither",
"nor",
"both",
"also"
]; ];
function splitToWords(text: string) { function splitToWords(text: string) {
@@ -212,8 +230,7 @@ function splitToWords(text: string) {
// special case for english plurals // special case for english plurals
else if (words[idx].length > 2 && words[idx].endsWith("es")) { else if (words[idx].length > 2 && words[idx].endsWith("es")) {
words[idx] = words[idx].substr(0, words[idx] - 2); words[idx] = words[idx].substr(0, words[idx] - 2);
} } else if (words[idx].length > 1 && words[idx].endsWith("s")) {
else if (words[idx].length > 1 && words[idx].endsWith("s")) {
words[idx] = words[idx].substr(0, words[idx] - 1); words[idx] = words[idx].substr(0, words[idx] - 1);
} }
} }
@@ -227,9 +244,7 @@ function splitToWords(text: string) {
* that it doesn't actually need to be shown to the user. * that it doesn't actually need to be shown to the user.
*/ */
function hasConnectingRelation(sourceNote: BNote, targetNote: BNote) { function hasConnectingRelation(sourceNote: BNote, targetNote: BNote) {
return sourceNote.getAttributes().find(attr => attr.type === 'relation' return sourceNote.getAttributes().find((attr) => attr.type === "relation" && ["includenotelink", "imagelink"].includes(attr.name) && attr.value === targetNote.noteId);
&& ['includenotelink', 'imagelink'].includes(attr.name)
&& attr.value === targetNote.noteId);
} }
async function findSimilarNotes(noteId: string) { async function findSimilarNotes(noteId: string) {
@@ -246,14 +261,13 @@ async function findSimilarNotes(noteId: string) {
try { try {
dateLimits = buildDateLimits(baseNote); dateLimits = buildDateLimits(baseNote);
} } catch (e: any) {
catch (e: any) {
throw new Error(`Date limits failed with ${e.message}, entity: ${JSON.stringify(baseNote.getPojo())}`); throw new Error(`Date limits failed with ${e.message}, entity: ${JSON.stringify(baseNote.getPojo())}`);
} }
const rewardMap = buildRewardMap(baseNote); const rewardMap = buildRewardMap(baseNote);
let ancestorRewardCache: Record<string, number> = {}; let ancestorRewardCache: Record<string, number> = {};
const ancestorNoteIds = new Set(baseNote.getAncestors().map(note => note.noteId)); const ancestorNoteIds = new Set(baseNote.getAncestors().map((note) => note.noteId));
ancestorNoteIds.add(baseNote.noteId); ancestorNoteIds.add(baseNote.noteId);
let displayRewards = false; let displayRewards = false;
@@ -270,7 +284,7 @@ async function findSimilarNotes(noteId: string) {
const lengthPenalization = 1 / Math.pow(text.length, 0.3); const lengthPenalization = 1 / Math.pow(text.length, 0.3);
for (const word of splitToWords(text)) { for (const word of splitToWords(text)) {
const reward = (rewardMap.get(word) * factor * lengthPenalization) || 0; const reward = rewardMap.get(word) * factor * lengthPenalization || 0;
if (displayRewards && reward > 0) { if (displayRewards && reward > 0) {
console.log(`Reward ${Math.round(reward * 10) / 10} for word: ${word}`); console.log(`Reward ${Math.round(reward * 10) / 10} for word: ${word}`);
@@ -294,7 +308,6 @@ async function findSimilarNotes(noteId: string) {
for (const parentNote of note.parents) { for (const parentNote of note.parents) {
if (!ancestorNoteIds.has(parentNote.noteId)) { if (!ancestorNoteIds.has(parentNote.noteId)) {
if (displayRewards) { if (displayRewards) {
console.log("Considering", parentNote.title); console.log("Considering", parentNote.title);
} }
@@ -304,8 +317,7 @@ async function findSimilarNotes(noteId: string) {
} }
for (const branch of parentNote.getParentBranches()) { for (const branch of parentNote.getParentBranches()) {
score += gatherRewards(branch.prefix, 0.3) score += gatherRewards(branch.prefix, 0.3) + gatherAncestorRewards(branch.parentNote);
+ gatherAncestorRewards(branch.parentNote);
} }
} }
} }
@@ -317,8 +329,7 @@ async function findSimilarNotes(noteId: string) {
} }
function computeScore(candidateNote: BNote) { function computeScore(candidateNote: BNote) {
let score = gatherRewards(trimMime(candidateNote.mime)) let score = gatherRewards(trimMime(candidateNote.mime)) + gatherAncestorRewards(candidateNote);
+ gatherAncestorRewards(candidateNote);
if (candidateNote.isDecrypted) { if (candidateNote.isDecrypted) {
score += gatherRewards(candidateNote.title); score += gatherRewards(candidateNote.title);
@@ -329,9 +340,7 @@ async function findSimilarNotes(noteId: string) {
} }
for (const attr of candidateNote.getAttributes()) { for (const attr of candidateNote.getAttributes()) {
if (attr.name.startsWith('child:') if (attr.name.startsWith("child:") || attr.name.startsWith("relation:") || attr.name.startsWith("label:")) {
|| attr.name.startsWith('relation:')
|| attr.name.startsWith('label:')) {
continue; continue;
} }
@@ -349,8 +358,7 @@ async function findSimilarNotes(noteId: string) {
if (!value.startsWith) { if (!value.startsWith) {
log.info(`Unexpected falsy value for attribute ${JSON.stringify(attr.getPojo())}`); log.info(`Unexpected falsy value for attribute ${JSON.stringify(attr.getPojo())}`);
continue; continue;
} } else if (value.startsWith("http")) {
else if (value.startsWith('http')) {
value = filterUrlValue(value); value = filterUrlValue(value);
// words in URLs are not that valuable // words in URLs are not that valuable
@@ -369,13 +377,13 @@ async function findSimilarNotes(noteId: string) {
} }
/** /**
* We want to improve the standing of notes which have been created in similar time to each other since * We want to improve the standing of notes which have been created in similar time to each other since
* there's a good chance they are related. * there's a good chance they are related.
* *
* But there's an exception - if they were created really close to each other (within few seconds) then * But there's an exception - if they were created really close to each other (within few seconds) then
* they are probably part of the import and not created by hand - these OTOH should not benefit. * they are probably part of the import and not created by hand - these OTOH should not benefit.
*/ */
const {utcDateCreated} = candidateNote; const { utcDateCreated } = candidateNote;
if (utcDateCreated < dateLimits.minExcludedDate || utcDateCreated > dateLimits.maxExcludedDate) { if (utcDateCreated < dateLimits.minExcludedDate || utcDateCreated > dateLimits.maxExcludedDate) {
if (utcDateCreated >= dateLimits.minDate && utcDateCreated <= dateLimits.maxDate) { if (utcDateCreated >= dateLimits.minDate && utcDateCreated <= dateLimits.maxDate) {
@@ -384,9 +392,7 @@ async function findSimilarNotes(noteId: string) {
} }
score += 1; score += 1;
} } else if (utcDateCreated.substr(0, 10) === dateLimits.minDate.substr(0, 10) || utcDateCreated.substr(0, 10) === dateLimits.maxDate.substr(0, 10)) {
else if (utcDateCreated.substr(0, 10) === dateLimits.minDate.substr(0, 10)
|| utcDateCreated.substr(0, 10) === dateLimits.maxDate.substr(0, 10)) {
if (displayRewards) { if (displayRewards) {
console.log("Adding reward for same day of creation"); console.log("Adding reward for same day of creation");
} }
@@ -400,9 +406,7 @@ async function findSimilarNotes(noteId: string) {
} }
for (const candidateNote of Object.values(becca.notes)) { for (const candidateNote of Object.values(becca.notes)) {
if (candidateNote.noteId === baseNote.noteId if (candidateNote.noteId === baseNote.noteId || hasConnectingRelation(candidateNote, baseNote) || hasConnectingRelation(baseNote, candidateNote)) {
|| hasConnectingRelation(candidateNote, baseNote)
|| hasConnectingRelation(baseNote, candidateNote)) {
continue; continue;
} }
@@ -420,7 +424,7 @@ async function findSimilarNotes(noteId: string) {
score -= 0.5; // archived penalization score -= 0.5; // archived penalization
} }
results.push({score, notePath, noteId: candidateNote.noteId}); results.push({ score, notePath, noteId: candidateNote.noteId });
} }
i++; i++;
@@ -430,13 +434,13 @@ async function findSimilarNotes(noteId: string) {
} }
} }
results.sort((a, b) => a.score > b.score ? -1 : 1); results.sort((a, b) => (a.score > b.score ? -1 : 1));
if (DEBUG) { if (DEBUG) {
console.log("REWARD MAP", rewardMap); console.log("REWARD MAP", rewardMap);
if (results.length >= 1) { if (results.length >= 1) {
for (const {noteId} of results) { for (const { noteId } of results) {
const note = becca.notes[noteId]; const note = becca.notes[noteId];
displayRewards = true; displayRewards = true;

View File

@@ -1,9 +1,9 @@
import { Router } from 'express'; import { Router } from "express";
import appInfo from "../services/app_info.js"; import appInfo from "../services/app_info.js";
import eu from "./etapi_utils.js"; import eu from "./etapi_utils.js";
function register(router: Router) { function register(router: Router) {
eu.route(router, 'get', '/etapi/app-info', (req, res, next) => { eu.route(router, "get", "/etapi/app-info", (req, res, next) => {
res.status(200).json(appInfo); res.status(200).json(appInfo);
}); });
} }

View File

@@ -3,21 +3,21 @@ import eu from "./etapi_utils.js";
import mappers from "./mappers.js"; import mappers from "./mappers.js";
import v from "./validators.js"; import v from "./validators.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import { Router } from 'express'; import { Router } from "express";
import { AttachmentRow } from '../becca/entities/rows.js'; import type { AttachmentRow } from "../becca/entities/rows.js";
import { ValidatorMap } from './etapi-interface.js'; import type { ValidatorMap } from "./etapi-interface.js";
function register(router: Router) { function register(router: Router) {
const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT: ValidatorMap = { const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT: ValidatorMap = {
'ownerId': [v.notNull, v.isNoteId], ownerId: [v.notNull, v.isNoteId],
'role': [v.notNull, v.isString], role: [v.notNull, v.isString],
'mime': [v.notNull, v.isString], mime: [v.notNull, v.isString],
'title': [v.notNull, v.isString], title: [v.notNull, v.isString],
'position': [v.notNull, v.isInteger], position: [v.notNull, v.isInteger],
'content': [v.isString], content: [v.isString]
}; };
eu.route(router, 'post', '/etapi/attachments', (req, res, next) => { eu.route(router, "post", "/etapi/attachments", (req, res, next) => {
const _params: Partial<AttachmentRow> = {}; const _params: Partial<AttachmentRow> = {};
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT); eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT);
const params = _params as AttachmentRow; const params = _params as AttachmentRow;
@@ -30,26 +30,25 @@ function register(router: Router) {
const attachment = note.saveAttachment(params); const attachment = note.saveAttachment(params);
res.status(201).json(mappers.mapAttachmentToPojo(attachment)); res.status(201).json(mappers.mapAttachmentToPojo(attachment));
} } catch (e: any) {
catch (e: any) {
throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message); throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message);
} }
}); });
eu.route(router, 'get', '/etapi/attachments/:attachmentId', (req, res, next) => { eu.route(router, "get", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId); const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
res.json(mappers.mapAttachmentToPojo(attachment)); res.json(mappers.mapAttachmentToPojo(attachment));
}); });
const ALLOWED_PROPERTIES_FOR_PATCH = { const ALLOWED_PROPERTIES_FOR_PATCH = {
'role': [v.notNull, v.isString], role: [v.notNull, v.isString],
'mime': [v.notNull, v.isString], mime: [v.notNull, v.isString],
'title': [v.notNull, v.isString], title: [v.notNull, v.isString],
'position': [v.notNull, v.isInteger], position: [v.notNull, v.isInteger]
}; };
eu.route(router, 'patch', '/etapi/attachments/:attachmentId', (req, res, next) => { eu.route(router, "patch", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId); const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) { if (attachment.isProtected) {
@@ -62,7 +61,7 @@ function register(router: Router) {
res.json(mappers.mapAttachmentToPojo(attachment)); res.json(mappers.mapAttachmentToPojo(attachment));
}); });
eu.route(router, 'get', '/etapi/attachments/:attachmentId/content', (req, res, next) => { eu.route(router, "get", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId); const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) { if (attachment.isProtected) {
@@ -71,15 +70,15 @@ function register(router: Router) {
const filename = utils.formatDownloadTitle(attachment.title, attachment.role, attachment.mime); const filename = utils.formatDownloadTitle(attachment.title, attachment.role, attachment.mime);
res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); res.setHeader("Content-Disposition", utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', attachment.mime); res.setHeader("Content-Type", attachment.mime);
res.send(attachment.getContent()); res.send(attachment.getContent());
}); });
eu.route(router, 'put', '/etapi/attachments/:attachmentId/content', (req, res, next) => { eu.route(router, "put", "/etapi/attachments/:attachmentId/content", (req, res, next) => {
const attachment = eu.getAndCheckAttachment(req.params.attachmentId); const attachment = eu.getAndCheckAttachment(req.params.attachmentId);
if (attachment.isProtected) { if (attachment.isProtected) {
@@ -91,7 +90,7 @@ function register(router: Router) {
return res.sendStatus(204); return res.sendStatus(204);
}); });
eu.route(router, 'delete', '/etapi/attachments/:attachmentId', (req, res, next) => { eu.route(router, "delete", "/etapi/attachments/:attachmentId", (req, res, next) => {
const attachment = becca.getAttachment(req.params.attachmentId); const attachment = becca.getAttachment(req.params.attachmentId);
if (!attachment) { if (!attachment) {

View File

@@ -3,29 +3,29 @@ import eu from "./etapi_utils.js";
import mappers from "./mappers.js"; import mappers from "./mappers.js";
import attributeService from "../services/attributes.js"; import attributeService from "../services/attributes.js";
import v from "./validators.js"; import v from "./validators.js";
import { Router } from 'express'; import { Router } from "express";
import { AttributeRow } from '../becca/entities/rows.js'; import type { AttributeRow } from "../becca/entities/rows.js";
import { ValidatorMap } from './etapi-interface.js'; import type { ValidatorMap } from "./etapi-interface.js";
function register(router: Router) { function register(router: Router) {
eu.route(router, 'get', '/etapi/attributes/:attributeId', (req, res, next) => { eu.route(router, "get", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId); const attribute = eu.getAndCheckAttribute(req.params.attributeId);
res.json(mappers.mapAttributeToPojo(attribute)); res.json(mappers.mapAttributeToPojo(attribute));
}); });
const ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE: ValidatorMap = { const ALLOWED_PROPERTIES_FOR_CREATE_ATTRIBUTE: ValidatorMap = {
'attributeId': [v.mandatory, v.notNull, v.isValidEntityId], attributeId: [v.mandatory, v.notNull, v.isValidEntityId],
'noteId': [v.mandatory, v.notNull, v.isNoteId], noteId: [v.mandatory, v.notNull, v.isNoteId],
'type': [v.mandatory, v.notNull, v.isAttributeType], type: [v.mandatory, v.notNull, v.isAttributeType],
'name': [v.mandatory, v.notNull, v.isString], name: [v.mandatory, v.notNull, v.isString],
'value': [v.notNull, v.isString], value: [v.notNull, v.isString],
'isInheritable': [v.notNull, v.isBoolean], isInheritable: [v.notNull, v.isBoolean],
'position': [v.notNull, v.isInteger] position: [v.notNull, v.isInteger]
}; };
eu.route(router, 'post', '/etapi/attributes', (req, res, next) => { eu.route(router, "post", "/etapi/attributes", (req, res, next) => {
if (req.body.type === 'relation') { if (req.body.type === "relation") {
eu.getAndCheckNote(req.body.value); eu.getAndCheckNote(req.body.value);
} }
@@ -37,27 +37,26 @@ function register(router: Router) {
const attr = attributeService.createAttribute(params); const attr = attributeService.createAttribute(params);
res.status(201).json(mappers.mapAttributeToPojo(attr)); res.status(201).json(mappers.mapAttributeToPojo(attr));
} } catch (e: any) {
catch (e: any) {
throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message); throw new eu.EtapiError(500, eu.GENERIC_CODE, e.message);
} }
}); });
const ALLOWED_PROPERTIES_FOR_PATCH_LABEL = { const ALLOWED_PROPERTIES_FOR_PATCH_LABEL = {
'value': [v.notNull, v.isString], value: [v.notNull, v.isString],
'position': [v.notNull, v.isInteger] position: [v.notNull, v.isInteger]
}; };
const ALLOWED_PROPERTIES_FOR_PATCH_RELATION = { const ALLOWED_PROPERTIES_FOR_PATCH_RELATION = {
'position': [v.notNull, v.isInteger] position: [v.notNull, v.isInteger]
}; };
eu.route(router, 'patch', '/etapi/attributes/:attributeId', (req, res, next) => { eu.route(router, "patch", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = eu.getAndCheckAttribute(req.params.attributeId); const attribute = eu.getAndCheckAttribute(req.params.attributeId);
if (attribute.type === 'label') { if (attribute.type === "label") {
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_LABEL); eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_LABEL);
} else if (attribute.type === 'relation') { } else if (attribute.type === "relation") {
eu.getAndCheckNote(req.body.value); eu.getAndCheckNote(req.body.value);
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_RELATION); eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_RELATION);
@@ -68,7 +67,7 @@ function register(router: Router) {
res.json(mappers.mapAttributeToPojo(attribute)); res.json(mappers.mapAttributeToPojo(attribute));
}); });
eu.route(router, 'delete', '/etapi/attributes/:attributeId', (req, res, next) => { eu.route(router, "delete", "/etapi/attributes/:attributeId", (req, res, next) => {
const attribute = becca.getAttribute(req.params.attributeId); const attribute = becca.getAttribute(req.params.attributeId);
if (!attribute) { if (!attribute) {

View File

@@ -2,24 +2,24 @@ import becca from "../becca/becca.js";
import eu from "./etapi_utils.js"; import eu from "./etapi_utils.js";
import passwordEncryptionService from "../services/encryption/password_encryption.js"; import passwordEncryptionService from "../services/encryption/password_encryption.js";
import etapiTokenService from "../services/etapi_tokens.js"; import etapiTokenService from "../services/etapi_tokens.js";
import { RequestHandler, Router } from 'express'; import type { RequestHandler, Router } from "express";
function register(router: Router, loginMiddleware: RequestHandler[]) { function register(router: Router, loginMiddleware: RequestHandler[]) {
eu.NOT_AUTHENTICATED_ROUTE(router, 'post', '/etapi/auth/login', loginMiddleware, (req, res, next) => { eu.NOT_AUTHENTICATED_ROUTE(router, "post", "/etapi/auth/login", loginMiddleware, (req, res, next) => {
const {password, tokenName} = req.body; const { password, tokenName } = req.body;
if (!passwordEncryptionService.verifyPassword(password)) { if (!passwordEncryptionService.verifyPassword(password)) {
throw new eu.EtapiError(401, "WRONG_PASSWORD", "Wrong password."); throw new eu.EtapiError(401, "WRONG_PASSWORD", "Wrong password.");
} }
const {authToken} = etapiTokenService.createToken(tokenName || "ETAPI login"); const { authToken } = etapiTokenService.createToken(tokenName || "ETAPI login");
res.status(201).json({ res.status(201).json({
authToken authToken
}); });
}); });
eu.route(router, 'post', '/etapi/auth/logout', (req, res, next) => { eu.route(router, "post", "/etapi/auth/logout", (req, res, next) => {
const parsed = etapiTokenService.parseAuthToken(req.headers.authorization); const parsed = etapiTokenService.parseAuthToken(req.headers.authorization);
if (!parsed || !parsed.etapiTokenId) { if (!parsed || !parsed.etapiTokenId) {
@@ -41,4 +41,4 @@ function register(router: Router, loginMiddleware: RequestHandler[]) {
export default { export default {
register register
} };

View File

@@ -4,7 +4,7 @@ import eu from "./etapi_utils.js";
import backupService from "../services/backup.js"; import backupService from "../services/backup.js";
function register(router: Router) { function register(router: Router) {
eu.route(router, 'put', '/etapi/backup/:backupName', async (req, res, next) => { eu.route(router, "put", "/etapi/backup/:backupName", async (req, res, next) => {
await backupService.backupNow(req.params.backupName); await backupService.backupNow(req.params.backupName);
res.sendStatus(204); res.sendStatus(204);

Some files were not shown because too many files have changed in this diff Show More