core: fix more typings

This commit is contained in:
Abdullah Atta
2023-08-21 16:24:05 +05:00
parent 1333d599cd
commit 41a6ea1230
14 changed files with 104 additions and 558 deletions

View File

@@ -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);
});
}

View File

@@ -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

View File

@@ -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
);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
});
}

View File

@@ -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: {

View File

@@ -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;
};

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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";
}

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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}`);
}