mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
core: fix more typings
This commit is contained in:
@@ -27,7 +27,7 @@ import { delay } from "../__tests__/utils";
|
||||
import { test, expect, vitest } from "vitest";
|
||||
import { login } from "./utils";
|
||||
|
||||
const TEST_TIMEOUT = 60 * 1000;
|
||||
const TEST_TIMEOUT = 30 * 1000;
|
||||
|
||||
test(
|
||||
"case 1: device A & B should only download the changes from device C (no uploading)",
|
||||
@@ -254,18 +254,33 @@ test(
|
||||
|
||||
await syncAndWait(deviceA, deviceB);
|
||||
|
||||
const colorId = await deviceA.colors.add({
|
||||
title: "yellow",
|
||||
colorCode: "#ffff22"
|
||||
});
|
||||
for (let noteId of noteIds) {
|
||||
await deviceA.notes.note(noteId).color("purple");
|
||||
expect(deviceB.notes.note(noteId)).toBeTruthy();
|
||||
expect(deviceB.notes.note(noteId).data.color).toBeUndefined();
|
||||
expect(
|
||||
deviceB.relations
|
||||
.from({ id: colorId, type: "color" }, "note")
|
||||
.findIndex((a) => a.to.id === noteId)
|
||||
).toBe(-1);
|
||||
|
||||
await deviceA.relations.add(
|
||||
{ id: colorId, type: "color" },
|
||||
{ id: noteId, type: "note" }
|
||||
);
|
||||
}
|
||||
|
||||
await syncAndWait(deviceA, deviceB);
|
||||
|
||||
const purpleColor = deviceB.colors.tag("purple");
|
||||
expect(noteIds.every((id) => purpleColor.noteIds.indexOf(id) > -1)).toBe(
|
||||
true
|
||||
);
|
||||
expect(deviceB.colors.exists(colorId)).toBeTruthy();
|
||||
const purpleNotes = deviceB.relations
|
||||
.from({ id: colorId, type: "color" }, "note")
|
||||
.resolved();
|
||||
expect(
|
||||
noteIds.every((id) => purpleNotes.findIndex((p) => p.id === id) > -1)
|
||||
).toBe(true);
|
||||
|
||||
console.log("issue colors log out");
|
||||
await cleanup(deviceA, deviceB);
|
||||
@@ -289,30 +304,25 @@ test(
|
||||
|
||||
expect(deviceB.notebooks.notebook(id)).toBeDefined();
|
||||
|
||||
await deviceA.notebooks.notebook(id).topics.add("Topic 1");
|
||||
await deviceA.notebooks.topics(id).add({ title: "Topic 1" });
|
||||
// to create a conflict
|
||||
await delay(1500);
|
||||
await deviceB.notebooks.notebook(id).topics.add("Topic 2");
|
||||
await deviceB.notebooks.topics(id).add({ title: "Topic 2" });
|
||||
|
||||
expect(deviceA.notebooks.notebook(id).topics.has("Topic 1")).toBeTruthy();
|
||||
expect(deviceB.notebooks.notebook(id).topics.has("Topic 2")).toBeTruthy();
|
||||
expect(deviceA.notebooks.topics(id).has("Topic 1")).toBeTruthy();
|
||||
expect(deviceB.notebooks.topics(id).has("Topic 2")).toBeTruthy();
|
||||
|
||||
expect(
|
||||
deviceB.notebooks.notebook(id).topics.topic("Topic 2")._topic.dateEdited
|
||||
).toBeGreaterThan(
|
||||
deviceA.notebooks.notebook(id).topics.topic("Topic 1")._topic.dateEdited
|
||||
);
|
||||
expect(deviceB.notebooks.notebook(id).dateModified).toBeGreaterThan(
|
||||
deviceA.notebooks.notebook(id).dateModified
|
||||
);
|
||||
await syncAndWait(deviceA, deviceB, false);
|
||||
|
||||
await syncAndWait(deviceB, deviceA, false);
|
||||
// await delay(1000);
|
||||
|
||||
expect(deviceA.notebooks.notebook(id).topics.has("Topic 1")).toBeTruthy();
|
||||
expect(deviceB.notebooks.notebook(id).topics.has("Topic 1")).toBeTruthy();
|
||||
// await syncAndWait(deviceB, deviceB, false);
|
||||
|
||||
expect(deviceA.notebooks.notebook(id).topics.has("Topic 2")).toBeTruthy();
|
||||
expect(deviceB.notebooks.notebook(id).topics.has("Topic 2")).toBeTruthy();
|
||||
expect(deviceA.notebooks.topics(id).has("Topic 1")).toBeTruthy();
|
||||
expect(deviceB.notebooks.topics(id).has("Topic 1")).toBeTruthy();
|
||||
|
||||
expect(deviceA.notebooks.topics(id).has("Topic 2")).toBeTruthy();
|
||||
expect(deviceB.notebooks.topics(id).has("Topic 2")).toBeTruthy();
|
||||
|
||||
console.log("issue new topic log out");
|
||||
await cleanup(deviceA, deviceB);
|
||||
@@ -330,10 +340,9 @@ test(
|
||||
|
||||
const id = await deviceA.notebooks.add({
|
||||
title: "Notebook 1",
|
||||
topics: ["Topic 1"]
|
||||
topics: [{ title: "Topic 1" }]
|
||||
});
|
||||
|
||||
const topic = deviceA.notebooks.notebook(id).topics.topic("Topic 1");
|
||||
const topic = deviceA.notebooks.topics(id).topic("Topic 1");
|
||||
|
||||
await syncAndWait(deviceA, deviceB, false);
|
||||
|
||||
@@ -342,45 +351,24 @@ test(
|
||||
const noteA = await deviceA.notes.add({ title: "Note 1" });
|
||||
await deviceA.notes.addToNotebook({ id, topic: topic.id }, noteA);
|
||||
|
||||
expect(
|
||||
deviceA.notebooks.notebook(id).topics.topic(topic.id).totalNotes
|
||||
).toBe(1);
|
||||
expect(topic.totalNotes).toBe(1);
|
||||
|
||||
await delay(2000);
|
||||
|
||||
const noteB = await deviceB.notes.add({ title: "Note 2" });
|
||||
await deviceB.notes.addToNotebook({ id, topic: topic.id }, noteB);
|
||||
|
||||
expect(
|
||||
deviceB.notebooks.notebook(id).topics.topic(topic.id).totalNotes
|
||||
).toBe(1);
|
||||
expect(deviceB.notebooks.topics(id).topic(topic.id).totalNotes).toBe(1);
|
||||
|
||||
ctx.onTestFailed(() => {
|
||||
console.log(deviceA.notes.topicReferences.get(topic.id), noteA);
|
||||
console.log(deviceB.notes.topicReferences.get(topic.id), noteB);
|
||||
|
||||
deviceB.notes.topicReferences.rebuild();
|
||||
deviceA.notes.topicReferences.rebuild();
|
||||
|
||||
console.log(deviceA.notes.topicReferences.get(topic.id), noteA);
|
||||
console.log(deviceB.notes.topicReferences.get(topic.id), noteB);
|
||||
});
|
||||
await syncAndWait(deviceB, deviceA, false);
|
||||
await syncAndWait(deviceA, deviceB, false);
|
||||
|
||||
expect(deviceA.notes.note(noteB)).toBeDefined();
|
||||
expect(deviceB.notes.note(noteA)).toBeDefined();
|
||||
expect(deviceA.notebooks.topics(id).topic(topic.id).totalNotes).toBe(2);
|
||||
expect(deviceB.notebooks.topics(id).topic(topic.id).totalNotes).toBe(2);
|
||||
|
||||
expect(deviceA.notes.note(noteA).data.notebooks).toHaveLength(1);
|
||||
expect(deviceA.notes.note(noteB).data.notebooks).toHaveLength(1);
|
||||
|
||||
expect(
|
||||
deviceA.notebooks.notebook(id).topics.topic(topic.id).totalNotes
|
||||
).toBe(2);
|
||||
expect(
|
||||
deviceB.notebooks.notebook(id).topics.topic(topic.id).totalNotes
|
||||
).toBe(2);
|
||||
|
||||
console.log("issue assigning 2 notes log out");
|
||||
await cleanup(deviceA, deviceB);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
@@ -407,7 +395,12 @@ async function initializeDevice(id, capabilities = []) {
|
||||
});
|
||||
|
||||
const device = new Database();
|
||||
device.setup(new NodeStorageInterface(), EventSource, FS, Compressor);
|
||||
device.setup({
|
||||
storage: new NodeStorageInterface(),
|
||||
eventsource: EventSource,
|
||||
fs: FS,
|
||||
compressor: Compressor
|
||||
});
|
||||
|
||||
await device.init();
|
||||
|
||||
@@ -442,22 +435,12 @@ async function cleanup(...devices) {
|
||||
* @returns
|
||||
*/
|
||||
function syncAndWait(deviceA, deviceB, force = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ref2 = deviceB.eventManager.subscribe(
|
||||
EVENTS.databaseSyncRequested,
|
||||
(full, force, lastSynced) => {
|
||||
console.log("sync requested by device A", full, force, lastSynced);
|
||||
ref2.unsubscribe();
|
||||
deviceB.sync(full, force, lastSynced).catch(reject);
|
||||
}
|
||||
);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const ref = deviceB.eventManager.subscribe(EVENTS.syncCompleted, () => {
|
||||
ref.unsubscribe();
|
||||
console.log("sync completed.");
|
||||
resolve();
|
||||
});
|
||||
|
||||
console.log(
|
||||
"waiting for sync...",
|
||||
"Device A:",
|
||||
@@ -465,7 +448,6 @@ function syncAndWait(deviceA, deviceB, force = false) {
|
||||
"Device B:",
|
||||
deviceB.syncer.sync.syncing
|
||||
);
|
||||
|
||||
deviceA.sync(true, force).catch(reject);
|
||||
deviceA.sync(true, force);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,15 +27,15 @@ test(
|
||||
databaseTest().then(async (db) => {
|
||||
await expect(login(db)).resolves.not.toThrow();
|
||||
|
||||
const token = await db.user.tokenManager.getToken();
|
||||
const token = await db.tokenManager.getToken();
|
||||
expect(token).toBeDefined();
|
||||
|
||||
expect(
|
||||
await Promise.all([
|
||||
db.user.tokenManager._refreshToken(true),
|
||||
db.user.tokenManager._refreshToken(true),
|
||||
db.user.tokenManager._refreshToken(true),
|
||||
db.user.tokenManager._refreshToken(true)
|
||||
db.tokenManager._refreshToken(true),
|
||||
db.tokenManager._refreshToken(true),
|
||||
db.tokenManager._refreshToken(true),
|
||||
db.tokenManager._refreshToken(true)
|
||||
])
|
||||
).toHaveLength(4);
|
||||
}),
|
||||
@@ -48,11 +48,11 @@ test(
|
||||
databaseTest().then(async (db) => {
|
||||
await expect(login(db)).resolves.not.toThrow();
|
||||
|
||||
const token = await db.user.tokenManager.getToken();
|
||||
const token = await db.tokenManager.getToken();
|
||||
expect(token).toBeDefined();
|
||||
for (let i = 0; i <= 5; ++i) {
|
||||
await db.user.tokenManager._refreshToken(true);
|
||||
await db.user.tokenManager.saveToken(token);
|
||||
await db.tokenManager._refreshToken(true);
|
||||
await db.tokenManager.saveToken(token);
|
||||
}
|
||||
}),
|
||||
30000
|
||||
|
||||
@@ -38,7 +38,7 @@ test(
|
||||
databaseTest().then(async (db) => {
|
||||
await expect(login(db)).resolves.not.toThrow();
|
||||
|
||||
await expect(db.user.tokenManager.getToken()).resolves.toBeDefined();
|
||||
await expect(db.tokenManager.getToken()).resolves.toBeDefined();
|
||||
}),
|
||||
30000
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { filter, parse } from "liqe";
|
||||
import Database from ".";
|
||||
import {
|
||||
Attachment,
|
||||
GroupedItems,
|
||||
Note,
|
||||
Notebook,
|
||||
Reminder,
|
||||
@@ -66,7 +67,7 @@ export default class Lookup {
|
||||
return this.byTitle(array, query);
|
||||
}
|
||||
|
||||
tags(array: Tag[], query: string) {
|
||||
tags(array: GroupedItems<Tag>, query: string) {
|
||||
return this.byTitle(array, query);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class Monographs {
|
||||
async init() {
|
||||
try {
|
||||
const user = await this._db.user.getUser();
|
||||
const token = await this._db.user.tokenManager.getAccessToken();
|
||||
const token = await this._db.tokenManager.getAccessToken();
|
||||
if (!user || !token || !user.isEmailConfirmed) return;
|
||||
let monographs = await this._db.storage().read("monographs", true);
|
||||
monographs = await http.get(`${Constants.API_HOST}/monographs`, token);
|
||||
@@ -82,7 +82,7 @@ class Monographs {
|
||||
let update = !!this.isPublished(noteId);
|
||||
|
||||
const user = await this._db.user.getUser();
|
||||
const token = await this._db.user.tokenManager.getAccessToken();
|
||||
const token = await this._db.tokenManager.getAccessToken();
|
||||
if (!user || !token) throw new Error("Please login to publish a note.");
|
||||
|
||||
const note = this._db.notes.note(noteId);
|
||||
@@ -136,7 +136,7 @@ class Monographs {
|
||||
if (!this.monographs) await this.init();
|
||||
|
||||
const user = await this._db.user.getUser();
|
||||
const token = await this._db.user.tokenManager.getAccessToken();
|
||||
const token = await this._db.tokenManager.getAccessToken();
|
||||
if (!user || !token) throw new Error("Please login to publish a note.");
|
||||
|
||||
// const note = this._db.notes.note(noteId);
|
||||
|
||||
@@ -1,449 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Database from "../../index";
|
||||
import { NodeStorageInterface } from "../../../../__mocks__/node-storage.mock";
|
||||
import { FS } from "../../../../__mocks__/fs.mock";
|
||||
import Compressor from "../../../../__mocks__/compressor.mock";
|
||||
import { CHECK_IDS, EV, EVENTS } from "../../../common";
|
||||
import { EventSource } from "event-source-polyfill";
|
||||
import { delay } from "../../../../__tests__/utils";
|
||||
import { test, expect, vitest } from "vitest";
|
||||
import { login } from "../../../../__e2e__/utils";
|
||||
|
||||
const TEST_TIMEOUT = 30 * 1000;
|
||||
|
||||
test(
|
||||
"case 1: device A & B should only download the changes from device C (no uploading)",
|
||||
async () => {
|
||||
const types = [];
|
||||
function onSyncProgress({ type }) {
|
||||
types.push(type);
|
||||
}
|
||||
|
||||
const [deviceA, deviceB, deviceC] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB"),
|
||||
initializeDevice("deviceC")
|
||||
]);
|
||||
|
||||
deviceA.eventManager.subscribe(EVENTS.syncProgress, onSyncProgress);
|
||||
deviceB.eventManager.subscribe(EVENTS.syncProgress, onSyncProgress);
|
||||
|
||||
await deviceC.notes.add({ title: "new note 1" });
|
||||
await syncAndWait(deviceC, deviceC);
|
||||
|
||||
expect(types.every((t) => t === "download")).toBe(true);
|
||||
|
||||
await cleanup(deviceA, deviceB, deviceC);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"case 3: Device A & B have unsynced changes but server has nothing",
|
||||
async () => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
|
||||
const note1Id = await deviceA.notes.add({
|
||||
title: "Test note from device A"
|
||||
});
|
||||
const note2Id = await deviceB.notes.add({
|
||||
title: "Test note from device B"
|
||||
});
|
||||
|
||||
await syncAndWait(deviceA, deviceB);
|
||||
|
||||
expect(deviceA.notes.note(note2Id)).toBeTruthy();
|
||||
expect(deviceB.notes.note(note1Id)).toBeTruthy();
|
||||
expect(deviceA.notes.note(note1Id)).toBeTruthy();
|
||||
expect(deviceB.notes.note(note2Id)).toBeTruthy();
|
||||
|
||||
await cleanup(deviceA, deviceA);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
|
||||
// test(
|
||||
// "case 4: Device A's sync is interrupted halfway and Device B makes some changes afterwards and syncs.",
|
||||
// async () => {
|
||||
// const deviceA = await initializeDevice("deviceA");
|
||||
// const deviceB = await initializeDevice("deviceB");
|
||||
|
||||
// const unsyncedNoteIds = [];
|
||||
// for (let i = 0; i < 10; ++i) {
|
||||
// const id = await deviceA.notes.add({
|
||||
// title: `Test note ${i} from device A`,
|
||||
// });
|
||||
// unsyncedNoteIds.push(id);
|
||||
// }
|
||||
|
||||
// const half = unsyncedNoteIds.length / 2 + 1;
|
||||
// deviceA.eventManager.subscribe(
|
||||
// EVENTS.syncProgress,
|
||||
// async ({ type, current }) => {
|
||||
// if (type === "upload" && current === half) {
|
||||
// await deviceA.syncer.stop();
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
// await expect(deviceA.sync(true)).rejects.toThrow();
|
||||
|
||||
// let syncedNoteIds = [];
|
||||
// for (let i = 0; i < unsyncedNoteIds.length; ++i) {
|
||||
// const expectedNoteId = unsyncedNoteIds[i];
|
||||
// if (deviceB.notes.note(expectedNoteId))
|
||||
// syncedNoteIds.push(expectedNoteId);
|
||||
// }
|
||||
// expect(
|
||||
// syncedNoteIds.length === half - 1 || syncedNoteIds.length === half
|
||||
// ).toBe(true);
|
||||
|
||||
// const deviceBNoteId = await deviceB.notes.add({
|
||||
// title: "Test note of case 4 from device B",
|
||||
// });
|
||||
|
||||
// await deviceB.sync(true);
|
||||
|
||||
// await syncAndWait(deviceA, deviceB);
|
||||
|
||||
// expect(deviceA.notes.note(deviceBNoteId)).toBeTruthy();
|
||||
// expect(
|
||||
// unsyncedNoteIds
|
||||
// .map((id) => !!deviceB.notes.note(id))
|
||||
// .every((res) => res === true)
|
||||
// ).toBe(true);
|
||||
|
||||
// await cleanup(deviceA, deviceB);
|
||||
// },
|
||||
//
|
||||
// );
|
||||
|
||||
// test.only(
|
||||
// "case 5: Device A's sync is interrupted halfway and Device B makes changes on the same note's content that didn't get synced on Device A due to interruption.",
|
||||
// async () => {
|
||||
// const deviceA = await initializeDevice("deviceA");
|
||||
// const deviceB = await initializeDevice("deviceB");
|
||||
|
||||
// const noteIds = [];
|
||||
// for (let i = 0; i < 10; ++i) {
|
||||
// const id = await deviceA.notes.add({
|
||||
// content: {
|
||||
// type: "tiptap",
|
||||
// data: `<p>deviceA=true</p>`,
|
||||
// },
|
||||
// });
|
||||
// noteIds.push(id);
|
||||
// }
|
||||
|
||||
// await deviceA.sync(true);
|
||||
// await deviceB.sync(true);
|
||||
|
||||
// const unsyncedNoteIds = [];
|
||||
// for (let id of noteIds) {
|
||||
// const noteId = await deviceA.notes.add({
|
||||
// id,
|
||||
// content: {
|
||||
// type: "tiptap",
|
||||
// data: `<p>deviceA=true+changed=true</p>`,
|
||||
// },
|
||||
// });
|
||||
// unsyncedNoteIds.push(noteId);
|
||||
// }
|
||||
|
||||
// deviceA.eventManager.subscribe(
|
||||
// EVENTS.syncProgress,
|
||||
// async ({ type, total, current }) => {
|
||||
// const half = total / 2 + 1;
|
||||
// if (type === "upload" && current === half) {
|
||||
// await deviceA.syncer.stop();
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
// await expect(deviceA.sync(true)).rejects.toThrow();
|
||||
|
||||
// await delay(10 * 1000);
|
||||
|
||||
// for (let id of unsyncedNoteIds) {
|
||||
// await deviceB.notes.add({
|
||||
// id,
|
||||
// content: {
|
||||
// type: "tiptap",
|
||||
// data: "<p>changes from device B</p>",
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
// const error = await withError(async () => {
|
||||
// await deviceB.sync(true);
|
||||
// await deviceA.sync(true);
|
||||
// });
|
||||
|
||||
// expect(error).not.toBeInstanceOf(NoErrorThrownError);
|
||||
// expect(error.message.includes("Merge")).toBeTruthy();
|
||||
|
||||
// await cleanup(deviceA, deviceB);
|
||||
// },
|
||||
//
|
||||
// );
|
||||
|
||||
test(
|
||||
"issue: running force sync from device A makes device B always download everything",
|
||||
async () => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
|
||||
await syncAndWait(deviceA, deviceB, true);
|
||||
|
||||
const handler = vitest.fn();
|
||||
deviceB.eventManager.subscribe(EVENTS.syncProgress, handler);
|
||||
|
||||
await deviceB.sync(true);
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
await cleanup(deviceB);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"issue: colors are not properly created if multiple notes are synced together",
|
||||
async () => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA", [CHECK_IDS.noteColor]),
|
||||
initializeDevice("deviceB", [CHECK_IDS.noteColor])
|
||||
]);
|
||||
|
||||
const noteIds = [];
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
const id = await deviceA.notes.add({
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: `<p>deviceA=true</p>`
|
||||
}
|
||||
});
|
||||
noteIds.push(id);
|
||||
}
|
||||
|
||||
await syncAndWait(deviceA, deviceB);
|
||||
|
||||
const colorId = await deviceA.colors.add({
|
||||
title: "yellow",
|
||||
colorCode: "#ffff22"
|
||||
});
|
||||
for (let noteId of noteIds) {
|
||||
expect(deviceB.notes.note(noteId)).toBeTruthy();
|
||||
expect(
|
||||
deviceB.relations
|
||||
.from({ id: colorId, type: "color" }, "note")
|
||||
.findIndex((a) => a.to.id === noteId)
|
||||
).toBe(-1);
|
||||
|
||||
await deviceA.relations.add(
|
||||
{ id: colorId, type: "color" },
|
||||
{ id: noteId, type: "note" }
|
||||
);
|
||||
}
|
||||
|
||||
await syncAndWait(deviceA, deviceB);
|
||||
|
||||
expect(deviceB.colors.exists(colorId)).toBeTruthy();
|
||||
const purpleNotes = deviceB.relations
|
||||
.from({ id: colorId, type: "color" }, "note")
|
||||
.resolved();
|
||||
expect(
|
||||
noteIds.every((id) => purpleNotes.findIndex((p) => p.id === id) > -1)
|
||||
).toBe(true);
|
||||
|
||||
await cleanup(deviceA, deviceB);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"issue: new topic on device A gets replaced by the new topic on device B",
|
||||
async () => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
// const deviceA = await initializeDevice("deviceA");
|
||||
// const deviceB = await initializeDevice("deviceB");
|
||||
|
||||
const id = await deviceA.notebooks.add({ title: "Notebook 1" });
|
||||
|
||||
await syncAndWait(deviceA, deviceB, false);
|
||||
|
||||
expect(deviceB.notebooks.notebook(id)).toBeDefined();
|
||||
|
||||
await deviceA.notebooks.topics(id).add({ title: "Topic 1" });
|
||||
// to create a conflict
|
||||
await delay(1500);
|
||||
await deviceB.notebooks.topics(id).add({ title: "Topic 2" });
|
||||
|
||||
expect(deviceA.notebooks.topics(id).has("Topic 1")).toBeTruthy();
|
||||
expect(deviceB.notebooks.topics(id).has("Topic 2")).toBeTruthy();
|
||||
|
||||
await syncAndWait(deviceA, deviceB, false);
|
||||
|
||||
// await delay(1000);
|
||||
|
||||
// await syncAndWait(deviceB, deviceB, false);
|
||||
|
||||
expect(deviceA.notebooks.topics(id).has("Topic 1")).toBeTruthy();
|
||||
expect(deviceB.notebooks.topics(id).has("Topic 1")).toBeTruthy();
|
||||
|
||||
expect(deviceA.notebooks.topics(id).has("Topic 2")).toBeTruthy();
|
||||
expect(deviceB.notebooks.topics(id).has("Topic 2")).toBeTruthy();
|
||||
|
||||
await cleanup(deviceA, deviceB);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
|
||||
test(
|
||||
"issue: remove notebook reference from notes that are removed from topic during merge",
|
||||
async () => {
|
||||
const [deviceA, deviceB] = await Promise.all([
|
||||
initializeDevice("deviceA"),
|
||||
initializeDevice("deviceB")
|
||||
]);
|
||||
|
||||
const id = await deviceA.notebooks.add({
|
||||
title: "Notebook 1",
|
||||
topics: [{ title: "Topic 1" }]
|
||||
});
|
||||
const topic = deviceA.notebooks.topics(id).topic("Topic 1");
|
||||
|
||||
await syncAndWait(deviceA, deviceB, false);
|
||||
|
||||
expect(deviceB.notebooks.notebook(id)).toBeDefined();
|
||||
|
||||
const noteA = await deviceA.notes.add({ title: "Note 1" });
|
||||
await deviceA.notes.addToNotebook({ id, topic: topic.id }, noteA);
|
||||
|
||||
expect(topic.totalNotes).toBe(1);
|
||||
|
||||
await delay(2000);
|
||||
|
||||
const noteB = await deviceB.notes.add({ title: "Note 2" });
|
||||
await deviceB.notes.addToNotebook({ id, topic: topic.id }, noteB);
|
||||
|
||||
expect(deviceB.notebooks.topics(id).topic(topic.id).totalNotes).toBe(1);
|
||||
|
||||
await syncAndWait(deviceB, deviceA, false);
|
||||
await syncAndWait(deviceA, deviceB, false);
|
||||
|
||||
expect(deviceA.notebooks.topics(id).topic(topic.id).totalNotes).toBe(2);
|
||||
expect(deviceB.notebooks.topics(id).topic(topic.id).totalNotes).toBe(2);
|
||||
|
||||
expect(deviceA.notes.note(noteA).data.notebooks).toHaveLength(1);
|
||||
expect(deviceA.notes.note(noteB).data.notebooks).toHaveLength(1);
|
||||
|
||||
await cleanup(deviceA, deviceB);
|
||||
},
|
||||
TEST_TIMEOUT
|
||||
);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Promise<Database>}
|
||||
*/
|
||||
async function initializeDevice(id, capabilities = []) {
|
||||
console.time(`Init ${id}`);
|
||||
EV.subscribe(EVENTS.userCheckStatus, async (type) => {
|
||||
return {
|
||||
type,
|
||||
result: capabilities.indexOf(type) > -1
|
||||
};
|
||||
});
|
||||
EV.subscribe(EVENTS.syncCheckStatus, async (type) => {
|
||||
return {
|
||||
type,
|
||||
result: true
|
||||
};
|
||||
});
|
||||
|
||||
const device = new Database();
|
||||
device.setup({
|
||||
storage: new NodeStorageInterface(),
|
||||
eventsource: EventSource,
|
||||
fs: FS,
|
||||
compressor: Compressor
|
||||
});
|
||||
|
||||
await device.init();
|
||||
|
||||
await login(device);
|
||||
|
||||
await device.user.resetUser(false);
|
||||
|
||||
await device.sync(true, false);
|
||||
|
||||
console.timeEnd(`Init ${id}`);
|
||||
return device;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {...Database} devices
|
||||
*/
|
||||
async function cleanup(...devices) {
|
||||
await Promise.all([
|
||||
devices.map(async (device) => {
|
||||
await device.syncer.stop();
|
||||
await device.user.logout();
|
||||
device.eventManager.unsubscribeAll();
|
||||
})
|
||||
]);
|
||||
EV.unsubscribeAll();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Database} deviceA
|
||||
* @param {Database} deviceB
|
||||
* @returns
|
||||
*/
|
||||
function syncAndWait(deviceA, deviceB, force = false) {
|
||||
return new Promise((resolve) => {
|
||||
const ref = deviceB.eventManager.subscribe(EVENTS.syncCompleted, () => {
|
||||
ref.unsubscribe();
|
||||
console.log("sync completed.");
|
||||
resolve();
|
||||
});
|
||||
console.log(
|
||||
"waiting for sync...",
|
||||
"Device A:",
|
||||
deviceA.syncer.sync.syncing,
|
||||
"Device B:",
|
||||
deviceB.syncer.sync.syncing
|
||||
);
|
||||
deviceA.sync(true, force);
|
||||
});
|
||||
}
|
||||
@@ -26,16 +26,18 @@ import { HealthCheck } from "./healthcheck";
|
||||
import Database from ".";
|
||||
import { Cipher, SerializedKey } from "@notesnook/crypto";
|
||||
|
||||
export type AuthenticatorType = "app" | "sms" | "email";
|
||||
export type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
isEmailConfirmed: boolean;
|
||||
salt: string;
|
||||
attachmentsKey?: Cipher;
|
||||
marketingConsent?: boolean;
|
||||
mfa: {
|
||||
isEnabled: boolean;
|
||||
primaryMethod: string;
|
||||
secondaryMethod: string;
|
||||
primaryMethod: AuthenticatorType;
|
||||
secondaryMethod?: AuthenticatorType;
|
||||
remainingValidCodes: number;
|
||||
};
|
||||
subscription: {
|
||||
|
||||
@@ -29,7 +29,6 @@ import { CHECK_IDS, checkIsUserPremium } from "../common";
|
||||
import { buildFromTemplate } from "../utils/templates";
|
||||
import {
|
||||
Note,
|
||||
UnencryptedContentItem,
|
||||
TrashOrItem,
|
||||
isTrashItem,
|
||||
MaybeDeletedItem,
|
||||
@@ -43,7 +42,7 @@ import { NoteContent } from "./session-content";
|
||||
type NotebookReference = { id: string; topic?: string; rebuildCache?: boolean };
|
||||
type ExportOptions = {
|
||||
format: "html" | "md" | "txt" | "md-frontmatter";
|
||||
contentItem?: UnencryptedContentItem;
|
||||
contentItem?: NoteContent<false>;
|
||||
rawContent?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ export default class Backup {
|
||||
constructor(private readonly db: Database) {}
|
||||
|
||||
lastBackupTime() {
|
||||
return this.db.storage().read("lastBackupTime");
|
||||
return this.db.storage().read<number>("lastBackupTime");
|
||||
}
|
||||
|
||||
async updateBackupTime() {
|
||||
|
||||
@@ -188,7 +188,7 @@ const migrations: Migration[] = [
|
||||
version: 5.9,
|
||||
items: {
|
||||
tag: async (item, db) => {
|
||||
const alias = db.settings?.getAlias(item.id);
|
||||
const alias = db.settings.getAlias(item.id);
|
||||
item.title = alias || item.title;
|
||||
item.id = getId(item.dateCreated);
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ export type GroupOptions = {
|
||||
sortDirection: "desc" | "asc";
|
||||
};
|
||||
|
||||
export type GroupedItems<T> = (T | GroupHeader)[];
|
||||
|
||||
export type GroupingKey =
|
||||
| "home"
|
||||
| "notes"
|
||||
@@ -33,12 +35,14 @@ export type GroupingKey =
|
||||
| "tags"
|
||||
| "topics"
|
||||
| "trash"
|
||||
| "reminders"
|
||||
| "favorites";
|
||||
|
||||
export type ValueOf<T> = T[keyof T];
|
||||
|
||||
export type GroupHeader = {
|
||||
type: "header";
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
@@ -342,3 +346,7 @@ export function isTrashItem(
|
||||
): item is TrashItem {
|
||||
return !isDeleted(item) && item.type === "trash";
|
||||
}
|
||||
|
||||
export function isGroupHeader(item: GroupHeader | Item): item is GroupHeader {
|
||||
return item.type === "header";
|
||||
}
|
||||
|
||||
@@ -18,13 +18,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { isReminderActive } from "../collections/reminders";
|
||||
import { GroupHeader, GroupOptions, GroupableItem, Reminder } from "../types";
|
||||
import {
|
||||
GroupedItems,
|
||||
GroupOptions,
|
||||
GroupableItem,
|
||||
Item,
|
||||
Reminder
|
||||
} from "../types";
|
||||
import { getWeekGroupFromTimestamp, MONTHS_FULL } from "./date";
|
||||
|
||||
type EvaluateKeyFunction<T> = (item: T) => string;
|
||||
type GroupedItems<T> = (T | GroupHeader)[];
|
||||
|
||||
const getSortValue = <T extends GroupableItem>(
|
||||
export const getSortValue = <T extends Item>(
|
||||
options: GroupOptions,
|
||||
item: T
|
||||
) => {
|
||||
@@ -121,11 +126,14 @@ export function groupArray<T extends GroupableItem>(
|
||||
}
|
||||
const groups: GroupedItems<T> = [];
|
||||
if (conflicted.length > 0)
|
||||
groups.push({ title: "Conflicted", type: "header" }, ...conflicted);
|
||||
groups.push(
|
||||
{ title: "Conflicted", type: "header", id: "conflicted" },
|
||||
...conflicted
|
||||
);
|
||||
if (pinned.length > 0)
|
||||
groups.push({ title: "Pinned", type: "header" }, ...pinned);
|
||||
groups.push({ title: "Pinned", type: "header", id: "pinned" }, ...pinned);
|
||||
if (others.length > 0)
|
||||
groups.push({ title: "All", type: "header" }, ...others);
|
||||
groups.push({ title: "All", type: "header", id: "all" }, ...others);
|
||||
return groups;
|
||||
}
|
||||
|
||||
@@ -181,6 +189,7 @@ function flattenGroups<T extends GroupableItem>(groups: Map<string, T[]>) {
|
||||
if (groupItems.length <= 0) return;
|
||||
items.push({
|
||||
title: groupTitle,
|
||||
id: groupTitle.toLowerCase(),
|
||||
type: "header"
|
||||
});
|
||||
groupItems.forEach((item) => items.push(item));
|
||||
|
||||
@@ -17,15 +17,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} size
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
export function randomBytes(size) {
|
||||
export function randomBytes(size: number) {
|
||||
if (!globalThis.crypto || !crypto)
|
||||
throw new Error("Crypto is not supported on this platform.");
|
||||
if (crypto.randomBytes) return crypto.randomBytes(size);
|
||||
if ("randomBytes" in crypto && typeof crypto.randomBytes === "function")
|
||||
return crypto.randomBytes(size);
|
||||
|
||||
if (!crypto.getRandomValues)
|
||||
throw new Error(
|
||||
@@ -39,7 +35,7 @@ export function randomBytes(size) {
|
||||
|
||||
export function randomInt() {
|
||||
const randomBuffer = randomBytes(1);
|
||||
let randomNumber = randomBuffer[0] / 0xff; // / (0xffffffff + 1);
|
||||
const randomNumber = randomBuffer[0] / 0xff; // / (0xffffffff + 1);
|
||||
|
||||
return Math.floor(randomNumber * 0xffffff);
|
||||
}
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { formatDate } from "./date";
|
||||
import { TimeFormat, formatDate } from "./date";
|
||||
|
||||
export const NEWLINE_STRIP_REGEX = /[\r\n\t\v]+/gm;
|
||||
|
||||
@@ -29,20 +29,18 @@ const TIMESTAMP_REGEX = /\$timestamp\$/g;
|
||||
const DATE_TIME_STRIP_REGEX = /[\\\-: ]/g;
|
||||
|
||||
export function formatTitle(
|
||||
titleFormat,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
headline,
|
||||
totalNotes
|
||||
titleFormat: string,
|
||||
dateFormat: string,
|
||||
timeFormat: TimeFormat,
|
||||
headline = "",
|
||||
totalNotes = 0
|
||||
) {
|
||||
const date = formatDate(Date.now(), {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
type: "date"
|
||||
});
|
||||
|
||||
const time = formatDate(Date.now(), {
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
type: "time"
|
||||
});
|
||||
@@ -54,5 +52,5 @@ export function formatTitle(
|
||||
.replace(TIME_REGEX, time)
|
||||
.replace(HEADLINE_REGEX, headline || "")
|
||||
.replace(TIMESTAMP_REGEX, timestamp)
|
||||
.replace(COUNT_REGEX, totalNotes + 1);
|
||||
.replace(COUNT_REGEX, `${totalNotes + 1}`);
|
||||
}
|
||||
Reference in New Issue
Block a user