mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 08:16:40 +01:00
chore(prettier): fix all files
This commit is contained in:
100
.github/workflows_old/docker.yaml
vendored
100
.github/workflows_old/docker.yaml
vendored
@@ -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 }}
|
||||||
|
|||||||
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@@ -1,6 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["lokalise.i18n-ally", "editorconfig.editorconfig"]
|
||||||
"lokalise.i18n-ally",
|
}
|
||||||
"editorconfig.editorconfig"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
44
.vscode/launch.json
vendored
44
.vscode/launch.json
vendored
@@ -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
45
.vscode/settings.json
vendored
@@ -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"
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
46
.vscode/snippets.code-snippets
vendored
46
.vscode/snippets.code-snippets
vendored
@@ -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>;"
|
}
|
||||||
]
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
174
bin/copy-dist.ts
174
bin/copy-dist.ts
@@ -8,108 +8,108 @@ 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/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));
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
150
bin/docs/assets/v0.63.6/libraries/normalize.min.css
vendored
150
bin/docs/assets/v0.63.6/libraries/normalize.min.css
vendored
@@ -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 */
|
||||||
/*# sourceMappingURL=normalize.min.css.map */
|
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 */
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = `\
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}...`);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
52
electron.ts
52
electron.ts
@@ -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");
|
||||||
|
|||||||
210
forge.config.cjs
210
forge.config.cjs
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import test, { expect } from "@playwright/test";
|
import test, { expect } from "@playwright/test";
|
||||||
|
|
||||||
test("User can change language from settings", async ({ page }) => {
|
test("User can change language from settings", 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();
|
||||||
|
|
||||||
// Go to options -> Appearance
|
// Go to options -> Appearance
|
||||||
await page.locator('#launcher-pane div').filter({ hasText: 'Options Open New Window' }).getByRole('button').click();
|
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("#launcher-pane").getByText("Options").click();
|
||||||
await page.locator('#center-pane').getByText('Appearance').click();
|
await page.locator("#center-pane").getByText("Appearance").click();
|
||||||
|
|
||||||
// Check that the default value (English) is set.
|
// Check that the default value (English) is set.
|
||||||
await expect(page.locator('#center-pane')).toContainText('Theme');
|
await expect(page.locator("#center-pane")).toContainText("Theme");
|
||||||
const languageCombobox = await page.getByRole('combobox').first();
|
const languageCombobox = await page.getByRole("combobox").first();
|
||||||
await expect(languageCombobox).toHaveValue("en");
|
await expect(languageCombobox).toHaveValue("en");
|
||||||
|
|
||||||
// Select Chinese and ensure the translation is set.
|
// Select Chinese and ensure the translation is set.
|
||||||
languageCombobox.selectOption("cn");
|
languageCombobox.selectOption("cn");
|
||||||
await expect(page.locator('#center-pane')).toContainText('主题');
|
await expect(page.locator("#center-pane")).toContainText("主题");
|
||||||
|
|
||||||
// Select English again.
|
// Select English again.
|
||||||
languageCombobox.selectOption("en");
|
languageCombobox.selectOption("en");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Restores language on start-up on desktop", async ({ page, context }) => {
|
test("Restores language on start-up on desktop", async ({ page, context }) => {
|
||||||
await page.goto('http://localhost:8082');
|
await page.goto("http://localhost:8082");
|
||||||
await expect(page.locator('#launcher-pane').first()).toContainText("Open New Window");
|
await expect(page.locator("#launcher-pane").first()).toContainText("Open New Window");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Restores language on start-up on mobile", async ({ page, context }) => {
|
test("Restores language on start-up on mobile", async ({ page, context }) => {
|
||||||
@@ -38,6 +38,6 @@ test("Restores language on start-up 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('#launcher-pane div').first()).toContainText("Open New Window");
|
await expect(page.locator("#launcher-pane div").first()).toContainText("Open New Window");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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}`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
"includeDate": false
|
"includeDate": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("./"));
|
||||||
|
|||||||
23
nodemon.json
23
nodemon.json
@@ -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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* Read environment variables from file.
|
||||||
@@ -11,75 +11,75 @@ import { defineConfig, devices } from '@playwright/test';
|
|||||||
* See https://playwright.dev/docs/test-configuration.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
*/
|
*/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: './integration-tests',
|
testDir: "./integration-tests",
|
||||||
/* 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. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: process.env.CI ? 1 : undefined,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: "html",
|
||||||
/* 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: {
|
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
||||||
// baseURL: 'http://127.0.0.1:3000',
|
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
||||||
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 */
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: "setup",
|
|
||||||
testMatch: /.*\.setup\.ts/
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "firefox",
|
|
||||||
use: {
|
use: {
|
||||||
...devices[ "Desktop Firefox" ],
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
storageState: "playwright/.auth/user.json"
|
// baseURL: 'http://127.0.0.1:3000',
|
||||||
},
|
|
||||||
dependencies: [ "setup" ]
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry"
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
webServer: {
|
||||||
// {
|
command: "npm run integration-mem-db",
|
||||||
// name: 'Mobile Chrome',
|
url: "http://127.0.0.1:8082",
|
||||||
// use: { ...devices['Pixel 5'] },
|
reuseExistingServer: true,
|
||||||
// },
|
stdout: "ignore",
|
||||||
// {
|
stderr: "pipe"
|
||||||
// name: 'Mobile Safari',
|
},
|
||||||
// use: { ...devices['iPhone 12'] },
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Test against branded browsers. */
|
/* Configure projects for major browsers */
|
||||||
// {
|
projects: [
|
||||||
// name: 'Microsoft Edge',
|
{
|
||||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
name: "setup",
|
||||||
// },
|
testMatch: /.*\.setup\.ts/
|
||||||
// {
|
},
|
||||||
// name: 'Google Chrome',
|
|
||||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
{
|
||||||
// webServer: {
|
name: "firefox",
|
||||||
// command: 'npm run start',
|
use: {
|
||||||
// url: 'http://127.0.0.1:3000',
|
...devices["Desktop Firefox"],
|
||||||
// reuseExistingServer: !process.env.CI,
|
storageState: "playwright/.auth/user.json"
|
||||||
// },
|
},
|
||||||
|
dependencies: ["setup"]
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
]
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
// webServer: {
|
||||||
|
// command: 'npm run start',
|
||||||
|
// url: 'http://127.0.0.1:3000',
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.`);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|||||||
@@ -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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
describe("Notes", () => {
|
describe("Notes", () => {
|
||||||
it("zzz", () => {
|
it("zzz", () => {});
|
||||||
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.`);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,81 +7,76 @@ import SearchResult from "../../src/services/search/search_result.js";
|
|||||||
import { NoteType } from "../../src/becca/entities/rows.js";
|
import { 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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
});
|
||||||
"\\",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,23 +3,9 @@ import { 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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,301 +17,297 @@ 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');
|
expect(exp.constructor.name).toEqual("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', () => {
|
describe("Parser", () => {
|
||||||
it('fulltext parser without content', () => {
|
it("fulltext parser without content", () => {
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: tokens(['hello', 'hi']),
|
fulltextTokens: tokens(["hello", "hi"]),
|
||||||
expressionTokens: [],
|
expressionTokens: [],
|
||||||
searchContext: new SearchContext({ excludeArchived: true }),
|
searchContext: new SearchContext({ excludeArchived: true })
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
expect(rootExp.subExpressions[0].constructor.name).toEqual('PropertyComparisonExp');
|
expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp");
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp");
|
||||||
expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual('NoteFlatTextExp');
|
expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp");
|
||||||
expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(['hello', 'hi']);
|
expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(["hello", "hi"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fulltext parser with content', () => {
|
it("fulltext parser with content", () => {
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: tokens(['hello', 'hi']),
|
fulltextTokens: tokens(["hello", "hi"]),
|
||||||
expressionTokens: [],
|
expressionTokens: [],
|
||||||
searchContext: new SearchContext(),
|
searchContext: new SearchContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
|
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp");
|
||||||
|
|
||||||
const subs = rootExp.subExpressions[2].subExpressions;
|
const subs = rootExp.subExpressions[2].subExpressions;
|
||||||
|
|
||||||
expect(subs[0].constructor.name).toEqual('NoteFlatTextExp');
|
expect(subs[0].constructor.name).toEqual("NoteFlatTextExp");
|
||||||
expect(subs[0].tokens).toEqual(['hello', 'hi']);
|
expect(subs[0].tokens).toEqual(["hello", "hi"]);
|
||||||
|
|
||||||
expect(subs[1].constructor.name).toEqual('NoteContentFulltextExp');
|
expect(subs[1].constructor.name).toEqual("NoteContentFulltextExp");
|
||||||
expect(subs[1].tokens).toEqual(['hello', 'hi']);
|
expect(subs[1].tokens).toEqual(["hello", "hi"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('simple label comparison', () => {
|
it("simple label comparison", () => {
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#mylabel', '=', 'text']),
|
expressionTokens: tokens(["#mylabel", "=", "text"]),
|
||||||
searchContext: new SearchContext(),
|
searchContext: new SearchContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(rootExp.subExpressions[2].attributeType).toEqual('label');
|
expect(rootExp.subExpressions[2].attributeType).toEqual("label");
|
||||||
expect(rootExp.subExpressions[2].attributeName).toEqual('mylabel');
|
expect(rootExp.subExpressions[2].attributeName).toEqual("mylabel");
|
||||||
expect(rootExp.subExpressions[2].comparator).toBeTruthy();
|
expect(rootExp.subExpressions[2].comparator).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('simple attribute negation', () => {
|
it("simple attribute negation", () => {
|
||||||
let rootExp = parse({
|
let rootExp = parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#!mylabel']),
|
expressionTokens: tokens(["#!mylabel"]),
|
||||||
searchContext: new SearchContext(),
|
searchContext: new SearchContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp");
|
||||||
expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp');
|
expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp");
|
||||||
expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('label');
|
expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("label");
|
||||||
expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('mylabel');
|
expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("mylabel");
|
||||||
|
|
||||||
rootExp = parse({
|
rootExp = parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['~!myrelation']),
|
expressionTokens: tokens(["~!myrelation"]),
|
||||||
searchContext: new SearchContext(),
|
searchContext: new SearchContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('NotExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp");
|
||||||
expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual('AttributeExistsExp');
|
expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp");
|
||||||
expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual('relation');
|
expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("relation");
|
||||||
expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual('myrelation');
|
expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("myrelation");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('simple label AND', () => {
|
it("simple label AND", () => {
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#first', '=', 'text', 'and', '#second', '=', 'text']),
|
expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]),
|
||||||
searchContext: new SearchContext(true),
|
searchContext: new SearchContext(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
|
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp");
|
||||||
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
||||||
|
|
||||||
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
|
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(firstSub.attributeName).toEqual('first');
|
expect(firstSub.attributeName).toEqual("first");
|
||||||
|
|
||||||
expect(secondSub.constructor.name).toEqual('LabelComparisonExp');
|
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(secondSub.attributeName).toEqual('second');
|
expect(secondSub.attributeName).toEqual("second");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('simple label AND without explicit AND', () => {
|
it("simple label AND without explicit AND", () => {
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#first', '=', 'text', '#second', '=', 'text']),
|
expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]),
|
||||||
searchContext: new SearchContext(),
|
searchContext: new SearchContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
|
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp");
|
||||||
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
||||||
|
|
||||||
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
|
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(firstSub.attributeName).toEqual('first');
|
expect(firstSub.attributeName).toEqual("first");
|
||||||
|
|
||||||
expect(secondSub.constructor.name).toEqual('LabelComparisonExp');
|
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(secondSub.attributeName).toEqual('second');
|
expect(secondSub.attributeName).toEqual("second");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('simple label OR', () => {
|
it("simple label OR", () => {
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#first', '=', 'text', 'or', '#second', '=', 'text']),
|
expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]),
|
||||||
searchContext: new SearchContext(),
|
searchContext: new SearchContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
|
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp");
|
||||||
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
||||||
|
|
||||||
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
|
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(firstSub.attributeName).toEqual('first');
|
expect(firstSub.attributeName).toEqual("first");
|
||||||
|
|
||||||
expect(secondSub.constructor.name).toEqual('LabelComparisonExp');
|
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(secondSub.attributeName).toEqual('second');
|
expect(secondSub.attributeName).toEqual("second");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fulltext and simple label', () => {
|
it("fulltext and simple label", () => {
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: tokens(['hello']),
|
fulltextTokens: tokens(["hello"]),
|
||||||
expressionTokens: tokens(['#mylabel', '=', 'text']),
|
expressionTokens: tokens(["#mylabel", "=", "text"]),
|
||||||
searchContext: new SearchContext({ excludeArchived: true }),
|
searchContext: new SearchContext({ excludeArchived: true })
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions;
|
const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions;
|
||||||
|
|
||||||
expect(firstSub.constructor.name).toEqual('PropertyComparisonExp');
|
expect(firstSub.constructor.name).toEqual("PropertyComparisonExp");
|
||||||
expect(firstSub.propertyName).toEqual('isArchived');
|
expect(firstSub.propertyName).toEqual("isArchived");
|
||||||
|
|
||||||
expect(thirdSub.constructor.name).toEqual('OrExp');
|
expect(thirdSub.constructor.name).toEqual("OrExp");
|
||||||
expect(thirdSub.subExpressions[0].constructor.name).toEqual('NoteFlatTextExp');
|
expect(thirdSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp");
|
||||||
expect(thirdSub.subExpressions[0].tokens).toEqual(['hello']);
|
expect(thirdSub.subExpressions[0].tokens).toEqual(["hello"]);
|
||||||
|
|
||||||
expect(fourth.constructor.name).toEqual('LabelComparisonExp');
|
expect(fourth.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(fourth.attributeName).toEqual('mylabel');
|
expect(fourth.attributeName).toEqual("mylabel");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('label sub-expression', () => {
|
it("label sub-expression", () => {
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#first', '=', 'text', 'or', ['#second', '=', 'text', 'and', '#third', '=', 'text']]),
|
expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]),
|
||||||
searchContext: new SearchContext(),
|
searchContext: new SearchContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
|
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('OrExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp");
|
||||||
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions;
|
||||||
|
|
||||||
expect(firstSub.constructor.name).toEqual('LabelComparisonExp');
|
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(firstSub.attributeName).toEqual('first');
|
expect(firstSub.attributeName).toEqual("first");
|
||||||
|
|
||||||
expect(secondSub.constructor.name).toEqual('AndExp');
|
expect(secondSub.constructor.name).toEqual("AndExp");
|
||||||
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
|
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
|
||||||
|
|
||||||
expect(firstSubSub.constructor.name).toEqual('LabelComparisonExp');
|
expect(firstSubSub.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(firstSubSub.attributeName).toEqual('second');
|
expect(firstSubSub.attributeName).toEqual("second");
|
||||||
|
|
||||||
expect(secondSubSub.constructor.name).toEqual('LabelComparisonExp');
|
expect(secondSubSub.constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(secondSubSub.attributeName).toEqual('third');
|
expect(secondSubSub.attributeName).toEqual("third");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('label sub-expression without explicit operator', () => {
|
it("label sub-expression without explicit operator", () => {
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#first', ['#second', 'or', '#third'], '#fourth']),
|
expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]),
|
||||||
searchContext: new SearchContext(),
|
searchContext: new SearchContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
|
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('AndExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp");
|
||||||
const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions;
|
const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions;
|
||||||
|
|
||||||
expect(firstSub.constructor.name).toEqual('AttributeExistsExp');
|
expect(firstSub.constructor.name).toEqual("AttributeExistsExp");
|
||||||
expect(firstSub.attributeName).toEqual('first');
|
expect(firstSub.attributeName).toEqual("first");
|
||||||
|
|
||||||
expect(secondSub.constructor.name).toEqual('OrExp');
|
expect(secondSub.constructor.name).toEqual("OrExp");
|
||||||
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
|
const [firstSubSub, secondSubSub] = secondSub.subExpressions;
|
||||||
|
|
||||||
expect(firstSubSub.constructor.name).toEqual('AttributeExistsExp');
|
expect(firstSubSub.constructor.name).toEqual("AttributeExistsExp");
|
||||||
expect(firstSubSub.attributeName).toEqual('second');
|
expect(firstSubSub.attributeName).toEqual("second");
|
||||||
|
|
||||||
expect(secondSubSub.constructor.name).toEqual('AttributeExistsExp');
|
expect(secondSubSub.constructor.name).toEqual("AttributeExistsExp");
|
||||||
expect(secondSubSub.attributeName).toEqual('third');
|
expect(secondSubSub.attributeName).toEqual("third");
|
||||||
|
|
||||||
expect(thirdSub.constructor.name).toEqual('AttributeExistsExp');
|
expect(thirdSub.constructor.name).toEqual("AttributeExistsExp");
|
||||||
expect(thirdSub.attributeName).toEqual('fourth');
|
expect(thirdSub.attributeName).toEqual("fourth");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Invalid expressions', () => {
|
describe("Invalid expressions", () => {
|
||||||
it('incomplete comparison', () => {
|
it("incomplete comparison", () => {
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
|
|
||||||
parse({
|
parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#first', '=']),
|
expressionTokens: tokens(["#first", "="]),
|
||||||
searchContext,
|
searchContext
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(searchContext.error).toEqual('Misplaced or incomplete expression "="');
|
expect(searchContext.error).toEqual('Misplaced or incomplete expression "="');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('comparison between labels is impossible', () => {
|
it("comparison between labels is impossible", () => {
|
||||||
let searchContext = new SearchContext();
|
let searchContext = new SearchContext();
|
||||||
searchContext.originalQuery = '#first = #second';
|
searchContext.originalQuery = "#first = #second";
|
||||||
|
|
||||||
parse({
|
parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#first', '=', '#second']),
|
expressionTokens: tokens(["#first", "=", "#second"]),
|
||||||
searchContext,
|
searchContext
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(searchContext.error).toEqual(
|
expect(searchContext.error).toEqual(`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`);
|
||||||
`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`
|
|
||||||
);
|
|
||||||
|
|
||||||
searchContext = new SearchContext();
|
searchContext = new SearchContext();
|
||||||
searchContext.originalQuery = '#first = note.relations.second';
|
searchContext.originalQuery = "#first = note.relations.second";
|
||||||
|
|
||||||
parse({
|
parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['#first', '=', 'note', '.', 'relations', 'second']),
|
expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]),
|
||||||
searchContext,
|
searchContext
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(searchContext.error).toEqual(
|
expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`);
|
||||||
`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`
|
|
||||||
);
|
|
||||||
|
|
||||||
const rootExp = parse({
|
const rootExp = parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: [
|
expressionTokens: [
|
||||||
{ token: '#first', inQuotes: false },
|
{ token: "#first", inQuotes: false },
|
||||||
{ token: '=', inQuotes: false },
|
{ token: "=", inQuotes: false },
|
||||||
{ token: '#second', inQuotes: true },
|
{ token: "#second", inQuotes: true }
|
||||||
],
|
],
|
||||||
searchContext: new SearchContext(),
|
searchContext: new SearchContext()
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(rootExp.constructor.name).toEqual('AndExp');
|
expect(rootExp.constructor.name).toEqual("AndExp");
|
||||||
assertIsArchived(rootExp.subExpressions[0]);
|
assertIsArchived(rootExp.subExpressions[0]);
|
||||||
|
|
||||||
expect(rootExp.subExpressions[2].constructor.name).toEqual('LabelComparisonExp');
|
expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp");
|
||||||
expect(rootExp.subExpressions[2].attributeType).toEqual('label');
|
expect(rootExp.subExpressions[2].attributeType).toEqual("label");
|
||||||
expect(rootExp.subExpressions[2].attributeName).toEqual('first');
|
expect(rootExp.subExpressions[2].attributeName).toEqual("first");
|
||||||
expect(rootExp.subExpressions[2].comparator).toBeTruthy();
|
expect(rootExp.subExpressions[2].comparator).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searching by relation without note property', () => {
|
it("searching by relation without note property", () => {
|
||||||
const searchContext = new SearchContext();
|
const searchContext = new SearchContext();
|
||||||
|
|
||||||
parse({
|
parse({
|
||||||
fulltextTokens: [],
|
fulltextTokens: [],
|
||||||
expressionTokens: tokens(['~first', '=', 'text', '-', 'abc']),
|
expressionTokens: tokens(["~first", "=", "text", "-", "abc"]),
|
||||||
searchContext,
|
searchContext
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""');
|
expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""');
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"spec_dir": "spec",
|
"spec_dir": "spec",
|
||||||
"spec_files": ["./**/*.spec.ts"],
|
"spec_files": ["./**/*.spec.ts"],
|
||||||
"helpers": ["helpers/**/*.js"],
|
"helpers": ["helpers/**/*.js"],
|
||||||
"stopSpecOnExpectationFailure": false,
|
"stopSpecOnExpectationFailure": false,
|
||||||
"random": true
|
"random": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ describe("Utils", () => {
|
|||||||
expect(trimIndentation`\
|
expect(trimIndentation`\
|
||||||
Hello
|
Hello
|
||||||
world
|
world
|
||||||
123`
|
123`).toBe(`\
|
||||||
).toBe(`\
|
|
||||||
Hello
|
Hello
|
||||||
world
|
world
|
||||||
123`);
|
123`);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
42
src/app.ts
42
src/app.ts
@@ -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;
|
||||||
|
|||||||
@@ -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 { 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) */
|
||||||
|
|||||||
@@ -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 { 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}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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, { 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`);
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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;
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ import { 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;
|
||||||
|
|||||||
@@ -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 { 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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 { 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
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ConstructorData } from './becca-interface.js';
|
import { 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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { AttachmentRow } from "../becca/entities/rows.js";
|
||||||
import { ValidatorMap } from './etapi-interface.js';
|
import { 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) {
|
||||||
|
|||||||
@@ -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 { AttributeRow } from "../becca/entities/rows.js";
|
||||||
import { ValidatorMap } from './etapi-interface.js';
|
import { 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) {
|
||||||
|
|||||||
@@ -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 { 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
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -9,21 +9,21 @@ import v from "./validators.js";
|
|||||||
import { BranchRow } from "../becca/entities/rows.js";
|
import { BranchRow } from "../becca/entities/rows.js";
|
||||||
|
|
||||||
function register(router: Router) {
|
function register(router: Router) {
|
||||||
eu.route(router, 'get', '/etapi/branches/:branchId', (req, res, next) => {
|
eu.route(router, "get", "/etapi/branches/:branchId", (req, res, next) => {
|
||||||
const branch = eu.getAndCheckBranch(req.params.branchId);
|
const branch = eu.getAndCheckBranch(req.params.branchId);
|
||||||
|
|
||||||
res.json(mappers.mapBranchToPojo(branch));
|
res.json(mappers.mapBranchToPojo(branch));
|
||||||
});
|
});
|
||||||
|
|
||||||
const ALLOWED_PROPERTIES_FOR_CREATE_BRANCH = {
|
const ALLOWED_PROPERTIES_FOR_CREATE_BRANCH = {
|
||||||
'noteId': [v.mandatory, v.notNull, v.isNoteId],
|
noteId: [v.mandatory, v.notNull, v.isNoteId],
|
||||||
'parentNoteId': [v.mandatory, v.notNull, v.isNoteId],
|
parentNoteId: [v.mandatory, v.notNull, v.isNoteId],
|
||||||
'notePosition': [v.notNull, v.isInteger],
|
notePosition: [v.notNull, v.isInteger],
|
||||||
'prefix': [v.isString],
|
prefix: [v.isString],
|
||||||
'isExpanded': [v.notNull, v.isBoolean]
|
isExpanded: [v.notNull, v.isBoolean]
|
||||||
};
|
};
|
||||||
|
|
||||||
eu.route(router, 'post', '/etapi/branches', (req, res, next) => {
|
eu.route(router, "post", "/etapi/branches", (req, res, next) => {
|
||||||
const _params = {};
|
const _params = {};
|
||||||
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_BRANCH);
|
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_BRANCH);
|
||||||
const params: BranchRow = _params as BranchRow;
|
const params: BranchRow = _params as BranchRow;
|
||||||
@@ -49,12 +49,12 @@ function register(router: Router) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
||||||
'notePosition': [v.notNull, v.isInteger],
|
notePosition: [v.notNull, v.isInteger],
|
||||||
'prefix': [v.isString],
|
prefix: [v.isString],
|
||||||
'isExpanded': [v.notNull, v.isBoolean]
|
isExpanded: [v.notNull, v.isBoolean]
|
||||||
};
|
};
|
||||||
|
|
||||||
eu.route(router, 'patch', '/etapi/branches/:branchId', (req, res, next) => {
|
eu.route(router, "patch", "/etapi/branches/:branchId", (req, res, next) => {
|
||||||
const branch = eu.getAndCheckBranch(req.params.branchId);
|
const branch = eu.getAndCheckBranch(req.params.branchId);
|
||||||
|
|
||||||
eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
eu.validateAndPatch(branch, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
||||||
@@ -63,7 +63,7 @@ function register(router: Router) {
|
|||||||
res.json(mappers.mapBranchToPojo(branch));
|
res.json(mappers.mapBranchToPojo(branch));
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'delete', '/etapi/branches/:branchId', (req, res, next) => {
|
eu.route(router, "delete", "/etapi/branches/:branchId", (req, res, next) => {
|
||||||
const branch = becca.getBranch(req.params.branchId);
|
const branch = becca.getBranch(req.params.branchId);
|
||||||
|
|
||||||
if (!branch) {
|
if (!branch) {
|
||||||
@@ -75,7 +75,7 @@ function register(router: Router) {
|
|||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'post', '/etapi/refresh-note-ordering/:parentNoteId', (req, res, next) => {
|
eu.route(router, "post", "/etapi/refresh-note-ordering/:parentNoteId", (req, res, next) => {
|
||||||
eu.getAndCheckNote(req.params.parentNoteId);
|
eu.getAndCheckNote(req.params.parentNoteId);
|
||||||
|
|
||||||
entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi");
|
entityChangesService.putNoteReorderingEntityChange(req.params.parentNoteId, "etapi");
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export type ValidatorFunc = (obj: unknown) => (string | undefined);
|
export type ValidatorFunc = (obj: unknown) => string | undefined;
|
||||||
|
|
||||||
export type ValidatorMap = Record<string, ValidatorFunc[]>;
|
export type ValidatorMap = Record<string, ValidatorFunc[]>;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@ import log from "../services/log.js";
|
|||||||
import becca from "../becca/becca.js";
|
import becca from "../becca/becca.js";
|
||||||
import etapiTokenService from "../services/etapi_tokens.js";
|
import etapiTokenService from "../services/etapi_tokens.js";
|
||||||
import config from "../services/config.js";
|
import config from "../services/config.js";
|
||||||
import { NextFunction, Request, RequestHandler, Response, Router } from 'express';
|
import { NextFunction, Request, RequestHandler, Response, Router } from "express";
|
||||||
import { ValidatorMap } from './etapi-interface.js';
|
import { ValidatorMap } from "./etapi-interface.js";
|
||||||
import { ApiRequestHandler } from "../routes/routes.js";
|
import { ApiRequestHandler } from "../routes/routes.js";
|
||||||
const GENERIC_CODE = "GENERIC";
|
const GENERIC_CODE = "GENERIC";
|
||||||
|
|
||||||
@@ -30,20 +30,21 @@ class EtapiError extends Error {
|
|||||||
|
|
||||||
function sendError(res: Response, statusCode: number, code: string, message: string) {
|
function sendError(res: Response, statusCode: number, code: string, message: string) {
|
||||||
return res
|
return res
|
||||||
.set('Content-Type', 'application/json')
|
.set("Content-Type", "application/json")
|
||||||
.status(statusCode)
|
.status(statusCode)
|
||||||
.send(JSON.stringify({
|
.send(
|
||||||
"status": statusCode,
|
JSON.stringify({
|
||||||
"code": code,
|
status: statusCode,
|
||||||
"message": message
|
code: code,
|
||||||
}));
|
message: message
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkEtapiAuth(req: Request, res: Response, next: NextFunction) {
|
function checkEtapiAuth(req: Request, res: Response, next: NextFunction) {
|
||||||
if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
|
if (noAuthentication || etapiTokenService.isValidAuthHeader(req.headers.authorization)) {
|
||||||
next();
|
next();
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
|
sendError(res, 401, "NOT_AUTHENTICATED", "Not authenticated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,8 +55,8 @@ function processRequest(req: Request, res: Response, routeHandler: ApiRequestHan
|
|||||||
cls.namespace.bindEmitter(res);
|
cls.namespace.bindEmitter(res);
|
||||||
|
|
||||||
cls.init(() => {
|
cls.init(() => {
|
||||||
cls.set('componentId', "etapi");
|
cls.set("componentId", "etapi");
|
||||||
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
|
cls.set("localNowDateTime", req.headers["trilium-local-now-datetime"]);
|
||||||
|
|
||||||
const cb = () => routeHandler(req, res, next);
|
const cb = () => routeHandler(req, res, next);
|
||||||
|
|
||||||
@@ -85,19 +86,17 @@ function getAndCheckNote(noteId: string) {
|
|||||||
|
|
||||||
if (note) {
|
if (note) {
|
||||||
return note;
|
return note;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
|
throw new EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAndCheckAttachment(attachmentId: string) {
|
function getAndCheckAttachment(attachmentId: string) {
|
||||||
const attachment = becca.getAttachment(attachmentId, {includeContentLength: true});
|
const attachment = becca.getAttachment(attachmentId, { includeContentLength: true });
|
||||||
|
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
return attachment;
|
return attachment;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
|
throw new EtapiError(404, "ATTACHMENT_NOT_FOUND", `Attachment '${attachmentId}' not found.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,8 +106,7 @@ function getAndCheckBranch(branchId: string) {
|
|||||||
|
|
||||||
if (branch) {
|
if (branch) {
|
||||||
return branch;
|
return branch;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
|
throw new EtapiError(404, "BRANCH_NOT_FOUND", `Branch '${branchId}' not found.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,8 +116,7 @@ function getAndCheckAttribute(attributeId: string) {
|
|||||||
|
|
||||||
if (attribute) {
|
if (attribute) {
|
||||||
return attribute;
|
return attribute;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
|
throw new EtapiError(404, "ATTRIBUTE_NOT_FOUND", `Attribute '${attributeId}' not found.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,8 +125,7 @@ function validateAndPatch(target: any, source: any, allowedProperties: Validator
|
|||||||
for (const key of Object.keys(source)) {
|
for (const key of Object.keys(source)) {
|
||||||
if (!(key in allowedProperties)) {
|
if (!(key in allowedProperties)) {
|
||||||
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for this method.`);
|
throw new EtapiError(400, "PROPERTY_NOT_ALLOWED", `Property '${key}' is not allowed for this method.`);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
for (const validator of allowedProperties[key]) {
|
for (const validator of allowedProperties[key]) {
|
||||||
const validationResult = validator(source[key]);
|
const validationResult = validator(source[key]);
|
||||||
|
|
||||||
@@ -157,4 +153,4 @@ export default {
|
|||||||
getAndCheckBranch,
|
getAndCheckBranch,
|
||||||
getAndCheckAttribute,
|
getAndCheckAttribute,
|
||||||
getAndCheckAttachment
|
getAndCheckAttachment
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ function mapNoteToPojo(note: BNote) {
|
|||||||
dateModified: note.dateModified,
|
dateModified: note.dateModified,
|
||||||
utcDateCreated: note.utcDateCreated,
|
utcDateCreated: note.utcDateCreated,
|
||||||
utcDateModified: note.utcDateModified,
|
utcDateModified: note.utcDateModified,
|
||||||
parentNoteIds: note.getParentNotes().map(p => p.noteId),
|
parentNoteIds: note.getParentNotes().map((p) => p.noteId),
|
||||||
childNoteIds: note.getChildNotes().map(ch => ch.noteId),
|
childNoteIds: note.getChildNotes().map((ch) => ch.noteId),
|
||||||
parentBranchIds: note.getParentBranches().map(p => p.branchId),
|
parentBranchIds: note.getParentBranches().map((p) => p.branchId),
|
||||||
childBranchIds: note.getChildBranches().map(ch => ch.branchId),
|
childBranchIds: note.getChildBranches().map((ch) => ch.branchId),
|
||||||
attributes: note.getAttributes().map(attr => mapAttributeToPojo(attr))
|
attributes: note.getAttributes().map((attr) => mapAttributeToPojo(attr))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,28 +9,28 @@ import searchService from "../services/search/services/search.js";
|
|||||||
import SearchContext from "../services/search/search_context.js";
|
import SearchContext from "../services/search/search_context.js";
|
||||||
import zipExportService from "../services/export/zip.js";
|
import zipExportService from "../services/export/zip.js";
|
||||||
import zipImportService from "../services/import/zip.js";
|
import zipImportService from "../services/import/zip.js";
|
||||||
import { Request, Router } from 'express';
|
import { Request, Router } from "express";
|
||||||
import { ParsedQs } from 'qs';
|
import { ParsedQs } from "qs";
|
||||||
import { NoteParams } from '../services/note-interface.js';
|
import { NoteParams } from "../services/note-interface.js";
|
||||||
import { SearchParams } from '../services/search/services/types.js';
|
import { SearchParams } from "../services/search/services/types.js";
|
||||||
import { ValidatorMap } from './etapi-interface.js';
|
import { ValidatorMap } from "./etapi-interface.js";
|
||||||
|
|
||||||
function register(router: Router) {
|
function register(router: Router) {
|
||||||
eu.route(router, 'get', '/etapi/notes', (req, res, next) => {
|
eu.route(router, "get", "/etapi/notes", (req, res, next) => {
|
||||||
const { search } = req.query;
|
const { search } = req.query;
|
||||||
|
|
||||||
if (typeof search !== "string" || !search?.trim()) {
|
if (typeof search !== "string" || !search?.trim()) {
|
||||||
throw new eu.EtapiError(400, 'SEARCH_QUERY_PARAM_MANDATORY', "'search' query parameter is mandatory.");
|
throw new eu.EtapiError(400, "SEARCH_QUERY_PARAM_MANDATORY", "'search' query parameter is mandatory.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = parseSearchParams(req);
|
const searchParams = parseSearchParams(req);
|
||||||
const searchContext = new SearchContext(searchParams);
|
const searchContext = new SearchContext(searchParams);
|
||||||
|
|
||||||
const searchResults = searchService.findResultsWithQuery(search, searchContext);
|
const searchResults = searchService.findResultsWithQuery(search, searchContext);
|
||||||
const foundNotes = searchResults.map(sr => becca.notes[sr.noteId]);
|
const foundNotes = searchResults.map((sr) => becca.notes[sr.noteId]);
|
||||||
|
|
||||||
const resp: any = {
|
const resp: any = {
|
||||||
results: foundNotes.map(note => mappers.mapNoteToPojo(note)),
|
results: foundNotes.map((note) => mappers.mapNoteToPojo(note))
|
||||||
};
|
};
|
||||||
|
|
||||||
if (searchContext.debugInfo) {
|
if (searchContext.debugInfo) {
|
||||||
@@ -40,27 +40,27 @@ function register(router: Router) {
|
|||||||
res.json(resp);
|
res.json(resp);
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'get', '/etapi/notes/:noteId', (req, res, next) => {
|
eu.route(router, "get", "/etapi/notes/:noteId", (req, res, next) => {
|
||||||
const note = eu.getAndCheckNote(req.params.noteId);
|
const note = eu.getAndCheckNote(req.params.noteId);
|
||||||
|
|
||||||
res.json(mappers.mapNoteToPojo(note));
|
res.json(mappers.mapNoteToPojo(note));
|
||||||
});
|
});
|
||||||
|
|
||||||
const ALLOWED_PROPERTIES_FOR_CREATE_NOTE: ValidatorMap = {
|
const ALLOWED_PROPERTIES_FOR_CREATE_NOTE: ValidatorMap = {
|
||||||
'parentNoteId': [v.mandatory, v.notNull, v.isNoteId],
|
parentNoteId: [v.mandatory, v.notNull, v.isNoteId],
|
||||||
'title': [v.mandatory, v.notNull, v.isString],
|
title: [v.mandatory, v.notNull, v.isString],
|
||||||
'type': [v.mandatory, v.notNull, v.isNoteType],
|
type: [v.mandatory, v.notNull, v.isNoteType],
|
||||||
'mime': [v.notNull, v.isString],
|
mime: [v.notNull, v.isString],
|
||||||
'content': [v.notNull, v.isString],
|
content: [v.notNull, v.isString],
|
||||||
'notePosition': [v.notNull, v.isInteger],
|
notePosition: [v.notNull, v.isInteger],
|
||||||
'prefix': [v.notNull, v.isString],
|
prefix: [v.notNull, v.isString],
|
||||||
'isExpanded': [v.notNull, v.isBoolean],
|
isExpanded: [v.notNull, v.isBoolean],
|
||||||
'noteId': [v.notNull, v.isValidEntityId],
|
noteId: [v.notNull, v.isValidEntityId],
|
||||||
'dateCreated': [v.notNull, v.isString, v.isLocalDateTime],
|
dateCreated: [v.notNull, v.isString, v.isLocalDateTime],
|
||||||
'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime]
|
utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime]
|
||||||
};
|
};
|
||||||
|
|
||||||
eu.route(router, 'post', '/etapi/create-note', (req, res, next) => {
|
eu.route(router, "post", "/etapi/create-note", (req, res, next) => {
|
||||||
const _params = {};
|
const _params = {};
|
||||||
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE);
|
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE);
|
||||||
const params = _params as NoteParams;
|
const params = _params as NoteParams;
|
||||||
@@ -72,21 +72,20 @@ function register(router: Router) {
|
|||||||
note: mappers.mapNoteToPojo(resp.note),
|
note: mappers.mapNoteToPojo(resp.note),
|
||||||
branch: mappers.mapBranchToPojo(resp.branch)
|
branch: mappers.mapBranchToPojo(resp.branch)
|
||||||
});
|
});
|
||||||
}
|
} catch (e: any) {
|
||||||
catch (e: any) {
|
|
||||||
return eu.sendError(res, 500, eu.GENERIC_CODE, e.message);
|
return eu.sendError(res, 500, eu.GENERIC_CODE, e.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
||||||
'title': [v.notNull, v.isString],
|
title: [v.notNull, v.isString],
|
||||||
'type': [v.notNull, v.isString],
|
type: [v.notNull, v.isString],
|
||||||
'mime': [v.notNull, v.isString],
|
mime: [v.notNull, v.isString],
|
||||||
'dateCreated': [v.notNull, v.isString, v.isLocalDateTime],
|
dateCreated: [v.notNull, v.isString, v.isLocalDateTime],
|
||||||
'utcDateCreated': [v.notNull, v.isString, v.isUtcDateTime]
|
utcDateCreated: [v.notNull, v.isString, v.isUtcDateTime]
|
||||||
};
|
};
|
||||||
|
|
||||||
eu.route(router, 'patch', '/etapi/notes/:noteId', (req, res, next) => {
|
eu.route(router, "patch", "/etapi/notes/:noteId", (req, res, next) => {
|
||||||
const note = eu.getAndCheckNote(req.params.noteId);
|
const note = eu.getAndCheckNote(req.params.noteId);
|
||||||
|
|
||||||
if (note.isProtected) {
|
if (note.isProtected) {
|
||||||
@@ -99,7 +98,7 @@ function register(router: Router) {
|
|||||||
res.json(mappers.mapNoteToPojo(note));
|
res.json(mappers.mapNoteToPojo(note));
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'delete', '/etapi/notes/:noteId', (req, res, next) => {
|
eu.route(router, "delete", "/etapi/notes/:noteId", (req, res, next) => {
|
||||||
const { noteId } = req.params;
|
const { noteId } = req.params;
|
||||||
|
|
||||||
const note = becca.getNote(noteId);
|
const note = becca.getNote(noteId);
|
||||||
@@ -108,12 +107,12 @@ function register(router: Router) {
|
|||||||
return res.sendStatus(204);
|
return res.sendStatus(204);
|
||||||
}
|
}
|
||||||
|
|
||||||
note.deleteNote(null, new TaskContext('no-progress-reporting'));
|
note.deleteNote(null, new TaskContext("no-progress-reporting"));
|
||||||
|
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'get', '/etapi/notes/:noteId/content', (req, res, next) => {
|
eu.route(router, "get", "/etapi/notes/:noteId/content", (req, res, next) => {
|
||||||
const note = eu.getAndCheckNote(req.params.noteId);
|
const note = eu.getAndCheckNote(req.params.noteId);
|
||||||
|
|
||||||
if (note.isProtected) {
|
if (note.isProtected) {
|
||||||
@@ -122,15 +121,15 @@ function register(router: Router) {
|
|||||||
|
|
||||||
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
|
const filename = utils.formatDownloadTitle(note.title, note.type, note.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', note.mime);
|
res.setHeader("Content-Type", note.mime);
|
||||||
|
|
||||||
res.send(note.getContent());
|
res.send(note.getContent());
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'put', '/etapi/notes/:noteId/content', (req, res, next) => {
|
eu.route(router, "put", "/etapi/notes/:noteId/content", (req, res, next) => {
|
||||||
const note = eu.getAndCheckNote(req.params.noteId);
|
const note = eu.getAndCheckNote(req.params.noteId);
|
||||||
|
|
||||||
if (note.isProtected) {
|
if (note.isProtected) {
|
||||||
@@ -144,7 +143,7 @@ function register(router: Router) {
|
|||||||
return res.sendStatus(204);
|
return res.sendStatus(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'get', '/etapi/notes/:noteId/export', (req, res, next) => {
|
eu.route(router, "get", "/etapi/notes/:noteId/export", (req, res, next) => {
|
||||||
const note = eu.getAndCheckNote(req.params.noteId);
|
const note = eu.getAndCheckNote(req.params.noteId);
|
||||||
const format = req.query.format || "html";
|
const format = req.query.format || "html";
|
||||||
|
|
||||||
@@ -152,7 +151,7 @@ function register(router: Router) {
|
|||||||
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`);
|
throw new eu.EtapiError(400, "UNRECOGNIZED_EXPORT_FORMAT", `Unrecognized export format '${format}', supported values are 'html' (default) or 'markdown'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskContext = new TaskContext('no-progress-reporting');
|
const taskContext = new TaskContext("no-progress-reporting");
|
||||||
|
|
||||||
// technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain
|
// technically a branch is being exported (includes prefix), but it's such a minor difference yet usability pain
|
||||||
// (e.g. branchIds are not seen in UI), that we export "note export" instead.
|
// (e.g. branchIds are not seen in UI), that we export "note export" instead.
|
||||||
@@ -161,19 +160,19 @@ function register(router: Router) {
|
|||||||
zipExportService.exportToZip(taskContext, branch, format as "html" | "markdown", res);
|
zipExportService.exportToZip(taskContext, branch, format as "html" | "markdown", res);
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'post', '/etapi/notes/:noteId/import', (req, res, next) => {
|
eu.route(router, "post", "/etapi/notes/:noteId/import", (req, res, next) => {
|
||||||
const note = eu.getAndCheckNote(req.params.noteId);
|
const note = eu.getAndCheckNote(req.params.noteId);
|
||||||
const taskContext = new TaskContext('no-progress-reporting');
|
const taskContext = new TaskContext("no-progress-reporting");
|
||||||
|
|
||||||
zipImportService.importZip(taskContext, req.body, note).then(importedNote => {
|
zipImportService.importZip(taskContext, req.body, note).then((importedNote) => {
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
note: mappers.mapNoteToPojo(importedNote),
|
note: mappers.mapNoteToPojo(importedNote),
|
||||||
branch: mappers.mapBranchToPojo(importedNote.getParentBranches()[0]),
|
branch: mappers.mapBranchToPojo(importedNote.getParentBranches()[0])
|
||||||
});
|
});
|
||||||
}); // we need better error handling here, async errors won't be properly processed.
|
}); // we need better error handling here, async errors won't be properly processed.
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'post', '/etapi/notes/:noteId/revision', (req, res, next) => {
|
eu.route(router, "post", "/etapi/notes/:noteId/revision", (req, res, next) => {
|
||||||
const note = eu.getAndCheckNote(req.params.noteId);
|
const note = eu.getAndCheckNote(req.params.noteId);
|
||||||
|
|
||||||
note.saveRevision();
|
note.saveRevision();
|
||||||
@@ -181,27 +180,25 @@ function register(router: Router) {
|
|||||||
return res.sendStatus(204);
|
return res.sendStatus(204);
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'get', '/etapi/notes/:noteId/attachments', (req, res, next) => {
|
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
|
||||||
const note = eu.getAndCheckNote(req.params.noteId);
|
const note = eu.getAndCheckNote(req.params.noteId);
|
||||||
const attachments = note.getAttachments({ includeContentLength: true })
|
const attachments = note.getAttachments({ includeContentLength: true });
|
||||||
|
|
||||||
res.json(
|
res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment)));
|
||||||
attachments.map(attachment => mappers.mapAttachmentToPojo(attachment))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSearchParams(req: Request) {
|
function parseSearchParams(req: Request) {
|
||||||
const rawSearchParams: SearchParams = {
|
const rawSearchParams: SearchParams = {
|
||||||
fastSearch: parseBoolean(req.query, 'fastSearch'),
|
fastSearch: parseBoolean(req.query, "fastSearch"),
|
||||||
includeArchivedNotes: parseBoolean(req.query, 'includeArchivedNotes'),
|
includeArchivedNotes: parseBoolean(req.query, "includeArchivedNotes"),
|
||||||
ancestorNoteId: parseString(req.query['ancestorNoteId']),
|
ancestorNoteId: parseString(req.query["ancestorNoteId"]),
|
||||||
ancestorDepth: parseString(req.query['ancestorDepth']), // e.g. "eq5"
|
ancestorDepth: parseString(req.query["ancestorDepth"]), // e.g. "eq5"
|
||||||
orderBy: parseString(req.query['orderBy']),
|
orderBy: parseString(req.query["orderBy"]),
|
||||||
// TODO: Check why the order direction was provided as a number, but it's a string everywhere else.
|
// TODO: Check why the order direction was provided as a number, but it's a string everywhere else.
|
||||||
orderDirection: parseOrderDirection(req.query, 'orderDirection') as unknown as string,
|
orderDirection: parseOrderDirection(req.query, "orderDirection") as unknown as string,
|
||||||
limit: parseInteger(req.query, 'limit'),
|
limit: parseInteger(req.query, "limit"),
|
||||||
debug: parseBoolean(req.query, 'debug')
|
debug: parseBoolean(req.query, "debug")
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchParams: SearchParams = {};
|
const searchParams: SearchParams = {};
|
||||||
@@ -230,11 +227,11 @@ function parseBoolean(obj: any, name: string) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['true', 'false'].includes(obj[name])) {
|
if (!["true", "false"].includes(obj[name])) {
|
||||||
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'.`);
|
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse boolean '${name}' value '${obj[name]}, allowed values are 'true' and 'false'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj[name] === 'true';
|
return obj[name] === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseOrderDirection(obj: any, name: string) {
|
function parseOrderDirection(obj: any, name: string) {
|
||||||
@@ -244,7 +241,7 @@ function parseOrderDirection(obj: any, name: string) {
|
|||||||
|
|
||||||
const integer = parseInt(obj[name]);
|
const integer = parseInt(obj[name]);
|
||||||
|
|
||||||
if (!['asc', 'desc'].includes(obj[name])) {
|
if (!["asc", "desc"].includes(obj[name])) {
|
||||||
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'.`);
|
throw new eu.EtapiError(400, SEARCH_PARAM_ERROR, `Cannot parse order direction value '${obj[name]}, allowed values are 'asc' and 'desc'.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
const specPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'etapi.openapi.yaml');
|
const specPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "etapi.openapi.yaml");
|
||||||
let spec: string | null = null;
|
let spec: string | null = null;
|
||||||
|
|
||||||
function register(router: Router) {
|
function register(router: Router) {
|
||||||
router.get('/etapi/etapi.openapi.yaml', (req, res, next) => {
|
router.get("/etapi/etapi.openapi.yaml", (req, res, next) => {
|
||||||
if (!spec) {
|
if (!spec) {
|
||||||
spec = fs.readFileSync(specPath, 'utf8');
|
spec = fs.readFileSync(specPath, "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
res.header('Content-Type', 'text/plain'); // so that it displays in browser
|
res.header("Content-Type", "text/plain"); // so that it displays in browser
|
||||||
res.status(200).send(spec);
|
res.status(200).send(spec);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import specialNotesService from "../services/special_notes.js";
|
|||||||
import dateNotesService from "../services/date_notes.js";
|
import dateNotesService from "../services/date_notes.js";
|
||||||
import eu from "./etapi_utils.js";
|
import eu from "./etapi_utils.js";
|
||||||
import mappers from "./mappers.js";
|
import mappers from "./mappers.js";
|
||||||
import { Router } from 'express';
|
import { Router } from "express";
|
||||||
|
|
||||||
const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
|
const getDateInvalidError = (date: string) => new eu.EtapiError(400, "DATE_INVALID", `Date "${date}" is not valid.`);
|
||||||
const getMonthInvalidError = (month: string)=> new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
|
const getMonthInvalidError = (month: string) => new eu.EtapiError(400, "MONTH_INVALID", `Month "${month}" is not valid.`);
|
||||||
const getYearInvalidError = (year: string) => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
|
const getYearInvalidError = (year: string) => new eu.EtapiError(400, "YEAR_INVALID", `Year "${year}" is not valid.`);
|
||||||
|
|
||||||
function isValidDate(date: string) {
|
function isValidDate(date: string) {
|
||||||
@@ -17,7 +17,7 @@ function isValidDate(date: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function register(router: Router) {
|
function register(router: Router) {
|
||||||
eu.route(router, 'get', '/etapi/inbox/:date', (req, res, next) => {
|
eu.route(router, "get", "/etapi/inbox/:date", (req, res, next) => {
|
||||||
const { date } = req.params;
|
const { date } = req.params;
|
||||||
|
|
||||||
if (!isValidDate(date)) {
|
if (!isValidDate(date)) {
|
||||||
@@ -28,7 +28,7 @@ function register(router: Router) {
|
|||||||
res.json(mappers.mapNoteToPojo(note));
|
res.json(mappers.mapNoteToPojo(note));
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'get', '/etapi/calendar/days/:date', (req, res, next) => {
|
eu.route(router, "get", "/etapi/calendar/days/:date", (req, res, next) => {
|
||||||
const { date } = req.params;
|
const { date } = req.params;
|
||||||
|
|
||||||
if (!isValidDate(date)) {
|
if (!isValidDate(date)) {
|
||||||
@@ -39,7 +39,7 @@ function register(router: Router) {
|
|||||||
res.json(mappers.mapNoteToPojo(note));
|
res.json(mappers.mapNoteToPojo(note));
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'get', '/etapi/calendar/weeks/:date', (req, res, next) => {
|
eu.route(router, "get", "/etapi/calendar/weeks/:date", (req, res, next) => {
|
||||||
const { date } = req.params;
|
const { date } = req.params;
|
||||||
|
|
||||||
if (!isValidDate(date)) {
|
if (!isValidDate(date)) {
|
||||||
@@ -50,7 +50,7 @@ function register(router: Router) {
|
|||||||
res.json(mappers.mapNoteToPojo(note));
|
res.json(mappers.mapNoteToPojo(note));
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'get', '/etapi/calendar/months/:month', (req, res, next) => {
|
eu.route(router, "get", "/etapi/calendar/months/:month", (req, res, next) => {
|
||||||
const { month } = req.params;
|
const { month } = req.params;
|
||||||
|
|
||||||
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
|
if (!/[0-9]{4}-[0-9]{2}/.test(month)) {
|
||||||
@@ -61,7 +61,7 @@ function register(router: Router) {
|
|||||||
res.json(mappers.mapNoteToPojo(note));
|
res.json(mappers.mapNoteToPojo(note));
|
||||||
});
|
});
|
||||||
|
|
||||||
eu.route(router, 'get', '/etapi/calendar/years/:year', (req, res, next) => {
|
eu.route(router, "get", "/etapi/calendar/years/:year", (req, res, next) => {
|
||||||
const { year } = req.params;
|
const { year } = req.params;
|
||||||
|
|
||||||
if (!/[0-9]{4}/.test(year)) {
|
if (!/[0-9]{4}/.test(year)) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function isString(obj: unknown) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj !== 'string') {
|
if (typeof obj !== "string") {
|
||||||
return `'${obj}' is not a string`;
|
return `'${obj}' is not a string`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ function isBoolean(obj: unknown) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj !== 'boolean') {
|
if (typeof obj !== "boolean") {
|
||||||
return `'${obj}' is not a boolean`;
|
return `'${obj}' is not a boolean`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ function isNoteId(obj: unknown) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj !== 'string') {
|
if (typeof obj !== "string") {
|
||||||
return `'${obj}' is not a valid noteId`;
|
return `'${obj}' is not a valid noteId`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ function isAttributeType(obj: unknown) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj !== "string" || !['label', 'relation'].includes(obj)) {
|
if (typeof obj !== "string" || !["label", "relation"].includes(obj)) {
|
||||||
return `'${obj}' is not a valid attribute type, allowed types are: label, relation`;
|
return `'${obj}' is not a valid attribute type, allowed types are: label, relation`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ function isValidEntityId(obj: unknown) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof obj !== 'string' || !/^[A-Za-z0-9_]{4,128}$/.test(obj)) {
|
if (typeof obj !== "string" || !/^[A-Za-z0-9_]{4,128}$/.test(obj)) {
|
||||||
return `'${obj}' is not a valid entityId. Only alphanumeric characters are allowed of length 4 to 32.`;
|
return `'${obj}' is not a valid entityId. Only alphanumeric characters are allowed of length 4 to 32.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/express.d.ts
vendored
6
src/express.d.ts
vendored
@@ -4,18 +4,18 @@ export declare module "express-serve-static-core" {
|
|||||||
interface Request {
|
interface Request {
|
||||||
session: Session & {
|
session: Session & {
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
},
|
};
|
||||||
headers: {
|
headers: {
|
||||||
"x-local-date"?: string;
|
"x-local-date"?: string;
|
||||||
"x-labels"?: string;
|
"x-labels"?: string;
|
||||||
|
|
||||||
"authorization"?: string;
|
authorization?: string;
|
||||||
"trilium-cred"?: string;
|
"trilium-cred"?: string;
|
||||||
"x-csrf-token"?: string;
|
"x-csrf-token"?: string;
|
||||||
|
|
||||||
"trilium-component-id"?: string;
|
"trilium-component-id"?: string;
|
||||||
"trilium-local-now-datetime"?: string;
|
"trilium-local-now-datetime"?: string;
|
||||||
"trilium-hoisted-note-id"?: string;
|
"trilium-hoisted-note-id"?: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export type CommandMappings = {
|
|||||||
showInfoDialog: ConfirmWithMessageOptions;
|
showInfoDialog: ConfirmWithMessageOptions;
|
||||||
showConfirmDialog: ConfirmWithMessageOptions;
|
showConfirmDialog: ConfirmWithMessageOptions;
|
||||||
openNewNoteSplit: NoteCommandData;
|
openNewNoteSplit: NoteCommandData;
|
||||||
openInWindow: NoteCommandData,
|
openInWindow: NoteCommandData;
|
||||||
openNoteInNewTab: CommandData;
|
openNoteInNewTab: CommandData;
|
||||||
openNoteInNewSplit: CommandData;
|
openNoteInNewSplit: CommandData;
|
||||||
openNoteInNewWindow: CommandData;
|
openNoteInNewWindow: CommandData;
|
||||||
@@ -139,11 +139,12 @@ export type CommandMappings = {
|
|||||||
resetLauncher: ContextMenuCommandData;
|
resetLauncher: ContextMenuCommandData;
|
||||||
|
|
||||||
executeInActiveNoteDetailWidget: CommandData & {
|
executeInActiveNoteDetailWidget: CommandData & {
|
||||||
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void
|
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void;
|
||||||
};
|
|
||||||
executeWithTextEditor: CommandData & ExecuteCommandData & {
|
|
||||||
callback?: GetTextEditorCallback;
|
|
||||||
};
|
};
|
||||||
|
executeWithTextEditor: CommandData &
|
||||||
|
ExecuteCommandData & {
|
||||||
|
callback?: GetTextEditorCallback;
|
||||||
|
};
|
||||||
executeWithCodeEditor: CommandData & ExecuteCommandData;
|
executeWithCodeEditor: CommandData & ExecuteCommandData;
|
||||||
executeWithContentElement: CommandData & ExecuteCommandData;
|
executeWithContentElement: CommandData & ExecuteCommandData;
|
||||||
executeWithTypeWidget: CommandData & ExecuteCommandData;
|
executeWithTypeWidget: CommandData & ExecuteCommandData;
|
||||||
@@ -177,8 +178,8 @@ export type CommandMappings = {
|
|||||||
/** Sets the active {@link Screen} (e.g. to toggle the tree sidebar). It triggers the {@link EventMappings.activeScreenChanged} event, but only if the provided <em>screen</em> is different than the current one. */
|
/** Sets the active {@link Screen} (e.g. to toggle the tree sidebar). It triggers the {@link EventMappings.activeScreenChanged} event, but only if the provided <em>screen</em> is different than the current one. */
|
||||||
setActiveScreen: CommandData & {
|
setActiveScreen: CommandData & {
|
||||||
screen: Screen;
|
screen: Screen;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
type EventMappings = {
|
type EventMappings = {
|
||||||
initialRenderComplete: {};
|
initialRenderComplete: {};
|
||||||
@@ -195,57 +196,57 @@ type EventMappings = {
|
|||||||
messages: string[];
|
messages: string[];
|
||||||
};
|
};
|
||||||
entitiesReloaded: {
|
entitiesReloaded: {
|
||||||
loadResults: LoadResults
|
loadResults: LoadResults;
|
||||||
};
|
};
|
||||||
addNewLabel: CommandData;
|
addNewLabel: CommandData;
|
||||||
addNewRelation: CommandData;
|
addNewRelation: CommandData;
|
||||||
sqlQueryResults: CommandData & {
|
sqlQueryResults: CommandData & {
|
||||||
results: SqlExecuteResults;
|
results: SqlExecuteResults;
|
||||||
},
|
};
|
||||||
readOnlyTemporarilyDisabled: {
|
readOnlyTemporarilyDisabled: {
|
||||||
noteContext: NoteContext
|
noteContext: NoteContext;
|
||||||
},
|
};
|
||||||
/** Triggered when the {@link CommandMappings.setActiveScreen} command is invoked. */
|
/** Triggered when the {@link CommandMappings.setActiveScreen} command is invoked. */
|
||||||
activeScreenChanged: {
|
activeScreenChanged: {
|
||||||
activeScreen: Screen;
|
activeScreen: Screen;
|
||||||
},
|
};
|
||||||
activeContextChanged: {
|
activeContextChanged: {
|
||||||
noteContext: NoteContext;
|
noteContext: NoteContext;
|
||||||
},
|
};
|
||||||
noteSwitched: {
|
noteSwitched: {
|
||||||
noteContext: NoteContext;
|
noteContext: NoteContext;
|
||||||
notePath: string;
|
notePath: string;
|
||||||
},
|
};
|
||||||
noteSwitchedAndActivatedEvent: {
|
noteSwitchedAndActivatedEvent: {
|
||||||
noteContext: NoteContext;
|
noteContext: NoteContext;
|
||||||
notePath: string;
|
notePath: string;
|
||||||
},
|
};
|
||||||
setNoteContext: {
|
setNoteContext: {
|
||||||
noteContext: NoteContext;
|
noteContext: NoteContext;
|
||||||
},
|
};
|
||||||
noteTypeMimeChangedEvent: {
|
noteTypeMimeChangedEvent: {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
},
|
};
|
||||||
reEvaluateHighlightsListWidgetVisibility: {
|
reEvaluateHighlightsListWidgetVisibility: {
|
||||||
noteId: string | undefined;
|
noteId: string | undefined;
|
||||||
},
|
};
|
||||||
showHighlightsListWidget: {
|
showHighlightsListWidget: {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export type EventListener<T extends EventNames> = {
|
export type EventListener<T extends EventNames> = {
|
||||||
[key in T as `${key}Event`]: (data: EventData<T>) => void
|
[key in T as `${key}Event`]: (data: EventData<T>) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type CommandListener<T extends CommandNames> = {
|
export type CommandListener<T extends CommandNames> = {
|
||||||
[key in T as `${key}Command`]: (data: CommandListenerData<T>) => void
|
[key in T as `${key}Command`]: (data: CommandListenerData<T>) => void;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type CommandListenerData<T extends CommandNames> = CommandMappings[T];
|
export type CommandListenerData<T extends CommandNames> = CommandMappings[T];
|
||||||
export type EventData<T extends EventNames> = EventMappings[T];
|
export type EventData<T extends EventNames> = EventMappings[T];
|
||||||
|
|
||||||
type CommandAndEventMappings = (CommandMappings & EventMappings);
|
type CommandAndEventMappings = CommandMappings & EventMappings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This type is a discriminated union which contains all the possible commands that can be triggered via {@link AppContext.triggerCommand}.
|
* This type is a discriminated union which contains all the possible commands that can be triggered via {@link AppContext.triggerCommand}.
|
||||||
@@ -253,7 +254,7 @@ type CommandAndEventMappings = (CommandMappings & EventMappings);
|
|||||||
export type CommandNames = keyof CommandMappings;
|
export type CommandNames = keyof CommandMappings;
|
||||||
type EventNames = keyof EventMappings;
|
type EventNames = keyof EventMappings;
|
||||||
|
|
||||||
type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType ? K : never; }[keyof T];
|
type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType ? K : never }[keyof T];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic which filters {@link CommandNames} to provide only those commands that take in as data the desired implementation of {@link CommandData}. Mostly useful for contextual menu, to enforce consistency in the commands.
|
* Generic which filters {@link CommandNames} to provide only those commands that take in as data the desired implementation of {@link CommandData}. Mostly useful for contextual menu, to enforce consistency in the commands.
|
||||||
@@ -261,7 +262,6 @@ type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType
|
|||||||
export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMappings, FilterByValueType<CommandMappings, T>>;
|
export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMappings, FilterByValueType<CommandMappings, T>>;
|
||||||
|
|
||||||
class AppContext extends Component {
|
class AppContext extends Component {
|
||||||
|
|
||||||
isMainWindow: boolean;
|
isMainWindow: boolean;
|
||||||
components: Component[];
|
components: Component[];
|
||||||
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
|
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
|
||||||
@@ -304,13 +304,7 @@ class AppContext extends Component {
|
|||||||
initComponents() {
|
initComponents() {
|
||||||
this.tabManager = new TabManager();
|
this.tabManager = new TabManager();
|
||||||
|
|
||||||
this.components = [
|
this.components = [this.tabManager, new RootCommandExecutor(), new Entrypoints(), new MainTreeExecutors(), new ShortcutComponent()];
|
||||||
this.tabManager,
|
|
||||||
new RootCommandExecutor(),
|
|
||||||
new Entrypoints(),
|
|
||||||
new MainTreeExecutors(),
|
|
||||||
new ShortcutComponent()
|
|
||||||
];
|
|
||||||
|
|
||||||
if (utils.isMobile()) {
|
if (utils.isMobile()) {
|
||||||
this.components.push(new MobileScreenSwitcherExecutor());
|
this.components.push(new MobileScreenSwitcherExecutor());
|
||||||
@@ -337,21 +331,21 @@ class AppContext extends Component {
|
|||||||
|
|
||||||
$("body").append($renderedWidget);
|
$("body").append($renderedWidget);
|
||||||
|
|
||||||
$renderedWidget.on('click', "[data-trigger-command]", function() {
|
$renderedWidget.on("click", "[data-trigger-command]", function () {
|
||||||
if ($(this).hasClass("disabled")) {
|
if ($(this).hasClass("disabled")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandName = $(this).attr('data-trigger-command');
|
const commandName = $(this).attr("data-trigger-command");
|
||||||
const $component = $(this).closest(".component");
|
const $component = $(this).closest(".component");
|
||||||
const component = $component.prop("component");
|
const component = $component.prop("component");
|
||||||
|
|
||||||
component.triggerCommand(commandName, {$el: $(this)});
|
component.triggerCommand(commandName, { $el: $(this) });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.child(rootWidget);
|
this.child(rootWidget);
|
||||||
|
|
||||||
this.triggerEvent('initialRenderComplete');
|
this.triggerEvent("initialRenderComplete");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove ignore once all commands are mapped out.
|
// TODO: Remove ignore once all commands are mapped out.
|
||||||
@@ -378,7 +372,7 @@ class AppContext extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getComponentByEl(el: HTMLElement) {
|
getComponentByEl(el: HTMLElement) {
|
||||||
return $(el).closest(".component").prop('component');
|
return $(el).closest(".component").prop("component");
|
||||||
}
|
}
|
||||||
|
|
||||||
addBeforeUnloadListener(obj: BeforeUploadListener) {
|
addBeforeUnloadListener(obj: BeforeUploadListener) {
|
||||||
@@ -394,10 +388,10 @@ class AppContext extends Component {
|
|||||||
const appContext = new AppContext(window.glob.isMainWindow);
|
const appContext = new AppContext(window.glob.isMainWindow);
|
||||||
|
|
||||||
// we should save all outstanding changes before the page/app is closed
|
// we should save all outstanding changes before the page/app is closed
|
||||||
$(window).on('beforeunload', () => {
|
$(window).on("beforeunload", () => {
|
||||||
let allSaved = true;
|
let allSaved = true;
|
||||||
|
|
||||||
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref());
|
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref());
|
||||||
|
|
||||||
for (const weakRef of appContext.beforeUnloadListeners) {
|
for (const weakRef of appContext.beforeUnloadListeners) {
|
||||||
const component = weakRef.deref();
|
const component = weakRef.deref();
|
||||||
@@ -420,8 +414,8 @@ $(window).on('beforeunload', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(window).on('hashchange', function() {
|
$(window).on("hashchange", function () {
|
||||||
const {notePath, ntxId, viewScope} = linkService.parseNavigationStateFromUrl(window.location.href);
|
const { notePath, ntxId, viewScope } = linkService.parseNavigationStateFromUrl(window.location.href);
|
||||||
|
|
||||||
if (notePath || ntxId) {
|
if (notePath || ntxId) {
|
||||||
appContext.tabManager.switchToNoteContext(ntxId, notePath, viewScope);
|
appContext.tabManager.switchToNoteContext(ntxId, notePath, viewScope);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import utils from '../services/utils.js';
|
import utils from "../services/utils.js";
|
||||||
import { CommandMappings, CommandNames } from './app_context.js';
|
import { CommandMappings, CommandNames } from "./app_context.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class for all components in the Trilium's frontend.
|
* Abstract class for all components in the Trilium's frontend.
|
||||||
@@ -28,7 +28,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
|
|
||||||
get sanitizedClassName() {
|
get sanitizedClassName() {
|
||||||
// webpack mangles names and sometimes uses unsafe characters
|
// webpack mangles names and sometimes uses unsafe characters
|
||||||
return this.constructor.name.replace(/[^A-Z0-9]/ig, "_");
|
return this.constructor.name.replace(/[^A-Z0-9]/gi, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
setParent(parent: TypedComponent<any>) {
|
setParent(parent: TypedComponent<any>) {
|
||||||
@@ -48,18 +48,13 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
|
|
||||||
handleEvent(name: string, data: unknown): Promise<unknown> | null {
|
handleEvent(name: string, data: unknown): Promise<unknown> | null {
|
||||||
try {
|
try {
|
||||||
const callMethodPromise = this.initialized
|
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
|
||||||
? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data))
|
|
||||||
: this.callMethod((this as any)[`${name}Event`], data);
|
|
||||||
|
|
||||||
const childrenPromise = this.handleEventInChildren(name, data);
|
const childrenPromise = this.handleEventInChildren(name, data);
|
||||||
|
|
||||||
// don't create promises if not needed (optimization)
|
// don't create promises if not needed (optimization)
|
||||||
return callMethodPromise && childrenPromise
|
return callMethodPromise && childrenPromise ? Promise.all([callMethodPromise, childrenPromise]) : callMethodPromise || childrenPromise;
|
||||||
? Promise.all([callMethodPromise, childrenPromise])
|
} catch (e: any) {
|
||||||
: (callMethodPromise || childrenPromise);
|
|
||||||
}
|
|
||||||
catch (e: any) {
|
|
||||||
console.error(`Handling of event '${name}' failed in ${this.constructor.name} with error ${e.message} ${e.stack}`);
|
console.error(`Handling of event '${name}' failed in ${this.constructor.name} with error ${e.message} ${e.stack}`);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -101,7 +96,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
callMethod(fun: (arg: unknown) => Promise<unknown>, data: unknown) {
|
callMethod(fun: (arg: unknown) => Promise<unknown>, data: unknown) {
|
||||||
if (typeof fun !== 'function') {
|
if (typeof fun !== "function") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +106,8 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
|
|||||||
|
|
||||||
const took = Date.now() - startTime;
|
const took = Date.now() - startTime;
|
||||||
|
|
||||||
if (glob.isDev && took > 20) { // measuring only sync handlers
|
if (glob.isDev && took > 20) {
|
||||||
|
// measuring only sync handlers
|
||||||
console.log(`Call to ${fun.name} in ${this.componentId} took ${took}ms`);
|
console.log(`Call to ${fun.name} in ${this.componentId} took ${took}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import dateNoteService from "../services/date_notes.js";
|
import dateNoteService from "../services/date_notes.js";
|
||||||
import protectedSessionHolder from '../services/protected_session_holder.js';
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import server from "../services/server.js";
|
import server from "../services/server.js";
|
||||||
import appContext, { NoteCommandData } from "./app_context.js";
|
import appContext, { NoteCommandData } from "./app_context.js";
|
||||||
import Component from "./component.js";
|
import Component from "./component.js";
|
||||||
@@ -41,7 +41,7 @@ export default class Entrypoints extends Component {
|
|||||||
|
|
||||||
openDevToolsCommand() {
|
openDevToolsCommand() {
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
utils.dynamicRequire('@electron/remote').getCurrentWindow().toggleDevTools();
|
utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,20 +52,20 @@ export default class Entrypoints extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {note} = await server.post<CreateChildrenResponse>(`notes/${inboxNote.noteId}/children?target=into`, {
|
const { note } = await server.post<CreateChildrenResponse>(`notes/${inboxNote.noteId}/children?target=into`, {
|
||||||
content: '',
|
content: "",
|
||||||
type: 'text',
|
type: "text",
|
||||||
isProtected: inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable()
|
isProtected: inboxNote.isProtected && protectedSessionHolder.isProtectedSessionAvailable()
|
||||||
});
|
});
|
||||||
|
|
||||||
await ws.waitForMaxKnownEntityChangeId();
|
await ws.waitForMaxKnownEntityChangeId();
|
||||||
|
|
||||||
await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, {activate: true});
|
await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, { activate: true });
|
||||||
|
|
||||||
appContext.triggerEvent('focusAndSelectTitle', {isNewNote: true});
|
appContext.triggerEvent("focusAndSelectTitle", { isNewNote: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleNoteHoistingCommand({noteId = appContext.tabManager.getActiveContextNoteId()}) {
|
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
|
||||||
if (!noteId) {
|
if (!noteId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -75,12 +75,12 @@ export default class Entrypoints extends Component {
|
|||||||
|
|
||||||
if (noteToHoist?.noteId === activeNoteContext.hoistedNoteId) {
|
if (noteToHoist?.noteId === activeNoteContext.hoistedNoteId) {
|
||||||
await activeNoteContext.unhoist();
|
await activeNoteContext.unhoist();
|
||||||
} else if (noteToHoist?.type !== 'search') {
|
} else if (noteToHoist?.type !== "search") {
|
||||||
await activeNoteContext.setHoistedNoteId(noteId);
|
await activeNoteContext.setHoistedNoteId(noteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async hoistNoteCommand({noteId}: { noteId: string }) {
|
async hoistNoteCommand({ noteId }: { noteId: string }) {
|
||||||
const noteContext = appContext.tabManager.getActiveContext();
|
const noteContext = appContext.tabManager.getActiveContext();
|
||||||
|
|
||||||
if (noteContext.hoistedNoteId !== noteId) {
|
if (noteContext.hoistedNoteId !== noteId) {
|
||||||
@@ -102,7 +102,7 @@ export default class Entrypoints extends Component {
|
|||||||
|
|
||||||
toggleFullscreenCommand() {
|
toggleFullscreenCommand() {
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
const win = utils.dynamicRequire('@electron/remote').getCurrentWindow();
|
const win = utils.dynamicRequire("@electron/remote").getCurrentWindow();
|
||||||
|
|
||||||
if (win.isFullScreenable()) {
|
if (win.isFullScreenable()) {
|
||||||
win.setFullScreen(!win.isFullScreen());
|
win.setFullScreen(!win.isFullScreen());
|
||||||
@@ -115,22 +115,20 @@ export default class Entrypoints extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logoutCommand() {
|
logoutCommand() {
|
||||||
const $logoutForm = $('<form action="logout" method="POST">')
|
const $logoutForm = $('<form action="logout" method="POST">').append($(`<input type='_hidden' name="_csrf" value="${glob.csrfToken}"/>`));
|
||||||
.append($(`<input type='_hidden' name="_csrf" value="${glob.csrfToken}"/>`));
|
|
||||||
|
|
||||||
$("body").append($logoutForm);
|
$("body").append($logoutForm);
|
||||||
$logoutForm.trigger('submit');
|
$logoutForm.trigger("submit");
|
||||||
}
|
}
|
||||||
|
|
||||||
backInNoteHistoryCommand() {
|
backInNoteHistoryCommand() {
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
// standard JS version does not work completely correctly in electron
|
// standard JS version does not work completely correctly in electron
|
||||||
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
|
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||||
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
||||||
|
|
||||||
webContents.goToIndex(activeIndex - 1);
|
webContents.goToIndex(activeIndex - 1);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
window.history.back();
|
window.history.back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,52 +136,50 @@ export default class Entrypoints extends Component {
|
|||||||
forwardInNoteHistoryCommand() {
|
forwardInNoteHistoryCommand() {
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
// standard JS version does not work completely correctly in electron
|
// standard JS version does not work completely correctly in electron
|
||||||
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
|
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||||
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
||||||
|
|
||||||
webContents.goToIndex(activeIndex + 1);
|
webContents.goToIndex(activeIndex + 1);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
window.history.forward();
|
window.history.forward();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchToDesktopVersionCommand() {
|
async switchToDesktopVersionCommand() {
|
||||||
utils.setCookie('trilium-device', 'desktop');
|
utils.setCookie("trilium-device", "desktop");
|
||||||
|
|
||||||
utils.reloadFrontendApp("Switching to desktop version");
|
utils.reloadFrontendApp("Switching to desktop version");
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchToMobileVersionCommand() {
|
async switchToMobileVersionCommand() {
|
||||||
utils.setCookie('trilium-device', 'mobile');
|
utils.setCookie("trilium-device", "mobile");
|
||||||
|
|
||||||
utils.reloadFrontendApp("Switching to mobile version");
|
utils.reloadFrontendApp("Switching to mobile version");
|
||||||
}
|
}
|
||||||
|
|
||||||
async openInWindowCommand({notePath, hoistedNoteId, viewScope}: NoteCommandData) {
|
async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) {
|
||||||
const extraWindowHash = linkService.calculateHash({notePath, hoistedNoteId, viewScope});
|
const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope });
|
||||||
|
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
const {ipcRenderer} = utils.dynamicRequire('electron');
|
const { ipcRenderer } = utils.dynamicRequire("electron");
|
||||||
|
|
||||||
ipcRenderer.send('create-extra-window', { extraWindowHash });
|
ipcRenderer.send("create-extra-window", { extraWindowHash });
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
|
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
|
||||||
|
|
||||||
window.open(url, '', 'width=1000,height=800');
|
window.open(url, "", "width=1000,height=800");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openNewWindowCommand() {
|
async openNewWindowCommand() {
|
||||||
this.openInWindowCommand({notePath: '', hoistedNoteId: 'root'});
|
this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async runActiveNoteCommand() {
|
async runActiveNoteCommand() {
|
||||||
const {ntxId, note} = appContext.tabManager.getActiveContext();
|
const { ntxId, note } = appContext.tabManager.getActiveContext();
|
||||||
|
|
||||||
// ctrl+enter is also used elsewhere, so make sure we're running only when appropriate
|
// ctrl+enter is also used elsewhere, so make sure we're running only when appropriate
|
||||||
if (!note || note.type !== 'code') {
|
if (!note || note.type !== "code") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,14 +188,14 @@ export default class Entrypoints extends Component {
|
|||||||
await bundleService.getAndExecuteBundle(note.noteId);
|
await bundleService.getAndExecuteBundle(note.noteId);
|
||||||
} else if (note.mime.endsWith("env=backend")) {
|
} else if (note.mime.endsWith("env=backend")) {
|
||||||
await server.post(`script/run/${note.noteId}`);
|
await server.post(`script/run/${note.noteId}`);
|
||||||
} else if (note.mime === 'text/x-sqlite;schema=trilium') {
|
} else if (note.mime === "text/x-sqlite;schema=trilium") {
|
||||||
const resp = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
|
const resp = await server.post<SqlExecuteResponse>(`sql/execute/${note.noteId}`);
|
||||||
|
|
||||||
if (!resp.success) {
|
if (!resp.success) {
|
||||||
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
|
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
|
||||||
}
|
}
|
||||||
|
|
||||||
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results});
|
await appContext.triggerEvent("sqlQueryResults", { ntxId: ntxId, results: resp.results });
|
||||||
}
|
}
|
||||||
|
|
||||||
toastService.showMessage(t("entrypoints.note-executed"));
|
toastService.showMessage(t("entrypoints.note-executed"));
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export default class MainTreeExecutors extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedOrActiveNoteIds = this.tree.getSelectedOrActiveNodes().map(node => node.data.noteId);
|
const selectedOrActiveNoteIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.noteId);
|
||||||
|
|
||||||
this.triggerCommand('cloneNoteIdsTo', {noteIds: selectedOrActiveNoteIds});
|
this.triggerCommand("cloneNoteIdsTo", { noteIds: selectedOrActiveNoteIds });
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveNotesToCommand() {
|
async moveNotesToCommand() {
|
||||||
@@ -29,9 +29,9 @@ export default class MainTreeExecutors extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedOrActiveBranchIds = this.tree.getSelectedOrActiveNodes().map(node => node.data.branchId);
|
const selectedOrActiveBranchIds = this.tree.getSelectedOrActiveNodes().map((node) => node.data.branchId);
|
||||||
|
|
||||||
this.triggerCommand('moveBranchIdsTo', {branchIds: selectedOrActiveBranchIds});
|
this.triggerCommand("moveBranchIdsTo", { branchIds: selectedOrActiveBranchIds });
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNoteIntoCommand() {
|
async createNoteIntoCommand() {
|
||||||
@@ -61,12 +61,12 @@ export default class MainTreeExecutors extends Component {
|
|||||||
const parentNotePath = treeService.getNotePath(node.getParent());
|
const parentNotePath = treeService.getNotePath(node.getParent());
|
||||||
const isProtected = treeService.getParentProtectedStatus(node);
|
const isProtected = treeService.getParentProtectedStatus(node);
|
||||||
|
|
||||||
if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
|
if (node.data.noteId === "root" || node.data.noteId === hoistedNoteService.getHoistedNoteId()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteCreateService.createNote(parentNotePath, {
|
await noteCreateService.createNote(parentNotePath, {
|
||||||
target: 'after',
|
target: "after",
|
||||||
targetBranchId: node.data.branchId,
|
targetBranchId: node.data.branchId,
|
||||||
isProtected: isProtected,
|
isProtected: isProtected,
|
||||||
saveSelection: false
|
saveSelection: false
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ import type { CommandListener, CommandListenerData } from "./app_context.js";
|
|||||||
|
|
||||||
export type Screen = "detail" | "tree";
|
export type Screen = "detail" | "tree";
|
||||||
|
|
||||||
export default class MobileScreenSwitcherExecutor extends Component
|
export default class MobileScreenSwitcherExecutor extends Component implements CommandListener<"setActiveScreen"> {
|
||||||
implements CommandListener<"setActiveScreen">
|
|
||||||
{
|
|
||||||
private activeScreen?: Screen;
|
private activeScreen?: Screen;
|
||||||
|
|
||||||
setActiveScreenCommand({screen}: CommandListenerData<"setActiveScreen">) {
|
setActiveScreenCommand({ screen }: CommandListenerData<"setActiveScreen">) {
|
||||||
if (screen !== this.activeScreen) {
|
if (screen !== this.activeScreen) {
|
||||||
this.activeScreen = screen;
|
this.activeScreen = screen;
|
||||||
this.triggerEvent('activeScreenChanged', {activeScreen: screen});
|
this.triggerEvent("activeScreenChanged", { activeScreen: screen });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ interface SetNoteOpts {
|
|||||||
|
|
||||||
export type GetTextEditorCallback = () => void;
|
export type GetTextEditorCallback = () => void;
|
||||||
|
|
||||||
class NoteContext extends Component
|
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
|
||||||
implements EventListener<"entitiesReloaded">
|
|
||||||
{
|
|
||||||
|
|
||||||
ntxId: string | null;
|
ntxId: string | null;
|
||||||
hoistedNoteId: string;
|
hoistedNoteId: string;
|
||||||
private mainNtxId: string | null;
|
private mainNtxId: string | null;
|
||||||
@@ -30,7 +27,7 @@ class NoteContext extends Component
|
|||||||
private parentNoteId?: string | null;
|
private parentNoteId?: string | null;
|
||||||
viewScope?: ViewScope;
|
viewScope?: ViewScope;
|
||||||
|
|
||||||
constructor(ntxId: string | null = null, hoistedNoteId: string = 'root', mainNtxId: string | null = null) {
|
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.ntxId = ntxId || NoteContext.generateNtxId();
|
this.ntxId = ntxId || NoteContext.generateNtxId();
|
||||||
@@ -50,7 +47,7 @@ class NoteContext extends Component
|
|||||||
this.parentNoteId = null;
|
this.parentNoteId = null;
|
||||||
// hoisted note is kept intentionally
|
// hoisted note is kept intentionally
|
||||||
|
|
||||||
this.triggerEvent('noteSwitched', {
|
this.triggerEvent("noteSwitched", {
|
||||||
noteContext: this,
|
noteContext: this,
|
||||||
notePath: this.notePath
|
notePath: this.notePath
|
||||||
});
|
});
|
||||||
@@ -81,20 +78,20 @@ class NoteContext extends Component
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.triggerEvent('beforeNoteSwitch', {noteContext: this});
|
await this.triggerEvent("beforeNoteSwitch", { noteContext: this });
|
||||||
|
|
||||||
utils.closeActiveDialog();
|
utils.closeActiveDialog();
|
||||||
|
|
||||||
this.notePath = resolvedNotePath;
|
this.notePath = resolvedNotePath;
|
||||||
this.viewScope = opts.viewScope;
|
this.viewScope = opts.viewScope;
|
||||||
({noteId: this.noteId, parentNoteId: this.parentNoteId} = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
|
({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
|
||||||
|
|
||||||
this.saveToRecentNotes(resolvedNotePath);
|
this.saveToRecentNotes(resolvedNotePath);
|
||||||
|
|
||||||
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
|
||||||
|
|
||||||
if (opts.triggerSwitchEvent) {
|
if (opts.triggerSwitchEvent) {
|
||||||
await this.triggerEvent('noteSwitched', {
|
await this.triggerEvent("noteSwitched", {
|
||||||
noteContext: this,
|
noteContext: this,
|
||||||
notePath: this.notePath
|
notePath: this.notePath
|
||||||
});
|
});
|
||||||
@@ -103,23 +100,20 @@ class NoteContext extends Component
|
|||||||
await this.setHoistedNoteIfNeeded();
|
await this.setHoistedNoteIfNeeded();
|
||||||
|
|
||||||
if (utils.isMobile()) {
|
if (utils.isMobile()) {
|
||||||
this.triggerCommand('setActiveScreen', {screen: 'detail'});
|
this.triggerCommand("setActiveScreen", { screen: "detail" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setHoistedNoteIfNeeded() {
|
async setHoistedNoteIfNeeded() {
|
||||||
if (this.hoistedNoteId === 'root'
|
if (this.hoistedNoteId === "root" && this.notePath?.startsWith("root/_hidden") && !this.note?.isLabelTruthy("keepCurrentHoisting")) {
|
||||||
&& this.notePath?.startsWith("root/_hidden")
|
|
||||||
&& !this.note?.isLabelTruthy("keepCurrentHoisting")
|
|
||||||
) {
|
|
||||||
// hidden subtree displays only when hoisted, so it doesn't make sense to keep root as hoisted note
|
// hidden subtree displays only when hoisted, so it doesn't make sense to keep root as hoisted note
|
||||||
|
|
||||||
let hoistedNoteId = '_hidden';
|
let hoistedNoteId = "_hidden";
|
||||||
|
|
||||||
if (this.note?.isLaunchBarConfig()) {
|
if (this.note?.isLaunchBarConfig()) {
|
||||||
hoistedNoteId = '_lbRoot';
|
hoistedNoteId = "_lbRoot";
|
||||||
} else if (this.note?.isOptions()) {
|
} else if (this.note?.isOptions()) {
|
||||||
hoistedNoteId = '_options';
|
hoistedNoteId = "_options";
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setHoistedNoteId(hoistedNoteId);
|
await this.setHoistedNoteId(hoistedNoteId);
|
||||||
@@ -127,7 +121,7 @@ class NoteContext extends Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSubContexts() {
|
getSubContexts() {
|
||||||
return appContext.tabManager.noteContexts.filter(nc => nc.ntxId === this.ntxId || nc.mainNtxId === this.ntxId);
|
return appContext.tabManager.noteContexts.filter((nc) => nc.ntxId === this.ntxId || nc.mainNtxId === this.ntxId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,13 +146,11 @@ class NoteContext extends Component
|
|||||||
if (this.mainNtxId) {
|
if (this.mainNtxId) {
|
||||||
try {
|
try {
|
||||||
return appContext.tabManager.getNoteContextById(this.mainNtxId);
|
return appContext.tabManager.getNoteContextById(this.mainNtxId);
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
this.mainNtxId = null;
|
this.mainNtxId = null;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +159,7 @@ class NoteContext extends Component
|
|||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
// we include the note in the recent list only if the user stayed on the note at least 5 seconds
|
// we include the note in the recent list only if the user stayed on the note at least 5 seconds
|
||||||
if (resolvedNotePath && resolvedNotePath === this.notePath) {
|
if (resolvedNotePath && resolvedNotePath === this.notePath) {
|
||||||
await server.post('recent-notes', {
|
await server.post("recent-notes", {
|
||||||
noteId: this.note?.noteId,
|
noteId: this.note?.noteId,
|
||||||
notePath: this.notePath
|
notePath: this.notePath
|
||||||
});
|
});
|
||||||
@@ -183,7 +175,7 @@ class NoteContext extends Component
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await hoistedNoteService.checkNoteAccess(resolvedNotePath, this) === false) {
|
if ((await hoistedNoteService.checkNoteAccess(resolvedNotePath, this)) === false) {
|
||||||
return; // note is outside of hoisted subtree and user chose not to unhoist
|
return; // note is outside of hoisted subtree and user chose not to unhoist
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +192,7 @@ class NoteContext extends Component
|
|||||||
|
|
||||||
/** @returns {string[]} */
|
/** @returns {string[]} */
|
||||||
get notePathArray() {
|
get notePathArray() {
|
||||||
return this.notePath ? this.notePath.split('/') : [];
|
return this.notePath ? this.notePath.split("/") : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive() {
|
isActive() {
|
||||||
@@ -208,7 +200,7 @@ class NoteContext extends Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPojoState() {
|
getPojoState() {
|
||||||
if (this.hoistedNoteId !== 'root') {
|
if (this.hoistedNoteId !== "root") {
|
||||||
// keeping empty hoisted tab is esp. important for mobile (e.g. opened launcher config)
|
// keeping empty hoisted tab is esp. important for mobile (e.g. opened launcher config)
|
||||||
|
|
||||||
if (!this.notePath && this.getSubContexts().length === 0) {
|
if (!this.notePath && this.getSubContexts().length === 0) {
|
||||||
@@ -223,11 +215,11 @@ class NoteContext extends Component
|
|||||||
hoistedNoteId: this.hoistedNoteId,
|
hoistedNoteId: this.hoistedNoteId,
|
||||||
active: this.isActive(),
|
active: this.isActive(),
|
||||||
viewScope: this.viewScope
|
viewScope: this.viewScope
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async unhoist() {
|
async unhoist() {
|
||||||
await this.setHoistedNoteId('root');
|
await this.setHoistedNoteId("root");
|
||||||
}
|
}
|
||||||
|
|
||||||
async setHoistedNoteId(noteIdToHoist: string) {
|
async setHoistedNoteId(noteIdToHoist: string) {
|
||||||
@@ -241,7 +233,7 @@ class NoteContext extends Component
|
|||||||
await this.setNote(noteIdToHoist);
|
await this.setNote(noteIdToHoist);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.triggerEvent('hoistedNoteChanged', {
|
await this.triggerEvent("hoistedNoteChanged", {
|
||||||
noteId: noteIdToHoist,
|
noteId: noteIdToHoist,
|
||||||
ntxId: this.ntxId
|
ntxId: this.ntxId
|
||||||
});
|
});
|
||||||
@@ -254,15 +246,15 @@ class NoteContext extends Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
// "readOnly" is a state valid only for text/code notes
|
// "readOnly" is a state valid only for text/code notes
|
||||||
if (!this.note || (this.note.type !== 'text' && this.note.type !== 'code')) {
|
if (!this.note || (this.note.type !== "text" && this.note.type !== "code")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.note.isLabelTruthy('readOnly')) {
|
if (this.note.isLabelTruthy("readOnly")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.viewScope?.viewMode === 'source') {
|
if (this.viewScope?.viewMode === "source") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,24 +263,20 @@ class NoteContext extends Component
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizeLimit = this.note.type === 'text'
|
const sizeLimit = this.note.type === "text" ? options.getInt("autoReadonlySizeText") : options.getInt("autoReadonlySizeCode");
|
||||||
? options.getInt('autoReadonlySizeText')
|
|
||||||
: options.getInt('autoReadonlySizeCode');
|
|
||||||
|
|
||||||
return sizeLimit
|
return sizeLimit && blob.contentLength > sizeLimit && !this.note.isLabelTruthy("autoReadOnlyDisabled");
|
||||||
&& blob.contentLength > sizeLimit
|
|
||||||
&& !this.note.isLabelTruthy('autoReadOnlyDisabled');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async entitiesReloadedEvent({loadResults}: EventData<"entitiesReloaded">) {
|
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
|
if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
|
||||||
const noteRow = loadResults.getEntityRow('notes', this.noteId);
|
const noteRow = loadResults.getEntityRow("notes", this.noteId);
|
||||||
|
|
||||||
if (noteRow.isDeleted) {
|
if (noteRow.isDeleted) {
|
||||||
this.noteId = null;
|
this.noteId = null;
|
||||||
this.notePath = null;
|
this.notePath = null;
|
||||||
|
|
||||||
this.triggerEvent('noteSwitched', {
|
this.triggerEvent("noteSwitched", {
|
||||||
noteContext: this,
|
noteContext: this,
|
||||||
notePath: this.notePath
|
notePath: this.notePath
|
||||||
});
|
});
|
||||||
@@ -297,48 +285,63 @@ class NoteContext extends Component
|
|||||||
}
|
}
|
||||||
|
|
||||||
hasNoteList() {
|
hasNoteList() {
|
||||||
return this.note
|
return (
|
||||||
&& this.viewScope?.viewMode === 'default'
|
this.note &&
|
||||||
&& this.note.hasChildren()
|
this.viewScope?.viewMode === "default" &&
|
||||||
&& ['book', 'text', 'code'].includes(this.note.type)
|
this.note.hasChildren() &&
|
||||||
&& this.note.mime !== 'text/x-sqlite;schema=trilium'
|
["book", "text", "code"].includes(this.note.type) &&
|
||||||
&& !this.note.isLabelTruthy('hideChildrenOverview');
|
this.note.mime !== "text/x-sqlite;schema=trilium" &&
|
||||||
|
!this.note.isLabelTruthy("hideChildrenOverview")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTextEditor(callback?: GetTextEditorCallback) {
|
async getTextEditor(callback?: GetTextEditorCallback) {
|
||||||
return this.timeout<TextEditor>(new Promise(resolve => appContext.triggerCommand('executeWithTextEditor', {
|
return this.timeout<TextEditor>(
|
||||||
callback,
|
new Promise((resolve) =>
|
||||||
resolve,
|
appContext.triggerCommand("executeWithTextEditor", {
|
||||||
ntxId: this.ntxId
|
callback,
|
||||||
})));
|
resolve,
|
||||||
|
ntxId: this.ntxId
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCodeEditor() {
|
async getCodeEditor() {
|
||||||
return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithCodeEditor', {
|
return this.timeout(
|
||||||
resolve,
|
new Promise((resolve) =>
|
||||||
ntxId: this.ntxId
|
appContext.triggerCommand("executeWithCodeEditor", {
|
||||||
})));
|
resolve,
|
||||||
|
ntxId: this.ntxId
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getContentElement() {
|
async getContentElement() {
|
||||||
return this.timeout<JQuery<HTMLElement>>(new Promise(resolve => appContext.triggerCommand('executeWithContentElement', {
|
return this.timeout<JQuery<HTMLElement>>(
|
||||||
resolve,
|
new Promise((resolve) =>
|
||||||
ntxId: this.ntxId
|
appContext.triggerCommand("executeWithContentElement", {
|
||||||
})));
|
resolve,
|
||||||
|
ntxId: this.ntxId
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTypeWidget() {
|
async getTypeWidget() {
|
||||||
return this.timeout(new Promise(resolve => appContext.triggerCommand('executeWithTypeWidget', {
|
return this.timeout(
|
||||||
resolve,
|
new Promise((resolve) =>
|
||||||
ntxId: this.ntxId
|
appContext.triggerCommand("executeWithTypeWidget", {
|
||||||
})));
|
resolve,
|
||||||
|
ntxId: this.ntxId
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout<T>(promise: Promise<T | null>) {
|
timeout<T>(promise: Promise<T | null>) {
|
||||||
return Promise.race([
|
return Promise.race([promise, new Promise((res) => setTimeout(() => res(null), 200))]) as Promise<T>;
|
||||||
promise,
|
|
||||||
new Promise(res => setTimeout(() => res(null), 200))
|
|
||||||
]) as Promise<T>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetViewScope() {
|
resetViewScope() {
|
||||||
@@ -355,9 +358,7 @@ class NoteContext extends Component
|
|||||||
|
|
||||||
const { note, viewScope } = this;
|
const { note, viewScope } = this;
|
||||||
|
|
||||||
let title = viewScope?.viewMode === 'default'
|
let title = viewScope?.viewMode === "default" ? note.title : `${note.title}: ${viewScope?.viewMode}`;
|
||||||
? note.title
|
|
||||||
: `${note.title}: ${viewScope?.viewMode}`;
|
|
||||||
|
|
||||||
if (viewScope?.attachmentId) {
|
if (viewScope?.attachmentId) {
|
||||||
// assuming the attachment has been already loaded
|
// assuming the attachment has been already loaded
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ export default class RootCommandExecutor extends Component {
|
|||||||
|
|
||||||
const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(sqlConsoleNote.noteId, { activate: true });
|
const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(sqlConsoleNote.noteId, { activate: true });
|
||||||
|
|
||||||
appContext.triggerEvent('focusOnDetail', {ntxId: noteContext.ntxId});
|
appContext.triggerEvent("focusOnDetail", { ntxId: noteContext.ntxId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchNotesCommand({searchString, ancestorNoteId}: CommandListenerData<"searchNotes">) {
|
async searchNotesCommand({ searchString, ancestorNoteId }: CommandListenerData<"searchNotes">) {
|
||||||
const searchNote = await dateNoteService.createSearchNote({searchString, ancestorNoteId});
|
const searchNote = await dateNoteService.createSearchNote({ searchString, ancestorNoteId });
|
||||||
if (!searchNote) {
|
if (!searchNote) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -41,13 +41,13 @@ export default class RootCommandExecutor extends Component {
|
|||||||
activate: true
|
activate: true
|
||||||
});
|
});
|
||||||
|
|
||||||
appContext.triggerCommand('focusOnSearchDefinition', {ntxId: noteContext.ntxId});
|
appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchInSubtreeCommand({notePath}: CommandListenerData<"searchInSubtree">) {
|
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
|
||||||
const noteId = treeService.getNoteIdFromUrl(notePath);
|
const noteId = treeService.getNoteIdFromUrl(notePath);
|
||||||
|
|
||||||
this.searchNotesCommand({ancestorNoteId: noteId});
|
this.searchNotesCommand({ ancestorNoteId: noteId });
|
||||||
}
|
}
|
||||||
|
|
||||||
openNoteExternallyCommand() {
|
openNoteExternallyCommand() {
|
||||||
@@ -83,11 +83,11 @@ export default class RootCommandExecutor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleLeftPaneCommand() {
|
toggleLeftPaneCommand() {
|
||||||
options.toggle('leftPaneVisible');
|
options.toggle("leftPaneVisible");
|
||||||
}
|
}
|
||||||
|
|
||||||
async showBackendLogCommand() {
|
async showBackendLogCommand() {
|
||||||
await appContext.tabManager.openTabWithNoteWithHoisting('_backendLog', { activate: true });
|
await appContext.tabManager.openTabWithNoteWithHoisting("_backendLog", { activate: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async showLaunchBarSubtreeCommand() {
|
async showLaunchBarSubtreeCommand() {
|
||||||
@@ -97,26 +97,26 @@ export default class RootCommandExecutor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async showShareSubtreeCommand() {
|
async showShareSubtreeCommand() {
|
||||||
await this.showAndHoistSubtree('_share');
|
await this.showAndHoistSubtree("_share");
|
||||||
}
|
}
|
||||||
|
|
||||||
async showHiddenSubtreeCommand() {
|
async showHiddenSubtreeCommand() {
|
||||||
await this.showAndHoistSubtree('_hidden');
|
await this.showAndHoistSubtree("_hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
async showOptionsCommand({section}: CommandListenerData<"showOptions">) {
|
async showOptionsCommand({ section }: CommandListenerData<"showOptions">) {
|
||||||
await appContext.tabManager.openContextWithNote(section || '_options', {
|
await appContext.tabManager.openContextWithNote(section || "_options", {
|
||||||
activate: true,
|
activate: true,
|
||||||
hoistedNoteId: '_options'
|
hoistedNoteId: "_options"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async showSQLConsoleHistoryCommand() {
|
async showSQLConsoleHistoryCommand() {
|
||||||
await this.showAndHoistSubtree('_sqlConsole');
|
await this.showAndHoistSubtree("_sqlConsole");
|
||||||
}
|
}
|
||||||
|
|
||||||
async showSearchHistoryCommand() {
|
async showSearchHistoryCommand() {
|
||||||
await this.showAndHoistSubtree('_search');
|
await this.showAndHoistSubtree("_search");
|
||||||
}
|
}
|
||||||
|
|
||||||
async showAndHoistSubtree(subtreeNoteId: string) {
|
async showAndHoistSubtree(subtreeNoteId: string) {
|
||||||
@@ -133,7 +133,7 @@ export default class RootCommandExecutor extends Component {
|
|||||||
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
||||||
activate: true,
|
activate: true,
|
||||||
viewScope: {
|
viewScope: {
|
||||||
viewMode: 'source'
|
viewMode: "source"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ export default class RootCommandExecutor extends Component {
|
|||||||
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
||||||
activate: true,
|
activate: true,
|
||||||
viewScope: {
|
viewScope: {
|
||||||
viewMode: 'attachments'
|
viewMode: "attachments"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ export default class RootCommandExecutor extends Component {
|
|||||||
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
|
||||||
activate: true,
|
activate: true,
|
||||||
viewScope: {
|
viewScope: {
|
||||||
viewMode: 'attachments'
|
viewMode: "attachments"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -167,23 +167,43 @@ export default class RootCommandExecutor extends Component {
|
|||||||
|
|
||||||
toggleTrayCommand() {
|
toggleTrayCommand() {
|
||||||
if (!utils.isElectron()) return;
|
if (!utils.isElectron()) return;
|
||||||
const {BrowserWindow} = utils.dynamicRequire('@electron/remote');
|
const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
|
||||||
const windows = (BrowserWindow.getAllWindows()) as Electron.BaseWindow[];
|
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
|
||||||
const isVisible = windows.every(w => w.isVisible());
|
const isVisible = windows.every((w) => w.isVisible());
|
||||||
const action = isVisible ? "hide" : "show"
|
const action = isVisible ? "hide" : "show";
|
||||||
for (const window of windows) window[action]();
|
for (const window of windows) window[action]();
|
||||||
}
|
}
|
||||||
|
|
||||||
firstTabCommand() { this.#goToTab(1); }
|
firstTabCommand() {
|
||||||
secondTabCommand() { this.#goToTab(2); }
|
this.#goToTab(1);
|
||||||
thirdTabCommand() { this.#goToTab(3); }
|
}
|
||||||
fourthTabCommand() { this.#goToTab(4); }
|
secondTabCommand() {
|
||||||
fifthTabCommand() { this.#goToTab(5); }
|
this.#goToTab(2);
|
||||||
sixthTabCommand() { this.#goToTab(6); }
|
}
|
||||||
seventhTabCommand() { this.#goToTab(7); }
|
thirdTabCommand() {
|
||||||
eigthTabCommand() { this.#goToTab(8); }
|
this.#goToTab(3);
|
||||||
ninthTabCommand() { this.#goToTab(9); }
|
}
|
||||||
lastTabCommand() { this.#goToTab(Number.POSITIVE_INFINITY); }
|
fourthTabCommand() {
|
||||||
|
this.#goToTab(4);
|
||||||
|
}
|
||||||
|
fifthTabCommand() {
|
||||||
|
this.#goToTab(5);
|
||||||
|
}
|
||||||
|
sixthTabCommand() {
|
||||||
|
this.#goToTab(6);
|
||||||
|
}
|
||||||
|
seventhTabCommand() {
|
||||||
|
this.#goToTab(7);
|
||||||
|
}
|
||||||
|
eigthTabCommand() {
|
||||||
|
this.#goToTab(8);
|
||||||
|
}
|
||||||
|
ninthTabCommand() {
|
||||||
|
this.#goToTab(9);
|
||||||
|
}
|
||||||
|
lastTabCommand() {
|
||||||
|
this.#goToTab(Number.POSITIVE_INFINITY);
|
||||||
|
}
|
||||||
|
|
||||||
#goToTab(tabNumber: number) {
|
#goToTab(tabNumber: number) {
|
||||||
const mainNoteContexts = appContext.tabManager.getMainNoteContexts();
|
const mainNoteContexts = appContext.tabManager.getMainNoteContexts();
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import Component from "./component.js";
|
|||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import { AttributeRow } from "../services/load_results.js";
|
import { AttributeRow } from "../services/load_results.js";
|
||||||
|
|
||||||
export default class ShortcutComponent extends Component
|
export default class ShortcutComponent extends Component implements EventListener<"entitiesReloaded"> {
|
||||||
implements EventListener<"entitiesReloaded">
|
|
||||||
{
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
server.get<AttributeRow[]>('keyboard-shortcuts-for-notes').then(shortcutAttributes => {
|
server.get<AttributeRow[]>("keyboard-shortcuts-for-notes").then((shortcutAttributes) => {
|
||||||
for (const attr of shortcutAttributes) {
|
for (const attr of shortcutAttributes) {
|
||||||
this.bindNoteShortcutHandler(attr);
|
this.bindNoteShortcutHandler(attr);
|
||||||
}
|
}
|
||||||
@@ -22,7 +20,8 @@ export default class ShortcutComponent extends Component
|
|||||||
const handler = () => appContext.tabManager.getActiveContext().setNote(labelOrRow.noteId);
|
const handler = () => appContext.tabManager.getActiveContext().setNote(labelOrRow.noteId);
|
||||||
const namespace = labelOrRow.attributeId;
|
const namespace = labelOrRow.attributeId;
|
||||||
|
|
||||||
if (labelOrRow.isDeleted) { // only applicable if row
|
if (labelOrRow.isDeleted) {
|
||||||
|
// only applicable if row
|
||||||
if (namespace) {
|
if (namespace) {
|
||||||
shortcutService.removeGlobalShortcut(namespace);
|
shortcutService.removeGlobalShortcut(namespace);
|
||||||
}
|
}
|
||||||
@@ -31,12 +30,12 @@ export default class ShortcutComponent extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async entitiesReloadedEvent({loadResults}: EventData<"entitiesReloaded">) {
|
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
for (const attr of loadResults.getAttributeRows()) {
|
for (const attr of loadResults.getAttributeRows()) {
|
||||||
if (attr.type === 'label' && attr.name === 'keyboardShortcut' && attr.noteId) {
|
if (attr.type === "label" && attr.name === "keyboardShortcut" && attr.noteId) {
|
||||||
const note = await froca.getNote(attr.noteId);
|
const note = await froca.getNote(attr.noteId);
|
||||||
// launcher shortcuts are handled specifically
|
// launcher shortcuts are handled specifically
|
||||||
if (note && attr && note.type !== 'launcher') {
|
if (note && attr && note.type !== "launcher") {
|
||||||
this.bindNoteShortcutHandler(attr);
|
this.bindNoteShortcutHandler(attr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user