mirror of
https://github.com/zadam/trilium.git
synced 2026-08-30 11:06:51 +02:00
feat(standalone/setup): set up new document
This commit is contained in:
@@ -179,10 +179,23 @@ function apiResultHandler(_req: any, res: ResultHandlerResponse, result: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op auth middleware for standalone — there's no authentication.
|
||||
* No-op middleware stubs for standalone mode.
|
||||
*
|
||||
* In a browser context there is no network authentication, rate limiting,
|
||||
* or multi-user access, so all auth/rate-limit middleware is a no-op.
|
||||
*
|
||||
* `checkAppNotInitialized` still guards setup routes: if the database is
|
||||
* already initialised the middleware throws so the route handler is never
|
||||
* reached (mirrors the server behaviour).
|
||||
*/
|
||||
function checkApiAuth() {
|
||||
// No authentication in standalone mode.
|
||||
function noopMiddleware() {
|
||||
// No-op.
|
||||
}
|
||||
|
||||
function checkAppNotInitialized() {
|
||||
if (sql_init.isDbInitialized()) {
|
||||
throw new Error("App already initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,11 +220,15 @@ export function registerRoutes(router: BrowserRouter): void {
|
||||
const apiRoute = createApiRoute(router, true);
|
||||
routes.buildSharedApiRoutes({
|
||||
route: createRoute(router),
|
||||
asyncRoute: createRoute(router),
|
||||
apiRoute,
|
||||
asyncApiRoute: createApiRoute(router, false),
|
||||
apiResultHandler,
|
||||
checkApiAuth,
|
||||
checkApiAuthOrElectron: checkApiAuth
|
||||
checkApiAuth: noopMiddleware,
|
||||
checkApiAuthOrElectron: noopMiddleware,
|
||||
checkAppNotInitialized,
|
||||
checkCredentials: noopMiddleware,
|
||||
loginRateLimiter: noopMiddleware
|
||||
});
|
||||
apiRoute('get', '/bootstrap', bootstrapRoute);
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { ComponentChildren, render } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { initLocale, t } from "./services/i18n";
|
||||
import server from "./services/server";
|
||||
import Button from "./widgets/react/Button";
|
||||
import { CardFrame } from "./widgets/react/Card";
|
||||
import FormGroup from "./widgets/react/FormGroup";
|
||||
import FormTextBox from "./widgets/react/FormTextBox";
|
||||
import Icon from "./widgets/react/Icon";
|
||||
|
||||
@@ -42,6 +42,9 @@ function SetupOptions({ setState }: { setState: (state: State) => void }) {
|
||||
icon="bx bx-file-blank"
|
||||
title={t("setup.new-document")}
|
||||
description={t("setup.new-document-description")}
|
||||
onClick={() => {
|
||||
server.post("setup/new-document");
|
||||
}}
|
||||
/>
|
||||
|
||||
<SetupOptionCard
|
||||
@@ -63,6 +66,16 @@ function SetupOptions({ setState }: { setState: (state: State) => void }) {
|
||||
}
|
||||
|
||||
function SyncFromServer({ setState }: { setState: (state: State) => void }) {
|
||||
const [serverUrl, setServerUrl] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
function handleFinishSetup() {
|
||||
server.post("setup/sync-from-server", {
|
||||
syncServerHost: serverUrl,
|
||||
password
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="page sync-from-server">
|
||||
<h1>{t("setup.sync-from-server-page-title")}</h1>
|
||||
@@ -71,18 +84,18 @@ function SyncFromServer({ setState }: { setState: (state: State) => void }) {
|
||||
<main>
|
||||
<form>
|
||||
<FormItemWithIcon icon="bx bx-server">
|
||||
<FormTextBox placeholder="https://example.com" />
|
||||
<FormTextBox placeholder="https://example.com" currentValue={serverUrl} onChange={setServerUrl} />
|
||||
</FormItemWithIcon>
|
||||
|
||||
<FormItemWithIcon icon="bx bx-lock">
|
||||
<FormTextBox placeholder={t("setup.password-placeholder")} type="password" />
|
||||
<FormTextBox placeholder={t("setup.password-placeholder")} type="password" currentValue={password} onChange={setPassword} />
|
||||
</FormItemWithIcon>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<Button text={t("setup.button-back")} onClick={() => setState("firstOptions")} kind="lowProfile" />
|
||||
<Button text={t("setup.button-finish-setup")} kind="primary" />
|
||||
<Button text={t("setup.button-finish-setup")} kind="primary" onClick={handleFinishSetup} />
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,6 @@ import passwordApiRoute from "./api/password.js";
|
||||
import recoveryCodes from './api/recovery_codes.js';
|
||||
import scriptRoute from "./api/script.js";
|
||||
import senderRoute from "./api/sender.js";
|
||||
import setupApiRoute from "./api/setup.js";
|
||||
import systemInfoRoute from "./api/system_info.js";
|
||||
import totp from './api/totp.js';
|
||||
// API routes
|
||||
@@ -83,11 +82,15 @@ function register(app: express.Application) {
|
||||
|
||||
routes.buildSharedApiRoutes({
|
||||
route,
|
||||
asyncRoute,
|
||||
apiRoute,
|
||||
asyncApiRoute,
|
||||
apiResultHandler,
|
||||
checkApiAuth: auth.checkApiAuth,
|
||||
checkApiAuthOrElectron: auth.checkApiAuthOrElectron
|
||||
checkApiAuthOrElectron: auth.checkApiAuthOrElectron,
|
||||
checkAppNotInitialized: auth.checkAppNotInitialized,
|
||||
checkCredentials: auth.checkCredentials,
|
||||
loginRateLimiter
|
||||
});
|
||||
|
||||
route(PUT, "/api/notes/:noteId/file", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], filesRoute.updateFile, apiResultHandler);
|
||||
@@ -149,13 +152,6 @@ function register(app: express.Application) {
|
||||
// docker health check
|
||||
route(GET, "/api/health-check", [], () => ({ status: "ok" }), apiResultHandler);
|
||||
|
||||
// group of the services below are meant to be executed from the outside
|
||||
route(GET, "/api/setup/status", [], setupApiRoute.getStatus, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/new-document", [auth.checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/sync-from-server", [auth.checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler);
|
||||
route(GET, "/api/setup/sync-seed", [loginRateLimiter, auth.checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/sync-seed", [auth.checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler);
|
||||
|
||||
route(PST, "/api/login/sync", [loginRateLimiter], loginApiRoute.loginSync, apiResultHandler);
|
||||
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
|
||||
apiRoute(PST, "/api/login/protected", loginApiRoute.loginToProtectedSession);
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import syncService from "./sync.js";
|
||||
import log from "./log.js";
|
||||
import sqlInit from "./sql_init.js";
|
||||
import optionService from "./options.js";
|
||||
import syncOptions from "./sync_options.js";
|
||||
import { request } from "@triliumnext/core";
|
||||
import appInfo from "./app_info.js";
|
||||
import { timeLimit } from "./utils.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import type { SetupStatusResponse, SetupSyncSeedResponse } from "./api-interface.js";
|
||||
|
||||
async function hasSyncServerSchemaAndSeed() {
|
||||
const response = await requestToSyncServer<SetupStatusResponse>("GET", "/api/setup/status");
|
||||
|
||||
if (response.syncVersion !== appInfo.syncVersion) {
|
||||
throw new Error(
|
||||
`Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${response.syncVersion}. To fix this issue, use same Trilium version on all instances.`
|
||||
);
|
||||
}
|
||||
|
||||
return response.schemaExists;
|
||||
}
|
||||
|
||||
function triggerSync() {
|
||||
log.info("Triggering sync.");
|
||||
|
||||
// it's ok to not wait for it here
|
||||
syncService.sync().then((res) => {
|
||||
if (res.success) {
|
||||
sqlInit.setDbAsInitialized();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function sendSeedToSyncServer() {
|
||||
log.info("Initiating sync to server");
|
||||
|
||||
await requestToSyncServer<void>("POST", "/api/setup/sync-seed", {
|
||||
options: getSyncSeedOptions(),
|
||||
syncVersion: appInfo.syncVersion
|
||||
});
|
||||
|
||||
// this is a completely new sync, need to reset counters. If this was not a new sync,
|
||||
// the previous request would have failed.
|
||||
optionService.setOption("lastSyncedPush", 0);
|
||||
optionService.setOption("lastSyncedPull", 0);
|
||||
}
|
||||
|
||||
async function requestToSyncServer<T>(method: string, path: string, body?: string | {}): Promise<T> {
|
||||
const timeout = syncOptions.getSyncTimeout();
|
||||
|
||||
return (await timeLimit(
|
||||
request.exec({
|
||||
method,
|
||||
url: syncOptions.getSyncServerHost() + path,
|
||||
body,
|
||||
proxy: syncOptions.getSyncProxy(),
|
||||
timeout: timeout
|
||||
}),
|
||||
timeout
|
||||
)) as T;
|
||||
}
|
||||
|
||||
async function setupSyncFromSyncServer(syncServerHost: string, syncProxy: string, password: string) {
|
||||
if (sqlInit.isDbInitialized()) {
|
||||
return {
|
||||
result: "failure",
|
||||
error: "DB is already initialized."
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Getting document options FROM sync server.");
|
||||
|
||||
// the response is expected to contain documentId and documentSecret options
|
||||
const resp = await request.exec<SetupSyncSeedResponse>({
|
||||
method: "get",
|
||||
url: `${syncServerHost}/api/setup/sync-seed`,
|
||||
auth: { password },
|
||||
proxy: syncProxy,
|
||||
timeout: 30000 // seed request should not take long
|
||||
});
|
||||
|
||||
if (resp.syncVersion !== appInfo.syncVersion) {
|
||||
const message = `Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${resp.syncVersion}. To fix this issue, use same Trilium version on all instances.`;
|
||||
|
||||
log.error(message);
|
||||
|
||||
return {
|
||||
result: "failure",
|
||||
error: message
|
||||
};
|
||||
}
|
||||
|
||||
await sqlInit.createDatabaseForSync(resp.options, syncServerHost, syncProxy);
|
||||
|
||||
triggerSync();
|
||||
|
||||
return { result: "success" };
|
||||
} catch (e: any) {
|
||||
log.error(`Sync failed: '${e.message}', stack: ${e.stack}`);
|
||||
|
||||
return {
|
||||
result: "failure",
|
||||
error: e.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getSyncSeedOptions() {
|
||||
return [becca.getOption("documentId"), becca.getOption("documentSecret")];
|
||||
}
|
||||
|
||||
export default {
|
||||
hasSyncServerSchemaAndSeed,
|
||||
triggerSync,
|
||||
sendSeedToSyncServer,
|
||||
setupSyncFromSyncServer,
|
||||
getSyncSeedOptions
|
||||
};
|
||||
@@ -23,95 +23,6 @@ const initDbConnection = coreSqlInit.initDbConnection;
|
||||
const initializeDb = coreSqlInit.initializeDb;
|
||||
export const getDbSize = coreSqlInit.getDbSize;
|
||||
|
||||
/**
|
||||
* Applies the database schema, creating the necessary tables and importing the demo content.
|
||||
*
|
||||
* @param skipDemoDb if set to `true`, then the demo database will not be imported, resulting in an empty root note.
|
||||
* @throws {Error} if the database is already initialized.
|
||||
*/
|
||||
async function createInitialDatabase(skipDemoDb?: boolean) {
|
||||
if (isDbInitialized()) {
|
||||
throw new Error("DB is already initialized");
|
||||
}
|
||||
|
||||
const schema = fs.readFileSync(`${resourceDir.DB_INIT_DIR}/schema.sql`, "utf-8");
|
||||
const demoFile = (!skipDemoDb ? fs.readFileSync(`${resourceDir.DB_INIT_DIR}/demo.zip`) : null);
|
||||
|
||||
let rootNote!: BNote;
|
||||
|
||||
// We have to import async since options init requires keyboard actions which require translations.
|
||||
const optionsInitService = (await import("./options_init.js")).default;
|
||||
const becca_loader = (await import("@triliumnext/core")).becca_loader;
|
||||
|
||||
sql.transactional(() => {
|
||||
log.info("Creating database schema ...");
|
||||
|
||||
sql.executeScript(schema);
|
||||
|
||||
becca_loader.load();
|
||||
|
||||
log.info("Creating root note ...");
|
||||
|
||||
rootNote = new BNote({
|
||||
noteId: "root",
|
||||
title: "root",
|
||||
type: "text",
|
||||
mime: "text/html"
|
||||
}).save();
|
||||
|
||||
rootNote.setContent("");
|
||||
|
||||
new BBranch({
|
||||
noteId: "root",
|
||||
parentNoteId: "none",
|
||||
isExpanded: true,
|
||||
notePosition: 10
|
||||
}).save();
|
||||
|
||||
optionsInitService.initDocumentOptions();
|
||||
optionsInitService.initNotSyncedOptions(true, {});
|
||||
optionsInitService.initStartupOptions();
|
||||
password.resetPassword();
|
||||
});
|
||||
|
||||
// Check hidden subtree.
|
||||
// This ensures the existence of system templates, for the demo content.
|
||||
console.log("Checking hidden subtree at first start.");
|
||||
cls.init(() => hidden_subtree.checkHiddenSubtree());
|
||||
|
||||
// Import demo content.
|
||||
log.info("Importing demo content...");
|
||||
|
||||
const dummyTaskContext = new TaskContext("no-progress-reporting", "importNotes", null);
|
||||
|
||||
if (demoFile) {
|
||||
await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
|
||||
}
|
||||
|
||||
// Post-demo.
|
||||
sql.transactional(() => {
|
||||
// this needs to happen after ZIP import,
|
||||
// the previous solution was to move option initialization here, but then the important parts of initialization
|
||||
// are not all in one transaction (because ZIP import is async and thus not transactional)
|
||||
|
||||
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
|
||||
|
||||
optionService.setOption(
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
log.info("Schema and initial content generated.");
|
||||
|
||||
initDbConnection();
|
||||
}
|
||||
|
||||
async function createDatabaseForSync(options: OptionRow[], syncServerHost = "", syncProxy = "") {
|
||||
log.info("Creating database for sync");
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use strict";
|
||||
|
||||
import sqlInit from "../../services/sql_init.js";
|
||||
import setupService from "../../services/setup.js";
|
||||
import log from "../../services/log.js";
|
||||
import { getLog } from "../../services/log.js";
|
||||
import appInfo from "../../services/app_info.js";
|
||||
import type { Request } from "express";
|
||||
|
||||
@@ -27,6 +25,7 @@ function setupSyncFromServer(req: Request) {
|
||||
function saveSyncSeed(req: Request) {
|
||||
const { options, syncVersion } = req.body;
|
||||
|
||||
const log = getLog();
|
||||
if (appInfo.syncVersion !== syncVersion) {
|
||||
const message = `Could not setup sync since local sync protocol version is ${appInfo.syncVersion} while remote is ${syncVersion}. To fix this issue, use same Trilium version on all instances.`;
|
||||
|
||||
@@ -42,7 +41,7 @@ function saveSyncSeed(req: Request) {
|
||||
|
||||
log.info("Saved sync seed.");
|
||||
|
||||
sqlInit.createDatabaseForSync(options);
|
||||
// sqlInit.createDatabaseForSync(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +73,7 @@ function saveSyncSeed(req: Request) {
|
||||
* - user-password: []
|
||||
*/
|
||||
function getSyncSeed() {
|
||||
log.info("Serving sync seed.");
|
||||
getLog().info("Serving sync seed.");
|
||||
|
||||
return {
|
||||
options: setupService.getSyncSeedOptions(),
|
||||
@@ -23,6 +23,7 @@ import syncApiRoute from "./api/sync";
|
||||
import autocompleteApiRoute from "./api/autocomplete";
|
||||
import similarNotesRoute from "./api/similar_notes";
|
||||
import imageRoute from "./api/image";
|
||||
import setupApiRoute from "./api/setup";
|
||||
|
||||
// TODO: Deduplicate with routes.ts
|
||||
const GET = "get",
|
||||
@@ -33,14 +34,18 @@ const GET = "get",
|
||||
|
||||
interface SharedApiRoutesContext {
|
||||
route: any;
|
||||
asyncRoute: any;
|
||||
apiRoute: any;
|
||||
asyncApiRoute: any;
|
||||
checkApiAuth: any;
|
||||
apiResultHandler: any;
|
||||
checkApiAuthOrElectron: any;
|
||||
checkAppNotInitialized: any;
|
||||
loginRateLimiter: any;
|
||||
checkCredentials: any;
|
||||
}
|
||||
|
||||
export function buildSharedApiRoutes({ route, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler, checkApiAuthOrElectron }: SharedApiRoutesContext) {
|
||||
export function buildSharedApiRoutes({ route, asyncRoute, apiRoute, asyncApiRoute, checkApiAuth, apiResultHandler, checkApiAuthOrElectron, checkAppNotInitialized, checkCredentials, loginRateLimiter }: SharedApiRoutesContext) {
|
||||
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
|
||||
apiRoute(PST, '/api/tree/load', treeApiRoute.load);
|
||||
|
||||
@@ -111,6 +116,13 @@ export function buildSharedApiRoutes({ route, apiRoute, asyncApiRoute, checkApiA
|
||||
route(GET, "/api/attachments/:attachmentId/image/:filename", [checkApiAuthOrElectron], imageRoute.returnAttachedImage);
|
||||
route(GET, "/api/images/:noteId/:filename", [checkApiAuthOrElectron], imageRoute.returnImageFromNote);
|
||||
|
||||
// group of the services below are meant to be executed from the outside
|
||||
route(GET, "/api/setup/status", [], setupApiRoute.getStatus, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/new-document", [checkAppNotInitialized], setupApiRoute.setupNewDocument, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/sync-from-server", [checkAppNotInitialized], setupApiRoute.setupSyncFromServer, apiResultHandler);
|
||||
route(GET, "/api/setup/sync-seed", [loginRateLimiter, checkCredentials], setupApiRoute.getSyncSeed, apiResultHandler);
|
||||
asyncRoute(PST, "/api/setup/sync-seed", [checkAppNotInitialized], setupApiRoute.saveSyncSeed, apiResultHandler);
|
||||
|
||||
asyncApiRoute(PST, "/api/sync/test", syncApiRoute.testSync);
|
||||
asyncApiRoute(PST, "/api/sync/now", syncApiRoute.syncNow);
|
||||
apiRoute(PST, "/api/sync/fill-entity-changes", syncApiRoute.fillEntityChanges);
|
||||
|
||||
@@ -7,6 +7,11 @@ import optionService from "./options";
|
||||
import eventService from "./events";
|
||||
import { getContext } from "./context";
|
||||
import config from "./config";
|
||||
import BNote from "../becca/entities/bnote";
|
||||
import BBranch from "../becca/entities/bbranch";
|
||||
import schema from "../assets/schema.sql?raw";
|
||||
import hidden_subtree from "./hidden_subtree";
|
||||
import TaskContext from "./task_context";
|
||||
|
||||
export const dbReady = deferred<void>();
|
||||
|
||||
@@ -122,4 +127,94 @@ function initializeDb() {
|
||||
});
|
||||
}
|
||||
|
||||
export default { isDbInitialized, createDatabaseForSync, setDbAsInitialized, schemaExists, getDbSize, initDbConnection, dbReady, initializeDb };
|
||||
/**
|
||||
* Applies the database schema, creating the necessary tables and importing the demo content.
|
||||
*
|
||||
* @param skipDemoDb if set to `true`, then the demo database will not be imported, resulting in an empty root note.
|
||||
* @throws {Error} if the database is already initialized.
|
||||
*/
|
||||
async function createInitialDatabase(skipDemoDb?: boolean) {
|
||||
if (isDbInitialized()) {
|
||||
throw new Error("DB is already initialized");
|
||||
}
|
||||
|
||||
let rootNote!: BNote;
|
||||
|
||||
// We have to import async since options init requires keyboard actions which require translations.
|
||||
const optionsInitService = (await import("./options_init.js")).default;
|
||||
const becca_loader = (await import("../becca/becca_loader.js")).default;
|
||||
|
||||
const sql = getSql();
|
||||
const log = getLog();
|
||||
sql.transactional(() => {
|
||||
log.info("Creating database schema ...");
|
||||
|
||||
console.log("Got schema:", schema.substring(0, 100)); // Log the first 100 characters of the schema to verify it's loaded correctly
|
||||
sql.executeScript(schema);
|
||||
|
||||
becca_loader.load();
|
||||
|
||||
log.info("Creating root note ...");
|
||||
|
||||
rootNote = new BNote({
|
||||
noteId: "root",
|
||||
title: "root",
|
||||
type: "text",
|
||||
mime: "text/html"
|
||||
}).save();
|
||||
|
||||
rootNote.setContent("");
|
||||
|
||||
new BBranch({
|
||||
noteId: "root",
|
||||
parentNoteId: "none",
|
||||
isExpanded: true,
|
||||
notePosition: 10
|
||||
}).save();
|
||||
|
||||
// Bring in option init.
|
||||
optionsInitService.initDocumentOptions();
|
||||
optionsInitService.initNotSyncedOptions(true, {});
|
||||
optionsInitService.initStartupOptions();
|
||||
// password.resetPassword();
|
||||
});
|
||||
|
||||
// Check hidden subtree.
|
||||
// This ensures the existence of system templates, for the demo content.
|
||||
console.log("Checking hidden subtree at first start.");
|
||||
getContext().init(() => hidden_subtree.checkHiddenSubtree());
|
||||
|
||||
// Import demo content.
|
||||
log.info("Importing demo content...");
|
||||
|
||||
const dummyTaskContext = new TaskContext("no-progress-reporting", "importNotes", null);
|
||||
|
||||
// if (demoFile) {
|
||||
// await zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
|
||||
// }
|
||||
|
||||
// Post-demo.
|
||||
sql.transactional(() => {
|
||||
// this needs to happen after ZIP import,
|
||||
// the previous solution was to move option initialization here, but then the important parts of initialization
|
||||
// are not all in one transaction (because ZIP import is async and thus not transactional)
|
||||
|
||||
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
|
||||
|
||||
optionService.setOption(
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
log.info("Schema and initial content generated.");
|
||||
|
||||
initDbConnection();
|
||||
}
|
||||
|
||||
export default { isDbInitialized, createDatabaseForSync, setDbAsInitialized, schemaExists, getDbSize, initDbConnection, dbReady, initializeDb, createInitialDatabase };
|
||||
|
||||
Reference in New Issue
Block a user