mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
2 Commits
3.4.5-andr
...
refactor/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f620242f8 | ||
|
|
e21e826175 |
@@ -17,10 +17,10 @@ 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 DB from "../../api";
|
||||
import DB from "../../src/api";
|
||||
import StorageInterface from "../../__mocks__/storage.mock";
|
||||
import dayjs from "dayjs";
|
||||
import { groupArray } from "../../utils/grouping";
|
||||
import { groupArray } from "../../src/utils/grouping";
|
||||
import FS from "../../__mocks__/fs.mock";
|
||||
import Compressor from "../../__mocks__/compressor.mock";
|
||||
|
||||
@@ -37,7 +37,7 @@ const TEST_NOTEBOOK2 = {
|
||||
};
|
||||
|
||||
function databaseTest() {
|
||||
let db = new DB(StorageInterface, null, FS, Compressor);
|
||||
const db = new DB(StorageInterface, null, FS, Compressor);
|
||||
return db.init().then(() => db);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ const notebookTest = (notebook = TEST_NOTEBOOK) =>
|
||||
return { db, id };
|
||||
});
|
||||
|
||||
var TEST_NOTE = {
|
||||
const TEST_NOTE = {
|
||||
content: {
|
||||
type: "tiptap",
|
||||
data: `<p>Hello<br/><span style="color:#f00">This is colorful</span></p>`
|
||||
@@ -1,75 +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 { EV, EVENTS } from "../common";
|
||||
import CachedCollection from "../database/cached-collection";
|
||||
import IndexedCollection from "../database/indexed-collection";
|
||||
|
||||
class Collection {
|
||||
static async new(db, name, cached = true, deferred = false) {
|
||||
const collection = new this(db, name, cached);
|
||||
|
||||
if (!deferred) await collection.init();
|
||||
else await collection._collection.indexer.init();
|
||||
|
||||
if (collection._collection.clear)
|
||||
EV.subscribe(
|
||||
EVENTS.userLoggedOut,
|
||||
async () => await collection._collection.clear()
|
||||
);
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (this.initialized) return;
|
||||
await this._collection.init();
|
||||
EV.publish(EVENTS.databaseCollectionInitiated, this.collectionName);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("../api").default} db
|
||||
*/
|
||||
constructor(db, name, cached) {
|
||||
this._db = db;
|
||||
this.collectionName = name;
|
||||
if (cached)
|
||||
this._collection = new CachedCollection(
|
||||
this._db.storage,
|
||||
name,
|
||||
this._db.eventManager
|
||||
);
|
||||
else
|
||||
this._collection = new IndexedCollection(
|
||||
this._db.storage,
|
||||
name,
|
||||
this._db.eventManager
|
||||
);
|
||||
}
|
||||
|
||||
async encrypted() {
|
||||
const data = await this._collection.indexer.readMulti(
|
||||
this._collection.indexer.getIndices()
|
||||
);
|
||||
return data.map((d) => d[1]);
|
||||
}
|
||||
}
|
||||
export default Collection;
|
||||
@@ -1,109 +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 IndexedCollection from "./indexed-collection";
|
||||
import MapStub from "../utils/map";
|
||||
|
||||
export default class CachedCollection extends IndexedCollection {
|
||||
constructor(context, type, eventManager) {
|
||||
super(context, type, eventManager);
|
||||
this.type = type;
|
||||
this.map = new Map();
|
||||
this.items = undefined;
|
||||
// this.eventManager = eventManager;
|
||||
// this.encryptionKeyFactory = encryptionKeyFactory;
|
||||
}
|
||||
|
||||
async init() {
|
||||
await super.init();
|
||||
let data = await this.indexer.readMulti(this.indexer.indices);
|
||||
if (this.map && this.map.dispose) this.map.dispose();
|
||||
|
||||
// const encryptionKey =
|
||||
// this.encryptionKeyFactory && (await this.encryptionKeyFactory());
|
||||
// if (encryptionKey) {
|
||||
// for (let item of data) {
|
||||
// const [_key, value] = item;
|
||||
// const decryptedValue = JSON.parse(
|
||||
// await this.indexer.decrypt(encryptionKey, value)
|
||||
// );
|
||||
// item[1] = decryptedValue;
|
||||
// }
|
||||
// }
|
||||
|
||||
this.map = new MapStub.Map(data, this.type);
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await super.clear();
|
||||
this.map.clear();
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
async updateItem(item) {
|
||||
await super.updateItem(item);
|
||||
this.map.set(item.id, item);
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
exists(id) {
|
||||
const item = this.getItem(id);
|
||||
return item && !item.deleted;
|
||||
}
|
||||
|
||||
has(id) {
|
||||
return this.map.has(id);
|
||||
}
|
||||
|
||||
count() {
|
||||
return this.map.size;
|
||||
}
|
||||
|
||||
getItem(id) {
|
||||
return this.map.get(id);
|
||||
}
|
||||
|
||||
async deleteItem(id) {
|
||||
this.map.delete(id);
|
||||
await super.deleteItem(id);
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
getRaw() {
|
||||
return Array.from(this.map.values());
|
||||
}
|
||||
|
||||
getItems(map = undefined) {
|
||||
if (this.items && this.items.length === this.map.size) return this.items;
|
||||
|
||||
this.items = [];
|
||||
this.map.forEach((value) => {
|
||||
if (!value || value.deleted || !value.id) return;
|
||||
value = map ? map(value) : value;
|
||||
this.items.push(value);
|
||||
});
|
||||
this.items.sort((a, b) => b.dateCreated - a.dateCreated);
|
||||
return this.items;
|
||||
}
|
||||
|
||||
invalidateCache() {
|
||||
this.items = undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +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 { randomBytes } from "../utils/random";
|
||||
|
||||
export default class Storage {
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
write(key, data) {
|
||||
return this.storage.write(key, data);
|
||||
}
|
||||
|
||||
readMulti(keys) {
|
||||
return this.storage.readMulti(keys);
|
||||
}
|
||||
|
||||
read(key, isArray = false) {
|
||||
return this.storage.read(key, isArray);
|
||||
}
|
||||
|
||||
clear() {
|
||||
return this.storage.clear();
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
return this.storage.remove(key);
|
||||
}
|
||||
|
||||
getAllKeys() {
|
||||
return this.storage.getAllKeys();
|
||||
}
|
||||
|
||||
encrypt(password, data) {
|
||||
return this.storage.encrypt(password, data);
|
||||
}
|
||||
|
||||
decrypt(password, cipher) {
|
||||
return this.storage.decrypt(password, cipher);
|
||||
}
|
||||
|
||||
deriveCryptoKey(name, data) {
|
||||
return this.storage.deriveCryptoKey(name, data);
|
||||
}
|
||||
|
||||
hash(password, userId) {
|
||||
return this.storage.hash(password, userId);
|
||||
}
|
||||
|
||||
getCryptoKey(name) {
|
||||
return this.storage.getCryptoKey(name);
|
||||
}
|
||||
|
||||
generateCryptoKey(password, salt) {
|
||||
return this.storage.generateCryptoKey(password, salt);
|
||||
}
|
||||
|
||||
async generateRandomKey() {
|
||||
const passwordBytes = randomBytes(124);
|
||||
const password = passwordBytes.toString("base64");
|
||||
return await this.storage.generateCryptoKey(password);
|
||||
}
|
||||
}
|
||||
1748
packages/core/package-lock.json
generated
1748
packages/core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "@notesnook/core",
|
||||
"version": "7.3.6",
|
||||
"main": "./api/index.js",
|
||||
"main": "./dist/api/index.js",
|
||||
"types": "./dist/api/index.d.ts",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -14,7 +15,9 @@
|
||||
"@babel/runtime": "^7.18.9",
|
||||
"@notesnook/crypto": "^1.0.1",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/mime-db": "^1.43.1",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/spark-md5": "^3.0.2",
|
||||
"abortcontroller-polyfill": "^1.7.3",
|
||||
"analyze-es6-modules": "^0.6.2",
|
||||
"babel-jest": "^28.1.3",
|
||||
@@ -28,9 +31,11 @@
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jsdom": "^20.0.0",
|
||||
"mockdate": "^3.0.5",
|
||||
"otplib": "^12.0.1"
|
||||
"otplib": "^12.0.1",
|
||||
"vitest": "^0.29.8"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test:e2e": "env-cmd -e e2e jest --forceExit",
|
||||
"test": "jest --forceExit"
|
||||
},
|
||||
@@ -46,9 +51,10 @@
|
||||
"htmlparser2": "^8.0.1",
|
||||
"linkedom": "^0.14.17",
|
||||
"liqe": "^1.13.0",
|
||||
"mime-db": "1.52.0",
|
||||
"qclone": "^1.2.0",
|
||||
"spark-md5": "^3.0.2",
|
||||
"mime-db": "1.52.0"
|
||||
"rfdc": "^1.3.0",
|
||||
"spark-md5": "^3.0.2"
|
||||
},
|
||||
"overrides": {
|
||||
"htmlparser2": "^8.0.1"
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Debug from "../debug";
|
||||
import { noteTest, notebookTest, databaseTest } from "../../__tests__/utils";
|
||||
import { noteTest, notebookTest, databaseTest } from "../../../__tests__/utils";
|
||||
import { enableFetchMocks, disableFetchMocks } from "jest-fetch-mock";
|
||||
|
||||
test("strip empty item shouldn't throw", () => {
|
||||
@@ -17,10 +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/>.
|
||||
*/
|
||||
|
||||
import { Item } from "../entities";
|
||||
import hosts from "../utils/constants";
|
||||
|
||||
export default class Debug {
|
||||
strip(item) {
|
||||
export class Debug {
|
||||
static strip(item: Item) {
|
||||
if (!item) return "{}";
|
||||
return JSON.stringify({
|
||||
title: !!item.title,
|
||||
@@ -42,18 +43,11 @@ export default class Debug {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* title: string,
|
||||
* body: string,
|
||||
* userId: string
|
||||
* }} reportData
|
||||
* @returns {Promise<string>} link to the github issue
|
||||
*/
|
||||
async report(reportData) {
|
||||
if (!reportData) return;
|
||||
|
||||
static async report(reportData: {
|
||||
title: string;
|
||||
body: string;
|
||||
userId: string;
|
||||
}): Promise<string | undefined> {
|
||||
const { title, body, userId } = reportData;
|
||||
const response = await fetch(`${hosts.ISSUES_HOST}/create/notesnook`, {
|
||||
method: "POST",
|
||||
@@ -18,17 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Notes from "../collections/notes";
|
||||
import Storage from "../database/storage";
|
||||
import FileStorage from "../database/fs";
|
||||
import Notebooks from "../collections/notebooks";
|
||||
import Trash from "../collections/trash";
|
||||
import Tags from "../collections/tags";
|
||||
import { Tags } from "../collections/tags";
|
||||
import Sync from "./sync";
|
||||
import Vault from "./vault";
|
||||
import Lookup from "./lookup";
|
||||
import { Lookup } from "./lookup";
|
||||
import Content from "../collections/content";
|
||||
import Backup from "../database/backup";
|
||||
import Session from "./session";
|
||||
import { SystemClock } from "./system-clock";
|
||||
import Constants from "../utils/constants";
|
||||
import { EV, EVENTS } from "../common";
|
||||
import Settings from "./settings";
|
||||
@@ -37,65 +36,63 @@ import Outbox from "./outbox";
|
||||
import UserManager from "./user-manager";
|
||||
import http from "../utils/http";
|
||||
import Monographs from "./monographs";
|
||||
import Offers from "./offers";
|
||||
import Attachments from "../collections/attachments";
|
||||
import Debug from "./debug";
|
||||
import { Mutex } from "async-mutex";
|
||||
import NoteHistory from "../collections/note-history";
|
||||
import MFAManager from "./mfa-manager";
|
||||
import EventManager from "../utils/event-manager";
|
||||
import Pricing from "./pricing";
|
||||
import { logger } from "../logger";
|
||||
import Shortcuts from "../collections/shortcuts";
|
||||
import Reminders from "../collections/reminders";
|
||||
import Relations from "../collections/relations";
|
||||
import Subscriptions from "./subscriptions";
|
||||
import { Relations } from "../collections/relations";
|
||||
import { Subscriptions } from "./subscriptions";
|
||||
import { ICompressor, IFileStorage, IStorage } from "../interfaces";
|
||||
import { TokenManager } from "./token-manager";
|
||||
|
||||
/**
|
||||
* @type {EventSource}
|
||||
*/
|
||||
var NNEventSource;
|
||||
// const DIFFERENCE_THRESHOLD = 20 * 1000;
|
||||
// const MAX_TIME_ERROR_FAILURES = 5;
|
||||
type Options = {
|
||||
storage: IStorage;
|
||||
eventsource: new (
|
||||
uri: string,
|
||||
init: EventSourceInit & { headers?: Record<string, string> }
|
||||
) => EventSource;
|
||||
fs: IFileStorage;
|
||||
compressor: ICompressor;
|
||||
};
|
||||
|
||||
class Database {
|
||||
/**
|
||||
*
|
||||
* @param {any} storage
|
||||
* @param {EventSource} eventsource
|
||||
*/
|
||||
constructor(storage, eventsource, fs, compressor) {
|
||||
/**
|
||||
* @type {EventSource}
|
||||
*/
|
||||
this.evtSource = null;
|
||||
this.sseMutex = new Mutex();
|
||||
this.lastHeartbeat = undefined; // { local: 0, server: 0 };
|
||||
this.timeErrorFailures = 0;
|
||||
this.eventManager = new EventManager();
|
||||
this.compressor = compressor;
|
||||
private readonly sseMutex = new Mutex();
|
||||
private sseConnection: EventSource | null = null;
|
||||
private readonly fileStorage: FileStorage;
|
||||
private readonly tokenManager: TokenManager;
|
||||
|
||||
this.storage = new Storage(storage);
|
||||
this.fs = new FileStorage(fs, storage);
|
||||
NNEventSource = eventsource;
|
||||
}
|
||||
readonly notes: Notes;
|
||||
readonly relations: Relations;
|
||||
readonly tags: Tags;
|
||||
readonly eventManager = new EventManager();
|
||||
readonly subscriptions: Subscriptions;
|
||||
|
||||
async _validate() {
|
||||
if (!(await this.session.valid())) {
|
||||
throw new Error(
|
||||
"Your system clock is not setup correctly. Please adjust your date and time and then retry."
|
||||
);
|
||||
}
|
||||
await this.session.set();
|
||||
constructor(private readonly options: Options) {
|
||||
this.notes = new Notes(options.storage);
|
||||
this.relations = new Relations(options.storage);
|
||||
this.tags = new Tags(options.storage);
|
||||
|
||||
this.fileStorage = new FileStorage(options.fs, options.storage);
|
||||
this.tokenManager = new TokenManager(options.storage);
|
||||
this.subscriptions = new Subscriptions(this.tokenManager);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.isSystemClockValid();
|
||||
|
||||
EV.subscribeMulti(
|
||||
[EVENTS.userLoggedIn, EVENTS.userFetched, EVENTS.tokenRefreshed],
|
||||
this.connectSSE,
|
||||
this
|
||||
);
|
||||
EV.subscribe(EVENTS.attachmentDeleted, async (attachment) => {
|
||||
await this.fs.cancel(attachment.metadata.hash);
|
||||
await this.fileStorage.cancel(attachment.metadata.hash);
|
||||
});
|
||||
EV.subscribe(EVENTS.userLoggedOut, async () => {
|
||||
await this.monographs.deinit();
|
||||
@@ -113,9 +110,6 @@ class Database {
|
||||
}
|
||||
});
|
||||
|
||||
this.session = new Session(this.storage);
|
||||
await this._validate();
|
||||
|
||||
this.user = new UserManager(this.storage, this);
|
||||
this.mfa = new MFAManager(this.storage, this);
|
||||
this.syncer = new Sync(this);
|
||||
@@ -126,14 +120,8 @@ class Database {
|
||||
this.migrations = new Migrations(this);
|
||||
this.outbox = new Outbox(this);
|
||||
this.monographs = new Monographs(this);
|
||||
this.offers = new Offers();
|
||||
this.debug = new Debug();
|
||||
this.pricing = new Pricing();
|
||||
this.subscriptions = new Subscriptions(this.user.tokenManager);
|
||||
|
||||
// collections
|
||||
/** @type {Notes} */
|
||||
this.notes = await Notes.new(this, "notes", true, true);
|
||||
/** @type {Notebooks} */
|
||||
this.notebooks = await Notebooks.new(this, "notebooks");
|
||||
/** @type {Tags} */
|
||||
@@ -166,29 +154,23 @@ class Database {
|
||||
}
|
||||
|
||||
disconnectSSE() {
|
||||
if (!this.evtSource) return;
|
||||
this.evtSource.onopen = null;
|
||||
this.evtSource.onmessage = null;
|
||||
this.evtSource.onerror = null;
|
||||
this.evtSource.close();
|
||||
this.evtSource = null;
|
||||
if (!this.sseConnection) return;
|
||||
this.sseConnection.onopen = null;
|
||||
this.sseConnection.onmessage = null;
|
||||
this.sseConnection.onerror = null;
|
||||
this.sseConnection.close();
|
||||
this.sseConnection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {{force: boolean, error: any}} args
|
||||
*/
|
||||
async connectSSE(args) {
|
||||
if (args && !!args.error) return;
|
||||
async connectSSE(args?: { force: boolean }) {
|
||||
await this.sseMutex.runExclusive(async () => {
|
||||
this.eventManager.publish(EVENTS.databaseSyncRequested, true, false);
|
||||
|
||||
const forceReconnect = args && args.force;
|
||||
if (
|
||||
!NNEventSource ||
|
||||
(!forceReconnect &&
|
||||
this.evtSource &&
|
||||
this.evtSource.readyState === this.evtSource.OPEN)
|
||||
!forceReconnect &&
|
||||
this.sseConnection &&
|
||||
this.sseConnection.readyState === this.sseConnection.OPEN
|
||||
)
|
||||
return;
|
||||
this.disconnectSSE();
|
||||
@@ -196,92 +178,95 @@ class Database {
|
||||
const token = await this.user.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
|
||||
this.evtSource = new NNEventSource(`${Constants.SSE_HOST}/sse`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
this.sseConnection = new this.options.eventsource(
|
||||
`${Constants.SSE_HOST}/sse`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
}
|
||||
);
|
||||
|
||||
this.evtSource.onopen = async () => {
|
||||
this.sseConnection.onopen = async () => {
|
||||
console.log("SSE: opened channel successfully!");
|
||||
};
|
||||
|
||||
this.evtSource.onerror = function (error) {
|
||||
this.sseConnection.onerror = function (error) {
|
||||
console.log("SSE: error:", error);
|
||||
};
|
||||
|
||||
this.evtSource.onmessage = async (event) => {
|
||||
this.sseConnection.onmessage = async (event) => {
|
||||
try {
|
||||
var { type, data } = JSON.parse(event.data);
|
||||
data = JSON.parse(data);
|
||||
const message = JSON.parse(event.data);
|
||||
const data = JSON.parse(message.data);
|
||||
|
||||
switch (message.type) {
|
||||
// TODO: increase reliablity for this.
|
||||
// case "heartbeat": {
|
||||
// const { t: serverTime } = data;
|
||||
// const localTime = Date.now();
|
||||
|
||||
// if (!this.lastHeartbeat) {
|
||||
// this.lastHeartbeat = { local: localTime, server: serverTime };
|
||||
// break;
|
||||
// }
|
||||
|
||||
// const timeElapsed = {
|
||||
// local: localTime - this.lastHeartbeat.local,
|
||||
// server: serverTime - this.lastHeartbeat.server,
|
||||
// };
|
||||
// const travelTime = timeElapsed.local - timeElapsed.server;
|
||||
// const actualTime = localTime - travelTime;
|
||||
|
||||
// const diff = actualTime - serverTime;
|
||||
|
||||
// // Fail several times consecutively before raising an error. This is done to root out
|
||||
// // false positives.
|
||||
// if (Math.abs(diff) > DIFFERENCE_THRESHOLD) {
|
||||
// if (this.timeErrorFailures >= MAX_TIME_ERROR_FAILURES) {
|
||||
// EV.publish(EVENTS.systemTimeInvalid, { serverTime, localTime });
|
||||
// } else this.timeErrorFailures++;
|
||||
// } else this.timeErrorFailures = 0;
|
||||
|
||||
// this.lastHeartbeat.local = localTime;
|
||||
// this.lastHeartbeat.server = serverTime;
|
||||
// break;
|
||||
// }
|
||||
case "upgrade": {
|
||||
const user = await this.user.getUser();
|
||||
user.subscription = data;
|
||||
await this.user.setUser(user);
|
||||
EV.publish(EVENTS.userSubscriptionUpdated, data);
|
||||
break;
|
||||
}
|
||||
case "userDeleted": {
|
||||
await this.user.logout(false, "Account Deleted");
|
||||
break;
|
||||
}
|
||||
case "userEmailChanged": {
|
||||
await this.user.logout(true, "Email changed");
|
||||
break;
|
||||
}
|
||||
case "userPasswordChanged": {
|
||||
await this.user.logout(true, "Password changed");
|
||||
break;
|
||||
}
|
||||
case "emailConfirmed": {
|
||||
const token = await this.storage.read("token");
|
||||
await this.user.tokenManager._refreshToken(token);
|
||||
await this.user.fetchUser(true);
|
||||
EV.publish(EVENTS.userEmailConfirmed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("SSE: Unsupported message. Message = ", event.data);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
// TODO: increase reliablity for this.
|
||||
// case "heartbeat": {
|
||||
// const { t: serverTime } = data;
|
||||
// const localTime = Date.now();
|
||||
|
||||
// if (!this.lastHeartbeat) {
|
||||
// this.lastHeartbeat = { local: localTime, server: serverTime };
|
||||
// break;
|
||||
// }
|
||||
|
||||
// const timeElapsed = {
|
||||
// local: localTime - this.lastHeartbeat.local,
|
||||
// server: serverTime - this.lastHeartbeat.server,
|
||||
// };
|
||||
// const travelTime = timeElapsed.local - timeElapsed.server;
|
||||
// const actualTime = localTime - travelTime;
|
||||
|
||||
// const diff = actualTime - serverTime;
|
||||
|
||||
// // Fail several times consecutively before raising an error. This is done to root out
|
||||
// // false positives.
|
||||
// if (Math.abs(diff) > DIFFERENCE_THRESHOLD) {
|
||||
// if (this.timeErrorFailures >= MAX_TIME_ERROR_FAILURES) {
|
||||
// EV.publish(EVENTS.systemTimeInvalid, { serverTime, localTime });
|
||||
// } else this.timeErrorFailures++;
|
||||
// } else this.timeErrorFailures = 0;
|
||||
|
||||
// this.lastHeartbeat.local = localTime;
|
||||
// this.lastHeartbeat.server = serverTime;
|
||||
// break;
|
||||
// }
|
||||
case "upgrade": {
|
||||
const user = await this.user.getUser();
|
||||
user.subscription = data;
|
||||
await this.user.setUser(user);
|
||||
EV.publish(EVENTS.userSubscriptionUpdated, data);
|
||||
break;
|
||||
}
|
||||
case "userDeleted": {
|
||||
await this.user.logout(false, "Account Deleted");
|
||||
break;
|
||||
}
|
||||
case "userEmailChanged": {
|
||||
await this.user.logout(true, "Email changed");
|
||||
break;
|
||||
}
|
||||
case "userPasswordChanged": {
|
||||
await this.user.logout(true, "Password changed");
|
||||
break;
|
||||
}
|
||||
case "emailConfirmed": {
|
||||
const token = await this.storage.read("token");
|
||||
await this.user.tokenManager._refreshToken(token);
|
||||
await this.user.fetchUser(true);
|
||||
EV.publish(EVENTS.userEmailConfirmed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async lastSynced() {
|
||||
return (await this.storage.read("lastSynced")) || 0;
|
||||
return (await this.options.storage.read<number>("lastSynced")) || 0;
|
||||
}
|
||||
|
||||
sync(full = true, force = false) {
|
||||
@@ -313,6 +298,16 @@ class Database {
|
||||
if (user) url += `?userId=${user.id}`;
|
||||
return http.get(url);
|
||||
}
|
||||
|
||||
private async isSystemClockValid() {
|
||||
const systemClock = new SystemClock(this.options.storage);
|
||||
if (!(await systemClock.valid())) {
|
||||
throw new Error(
|
||||
"Your system clock is not setup correctly. Please adjust your date and time and then retry."
|
||||
);
|
||||
}
|
||||
await systemClock.set();
|
||||
}
|
||||
}
|
||||
|
||||
export default Database;
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { filter, parse } from "liqe";
|
||||
|
||||
export default class Lookup {
|
||||
export class Lookup {
|
||||
/**
|
||||
*
|
||||
* @param {import('./index').default} db
|
||||
@@ -33,7 +33,7 @@ class MFAManager {
|
||||
/**
|
||||
*
|
||||
* @param {import("../database/storage").default} storage
|
||||
* @param {import("../api/index").default} db
|
||||
* @param {import("./index").default} db
|
||||
*/
|
||||
constructor(storage, db) {
|
||||
this._storage = storage;
|
||||
@@ -21,8 +21,8 @@ import { CLIENT_ID } from "../common";
|
||||
import hosts from "../utils/constants";
|
||||
import http from "../utils/http";
|
||||
|
||||
export default class Offers {
|
||||
async getCode(promo, platform) {
|
||||
export class Offers {
|
||||
static async getCode(promo: string, platform: "ios" | "android" | "web") {
|
||||
const result = await http.get(
|
||||
`${hosts.SUBSCRIPTIONS_HOST}/offers?promoCode=${promo}&clientId=${CLIENT_ID}&platformId=${platform}`
|
||||
);
|
||||
@@ -19,35 +19,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import http from "../utils/http";
|
||||
|
||||
type Product = {
|
||||
country: string;
|
||||
countryCode: string;
|
||||
sku?: string;
|
||||
price?: string;
|
||||
discount: number;
|
||||
};
|
||||
|
||||
const BASE_URL = `https://notesnook.com/api/v1/prices`;
|
||||
class Pricing {
|
||||
/**
|
||||
*
|
||||
* @param {"android"|"ios"|"web"} platform
|
||||
* @param {"monthly"|"yearly"} period
|
||||
* @returns {Promise<{
|
||||
* country: string,
|
||||
* countryCode: string,
|
||||
* sku: string,
|
||||
* discount: number
|
||||
* }>}
|
||||
*/
|
||||
sku(platform, period) {
|
||||
export class Pricing {
|
||||
static sku(
|
||||
platform: "android" | "ios" | "web",
|
||||
period: "monthly" | "yearly"
|
||||
): Promise<Product> {
|
||||
return http.get(`${BASE_URL}/skus/${platform}/${period}`);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"monthly"|"yearly"} period
|
||||
* @returns {Promise<{
|
||||
* country: string,
|
||||
* countryCode: string,
|
||||
* price: string,
|
||||
* discount: number
|
||||
* }>}
|
||||
*/
|
||||
price(period = "monthly") {
|
||||
static price(period: "monthly" | "yearly" = "monthly"): Promise<Product> {
|
||||
return http.get(`${BASE_URL}/${period}`);
|
||||
}
|
||||
}
|
||||
export default Pricing;
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { EV, EVENTS } from "../common";
|
||||
import id from "../utils/id";
|
||||
import "../types";
|
||||
import "../../types";
|
||||
|
||||
class Settings {
|
||||
/**
|
||||
@@ -19,23 +19,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hosts from "../utils/constants";
|
||||
import http from "../utils/http";
|
||||
import { ITokenManager } from "./token-manager";
|
||||
|
||||
export default class Subscriptions {
|
||||
/**
|
||||
* @param {import("../api/token-manager").default} tokenManager
|
||||
*/
|
||||
constructor(tokenManager) {
|
||||
this._tokenManager = tokenManager;
|
||||
}
|
||||
export class Subscriptions {
|
||||
constructor(private readonly tokenManager: ITokenManager) {}
|
||||
|
||||
async cancel() {
|
||||
const token = this._tokenManager.getAccessToken();
|
||||
const token = this.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
await http.delete(`${hosts.SUBSCRIPTIONS_HOST}/subscriptions`, token);
|
||||
}
|
||||
|
||||
async updateUrl() {
|
||||
const token = this._tokenManager.getAccessToken();
|
||||
const token = this.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
return await http.get(
|
||||
`${hosts.SUBSCRIPTIONS_HOST}/subscriptions/update_url`,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
TEST_NOTE,
|
||||
delay,
|
||||
StorageInterface
|
||||
} from "../../../__tests__/utils";
|
||||
} from "../../../../__tests__/utils";
|
||||
import Collector from "../collector";
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -18,12 +18,12 @@ 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 { 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 "eventsource";
|
||||
import { delay } from "../../../__tests__/utils";
|
||||
import { delay } from "../../../../__tests__/utils";
|
||||
|
||||
jest.setTimeout(100 * 1000);
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { EVENTS } from "../../common";
|
||||
import { logger } from "../../logger";
|
||||
import { logger } from "../../../logger";
|
||||
|
||||
export class AutoSync {
|
||||
/**
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { CURRENT_DATABASE_VERSION } from "../../common";
|
||||
import { logger } from "../../logger";
|
||||
import { logger } from "../../../logger";
|
||||
|
||||
class Collector {
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ import Conflicts from "./conflicts";
|
||||
import { AutoSync } from "./auto-sync";
|
||||
import { toChunks } from "../../utils/array";
|
||||
import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack";
|
||||
import { logger } from "../../logger";
|
||||
import { logger } from "../../../logger";
|
||||
import { Mutex } from "async-mutex";
|
||||
|
||||
/**
|
||||
@@ -17,9 +17,9 @@ 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 { migrateItem } from "../../migrations";
|
||||
import { migrateItem } from "../../../migrations";
|
||||
import setManipulator from "../../utils/set";
|
||||
import { logger } from "../../logger";
|
||||
import { logger } from "../../../logger";
|
||||
import { isHTMLEqual } from "../../utils/html-diff";
|
||||
|
||||
class Merger {
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import set from "../../utils/set";
|
||||
import qclone from "qclone";
|
||||
import { logger } from "../../logger";
|
||||
import { logger } from "../../../logger";
|
||||
|
||||
export class SyncQueue {
|
||||
/**
|
||||
@@ -17,21 +17,17 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
class Session {
|
||||
/**
|
||||
*
|
||||
* @param {import("../database/storage").default} context
|
||||
*/
|
||||
constructor(context) {
|
||||
this._storage = context;
|
||||
}
|
||||
import { IStorage } from "../interfaces";
|
||||
|
||||
export class SystemClock {
|
||||
constructor(private readonly storage: IStorage) {}
|
||||
|
||||
get() {
|
||||
return this._storage.read("t");
|
||||
return this.storage.read<number>("t");
|
||||
}
|
||||
|
||||
set() {
|
||||
return this._storage.write("t", Date.now());
|
||||
return this.storage.write("t", Date.now());
|
||||
}
|
||||
|
||||
async valid() {
|
||||
@@ -39,4 +35,3 @@ class Session {
|
||||
return !t || t < Date.now();
|
||||
}
|
||||
}
|
||||
export default Session;
|
||||
@@ -22,6 +22,16 @@ import constants from "../utils/constants";
|
||||
import { EV, EVENTS } from "../common";
|
||||
import { withTimeout, Mutex } from "async-mutex";
|
||||
import { logger } from "../logger";
|
||||
import { IStorage } from "../interfaces";
|
||||
import { ILogger } from "@notesnook/logger";
|
||||
|
||||
type AccessToken = {
|
||||
access_token: string;
|
||||
scope: string;
|
||||
refresh_token: string;
|
||||
t: number;
|
||||
expires_in: number;
|
||||
};
|
||||
|
||||
const ENDPOINTS = {
|
||||
token: "/connect/token",
|
||||
@@ -30,41 +40,45 @@ const ENDPOINTS = {
|
||||
logout: "/account/logout"
|
||||
};
|
||||
|
||||
class TokenManager {
|
||||
/**
|
||||
*
|
||||
* @param {import("../database/storage").default} storage
|
||||
*/
|
||||
constructor(storage) {
|
||||
this._storage = storage;
|
||||
this._refreshTokenMutex = withTimeout(new Mutex(), 10 * 1000);
|
||||
export interface ITokenManager {
|
||||
getAccessToken(forceRenew?: boolean): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
export class TokenManager implements ITokenManager {
|
||||
private readonly refreshTokenMutex = withTimeout(new Mutex(), 10 * 1000);
|
||||
private readonly logger: ILogger;
|
||||
|
||||
constructor(private readonly storage: IStorage) {
|
||||
this.logger = logger.scope("TokenManager");
|
||||
}
|
||||
|
||||
async getToken(renew = true, forceRenew = false) {
|
||||
let token = await this._storage.read("token");
|
||||
async getToken(
|
||||
renew = true,
|
||||
forceRenew = false
|
||||
): Promise<AccessToken | undefined> {
|
||||
const token = await this.storage.read<AccessToken>("token");
|
||||
if (!token || !token.access_token) return;
|
||||
|
||||
this.logger.info("Access token requested", {
|
||||
accessToken: token.access_token.slice(0, 10)
|
||||
});
|
||||
|
||||
const isExpired = renew && this._isTokenExpired(token);
|
||||
if (this._isTokenRefreshable(token) && (forceRenew || isExpired)) {
|
||||
await this._refreshToken(forceRenew);
|
||||
const isExpired = renew && this.isTokenExpired(token);
|
||||
if (this.isTokenRefreshable(token) && (forceRenew || isExpired)) {
|
||||
await this.refreshToken(forceRenew);
|
||||
return await this.getToken(false, false);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
_isTokenExpired(token) {
|
||||
private isTokenExpired(token: AccessToken) {
|
||||
const { t, expires_in } = token;
|
||||
const expiryMs = t + expires_in * 1000;
|
||||
return Date.now() >= expiryMs;
|
||||
}
|
||||
|
||||
_isTokenRefreshable(token) {
|
||||
private isTokenRefreshable(token: AccessToken) {
|
||||
const { scope, refresh_token } = token;
|
||||
if (!refresh_token || !scope) return false;
|
||||
|
||||
@@ -80,12 +94,12 @@ class TokenManager {
|
||||
}, "Error getting access token:");
|
||||
}
|
||||
|
||||
async _refreshToken(forceRenew = false) {
|
||||
await this._refreshTokenMutex.runExclusive(async () => {
|
||||
private async refreshToken(forceRenew = false) {
|
||||
await this.refreshTokenMutex.runExclusive(async () => {
|
||||
this.logger.info("Refreshing access token");
|
||||
|
||||
const token = await this.getToken(false, false);
|
||||
if (!forceRenew && !this._isTokenExpired(token)) {
|
||||
if (!token || (!forceRenew && !this.isTokenExpired(token))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,13 +134,13 @@ class TokenManager {
|
||||
);
|
||||
}
|
||||
|
||||
saveToken(tokenResponse) {
|
||||
private saveToken(tokenResponse: AccessToken) {
|
||||
if (!tokenResponse) return;
|
||||
let token = { ...tokenResponse, t: Date.now() };
|
||||
return this._storage.write("token", token);
|
||||
const token = { ...tokenResponse, t: Date.now() };
|
||||
return this.storage.write("token", token);
|
||||
}
|
||||
|
||||
async getAccessTokenFromAuthorizationCode(userId, authCode) {
|
||||
async getAccessTokenFromAuthorizationCode(userId: string, authCode: string) {
|
||||
return await this.saveToken(
|
||||
await http.post(`${constants.AUTH_HOST}${ENDPOINTS.temporaryToken}`, {
|
||||
authorization_code: authCode,
|
||||
@@ -136,14 +150,16 @@ class TokenManager {
|
||||
);
|
||||
}
|
||||
}
|
||||
export default TokenManager;
|
||||
|
||||
async function getSafeToken(action, errorMessage) {
|
||||
async function getSafeToken<T>(action: () => Promise<T>, errorMessage: string) {
|
||||
try {
|
||||
return await action();
|
||||
} catch (e) {
|
||||
console.error(errorMessage, e);
|
||||
if (e.message === "invalid_grant" || e.message === "invalid_client") {
|
||||
if (
|
||||
e instanceof Error &&
|
||||
(e.message === "invalid_grant" || e.message === "invalid_client")
|
||||
) {
|
||||
EV.publish(EVENTS.userSessionExpired);
|
||||
}
|
||||
throw e;
|
||||
@@ -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 "../types";
|
||||
import "../../types";
|
||||
import http from "../utils/http";
|
||||
import constants from "../utils/constants";
|
||||
import TokenManager from "./token-manager";
|
||||
@@ -41,7 +41,7 @@ class UserManager {
|
||||
/**
|
||||
*
|
||||
* @param {import("../database/storage").default} storage
|
||||
* @param {import("../api/index").default} db
|
||||
* @param {import("./index").default} db
|
||||
*/
|
||||
constructor(storage, db) {
|
||||
this._storage = storage;
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { CHECK_IDS, EV, EVENTS, checkIsUserPremium } from "../common";
|
||||
import { tinyToTiptap } from "../migrations";
|
||||
import { tinyToTiptap } from "../../migrations";
|
||||
|
||||
const ERASE_TIME = 1000 * 60 * 30;
|
||||
var ERASER_TIMEOUT = null;
|
||||
81
packages/core/src/collections/collection.ts
Normal file
81
packages/core/src/collections/collection.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
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 CachedCollection from "../database/cached-collection";
|
||||
import { BaseItem, Collections, CollectionType } from "../entities";
|
||||
import { IStorage } from "../interfaces";
|
||||
|
||||
export interface ICollection<
|
||||
TCollectionType extends CollectionType,
|
||||
T extends BaseItem<Collections[TCollectionType]>
|
||||
> {
|
||||
merge(item: T): Promise<void>;
|
||||
// add(item: Partial<T>): Promise<string | undefined>;
|
||||
// delete(...ids: string[]): Promise<void>;
|
||||
}
|
||||
|
||||
abstract class Collection<
|
||||
TCollectionType extends CollectionType,
|
||||
T extends BaseItem<Collections[TCollectionType]>
|
||||
> {
|
||||
protected collection: CachedCollection<TCollectionType, T>;
|
||||
|
||||
// static async new(db, name, cached = true, deferred = false) {
|
||||
// const collection = new this(db, name, cached);
|
||||
|
||||
// if (!deferred) await collection.init();
|
||||
// else await collection._collection.indexer.init();
|
||||
|
||||
// if (collection._collection.clear)
|
||||
// EV.subscribe(
|
||||
// EVENTS.userLoggedOut,
|
||||
// async () => await collection._collection.clear()
|
||||
// );
|
||||
|
||||
// return collection;
|
||||
// }
|
||||
|
||||
// async init() {
|
||||
// if (this.initialized) return;
|
||||
// await this._collection.init();
|
||||
// EV.publish(EVENTS.databaseCollectionInitiated, this.collectionName);
|
||||
// this.initialized = true;
|
||||
// }
|
||||
|
||||
constructor(storage: IStorage, name: TCollectionType) {
|
||||
this.collection = new CachedCollection(storage, name);
|
||||
|
||||
// this._db = db;
|
||||
// this.collectionName = name;
|
||||
// if (cached)
|
||||
// this._collection = new CachedCollection(
|
||||
// this._db.storage,
|
||||
// name,
|
||||
// this._db.eventManager
|
||||
// );
|
||||
// else
|
||||
// this._collection = new IndexedCollection(
|
||||
// this._db.storage,
|
||||
// name,
|
||||
// this._db.eventManager
|
||||
// );
|
||||
}
|
||||
abstract merge(item: T): Promise<void>;
|
||||
}
|
||||
export default Collection;
|
||||
@@ -17,61 +17,72 @@ 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 Collection from "./collection";
|
||||
import Note from "../models/note";
|
||||
import { ICollection } from "./collection";
|
||||
// import Note from "../models/note";
|
||||
import {
|
||||
MaybeDeletedItem,
|
||||
Note,
|
||||
TrashOrItem,
|
||||
isDeleted,
|
||||
isTrashItem
|
||||
} from "../entities";
|
||||
import getId from "../utils/id";
|
||||
import { getContentFromData } from "../content-types";
|
||||
import qclone from "qclone";
|
||||
import { deleteItem, findById } from "../utils/array";
|
||||
import CachedCollection from "../database/cached-collection";
|
||||
import { IStorage } from "../interfaces";
|
||||
import { clone } from "../utils/clone";
|
||||
|
||||
/**
|
||||
* @typedef {{ id: string, topic?: string, rebuildCache?: boolean }} NotebookReference
|
||||
*/
|
||||
type NotebookReference = {
|
||||
id: string;
|
||||
topic?: string;
|
||||
rebuildCache?: boolean;
|
||||
};
|
||||
|
||||
export default class Notes extends Collection {
|
||||
constructor(db, name, cached) {
|
||||
super(db, name, cached);
|
||||
this.topicReferences = new NoteIdCache(this._db);
|
||||
export default class Notes implements ICollection<"notes", Note> {
|
||||
private readonly collection: CachedCollection<"notes", TrashOrItem<Note>>;
|
||||
private readonly topicReferences: NoteIdCache;
|
||||
constructor(storage: IStorage) {
|
||||
this.collection = new CachedCollection(storage, "notes");
|
||||
this.topicReferences = new NoteIdCache(this);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await super.init();
|
||||
await this.collection.init();
|
||||
this.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
trashed(id) {
|
||||
return this.raw.find((item) => item.dateDeleted > 0 && item.id === id);
|
||||
}
|
||||
|
||||
async merge(remoteNote) {
|
||||
async merge(remoteNote: MaybeDeletedItem<TrashOrItem<Note>>) {
|
||||
if (!remoteNote) return;
|
||||
if (isDeleted(remoteNote))
|
||||
return await this.collection.removeItem(remoteNote.id);
|
||||
|
||||
const id = remoteNote.id;
|
||||
const localNote = this._collection.getItem(id);
|
||||
const localNote = this.collection.getItem(id);
|
||||
if (localNote) {
|
||||
if (localNote.color) await this._db.colors.untag(localNote.color, id);
|
||||
|
||||
for (let tag of localNote.tags || []) {
|
||||
for (const tag of localNote.tags || []) {
|
||||
await this._db.tags.untag(tag, id);
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteNote.deleted) return await this._collection.addItem(remoteNote);
|
||||
if (isTrashItem(remoteNote))
|
||||
return await this.collection.addItem(remoteNote);
|
||||
|
||||
await this._resolveColorAndTags(remoteNote);
|
||||
await this.resolveColorAndTags(remoteNote);
|
||||
|
||||
return await this._collection.addItem(remoteNote);
|
||||
return await this.collection.addItem(remoteNote);
|
||||
}
|
||||
|
||||
async add(noteArg) {
|
||||
if (!noteArg) return;
|
||||
async add(noteArg: Partial<Note>) {
|
||||
if (noteArg.remote)
|
||||
throw new Error("Please use db.notes.merge to merge remote notes.");
|
||||
|
||||
let id = noteArg.id || getId();
|
||||
let oldNote = this._collection.getItem(id);
|
||||
const id = noteArg.id || getId();
|
||||
const oldNote = this.collection.getItem(id);
|
||||
|
||||
let note = {
|
||||
let note: Note = {
|
||||
...oldNote,
|
||||
...noteArg
|
||||
};
|
||||
@@ -134,34 +145,25 @@ export default class Notes extends Collection {
|
||||
dateModified: note.dateModified
|
||||
};
|
||||
|
||||
await this._collection.addItem(note);
|
||||
await this.collection.addItem(note);
|
||||
|
||||
await this._resolveColorAndTags(note);
|
||||
await this.resolveColorAndTags(note);
|
||||
return note.id;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id The id of note
|
||||
* @returns {Note} The note of the given id
|
||||
*/
|
||||
note(id) {
|
||||
note(id: string | TrashOrItem<Note>) {
|
||||
if (!id) return;
|
||||
let note = id.type ? id : this._collection.getItem(id);
|
||||
if (!note || note.deleted) return;
|
||||
const note = typeof id === "string" ? this.collection.getItem(id) : id;
|
||||
if (!note || isTrashItem(note)) return;
|
||||
return new Note(note, this._db);
|
||||
}
|
||||
|
||||
get raw() {
|
||||
return this._collection.getRaw();
|
||||
return this.collection.getRaw();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {any[]}
|
||||
*/
|
||||
get all() {
|
||||
const items = this._collection.getItems();
|
||||
return items;
|
||||
return this.collection.getItems();
|
||||
}
|
||||
|
||||
get pinned() {
|
||||
@@ -176,30 +178,31 @@ export default class Notes extends Collection {
|
||||
return this.all.filter((item) => item.favorite === true);
|
||||
}
|
||||
|
||||
get deleted() {
|
||||
return this.raw.filter((item) => item.dateDeleted > 0);
|
||||
get trashed() {
|
||||
return this.all.filter((item) => isTrashItem(item));
|
||||
}
|
||||
|
||||
get locked() {
|
||||
return this.all.filter((item) => item.locked === true);
|
||||
}
|
||||
|
||||
tagged(tagId) {
|
||||
return this._getTagItems(tagId, "tags");
|
||||
isTrashed(id: string) {
|
||||
return this.all.find((item) => item.id === id && isTrashItem(item));
|
||||
}
|
||||
|
||||
colored(colorId) {
|
||||
return this._getTagItems(colorId, "colors");
|
||||
tagged(tagId: string) {
|
||||
return this.getTagItems(tagId, "tags");
|
||||
}
|
||||
|
||||
exists(id) {
|
||||
return this._collection.exists(id);
|
||||
colored(colorId: string) {
|
||||
return this.getTagItems(colorId, "colors");
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getTagItems(tagId, collection) {
|
||||
exists(id: string) {
|
||||
return this.collection.exists(id);
|
||||
}
|
||||
|
||||
private getTagItems(tagId: string, collection) {
|
||||
const tag = this._db[collection].tag(tagId);
|
||||
if (!tag || tag.noteIds.length <= 0) return [];
|
||||
const array = tag.noteIds.reduce((arr, id) => {
|
||||
@@ -210,26 +213,23 @@ export default class Notes extends Collection {
|
||||
return array.sort((a, b) => b.dateCreated - a.dateCreated);
|
||||
}
|
||||
|
||||
delete(...ids) {
|
||||
return this._delete(true, ...ids);
|
||||
delete(...ids: string[]) {
|
||||
return this.deleteOrRemove(true, ...ids);
|
||||
}
|
||||
|
||||
remove(...ids) {
|
||||
return this._delete(false, ...ids);
|
||||
remove(...ids: string[]) {
|
||||
return this.deleteOrRemove(false, ...ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _delete(moveToTrash = true, ...ids) {
|
||||
for (let id of ids) {
|
||||
let item = this.note(id);
|
||||
private async deleteOrRemove(moveToTrash = true, ...ids: string[]) {
|
||||
for (const id of ids) {
|
||||
const item = this.note(id);
|
||||
if (!item) continue;
|
||||
const itemData = qclone(item.data);
|
||||
const itemData = clone(item.data);
|
||||
|
||||
if (itemData.notebooks && !moveToTrash) {
|
||||
for (let notebook of itemData.notebooks) {
|
||||
for (let topicId of notebook.topics) {
|
||||
for (const notebook of itemData.notebooks) {
|
||||
for (const topicId of notebook.topics) {
|
||||
await this.removeFromNotebook(
|
||||
{ id: notebook.id, topic: topicId, rebuildCache: false },
|
||||
id
|
||||
@@ -238,7 +238,7 @@ export default class Notes extends Collection {
|
||||
}
|
||||
}
|
||||
|
||||
for (let tag of itemData.tags) {
|
||||
for (const tag of itemData.tags) {
|
||||
await this._db.tags.untag(tag, id);
|
||||
}
|
||||
|
||||
@@ -247,24 +247,24 @@ export default class Notes extends Collection {
|
||||
}
|
||||
|
||||
const attachments = this._db.attachments.ofNote(itemData.id, "all");
|
||||
for (let attachment of attachments) {
|
||||
for (const attachment of attachments) {
|
||||
await this._db.attachments.delete(
|
||||
attachment.metadata.hash,
|
||||
itemData.id
|
||||
);
|
||||
}
|
||||
|
||||
// await this._collection.removeItem(id);
|
||||
// await this.collection.removeItem(id);
|
||||
if (moveToTrash) await this._db.trash.add(itemData);
|
||||
else {
|
||||
await this._collection.removeItem(id);
|
||||
await this.collection.removeItem(id);
|
||||
await this._db.content.remove(itemData.contentId);
|
||||
}
|
||||
}
|
||||
this.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
async _resolveColorAndTags(note) {
|
||||
private async resolveColorAndTags(note: Note) {
|
||||
const { color, tags, id } = note;
|
||||
|
||||
if (color) {
|
||||
@@ -285,17 +285,14 @@ export default class Notes extends Collection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NotebookReference} to
|
||||
*/
|
||||
async addToNotebook(to, ...noteIds) {
|
||||
private async addToNotebook(to: NotebookReference, ...noteIds: string[]) {
|
||||
if (!to) throw new Error("The destination notebook cannot be undefined.");
|
||||
if (!to.id) throw new Error("The destination notebook must contain id.");
|
||||
|
||||
const { id: notebookId, topic: topicId } = to;
|
||||
|
||||
for (let noteId of noteIds) {
|
||||
let note = this._db.notes.note(noteId);
|
||||
for (const noteId of noteIds) {
|
||||
const note = this.note(noteId);
|
||||
if (!note || note.data.deleted) continue;
|
||||
|
||||
if (topicId) {
|
||||
@@ -316,7 +313,7 @@ export default class Notes extends Collection {
|
||||
}
|
||||
|
||||
if (!noteHasNotebook || !noteHasTopic) {
|
||||
await this._db.notes.add({
|
||||
await this.add({
|
||||
id: noteId,
|
||||
notebooks
|
||||
});
|
||||
@@ -331,10 +328,7 @@ export default class Notes extends Collection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NotebookReference} to
|
||||
*/
|
||||
async removeFromNotebook(to, ...noteIds) {
|
||||
async removeFromNotebook(to: NotebookReference, ...noteIds: string[]) {
|
||||
if (!to) throw new Error("The destination notebook cannot be undefined.");
|
||||
if (!to.id) throw new Error("The destination notebook must contain id.");
|
||||
|
||||
@@ -357,7 +351,7 @@ export default class Notes extends Collection {
|
||||
|
||||
if (topics.length <= 0) deleteItem(notebooks, notebook);
|
||||
|
||||
await this._db.notes.add({
|
||||
await this.add({
|
||||
id: noteId,
|
||||
notebooks
|
||||
});
|
||||
@@ -371,14 +365,14 @@ export default class Notes extends Collection {
|
||||
if (rebuildCache) this.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
async removeFromAllNotebooks(...noteIds) {
|
||||
async removeFromAllNotebooks(...noteIds: string[]) {
|
||||
for (const noteId of noteIds) {
|
||||
const note = this.note(noteId);
|
||||
if (!note || note.deleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this._db.notes.add({
|
||||
await this.add({
|
||||
id: noteId,
|
||||
notebooks: []
|
||||
});
|
||||
@@ -387,30 +381,30 @@ export default class Notes extends Collection {
|
||||
this.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
async _clearAllNotebookReferences(notebookId) {
|
||||
const notes = this._db.notes.all;
|
||||
async _clearAllNotebookReferences(notebookId: string) {
|
||||
const notes = this.all;
|
||||
|
||||
for (const note of notes) {
|
||||
const { notebooks } = note;
|
||||
if (!notebooks) continue;
|
||||
|
||||
for (let notebook of notebooks) {
|
||||
for (const notebook of notebooks) {
|
||||
if (notebook.id !== notebookId) continue;
|
||||
if (!deleteItem(notebooks, notebook)) continue;
|
||||
}
|
||||
|
||||
await this._collection.updateItem(note);
|
||||
await this.collection.updateItem(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNoteHeadline(note, content) {
|
||||
function getNoteHeadline(note: Note, content) {
|
||||
if (note.locked) return "";
|
||||
return content.toHeadline();
|
||||
}
|
||||
|
||||
const NEWLINE_STRIP_REGEX = /[\r\n\t\v]+/gm;
|
||||
function getNoteTitle(note, oldNote) {
|
||||
function getNoteTitle(note: Note, oldNote?: Note) {
|
||||
if (note.title && note.title.trim().length > 0) {
|
||||
return note.title.replace(NEWLINE_STRIP_REGEX, " ");
|
||||
} else if (oldNote && oldNote.title && oldNote.title.trim().length > 0) {
|
||||
@@ -424,32 +418,28 @@ function getNoteTitle(note, oldNote) {
|
||||
}
|
||||
|
||||
class NoteIdCache {
|
||||
/**
|
||||
*
|
||||
* @param {import("../api/index").default} db
|
||||
*/
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
private cache: Map<string, string[]>;
|
||||
constructor(private readonly notes: Notes) {
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
rebuild() {
|
||||
this.cache = new Map();
|
||||
const notes = this._db.notes.all;
|
||||
this.cache = new Map<string, string[]>();
|
||||
const notes = this.notes.all;
|
||||
|
||||
for (const note of notes) {
|
||||
const { notebooks } = note;
|
||||
if (!notebooks) continue;
|
||||
|
||||
for (let notebook of notebooks) {
|
||||
for (let topic of notebook.topics) {
|
||||
for (const notebook of notebooks) {
|
||||
for (const topic of notebook.topics) {
|
||||
this.add(topic, note.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(topicId, noteId) {
|
||||
add(topicId: string, noteId: string) {
|
||||
let noteIds = this.cache.get(topicId);
|
||||
if (!noteIds) noteIds = [];
|
||||
if (noteIds.includes(noteId)) return;
|
||||
@@ -457,19 +447,19 @@ class NoteIdCache {
|
||||
this.cache.set(topicId, noteIds);
|
||||
}
|
||||
|
||||
has(topicId, noteId) {
|
||||
let noteIds = this.cache.get(topicId);
|
||||
has(topicId: string, noteId: string) {
|
||||
const noteIds = this.cache.get(topicId);
|
||||
if (!noteIds) return false;
|
||||
return noteIds.includes(noteId);
|
||||
}
|
||||
|
||||
count(topicId) {
|
||||
let noteIds = this.cache.get(topicId);
|
||||
count(topicId: string) {
|
||||
const noteIds = this.cache.get(topicId);
|
||||
if (!noteIds) return 0;
|
||||
return noteIds.length;
|
||||
}
|
||||
|
||||
get(topicId) {
|
||||
get(topicId: string) {
|
||||
return this.cache.get(topicId) || [];
|
||||
}
|
||||
}
|
||||
@@ -17,37 +17,24 @@ 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 CachedCollection from "../database/cached-collection";
|
||||
import { ItemReference, ItemType, Relation } from "../entities";
|
||||
import { IStorage } from "../interfaces";
|
||||
import { makeId } from "../utils/id";
|
||||
import Collection from "./collection";
|
||||
import { ICollection } from "./collection";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* id: string;
|
||||
* type: string;
|
||||
* }} ItemReference
|
||||
*
|
||||
* @typedef {{
|
||||
* id: string;
|
||||
* type: string;
|
||||
* from: ItemReference;
|
||||
* to: ItemReference;
|
||||
* dateCreated: number;
|
||||
* dateModified: number;
|
||||
* }} Relation
|
||||
*/
|
||||
export class Relations implements ICollection<"relations", Relation> {
|
||||
private readonly collection: CachedCollection<"relations", Relation>;
|
||||
|
||||
export default class Relations extends Collection {
|
||||
async merge(relation) {
|
||||
if (!relation) return;
|
||||
await this._collection.addItem(relation);
|
||||
constructor(storage: IStorage) {
|
||||
this.collection = new CachedCollection(storage, "relations");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ItemReference} from
|
||||
* @param {ItemReference} to
|
||||
*/
|
||||
async add(from, to) {
|
||||
async merge(relation: Relation) {
|
||||
await this.collection.addItem(relation);
|
||||
}
|
||||
|
||||
async add(from: ItemReference, to: ItemReference) {
|
||||
if (
|
||||
this.all.find(
|
||||
(a) =>
|
||||
@@ -56,7 +43,7 @@ export default class Relations extends Collection {
|
||||
)
|
||||
return;
|
||||
|
||||
const relation = {
|
||||
const relation: Relation = {
|
||||
id: generateId(from, to),
|
||||
type: "relation",
|
||||
dateCreated: Date.now(),
|
||||
@@ -65,27 +52,17 @@ export default class Relations extends Collection {
|
||||
to: { id: to.id, type: to.type }
|
||||
};
|
||||
|
||||
await this._collection.addItem(relation);
|
||||
await this.collection.addItem(relation);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ItemReference} reference
|
||||
* @param {string} type
|
||||
*/
|
||||
from(reference, type) {
|
||||
from(reference: ItemReference, type: ItemType) {
|
||||
const relations = this.all.filter(
|
||||
(a) => compareItemReference(a.from, reference) && a.to.type === type
|
||||
);
|
||||
return this.resolve(relations, "to");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ItemReference} reference
|
||||
* @param {string} type
|
||||
*/
|
||||
to(reference, type) {
|
||||
to(reference: ItemReference, type: ItemType) {
|
||||
const relations = this.all.filter(
|
||||
(a) => compareItemReference(a.to, reference) && a.from.type === type
|
||||
);
|
||||
@@ -94,45 +71,32 @@ export default class Relations extends Collection {
|
||||
|
||||
/**
|
||||
* Count number of from -> to relations
|
||||
* @param {ItemReference} reference
|
||||
* @param {string} type
|
||||
*/
|
||||
count(reference, type) {
|
||||
count(reference: ItemReference, type: ItemType) {
|
||||
return this.all.filter(
|
||||
(a) => compareItemReference(a.from, reference) && a.to.type === type
|
||||
).length;
|
||||
}
|
||||
|
||||
get raw() {
|
||||
return this._collection.getRaw();
|
||||
return this.collection.getRaw();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Relation[]}
|
||||
*/
|
||||
get all() {
|
||||
return this._collection.getItems();
|
||||
return this.collection.getItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Relation}
|
||||
*/
|
||||
relation(id) {
|
||||
return this._collection.getItem(id);
|
||||
relation(id: string) {
|
||||
return this.collection.getItem(id);
|
||||
}
|
||||
|
||||
async remove(...ids) {
|
||||
async remove(...ids: string[]) {
|
||||
for (const id of ids) {
|
||||
await this._collection.removeItem(id);
|
||||
await this.collection.removeItem(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ItemReference} from
|
||||
* @param {ItemReference} to
|
||||
*/
|
||||
async unlink(from, to) {
|
||||
async unlink(from: ItemReference, to: ItemReference) {
|
||||
const relation = this.all.find(
|
||||
(a) =>
|
||||
compareItemReference(a.from, from) && compareItemReference(a.to, to)
|
||||
@@ -142,12 +106,7 @@ export default class Relations extends Collection {
|
||||
await this.remove(relation.id);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ItemReference} from
|
||||
* @param {string} type
|
||||
*/
|
||||
async unlinkAll(to, type) {
|
||||
async unlinkAll(to: ItemReference, type: ItemType) {
|
||||
for (const relation of this.all.filter(
|
||||
(a) => compareItemReference(a.to, to) && a.from.type === type
|
||||
)) {
|
||||
@@ -155,14 +114,7 @@ export default class Relations extends Collection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Relation[]} relations
|
||||
* @param {"from" | "to"} resolveType
|
||||
* @private
|
||||
*
|
||||
* @returns {Relation[]}
|
||||
*/
|
||||
resolve(relations, resolveType) {
|
||||
private resolve(relations: Relation[], resolveType: "from" | "to") {
|
||||
const items = [];
|
||||
for (const relation of relations) {
|
||||
const reference = resolveType === "from" ? relation.from : relation.to;
|
||||
@@ -190,10 +142,10 @@ export default class Relations extends Collection {
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
const relations = this._collection.getItems();
|
||||
const relations = this.collection.getItems();
|
||||
for (const relation of relations) {
|
||||
const references = [relation.to, relation.from];
|
||||
for (let reference of references) {
|
||||
for (const reference of references) {
|
||||
let exists = false;
|
||||
switch (reference.type) {
|
||||
case "reminder":
|
||||
@@ -216,21 +168,14 @@ export default class Relations extends Collection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ItemReference} a
|
||||
* @param {ItemReference} b
|
||||
*/
|
||||
function compareItemReference(a, b) {
|
||||
function compareItemReference(a: ItemReference, b: ItemReference) {
|
||||
return a.id === b.id && a.type === b.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate deterministic constant id from `a` & `b` item reference.
|
||||
* @param {ItemReference} a
|
||||
* @param {ItemReference} b
|
||||
* Generate a deterministic constant id from `a` & `b` item reference.
|
||||
*/
|
||||
function generateId(a, b) {
|
||||
function generateId(a: ItemReference, b: ItemReference) {
|
||||
const str = `${a.id}${b.id}${a.type}${b.type}`;
|
||||
return makeId(str);
|
||||
}
|
||||
@@ -17,30 +17,34 @@ 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 Collection from "./collection";
|
||||
import { ICollection } from "./collection";
|
||||
import { makeId } from "../utils/id";
|
||||
import { deleteItems, hasItem } from "../utils/array";
|
||||
import setManipulator from "../utils/set";
|
||||
import { set } from "../utils/set";
|
||||
import { Mutex } from "async-mutex";
|
||||
import { Tag } from "../entities";
|
||||
import CachedCollection from "../database/cached-collection";
|
||||
import { IStorage } from "../interfaces";
|
||||
|
||||
export default class Tags extends Collection {
|
||||
constructor(db, name, cached) {
|
||||
super(db, name, cached);
|
||||
|
||||
export class Tags implements ICollection<"tags", Tag> {
|
||||
private readonly collection: CachedCollection<"tags", Tag>;
|
||||
private readonly mutex: Mutex;
|
||||
constructor(storage: IStorage) {
|
||||
this.collection = new CachedCollection(storage, "tags");
|
||||
this.mutex = new Mutex();
|
||||
}
|
||||
|
||||
tag(id) {
|
||||
tag(id: string) {
|
||||
const tagItem = this.all.find((t) => t.id === id || t.title === id);
|
||||
return tagItem;
|
||||
}
|
||||
|
||||
async merge(tag) {
|
||||
async merge(tag: Tag) {
|
||||
if (!tag) return;
|
||||
await this._collection.addItem(tag);
|
||||
await this.collection.addItem(tag);
|
||||
}
|
||||
|
||||
async add(tagId, ...noteIds) {
|
||||
async add(tagId: string, ...noteIds: string[]) {
|
||||
return this.mutex.runExclusive(async () => {
|
||||
tagId = this.sanitize(tagId);
|
||||
if (!tagId) throw new Error("Tag title cannot be empty.");
|
||||
@@ -61,33 +65,32 @@ export default class Tags extends Collection {
|
||||
type: "tag",
|
||||
id,
|
||||
title: tag.title,
|
||||
noteIds: setManipulator.union(notes, noteIds),
|
||||
noteIds: set.union(notes, noteIds),
|
||||
localOnly: true
|
||||
};
|
||||
|
||||
await this._collection.addItem(tag);
|
||||
await this.collection.addItem(tag);
|
||||
if (!this._db.settings.getAlias(tag.id))
|
||||
await this._db.settings.setAlias(tag.id, tag.title);
|
||||
return tag;
|
||||
});
|
||||
}
|
||||
|
||||
async rename(tagId, newName) {
|
||||
let tag = this.tag(tagId);
|
||||
async rename(tagId: string, newName: string) {
|
||||
const tag = this.tag(tagId);
|
||||
if (!tag) {
|
||||
console.error(`No tag found. Tag id:`, tagId);
|
||||
return;
|
||||
}
|
||||
|
||||
newName = this.sanitize(newName);
|
||||
if (!newName) throw new Error("Tag title cannot be empty.");
|
||||
|
||||
await this._db.settings.setAlias(tagId, newName);
|
||||
await this._collection.addItem({ ...tag, alias: newName });
|
||||
await this.collection.addItem({ ...tag, alias: newName });
|
||||
}
|
||||
|
||||
alias(tagId) {
|
||||
let tag = this.tag(tagId);
|
||||
alias(tagId: string) {
|
||||
const tag = this.tag(tagId);
|
||||
if (!tag) {
|
||||
console.error(`No tag found. Tag id:`, tagId);
|
||||
return;
|
||||
@@ -97,38 +100,35 @@ export default class Tags extends Collection {
|
||||
}
|
||||
|
||||
get raw() {
|
||||
return this._collection.getRaw();
|
||||
return this.collection.getRaw();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {any[]}
|
||||
*/
|
||||
get all() {
|
||||
return this._collection.getItems((item) => {
|
||||
return this.collection.getItems((item) => {
|
||||
item.alias = this._db.settings.getAlias(item.id) || item.title;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
async remove(tagId) {
|
||||
let tag = this.tag(tagId);
|
||||
async remove(tagId: string) {
|
||||
const tag = this.tag(tagId);
|
||||
if (!tag) {
|
||||
console.error(`No tag found. Tag id:`, tagId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let noteId of tag.noteIds) {
|
||||
for (const noteId of tag.noteIds) {
|
||||
const note = this._db.notes.note(noteId);
|
||||
if (!note) continue;
|
||||
if (hasItem(note.tags, tag.title)) await note.untag(tag.title);
|
||||
}
|
||||
|
||||
await this._db.shortcuts.remove(tagId);
|
||||
await this._collection.deleteItem(tagId);
|
||||
await this.collection.deleteItem(tagId);
|
||||
}
|
||||
|
||||
async untag(tagId, ...noteIds) {
|
||||
let tag = this.tag(tagId);
|
||||
async untag(tagId: string, ...noteIds: string[]) {
|
||||
const tag = this.tag(tagId);
|
||||
if (!tag) {
|
||||
console.error(`No such tag found. Tag title:`, tagId);
|
||||
return;
|
||||
@@ -136,15 +136,14 @@ export default class Tags extends Collection {
|
||||
|
||||
deleteItems(tag.noteIds, ...noteIds);
|
||||
|
||||
if (tag.noteIds.length > 0) await this._collection.addItem(tag);
|
||||
if (tag.noteIds.length > 0) await this.collection.addItem(tag);
|
||||
else {
|
||||
await this._db.shortcuts.remove(tag.id);
|
||||
await this._collection.deleteItem(tag.id);
|
||||
await this.collection.deleteItem(tag.id);
|
||||
}
|
||||
}
|
||||
|
||||
sanitize(tag) {
|
||||
if (!tag) return;
|
||||
sanitize(tag: string) {
|
||||
let sanitized = tag.toLocaleLowerCase();
|
||||
sanitized = sanitized.replace(/[\s]+/g, "");
|
||||
// sanitized = sanitized.replace(/[+!@#$%^&*()+{}\][:;'"<>?/.\s=,]+/g, "");
|
||||
115
packages/core/src/database/cached-collection.ts
Normal file
115
packages/core/src/database/cached-collection.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
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 IndexedCollection from "./indexed-collection";
|
||||
import { MapStub } from "../utils/map";
|
||||
import {
|
||||
BaseItem,
|
||||
Collections,
|
||||
CollectionType,
|
||||
isDeleted,
|
||||
MaybeDeletedItem
|
||||
} from "../entities";
|
||||
import { IStorage } from "../interfaces";
|
||||
|
||||
export default class CachedCollection<
|
||||
TCollectionType extends CollectionType,
|
||||
T extends BaseItem<Collections[TCollectionType]>
|
||||
> extends IndexedCollection<TCollectionType, T> {
|
||||
private cache = new Map<string, MaybeDeletedItem<T>>();
|
||||
private items?: T[];
|
||||
|
||||
constructor(storage: IStorage, type: TCollectionType) {
|
||||
super(storage, type);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await super.init();
|
||||
const data = await this.indexer.readMulti(this.indexer.getIndices());
|
||||
if ("dispose" in this.cache && typeof this.cache.dispose === "function")
|
||||
this.cache.dispose();
|
||||
|
||||
this.cache = new MapStub.Map(data);
|
||||
this.resetCache();
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await super.clear();
|
||||
this.cache.clear();
|
||||
this.resetCache();
|
||||
}
|
||||
|
||||
async updateItem(item: T) {
|
||||
await super.updateItem(item);
|
||||
this.cache.set(item.id, item);
|
||||
this.resetCache();
|
||||
}
|
||||
|
||||
async deleteItem(id: string) {
|
||||
this.cache.delete(id);
|
||||
await super.deleteItem(id);
|
||||
this.resetCache();
|
||||
}
|
||||
|
||||
async removeItem(id: string) {
|
||||
this.cache.set(id, { id, deleted: true });
|
||||
await super.removeItem(id);
|
||||
this.resetCache();
|
||||
}
|
||||
|
||||
exists(id: string) {
|
||||
const item = this.cache.get(id);
|
||||
return super.exists(id) && !!item && !isDeleted(item);
|
||||
}
|
||||
|
||||
has(id: string) {
|
||||
return this.cache.has(id);
|
||||
}
|
||||
|
||||
count() {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
getItem(id: string) {
|
||||
const item = this.cache.get(id);
|
||||
if (!item || isDeleted(item)) return;
|
||||
return item;
|
||||
}
|
||||
|
||||
getRaw() {
|
||||
return Array.from(this.cache.values());
|
||||
}
|
||||
|
||||
getItems(manipulate?: (item: T) => T) {
|
||||
if (this.items && this.items.length === this.cache.size) return this.items;
|
||||
|
||||
this.items = [];
|
||||
this.cache.forEach((value) => {
|
||||
if (isDeleted(value)) return;
|
||||
value = manipulate ? manipulate(value) : value;
|
||||
this.items?.push(value);
|
||||
});
|
||||
this.items.sort((a, b) => b.dateCreated - a.dateCreated);
|
||||
return this.items;
|
||||
}
|
||||
|
||||
resetCache() {
|
||||
this.items = undefined;
|
||||
}
|
||||
}
|
||||
@@ -19,69 +19,82 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import hosts from "../utils/constants";
|
||||
import TokenManager from "../api/token-manager";
|
||||
import { IFileStorage, IStorage } from "../interfaces";
|
||||
import { Cipher, SerializedKey } from "@notesnook/crypto/dist/src/types";
|
||||
|
||||
type QueueEntry = {
|
||||
groupId: string;
|
||||
filename: string;
|
||||
cancel: (reason?: string) => Promise<void>;
|
||||
type: "download" | "upload";
|
||||
};
|
||||
|
||||
export default class FileStorage {
|
||||
constructor(fs, storage) {
|
||||
this.fs = fs;
|
||||
private readonly tokenManager: TokenManager;
|
||||
private readonly queue: QueueEntry[];
|
||||
constructor(private readonly fs: IFileStorage, storage: IStorage) {
|
||||
this.tokenManager = new TokenManager(storage);
|
||||
this._queue = [];
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
async downloadFile(groupId, filename, chunkSize, metadata) {
|
||||
async downloadFile(groupId: string, filename: string, chunkSize: number) {
|
||||
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
const { execute, cancel } = this.fs.downloadFile(filename, {
|
||||
metadata,
|
||||
url,
|
||||
chunkSize,
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
this._queue.push({ groupId, filename, cancel, type: "download" });
|
||||
this.queue.push({ groupId, filename, cancel, type: "download" });
|
||||
const result = await execute();
|
||||
this._deleteOp(groupId, "download");
|
||||
this.deleteOp(groupId, "download");
|
||||
return result;
|
||||
}
|
||||
|
||||
async uploadFile(groupId, filename) {
|
||||
async uploadFile(groupId: string, filename: string) {
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
||||
const { execute, cancel } = this.fs.uploadFile(filename, {
|
||||
url,
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
this._queue.push({ groupId, filename, cancel, type: "upload" });
|
||||
this.queue.push({ groupId, filename, cancel, type: "upload" });
|
||||
const result = await execute();
|
||||
this._deleteOp(groupId, "upload");
|
||||
this.deleteOp(groupId, "upload");
|
||||
return result;
|
||||
}
|
||||
|
||||
async cancel(groupId, type = undefined) {
|
||||
const [op] = this._deleteOp(groupId, type);
|
||||
async cancel(groupId: string, type = undefined) {
|
||||
const [op] = this.deleteOp(groupId, type);
|
||||
if (!op) return;
|
||||
await op.cancel("Operation canceled.");
|
||||
}
|
||||
|
||||
_deleteOp(groupId, type = undefined) {
|
||||
const opIndex = this._queue.findIndex(
|
||||
private deleteOp(groupId: string, type?: QueueEntry["type"]) {
|
||||
const opIndex = this.queue.findIndex(
|
||||
(item) => item.groupId === groupId && (!type || item.type === type)
|
||||
);
|
||||
if (opIndex < 0) return [];
|
||||
return this._queue.splice(opIndex, 1);
|
||||
return this.queue.splice(opIndex, 1);
|
||||
}
|
||||
|
||||
readEncrypted(filename, encryptionKey, cipherData) {
|
||||
readEncrypted(
|
||||
filename: string,
|
||||
encryptionKey: SerializedKey,
|
||||
cipherData: Cipher
|
||||
) {
|
||||
return this.fs.readEncrypted(filename, encryptionKey, cipherData);
|
||||
}
|
||||
|
||||
writeEncryptedBase64(data, encryptionKey, mimeType) {
|
||||
return this.fs.writeEncryptedBase64({
|
||||
data,
|
||||
key: encryptionKey,
|
||||
mimeType
|
||||
});
|
||||
writeEncryptedBase64(
|
||||
data: string,
|
||||
encryptionKey: SerializedKey,
|
||||
mimeType: string
|
||||
) {
|
||||
return this.fs.writeEncryptedBase64(data, encryptionKey, mimeType);
|
||||
}
|
||||
|
||||
async deleteFile(filename, localOnly) {
|
||||
async deleteFile(filename: string, localOnly = false) {
|
||||
if (localOnly) return await this.fs.deleteFile(filename);
|
||||
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
@@ -92,28 +105,15 @@ export default class FileStorage {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} filename
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
exists(filename) {
|
||||
exists(filename: string) {
|
||||
return this.fs.exists(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
clear() {
|
||||
return this.fs.clearFileStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} data
|
||||
* @returns {Promise<{hash: string, type: string}>}
|
||||
*/
|
||||
hashBase64(data) {
|
||||
hashBase64(data: string) {
|
||||
return this.fs.hashBase64(data);
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { EVENTS } from "../common";
|
||||
import { BaseItem, Collections, CollectionType } from "../entities";
|
||||
import { IStorage } from "../interfaces";
|
||||
import Indexer from "./indexer";
|
||||
|
||||
export default class IndexedCollection {
|
||||
constructor(context, type, eventManager) {
|
||||
this.indexer = new Indexer(context, type);
|
||||
this.eventManager = eventManager;
|
||||
// this.encryptionKeyFactory = encryptionKeyFactory;
|
||||
export default class IndexedCollection<
|
||||
TCollectionType extends CollectionType,
|
||||
T extends BaseItem<Collections[TCollectionType]>
|
||||
> {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
readonly indexer: Indexer<T>;
|
||||
constructor(
|
||||
protected readonly storage: IStorage,
|
||||
type: TCollectionType
|
||||
// eventManager
|
||||
) {
|
||||
this.indexer = new Indexer(storage, type);
|
||||
// this.eventManager = eventManager;
|
||||
}
|
||||
|
||||
clear() {
|
||||
@@ -35,7 +47,7 @@ export default class IndexedCollection {
|
||||
await this.indexer.init();
|
||||
}
|
||||
|
||||
async addItem(item) {
|
||||
async addItem(item: T) {
|
||||
if (!item.id) throw new Error("The item must contain the id field.");
|
||||
|
||||
const exists = this.exists(item.id);
|
||||
@@ -46,9 +58,9 @@ export default class IndexedCollection {
|
||||
}
|
||||
}
|
||||
|
||||
async updateItem(item) {
|
||||
async updateItem(item: T) {
|
||||
if (!item.id) throw new Error("The item must contain the id field.");
|
||||
this.eventManager.publish(EVENTS.databaseUpdated, item.id, item);
|
||||
this.notify(item.id, item);
|
||||
|
||||
// if item is newly synced, remote will be true.
|
||||
if (!item.remote) {
|
||||
@@ -58,59 +70,39 @@ export default class IndexedCollection {
|
||||
// the item has become local now, so remove the flags
|
||||
delete item.remote;
|
||||
|
||||
// if (await this.getEncryptionKey()) {
|
||||
// const encrypted = await this.indexer.encrypt(
|
||||
// await this.getEncryptionKey(),
|
||||
// JSON.stringify(item)
|
||||
// );
|
||||
// encrypted.dateModified = item.dateModified;
|
||||
// encrypted.localOnly = item.localOnly;
|
||||
// encrypted.id = item.id;
|
||||
// await this.indexer.write(item.id, encrypted);
|
||||
// } else
|
||||
|
||||
await this.indexer.write(item.id, item);
|
||||
}
|
||||
|
||||
removeItem(id) {
|
||||
this.eventManager.publish(EVENTS.databaseUpdated, id);
|
||||
return this.updateItem({
|
||||
removeItem(id: string) {
|
||||
this.notify(id);
|
||||
return this.indexer.write(id, {
|
||||
id,
|
||||
deleted: true
|
||||
});
|
||||
}
|
||||
|
||||
async deleteItem(id) {
|
||||
this.eventManager.publish(EVENTS.databaseUpdated, id);
|
||||
async deleteItem(id: string) {
|
||||
this.notify(id);
|
||||
await this.indexer.deindex(id);
|
||||
return await this.indexer.remove(id);
|
||||
}
|
||||
|
||||
exists(id) {
|
||||
exists(id: string) {
|
||||
return this.indexer.exists(id);
|
||||
}
|
||||
|
||||
async getItem(id) {
|
||||
async getItemAsync(id: string) {
|
||||
const item = await this.indexer.read(id);
|
||||
if (!item) return;
|
||||
|
||||
// if ((await this.getEncryptionKey()) && item.iv && item.cipher) {
|
||||
// return JSON.parse(
|
||||
// await this.indexer.decrypt(await this.getEncryptionKey(), item)
|
||||
// );
|
||||
// } else
|
||||
return item;
|
||||
}
|
||||
|
||||
async getItems(indices) {
|
||||
const data = await this.indexer.readMulti(indices);
|
||||
async getItemsAsync(ids: string[]) {
|
||||
const data = await this.indexer.readMulti(ids);
|
||||
return Object.fromEntries(data);
|
||||
}
|
||||
|
||||
async getEncryptionKey() {
|
||||
if (!this.encryptionKeyFactory) return;
|
||||
if (this.encryptionKey) return this.encryptionKey;
|
||||
this.encryptionKey = await this.encryptionKeyFactory();
|
||||
return this.encryptionKey;
|
||||
private notify(id: string, item?: Partial<T>) {
|
||||
// this.eventManager.publish(EVENTS.databaseUpdated, id, item);
|
||||
}
|
||||
}
|
||||
@@ -17,58 +17,61 @@ 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 Storage from "./storage";
|
||||
import { MaybeDeletedItem } from "../entities";
|
||||
import { IStorage } from "../interfaces";
|
||||
|
||||
export default class Indexer extends Storage {
|
||||
constructor(storage, type) {
|
||||
super(storage);
|
||||
this.type = type;
|
||||
this.indices = [];
|
||||
}
|
||||
export default class Indexer<T> {
|
||||
private indices: string[] = [];
|
||||
constructor(
|
||||
private readonly storage: IStorage,
|
||||
private readonly type: string
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
this.indices = (await super.read(this.type, true)) || [];
|
||||
this.indices = (await this.storage.read(this.type, true)) || [];
|
||||
}
|
||||
|
||||
exists(key) {
|
||||
exists(key: string) {
|
||||
return this.indices.includes(key);
|
||||
}
|
||||
|
||||
async index(key) {
|
||||
async index(key: string) {
|
||||
if (this.exists(key)) return;
|
||||
this.indices.push(key);
|
||||
await super.write(this.type, this.indices);
|
||||
await this.storage.write(this.type, this.indices);
|
||||
}
|
||||
|
||||
getIndices() {
|
||||
return this.indices;
|
||||
}
|
||||
|
||||
async deindex(key) {
|
||||
async deindex(key: string) {
|
||||
if (!this.exists(key)) return;
|
||||
this.indices.splice(this.indices.indexOf(key), 1);
|
||||
await super.write(this.type, this.indices);
|
||||
await this.storage.write(this.type, this.indices);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
this.indices = [];
|
||||
await super.clear();
|
||||
await this.storage.clear();
|
||||
}
|
||||
|
||||
read(key, isArray = false) {
|
||||
return super.read(this.makeId(key), isArray);
|
||||
read(key: string, isArray = false): Promise<MaybeDeletedItem<T> | undefined> {
|
||||
return this.storage.read(this.makeId(key), isArray);
|
||||
}
|
||||
|
||||
write(key, data) {
|
||||
return super.write(this.makeId(key), data);
|
||||
write(key: string, data: MaybeDeletedItem<T>) {
|
||||
return this.storage.write(this.makeId(key), data);
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
return super.remove(this.makeId(key));
|
||||
remove(key: string) {
|
||||
return this.storage.remove(this.makeId(key));
|
||||
}
|
||||
|
||||
async readMulti(keys) {
|
||||
const entries = await super.readMulti(keys.map(this.makeId));
|
||||
async readMulti(keys: string[]) {
|
||||
const entries = await this.storage.readMulti<MaybeDeletedItem<T>>(
|
||||
keys.map(this.makeId)
|
||||
);
|
||||
entries.forEach((entry) => {
|
||||
entry[0] = entry[0].replace(`_${this.type}`, "");
|
||||
});
|
||||
@@ -76,11 +79,11 @@ export default class Indexer extends Storage {
|
||||
}
|
||||
|
||||
async migrateIndices() {
|
||||
const keys = (await super.getAllKeys()).filter(
|
||||
const keys = (await this.storage.getAllKeys()).filter(
|
||||
(key) => !key.endsWith(`_${this.type}`) && this.exists(key)
|
||||
);
|
||||
for (const id of keys) {
|
||||
const item = await super.read(id);
|
||||
const item = await this.storage.read<T>(id);
|
||||
if (!item) continue;
|
||||
|
||||
await this.write(id, item);
|
||||
@@ -88,11 +91,11 @@ export default class Indexer extends Storage {
|
||||
|
||||
// remove old ids once they have been moved
|
||||
for (const id of keys) {
|
||||
await super.remove(id);
|
||||
await this.storage.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
makeId = (id) => {
|
||||
private makeId(id: string) {
|
||||
return `${id}_${this.type}`;
|
||||
};
|
||||
}
|
||||
}
|
||||
176
packages/core/src/entities.ts
Normal file
176
packages/core/src/entities.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
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 { Cipher } from "@notesnook/crypto/dist/src/types";
|
||||
import { ValueOf } from "./types";
|
||||
|
||||
export type Collections = {
|
||||
notes: "note" | "trash";
|
||||
notebooks: "notebook" | "trash";
|
||||
attachments: "attachment";
|
||||
reminders: "reminder";
|
||||
relations: "relation";
|
||||
content: "tiny" | "tiptap";
|
||||
shortcuts: "shortcut";
|
||||
tags: "tag";
|
||||
colors: "color";
|
||||
};
|
||||
|
||||
export type CollectionType = keyof Collections;
|
||||
|
||||
export type ItemType =
|
||||
| ValueOf<Collections>
|
||||
// TODO: ideally there should be no extra types here.
|
||||
// everything should have its own collection
|
||||
| "topic"
|
||||
| "settings";
|
||||
|
||||
export type Item =
|
||||
| Note
|
||||
| Notebook
|
||||
| Topic
|
||||
| Attachment
|
||||
| Tag
|
||||
| Trash
|
||||
| Relation;
|
||||
|
||||
/**
|
||||
* Base item type from which all other item types derive containing
|
||||
* all the common properties
|
||||
*/
|
||||
export interface BaseItem<TType extends ItemType> {
|
||||
id: string;
|
||||
type: TType;
|
||||
dateModified: number;
|
||||
dateCreated: number;
|
||||
|
||||
// flags
|
||||
migrated?: boolean;
|
||||
remote?: boolean;
|
||||
synced?: boolean;
|
||||
}
|
||||
|
||||
export type NotebookReference = {
|
||||
id: string;
|
||||
topics: string[];
|
||||
};
|
||||
|
||||
export interface DeletedItem {
|
||||
id: string;
|
||||
deleted: true;
|
||||
}
|
||||
|
||||
export type MaybeDeletedItem<T> = T | DeletedItem;
|
||||
export type TrashOrItem<T extends BaseItem<"note" | "notebook">> =
|
||||
| T
|
||||
| TrashItem<T>;
|
||||
|
||||
export interface Note extends BaseItem<"note"> {
|
||||
title: string;
|
||||
notebooks: NotebookReference[];
|
||||
tags: string[];
|
||||
dateEdited: number;
|
||||
|
||||
pinned: boolean;
|
||||
locked: boolean;
|
||||
favorite: boolean;
|
||||
localOnly: boolean;
|
||||
conflicted: boolean;
|
||||
readonly: boolean;
|
||||
|
||||
contentId?: string;
|
||||
sessionId?: string;
|
||||
headline?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface Notebook extends BaseItem<"notebook"> {
|
||||
title: string;
|
||||
description?: string;
|
||||
dateEdited: number;
|
||||
pinned: boolean;
|
||||
topics: Topic[];
|
||||
}
|
||||
|
||||
export interface Topic extends BaseItem<"topic"> {
|
||||
title: string;
|
||||
notebookId: string;
|
||||
notes: string[];
|
||||
dateEdited: number;
|
||||
}
|
||||
|
||||
export interface Attachment extends BaseItem<"attachment"> {
|
||||
noteIds: string[];
|
||||
iv: string;
|
||||
salt: string;
|
||||
length: number;
|
||||
alg: string; // TODO
|
||||
chunkSize: number;
|
||||
key: Cipher;
|
||||
metadata: {
|
||||
hash: string;
|
||||
hashType: string; // TODO
|
||||
filename: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
dateEdited: number;
|
||||
dateUploaded: number;
|
||||
dateDeleted: number;
|
||||
}
|
||||
|
||||
export type ItemReference = {
|
||||
id: string;
|
||||
type: ItemType;
|
||||
};
|
||||
|
||||
export interface Relation extends BaseItem<"relation"> {
|
||||
from: ItemReference;
|
||||
to: ItemReference;
|
||||
}
|
||||
|
||||
interface BaseTag<TType extends "tag" | "color"> extends BaseItem<TType> {
|
||||
title: string;
|
||||
alias?: string;
|
||||
noteIds: string[];
|
||||
}
|
||||
|
||||
export type Tag = BaseTag<"tag">;
|
||||
export type Color = BaseTag<"color">;
|
||||
|
||||
type TrashItem<TItem extends BaseItem<"note" | "notebook">> =
|
||||
BaseItem<"trash"> & {
|
||||
title: string;
|
||||
itemType: TItem["type"];
|
||||
dateDeleted: number;
|
||||
} & Omit<TItem, "id" | "type">;
|
||||
|
||||
type Trash = TrashItem<Note> | TrashItem<Notebook>;
|
||||
|
||||
export function isDeleted<T extends BaseItem<ItemType>>(
|
||||
item: T | MaybeDeletedItem<T>
|
||||
): item is DeletedItem {
|
||||
return "deleted" in item;
|
||||
}
|
||||
|
||||
export function isTrashItem(
|
||||
item: MaybeDeletedItem<TrashOrItem<BaseItem<"note" | "notebook">>>
|
||||
): item is Trash {
|
||||
return !isDeleted(item) && item.type === "trash";
|
||||
}
|
||||
93
packages/core/src/events.ts
Normal file
93
packages/core/src/events.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
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 { ValueOf } from "./types";
|
||||
|
||||
export const CHECK_IDS = {
|
||||
"note:color": "note:color",
|
||||
"note:tag": "note:tag",
|
||||
"note:export": "note:export",
|
||||
"vault:add": "vault:add",
|
||||
"notebook:add": "notebook:add",
|
||||
"backup:encrypt": "backup:encrypt",
|
||||
"database:sync": "database:sync"
|
||||
} as const;
|
||||
|
||||
export const EVENTS = {
|
||||
userCheckStatus: "user:checkStatus",
|
||||
userSubscriptionUpdated: "user:subscriptionUpdated",
|
||||
userEmailConfirmed: "user:emailConfirmed",
|
||||
userLoggedIn: "user:loggedIn",
|
||||
userLoggedOut: "user:loggedOut",
|
||||
userFetched: "user:fetched",
|
||||
userSignedUp: "user:signedUp",
|
||||
userSessionExpired: "user:sessionExpired",
|
||||
databaseSyncRequested: "db:syncRequested",
|
||||
databaseMigrated: "db:migrated",
|
||||
databaseUpdated: "db:updated",
|
||||
databaseCollectionInitiated: "db:collectionInitiated",
|
||||
appRefreshRequested: "app:refreshRequested",
|
||||
noteRemoved: "note:removed",
|
||||
tokenRefreshed: "token:refreshed",
|
||||
userUnauthorized: "user:unauthorized",
|
||||
attachmentsLoading: "attachments:loading",
|
||||
attachmentDeleted: "attachment:deleted",
|
||||
mediaAttachmentDownloaded: "attachments:mediaDownloaded",
|
||||
vaultLocked: "vault:locked",
|
||||
systemTimeInvalid: "system:invalidTime"
|
||||
} as const;
|
||||
|
||||
export type EventMap = {
|
||||
[EVENTS.userCheckStatus]: UserCheckStatusEvent;
|
||||
// "user:subscriptionUpdated":
|
||||
// "user:emailConfirmed":
|
||||
// "user:loggedIn":
|
||||
// "user:loggedOut":
|
||||
// "user:fetched":
|
||||
// "user:signedUp":
|
||||
// "user:sessionExpired":
|
||||
// "db:syncRequested":
|
||||
// "db:migrated":
|
||||
[EVENTS.databaseUpdated]: DatabaseUpdatedEvent;
|
||||
// "app:refreshRequested":
|
||||
// "note:removed":
|
||||
// "token:refreshed":
|
||||
[EVENTS.attachmentsLoading]: AttachmentsProgressEvent;
|
||||
// "attachment:deleted":
|
||||
// "attachments:mediaDownloaded":
|
||||
};
|
||||
|
||||
export type EventName = keyof EventMap;
|
||||
export type Event = ValueOf<EventMap>;
|
||||
|
||||
export interface AttachmentsProgressEvent {
|
||||
type: "upload" | "download" | "encrypt";
|
||||
groupId?: string;
|
||||
total: number;
|
||||
current?: number;
|
||||
}
|
||||
|
||||
export interface UserCheckStatusEvent {
|
||||
type: keyof typeof CHECK_IDS;
|
||||
}
|
||||
|
||||
export interface DatabaseUpdatedEvent {
|
||||
op: "upsert" | "remove" | "delete";
|
||||
id: string;
|
||||
}
|
||||
83
packages/core/src/interfaces.ts
Normal file
83
packages/core/src/interfaces.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
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 { Cipher, SerializedKey } from "@notesnook/crypto/dist/src/types";
|
||||
|
||||
export interface IStorage {
|
||||
write<T>(key: string, data: T): Promise<void>;
|
||||
readMulti<T>(keys: string[]): Promise<[string, T][]>;
|
||||
read<T>(key: string, isArray?: boolean): Promise<T | undefined>;
|
||||
remove(key: string): Promise<void>;
|
||||
clear(): Promise<void>;
|
||||
getAllKeys(): Promise<string[]>;
|
||||
encrypt(key: SerializedKey, plainText: string): Promise<Cipher>;
|
||||
decrypt(key: SerializedKey, cipherData: Cipher): Promise<string | undefined>;
|
||||
deriveCryptoKey(name: string, credentials: SerializedKey): Promise<void>;
|
||||
hash(password: string, email: string): Promise<string>;
|
||||
getCryptoKey(name: string): Promise<string | undefined>;
|
||||
generateCryptoKey(password: string, salt?: string): Promise<SerializedKey>;
|
||||
|
||||
// async generateRandomKey() {
|
||||
// const passwordBytes = randomBytes(124);
|
||||
// const password = passwordBytes.toString("base64");
|
||||
// return await this.storage.generateCryptoKey(password);
|
||||
// }
|
||||
}
|
||||
|
||||
export interface ICompressor {
|
||||
compress(data: string): Promise<string>;
|
||||
decompress(data: string): Promise<string>;
|
||||
}
|
||||
|
||||
type RequestOptions = {
|
||||
url: string;
|
||||
chunkSize?: number;
|
||||
headers: { Authorization: string };
|
||||
};
|
||||
type Cancellable<T> = {
|
||||
execute(): Promise<T>;
|
||||
cancel(reason?: string): Promise<void>;
|
||||
};
|
||||
export interface IFileStorage {
|
||||
downloadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions
|
||||
): Cancellable<boolean>;
|
||||
uploadFile(
|
||||
filename: string,
|
||||
requestOptions: RequestOptions
|
||||
): Cancellable<boolean>;
|
||||
readEncrypted(
|
||||
filename: string,
|
||||
encryptionKey: SerializedKey,
|
||||
cipherData: Cipher
|
||||
): Promise<string | Uint8Array>;
|
||||
writeEncryptedBase64(
|
||||
data: string,
|
||||
encryptionKey: SerializedKey,
|
||||
mimeType: string
|
||||
): Promise<Cipher>;
|
||||
deleteFile(
|
||||
filename: string,
|
||||
requestOptions?: RequestOptions
|
||||
): Promise<boolean>;
|
||||
exists(filename: string): Promise<boolean>;
|
||||
clearFileStorage(): Promise<void>;
|
||||
hashBase64(data: string): Promise<{ hash: string; type: string }>;
|
||||
}
|
||||
@@ -23,8 +23,12 @@ import {
|
||||
consoleReporter,
|
||||
format,
|
||||
LogLevel,
|
||||
NoopLogger
|
||||
NoopLogger,
|
||||
LogMessage,
|
||||
ILogReporter,
|
||||
ILogger
|
||||
} from "@notesnook/logger";
|
||||
import { IStorage } from "./interfaces";
|
||||
|
||||
// Database logger reporter:
|
||||
// 1. Log to new key on every instance
|
||||
@@ -35,30 +39,21 @@ import {
|
||||
const MAX_RETENTION_LENGTH = 14;
|
||||
|
||||
class DatabaseLogReporter {
|
||||
/**
|
||||
*
|
||||
* @param {import("./database/storage").default} storage
|
||||
*/
|
||||
constructor(storage) {
|
||||
private readonly writer: DatabaseLogWriter;
|
||||
constructor(storage: IStorage) {
|
||||
this.writer = new DatabaseLogWriter(storage);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("@notesnook/logger").LogMessage} log
|
||||
*/
|
||||
write(log) {
|
||||
write(log: LogMessage) {
|
||||
this.writer.push(log);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseLogWriter {
|
||||
/**
|
||||
*
|
||||
* @param {import("./database/storage").default} storage
|
||||
*/
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
private queue: LogMessage[];
|
||||
private readonly key: string;
|
||||
|
||||
constructor(private readonly storage: IStorage) {
|
||||
this.key = new Date().toLocaleDateString();
|
||||
this.queue = [];
|
||||
|
||||
@@ -67,12 +62,12 @@ class DatabaseLogWriter {
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
push(message) {
|
||||
push(message: LogMessage) {
|
||||
this.queue.push(message);
|
||||
}
|
||||
|
||||
async read() {
|
||||
return await this.storage.read(this.key, true);
|
||||
return (await this.storage.read<LogMessage[]>(this.key, true)) || [];
|
||||
}
|
||||
|
||||
async flush() {
|
||||
@@ -106,13 +101,7 @@ class DatabaseLogWriter {
|
||||
}
|
||||
|
||||
class DatabaseLogManager {
|
||||
/**
|
||||
*
|
||||
* @param {import("./database/storage").default} storage
|
||||
*/
|
||||
constructor(storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
constructor(private readonly storage: IStorage) {}
|
||||
|
||||
async get() {
|
||||
const logKeys = await this.storage.getAllKeys();
|
||||
@@ -131,14 +120,14 @@ class DatabaseLogManager {
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key) {
|
||||
async delete(key: string) {
|
||||
await this.storage.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
function initalize(storage, disableConsoleLogs) {
|
||||
function initalize(storage: IStorage, disableConsoleLogs = false) {
|
||||
if (storage) {
|
||||
let reporters = [new DatabaseLogReporter(storage)];
|
||||
const reporters: ILogReporter[] = [new DatabaseLogReporter(storage)];
|
||||
if (process.env.NODE_ENV !== "production" && !disableConsoleLogs)
|
||||
reporters.push(consoleReporter);
|
||||
logger = new Logger({
|
||||
@@ -149,14 +138,7 @@ function initalize(storage, disableConsoleLogs) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {import("@notesnook/logger").ILogger}
|
||||
*/
|
||||
var logger = new NoopLogger();
|
||||
|
||||
/**
|
||||
* @type {DatabaseLogManager | undefined}
|
||||
*/
|
||||
var logManager;
|
||||
let logger: ILogger = new NoopLogger();
|
||||
let logManager: DatabaseLogManager | undefined;
|
||||
|
||||
export { logger, logManager, initalize, format, LogLevel };
|
||||
@@ -24,85 +24,107 @@ import { getContentFromData } from "../content-types";
|
||||
import { CHECK_IDS, checkIsUserPremium } from "../common";
|
||||
import { addItem, deleteItem } from "../utils/array";
|
||||
import { formatDate } from "../utils/date";
|
||||
import { Note, NotebookReference } from "../entities";
|
||||
|
||||
export default class Note {
|
||||
/**
|
||||
*
|
||||
* @param {import('../api').default} db
|
||||
* @param {Object} note
|
||||
*/
|
||||
constructor(note, db) {
|
||||
this._note = note;
|
||||
this._db = db;
|
||||
}
|
||||
interface INoteModel extends Readonly<Note> {
|
||||
export(
|
||||
to?: "html" | "md" | "txt",
|
||||
rawContent?: string
|
||||
): Promise<string | false | undefined>;
|
||||
|
||||
get data() {
|
||||
return this._note;
|
||||
}
|
||||
readonly data: Note;
|
||||
}
|
||||
|
||||
get headline() {
|
||||
return this._note.headline;
|
||||
}
|
||||
export class NoteModel implements INoteModel {
|
||||
constructor(private readonly note: Note) {}
|
||||
|
||||
get title() {
|
||||
return this._note.title;
|
||||
return this.note.title;
|
||||
}
|
||||
|
||||
get tags() {
|
||||
return this._note.tags;
|
||||
}
|
||||
|
||||
get colors() {
|
||||
return this._note.colors;
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this._note.id;
|
||||
}
|
||||
|
||||
get notebooks() {
|
||||
return this._note.notebooks;
|
||||
return this.note.notebooks;
|
||||
}
|
||||
|
||||
get deleted() {
|
||||
return this._note.deleted;
|
||||
get tags() {
|
||||
return this.note.tags;
|
||||
}
|
||||
|
||||
get dateEdited() {
|
||||
return this._note.dateEdited;
|
||||
return this.note.dateEdited;
|
||||
}
|
||||
get pinned() {
|
||||
return this.note.pinned;
|
||||
}
|
||||
get locked() {
|
||||
return this.note.locked;
|
||||
}
|
||||
get favorite() {
|
||||
return this.note.favorite;
|
||||
}
|
||||
get localOnly() {
|
||||
return this.note.localOnly;
|
||||
}
|
||||
get conflicted() {
|
||||
return this.note.conflicted;
|
||||
}
|
||||
get readonly() {
|
||||
return this.note.readonly;
|
||||
}
|
||||
get contentId() {
|
||||
return this.note.contentId;
|
||||
}
|
||||
get sessionId() {
|
||||
return this.note.sessionId;
|
||||
}
|
||||
get headline() {
|
||||
return this.note.headline;
|
||||
}
|
||||
get color() {
|
||||
return this.note.color;
|
||||
}
|
||||
get id() {
|
||||
return this.note.id;
|
||||
}
|
||||
get type() {
|
||||
return this.note.type;
|
||||
}
|
||||
|
||||
get dateModified() {
|
||||
return this._note.dateModified;
|
||||
return this.note.dateModified;
|
||||
}
|
||||
get dateCreated() {
|
||||
return this.note.dateCreated;
|
||||
}
|
||||
get migrated() {
|
||||
return this.note.migrated;
|
||||
}
|
||||
get remote() {
|
||||
return this.note.remote;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"html"|"md"|"txt"} format - Format to export into
|
||||
* @param {string?} rawContent - Use this raw content instead of generating itself
|
||||
* @returns {Promise<string | false | undefined>}
|
||||
* @deprecated use the model directly
|
||||
*/
|
||||
async export(to = "html", rawContent) {
|
||||
get data() {
|
||||
return this.note;
|
||||
}
|
||||
|
||||
async export(to = "html", rawContent?: string) {
|
||||
if (to !== "txt" && !(await checkIsUserPremium(CHECK_IDS.noteExport)))
|
||||
return false;
|
||||
|
||||
const templateData = {
|
||||
metadata: this.data,
|
||||
metadata: this,
|
||||
title: this.title,
|
||||
editedOn: formatDate(this.dateEdited),
|
||||
headline: this.headline,
|
||||
createdOn: formatDate(this.data.dateCreated),
|
||||
createdOn: formatDate(this.dateCreated),
|
||||
tags: this.tags.join(", ")
|
||||
};
|
||||
const contentItem = await this._db.content.raw(this._note.contentId);
|
||||
const contentItem = await this._db.content.raw(this.contentId);
|
||||
if (!contentItem) return false;
|
||||
const { data, type } = await this._db.content.downloadMedia(
|
||||
`export-${this.id}`,
|
||||
contentItem,
|
||||
false
|
||||
);
|
||||
let content = getContentFromData(type, data);
|
||||
|
||||
const content = getContentFromData(type, data);
|
||||
switch (to) {
|
||||
case "html":
|
||||
templateData.content = rawContent || content.toHTML();
|
||||
@@ -119,14 +141,14 @@ export default class Note {
|
||||
}
|
||||
|
||||
async content() {
|
||||
const content = await this._db.content.raw(this._note.contentId);
|
||||
const content = await this._db.content.raw(this.contentId);
|
||||
return content ? content.data : null;
|
||||
}
|
||||
|
||||
async duplicate() {
|
||||
const content = await this._db.content.raw(this._note.contentId);
|
||||
const content = await this._db.content.raw(this.contentId);
|
||||
return await this._db.notes.add({
|
||||
...this._note,
|
||||
...this.note,
|
||||
id: undefined,
|
||||
content: {
|
||||
type: content.type,
|
||||
@@ -136,17 +158,16 @@ export default class Note {
|
||||
favorite: false,
|
||||
pinned: false,
|
||||
contentId: null,
|
||||
title: this._note.title + " (Copy)",
|
||||
title: this.title + " (Copy)",
|
||||
dateEdited: null,
|
||||
dateCreated: null,
|
||||
dateModified: null
|
||||
});
|
||||
}
|
||||
|
||||
async color(color) {
|
||||
async color(color: string) {
|
||||
if (!(await checkIsUserPremium(CHECK_IDS.noteColor))) return;
|
||||
if (this._note.color)
|
||||
await this._db.colors.untag(this._note.color, this._note.id);
|
||||
if (this.color) await this._db.colors.untag(this.color, this.id);
|
||||
await this._db.notes.add({
|
||||
id: this.id,
|
||||
color: this._db.colors.sanitize(color)
|
||||
@@ -154,8 +175,8 @@ export default class Note {
|
||||
}
|
||||
|
||||
async uncolor() {
|
||||
if (!this._note.color) return;
|
||||
await this._db.colors.untag(this._note.color, this._note.id);
|
||||
if (!this.color) return;
|
||||
await this._db.colors.untag(this.color, this.id);
|
||||
await this._db.notes.add({
|
||||
id: this.id,
|
||||
color: undefined
|
||||
@@ -169,40 +190,38 @@ export default class Note {
|
||||
!(await checkIsUserPremium(CHECK_IDS.noteTag))
|
||||
)
|
||||
return;
|
||||
|
||||
let tagItem = await this._db.tags.add(tag, this._note.id);
|
||||
if (addItem(this._note.tags, tagItem.title))
|
||||
await this._db.notes.add(this._note);
|
||||
let tagItem = await this._db.tags.add(tag, this.id);
|
||||
if (addItem(this.tags, tagItem.title)) await this._db.notes.add(this);
|
||||
}
|
||||
|
||||
async untag(tag) {
|
||||
if (deleteItem(this._note.tags, tag)) {
|
||||
await this._db.notes.add(this._note);
|
||||
if (deleteItem(this.tags, tag)) {
|
||||
await this._db.notes.add(this);
|
||||
} else console.error("This note is not tagged by the specified tag.", tag);
|
||||
await this._db.tags.untag(tag, this._note.id);
|
||||
}
|
||||
|
||||
_toggle(prop) {
|
||||
return this._db.notes.add({ id: this._note.id, [prop]: !this._note[prop] });
|
||||
await this._db.tags.untag(tag, this.id);
|
||||
}
|
||||
|
||||
localOnly() {
|
||||
return this._toggle("localOnly");
|
||||
return this.toggle("localOnly");
|
||||
}
|
||||
|
||||
favorite() {
|
||||
return this._toggle("favorite");
|
||||
return this.toggle("favorite");
|
||||
}
|
||||
|
||||
pin() {
|
||||
return this._toggle("pinned");
|
||||
return this.toggle("pinned");
|
||||
}
|
||||
|
||||
readonly() {
|
||||
return this._toggle("readonly");
|
||||
return this.toggle("readonly");
|
||||
}
|
||||
|
||||
synced() {
|
||||
return !this.data.contentId || this._db.content.exists(this.data.contentId);
|
||||
get synced() {
|
||||
return !this.contentId || this._db.content.exists(this.contentId);
|
||||
}
|
||||
|
||||
private toggle(prop: "localOnly" | "readonly" | "pinned" | "favorite") {
|
||||
return this._db.notes.add({ id: this.id, [prop]: !this[prop] });
|
||||
}
|
||||
}
|
||||
62
packages/core/src/types.ts
Normal file
62
packages/core/src/types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
export type GroupOptions = {
|
||||
groupBy: "abc" | "year" | "month" | "week" | undefined;
|
||||
sortBy: "dateCreated" | "dateDeleted" | "dateEdited" | "title";
|
||||
sortDirection: "desc" | "asc";
|
||||
};
|
||||
|
||||
export type GroupingKey =
|
||||
| "home"
|
||||
| "notes"
|
||||
| "notebooks"
|
||||
| "tags"
|
||||
| "topics"
|
||||
| "trash"
|
||||
| "favorites";
|
||||
|
||||
export type ValueOf<T> = T[keyof T];
|
||||
|
||||
export type GroupHeader = {
|
||||
type: "header";
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
isEmailConfirmed: boolean;
|
||||
mfa: {
|
||||
isEnabled: boolean;
|
||||
primaryMethod: string;
|
||||
secondaryMethod: string;
|
||||
remainingValidCodes: number;
|
||||
};
|
||||
subscription: {
|
||||
appId: 0;
|
||||
cancelURL: string | null;
|
||||
expiry: number;
|
||||
productId: string;
|
||||
provider: 0 | 1 | 2 | 3;
|
||||
start: number;
|
||||
type: 0 | 1 | 2 | 5 | 6 | 7;
|
||||
updateURL: string | null;
|
||||
};
|
||||
};
|
||||
@@ -17,45 +17,48 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function findItemAndDelete(array, predicate) {
|
||||
export function findItemAndDelete<T>(
|
||||
array: T[],
|
||||
predicate: (value: T, index: number, obj: T[]) => unknown
|
||||
) {
|
||||
return deleteAtIndex(array, array.findIndex(predicate));
|
||||
}
|
||||
|
||||
export function addItem(array, item) {
|
||||
export function addItem<T>(array: T[], item: T): boolean {
|
||||
const index = array.indexOf(item);
|
||||
if (index > -1) return false;
|
||||
array.push(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function deleteItem(array, item) {
|
||||
export function deleteItem<T>(array: T[], item: T): boolean {
|
||||
return deleteAtIndex(array, array.indexOf(item));
|
||||
}
|
||||
|
||||
export function deleteItems(array, ...items) {
|
||||
for (let item of items) {
|
||||
export function deleteItems<T>(array: T[], ...items: T[]): void {
|
||||
for (const item of items) {
|
||||
deleteItem(array, item);
|
||||
}
|
||||
}
|
||||
|
||||
export function findById(array, id) {
|
||||
export function findById<T extends { id: string }>(array: T[], id: string) {
|
||||
if (!array) return false;
|
||||
return array.find((item) => item.id === id);
|
||||
}
|
||||
|
||||
export function hasItem(array, item) {
|
||||
export function hasItem<T>(array: T[], item: T) {
|
||||
if (!array) return false;
|
||||
return array.indexOf(item) > -1;
|
||||
}
|
||||
|
||||
function deleteAtIndex(array, index) {
|
||||
function deleteAtIndex<T>(array: T[], index: number) {
|
||||
if (index === -1) return false;
|
||||
array.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function toChunks(array, chunkSize) {
|
||||
let chunks = [];
|
||||
export function toChunks<T>(array: T[], chunkSize: number): T[][] {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < array.length; i += chunkSize) {
|
||||
const chunk = array.slice(i, i + chunkSize);
|
||||
chunks.push(chunk);
|
||||
21
packages/core/src/utils/clone.ts
Normal file
21
packages/core/src/utils/clone.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
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 _clone from "rfdc";
|
||||
export const clone = _clone();
|
||||
@@ -19,14 +19,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
const REGEX = /^data:(image\/.+);base64,(.+)/;
|
||||
|
||||
function toObject(dataurl) {
|
||||
function toObject(dataurl: string) {
|
||||
const regexResult = REGEX.exec(dataurl);
|
||||
if (!regexResult || regexResult.length < 3) return {};
|
||||
const [, mime, data] = regexResult;
|
||||
const [_, mime, data] = regexResult;
|
||||
return { mime, data };
|
||||
}
|
||||
|
||||
function fromObject({ type, data }) {
|
||||
function fromObject({ type, data }: { type: string, data: string }) {
|
||||
if (REGEX.test(data)) return data;
|
||||
return `data:${type};base64,${data}`;
|
||||
}
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
export function getWeekGroupFromTimestamp(timestamp) {
|
||||
export function getWeekGroupFromTimestamp(timestamp: number) {
|
||||
const date = new Date(timestamp);
|
||||
const { start, end } = getWeek(date);
|
||||
|
||||
@@ -37,8 +37,8 @@ const MS_IN_HOUR = 3600000;
|
||||
* @param {Date} date
|
||||
* @returns
|
||||
*/
|
||||
function getWeek(date) {
|
||||
var day = date.getDay() || 7;
|
||||
function getWeek(date: Date) {
|
||||
const day = date.getDay() || 7;
|
||||
if (day !== 1) {
|
||||
const hours = 24 * (day - 1);
|
||||
date.setTime(date.getTime() - MS_IN_HOUR * hours);
|
||||
@@ -61,15 +61,9 @@ function getWeek(date) {
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} date
|
||||
* @param {Intl.DateTimeFormatOptions} options
|
||||
* @returns
|
||||
*/
|
||||
export function formatDate(
|
||||
date,
|
||||
options = {
|
||||
date: number,
|
||||
options: Intl.DateTimeFormatOptions = {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short"
|
||||
}
|
||||
97
packages/core/src/utils/event-manager.ts
Normal file
97
packages/core/src/utils/event-manager.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
type EventMap = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type EventReference<T extends EventMap> = {
|
||||
name: keyof T;
|
||||
once: boolean;
|
||||
};
|
||||
|
||||
type EventResult = { result: boolean };
|
||||
|
||||
type EventHandler<TPayload = unknown, TResult = unknown> = (
|
||||
payload: TPayload
|
||||
) => TResult;
|
||||
|
||||
type SubscribeResult = { unsubscribe: () => boolean };
|
||||
|
||||
export class EventManager<TEventMap extends EventMap> {
|
||||
#registry: Map<EventHandler, EventReference<TEventMap>> = new Map<
|
||||
EventHandler,
|
||||
EventReference<TEventMap>
|
||||
>();
|
||||
|
||||
unsubscribeAll() {
|
||||
this.#registry.clear();
|
||||
}
|
||||
|
||||
subscribe<T extends keyof TEventMap>(
|
||||
name: T,
|
||||
handler: EventHandler<TEventMap[T], Promise<void> | void>,
|
||||
once = false
|
||||
): SubscribeResult {
|
||||
if (!name || !handler) throw new Error("name and handler are required.");
|
||||
this.#registry.set(<EventHandler>handler, { name, once });
|
||||
return { unsubscribe: () => this.unsubscribe(handler) };
|
||||
}
|
||||
|
||||
unsubscribe<T extends keyof TEventMap>(handler: EventHandler<TEventMap[T]>) {
|
||||
return this.#registry.delete(<EventHandler>handler);
|
||||
}
|
||||
|
||||
publish<T extends keyof TEventMap>(name: T, payload: TEventMap[T]) {
|
||||
this.#registry.forEach((props, handler) => {
|
||||
if (props.name === name) handler(payload);
|
||||
if (props.once) this.#registry.delete(handler);
|
||||
});
|
||||
}
|
||||
|
||||
async publishWithResult<T extends keyof TEventMap>(
|
||||
name: T,
|
||||
payload: TEventMap[T]
|
||||
): Promise<EventResult[]> {
|
||||
const handlers: EventHandler[] = [];
|
||||
this.#registry.forEach((props, handler) => {
|
||||
if (props.name === name) handlers.push(handler);
|
||||
if (props.once) this.#registry.delete(handler);
|
||||
});
|
||||
|
||||
if (handlers.length <= 0) return [];
|
||||
|
||||
return await Promise.all(
|
||||
handlers.map(async (handler) => {
|
||||
const result = await handler(payload);
|
||||
return isEventResult(result) ? result : { result: false };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
remove<T extends keyof TEventMap>(...names: T[]) {
|
||||
this.#registry.forEach((props, handler) => {
|
||||
if (names.includes(<T>props.name)) this.#registry.delete(handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isEventResult(result: unknown): result is EventResult {
|
||||
return typeof result === "object" && !!result && "result" in result;
|
||||
}
|
||||
@@ -16,26 +16,14 @@ 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 mimedb from "mime-db";
|
||||
|
||||
// type MimeTypeInfo = {
|
||||
// source: string;
|
||||
// extensions?: string[];
|
||||
// charset?: string;
|
||||
// compressible?: boolean;
|
||||
// };
|
||||
|
||||
let db; // : Record<string, MimeTypeInfo>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} filename
|
||||
* @param {string | undefined} mime
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getFileNameWithExtension(filename, mime) {
|
||||
export function getFileNameWithExtension(
|
||||
filename: string,
|
||||
mime: string | undefined
|
||||
): string {
|
||||
if (!mime || mime === "application/octet-stream") return filename;
|
||||
if (!db) db = require("mime-db");
|
||||
const mimeData = db[mime];
|
||||
const mimeData = mimedb[mime];
|
||||
if (!mimeData || !mimeData.extensions || mimeData.extensions.length === 0)
|
||||
return filename;
|
||||
const extension = mimeData.extensions[0];
|
||||
@@ -17,8 +17,8 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function extractHostname(url) {
|
||||
var hostname;
|
||||
export function extractHostname(url: string) {
|
||||
let hostname = url;
|
||||
//find & remove protocol (http, ftp, etc.) and get hostname
|
||||
|
||||
if (url.indexOf("//") > -1) {
|
||||
@@ -21,13 +21,13 @@ import { Parser } from "htmlparser2";
|
||||
|
||||
const ALLOWED_ATTRIBUTES = ["href", "src", "data-hash"];
|
||||
|
||||
export function isHTMLEqual(one, two) {
|
||||
export function isHTMLEqual(one: unknown, two: unknown) {
|
||||
if (typeof one !== "string" || typeof two !== "string") return false;
|
||||
|
||||
return toDiffable(one) === toDiffable(two);
|
||||
}
|
||||
|
||||
function toDiffable(html) {
|
||||
function toDiffable(html: string) {
|
||||
let text = "";
|
||||
const parser = new Parser(
|
||||
{
|
||||
@@ -41,8 +41,8 @@ function toDiffable(html) {
|
||||
}
|
||||
},
|
||||
{
|
||||
lowerCaseTags: false,
|
||||
parseAttributes: true
|
||||
lowerCaseTags: false
|
||||
// parseAttributes: true
|
||||
}
|
||||
);
|
||||
parser.end(html);
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { decodeHTML5 } from "entities";
|
||||
import { Parser } from "htmlparser2";
|
||||
|
||||
export const parseHTML = (input) =>
|
||||
export const parseHTML = (input: string) =>
|
||||
new globalThis.DOMParser().parseFromString(
|
||||
wrapIntoHTMLDocument(input),
|
||||
"text/html"
|
||||
@@ -31,18 +31,18 @@ export function getDummyDocument() {
|
||||
return doc;
|
||||
}
|
||||
|
||||
export function getInnerText(element) {
|
||||
return decodeHTML5(element.innerText || element.textContent);
|
||||
export function getInnerText(element: HTMLElement) {
|
||||
return decodeHTML5(element.textContent || element.innerText);
|
||||
}
|
||||
|
||||
function wrapIntoHTMLDocument(input) {
|
||||
function wrapIntoHTMLDocument(input: string) {
|
||||
if (typeof input !== "string") return input;
|
||||
if (input.includes("<body>")) return input;
|
||||
|
||||
return `<!doctype html><html lang="en"><head><title>Document Fragment</title></head><body>${input}</body></html>`;
|
||||
}
|
||||
|
||||
export function extractFirstParagraph(html) {
|
||||
export function extractFirstParagraph(html: string) {
|
||||
let text = "";
|
||||
let start = false;
|
||||
const parser = new Parser(
|
||||
@@ -69,3 +69,40 @@ export function extractFirstParagraph(html) {
|
||||
parser.end(html);
|
||||
return text;
|
||||
}
|
||||
|
||||
type OnTagHandler = (
|
||||
name: string,
|
||||
attr: Record<string, string>,
|
||||
pos: { start: number; end: number }
|
||||
) => void;
|
||||
|
||||
export class HTMLParser {
|
||||
private parser: Parser;
|
||||
constructor(options: { ontag?: OnTagHandler } = {}) {
|
||||
const { ontag } = options;
|
||||
|
||||
this.parser = new Parser(
|
||||
{
|
||||
onopentag: (name, attr) =>
|
||||
ontag &&
|
||||
ontag(name, attr, {
|
||||
start: this.parser.startIndex,
|
||||
end: this.parser.endIndex
|
||||
})
|
||||
},
|
||||
{
|
||||
recognizeSelfClosing: true,
|
||||
xmlMode: false,
|
||||
decodeEntities: false,
|
||||
lowerCaseAttributeNames: false,
|
||||
lowerCaseTags: false,
|
||||
recognizeCDATA: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
parse(html: string) {
|
||||
this.parser.end(html);
|
||||
this.parser.reset();
|
||||
}
|
||||
}
|
||||
@@ -19,25 +19,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Parser } from "htmlparser2";
|
||||
|
||||
type OnTagHandler = (
|
||||
name: string,
|
||||
attr: Record<string, string>,
|
||||
pos: { start: number; end: number }
|
||||
) => false | { name: string; attr: Record<string, string> } | undefined;
|
||||
|
||||
export class HTMLRewriter {
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* ontag?: (name: string, attr: string, pos: {start: number, end: number}) => false | {name: string, attr: string} | undefined
|
||||
* }} options
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
private transformed = "";
|
||||
private currentTag: string | null = null;
|
||||
private ignoreIndex: number | null = null;
|
||||
private parser: Parser;
|
||||
|
||||
constructor(
|
||||
options: {
|
||||
ontag?: OnTagHandler;
|
||||
} = {}
|
||||
) {
|
||||
const { ontag } = options;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this.transformed = "";
|
||||
|
||||
/** @private */
|
||||
this.currentTag = null;
|
||||
|
||||
/** @private */
|
||||
this.ignoreIndex = null;
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -131,7 +130,7 @@ export class HTMLRewriter {
|
||||
}
|
||||
}
|
||||
|
||||
transform(html) {
|
||||
transform(html: string) {
|
||||
this.parser.end(html);
|
||||
return this.transformed;
|
||||
}
|
||||
@@ -140,48 +139,7 @@ export class HTMLRewriter {
|
||||
this.parser.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
write(html) {
|
||||
private write(html: string) {
|
||||
this.transformed += html;
|
||||
}
|
||||
}
|
||||
|
||||
export class HTMLParser {
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* ontag?: (name: string, attr: Record<string, string>, pos: {start: number, end: number}) => void
|
||||
* }} options
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
const { ontag } = options;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this.parser = new Parser(
|
||||
{
|
||||
onopentag: (name, attr) =>
|
||||
ontag(name, attr, {
|
||||
start: this.parser.startIndex,
|
||||
end: this.parser.endIndex
|
||||
})
|
||||
},
|
||||
{
|
||||
recognizeSelfClosing: true,
|
||||
xmlMode: false,
|
||||
decodeEntities: false,
|
||||
lowerCaseAttributeNames: false,
|
||||
lowerCaseTags: false,
|
||||
recognizeCDATA: false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
parse(html) {
|
||||
this.parser.end(html);
|
||||
this.parser.reset();
|
||||
}
|
||||
}
|
||||
@@ -18,21 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SparkMD5 from "spark-md5";
|
||||
import ObjectID from "./object-id";
|
||||
import { objectId } from "./object-id";
|
||||
|
||||
export default function () {
|
||||
return new ObjectID().toHexString();
|
||||
return objectId().toString("hex");
|
||||
}
|
||||
|
||||
export function makeId(text) {
|
||||
export function makeId(text: string) {
|
||||
return SparkMD5.hash(text);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} noteId id of a note
|
||||
* @returns {string} An id with postfix of "_index"
|
||||
*/
|
||||
export function makeSessionContentId(sessionId) {
|
||||
export function makeSessionContentId(sessionId: string): string {
|
||||
return sessionId + "_content";
|
||||
}
|
||||
@@ -17,16 +17,16 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var map = Map;
|
||||
class MapStub {
|
||||
override(replacement) {
|
||||
map = replacement;
|
||||
}
|
||||
class _MapStub {
|
||||
private map: MapConstructor = Map;
|
||||
|
||||
get Map() {
|
||||
return map;
|
||||
return this.map;
|
||||
}
|
||||
|
||||
set Map(replacement: MapConstructor) {
|
||||
this.map = replacement;
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new MapStub();
|
||||
module.exports = instance;
|
||||
export const MapStub = new _MapStub();
|
||||
72
packages/core/src/utils/object-id.ts
Normal file
72
packages/core/src/utils/object-id.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
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 { randomBytes } from "./random";
|
||||
|
||||
export class InvalidObjectId extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "InvalidObjectId";
|
||||
this.message = "Invalid ObjectId length";
|
||||
}
|
||||
}
|
||||
|
||||
const PROCESS_UNIQUE = randomBytes(5);
|
||||
let index = ~~(Math.random() * 0xffffff);
|
||||
|
||||
export function objectId(date = Date.now()): Buffer {
|
||||
index = (index + 1) % 0xffffff;
|
||||
const objectId = new Uint8Array(12);
|
||||
const time = ~~(date / 1000);
|
||||
|
||||
// 4-byte timestamp
|
||||
new DataView(objectId.buffer, 0, 4).setUint32(0, time);
|
||||
|
||||
// 5-byte process unique
|
||||
objectId[4] = PROCESS_UNIQUE[0];
|
||||
objectId[5] = PROCESS_UNIQUE[1];
|
||||
objectId[6] = PROCESS_UNIQUE[2];
|
||||
objectId[7] = PROCESS_UNIQUE[3];
|
||||
objectId[8] = PROCESS_UNIQUE[4];
|
||||
|
||||
// 3-byte counter
|
||||
objectId[11] = index & 0xff;
|
||||
objectId[10] = (index >> 8) & 0xff;
|
||||
objectId[9] = (index >> 16) & 0xff;
|
||||
|
||||
return Buffer.from(objectId.buffer);
|
||||
}
|
||||
|
||||
export function isValid(oid: Uint8Array): boolean {
|
||||
return oid.length === 12;
|
||||
}
|
||||
|
||||
// export function fromHex(hex: string): Uint8Array {
|
||||
// const oid = stdDecodeString(hex);
|
||||
// if (!isValid(oid)) throw new InvalidObjectId();
|
||||
// return oid;
|
||||
// }
|
||||
|
||||
export function getDate(oid: Uint8Array): Date {
|
||||
const date = new Date();
|
||||
const time = new DataView(oid.buffer, 0, 4).getUint32(0);
|
||||
|
||||
date.setTime(~~time * 1000);
|
||||
return date;
|
||||
}
|
||||
@@ -17,29 +17,26 @@ 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}
|
||||
*/
|
||||
module.exports.randomBytes = function randomBytes(size) {
|
||||
export function randomBytes(size: number): Buffer {
|
||||
if (!global.crypto || !crypto)
|
||||
throw new Error("Crypto is not supported on this platform.");
|
||||
if (crypto.randomBytes) return crypto.randomBytes(size);
|
||||
throw new Error("crypto is not supported on this platform.");
|
||||
|
||||
if (!crypto.getRandomValues)
|
||||
if ("randomBytes" in crypto && typeof crypto.randomBytes === "function")
|
||||
return crypto.randomBytes(size);
|
||||
|
||||
if (!("getRandomValues" in crypto))
|
||||
throw new Error(
|
||||
"Crypto.getRandomValues is not available on this platform."
|
||||
"crypto.getRandomValues is not available on this platform."
|
||||
);
|
||||
|
||||
const buffer = Buffer.allocUnsafe(size);
|
||||
crypto.getRandomValues(buffer);
|
||||
return buffer;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports.randomInt = function () {
|
||||
const randomBuffer = module.exports.randomBytes(1);
|
||||
let randomNumber = randomBuffer[0] / 0xff; // / (0xffffffff + 1);
|
||||
export function randomInt(): number {
|
||||
const randomBuffer = randomBytes(1);
|
||||
const randomNumber = randomBuffer[0] / 0xff; // / (0xffffffff + 1);
|
||||
|
||||
return Math.floor(randomNumber * 0xffffff);
|
||||
};
|
||||
}
|
||||
@@ -17,45 +17,53 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// SetManipulator MIT Licence © 2016 Edwin Monk-Fromont http://github.com/edmofro
|
||||
// Based on setOps.js MIT License © 2014 James Abney http://github.com/jabney
|
||||
|
||||
// Set operations union, intersection, symmetric difference,
|
||||
// relative complement, equals. Set operations are fast.
|
||||
export class SetManipulator {
|
||||
type KeySelector<T> = (item: T) => string;
|
||||
type Histogram<T> = Record<string, { value: T; frequency: number }>;
|
||||
type HistogramEvaluator = (frequency: number) => boolean;
|
||||
|
||||
class SetManipulator {
|
||||
constructor() {}
|
||||
|
||||
// Processes a histogram consructed from two arrays, 'a' and 'b'.
|
||||
// This function is used generically by the below set operation
|
||||
// methods, a.k.a, 'evaluators', to return some subset of
|
||||
// a set union, based on frequencies in the histogram.
|
||||
process(a, b, getKey = (k) => k, evaluator) {
|
||||
process<T>(
|
||||
a: T[],
|
||||
b: T[],
|
||||
key: KeySelector<T> = (item) => String(item),
|
||||
evaluator?: HistogramEvaluator
|
||||
): Histogram<T> | T[] {
|
||||
// If identity extractor passed in, push it on the stack
|
||||
//if (identityExtractor) this.pushIdentityExtractor(identityExtractor);
|
||||
// Create a histogram of 'a'.
|
||||
const hist = {};
|
||||
const hist: Histogram<T> = {};
|
||||
const out = [];
|
||||
let ukey;
|
||||
a.forEach((value) => {
|
||||
ukey = getKey(value);
|
||||
ukey = key(value);
|
||||
if (!hist[ukey]) {
|
||||
hist[ukey] = { value: value, freq: 1 };
|
||||
hist[ukey] = { value, frequency: 1 };
|
||||
}
|
||||
});
|
||||
|
||||
// Merge 'b' into the histogram.
|
||||
b.forEach((value) => {
|
||||
ukey = getKey(value);
|
||||
ukey = key(value);
|
||||
if (hist[ukey]) {
|
||||
if (hist[ukey].freq === 1) hist[ukey].freq = 3;
|
||||
} else hist[ukey] = { value: value, freq: 2 };
|
||||
if (hist[ukey].frequency === 1) hist[ukey].frequency = 3;
|
||||
} else hist[ukey] = { value: value, frequency: 2 };
|
||||
});
|
||||
|
||||
// Pop any new identity extractor
|
||||
//if (identityExtractor) this.popIdentityExtractor(identityExtractor);
|
||||
// Call the given evaluator.
|
||||
if (evaluator) {
|
||||
for (const key in hist) {
|
||||
//if (!hist.hasOwnProperty(key)) continue; // Property from object prototype, skip
|
||||
if (evaluator(hist[key].freq)) out.push(hist[key].value);
|
||||
if (evaluator(hist[key].frequency)) out.push(hist[key].value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -64,45 +72,45 @@ export class SetManipulator {
|
||||
|
||||
// Join two sets together.
|
||||
// Set.union([1, 2, 2], [2, 3]) => [1, 2, 3]
|
||||
union(a, b, getKey) {
|
||||
return this.process(a, b, getKey, () => true);
|
||||
union<T>(a: T[], b: T[], key?: KeySelector<T>) {
|
||||
return <T[]>this.process(a, b, key, () => true);
|
||||
}
|
||||
|
||||
// Return items common to both sets.
|
||||
// Set.intersection([1, 1, 2], [2, 2, 3]) => [2]
|
||||
intersection(a, b) {
|
||||
return this.process(a, b, undefined, (freq) => freq === 3);
|
||||
intersection<T>(a: T[], b: T[], key: KeySelector<T>) {
|
||||
return this.process(a, b, key, (freq) => freq === 3);
|
||||
}
|
||||
|
||||
// Symmetric difference. Items from either set that
|
||||
// are not in both sets.
|
||||
// Set.difference([1, 1, 2], [2, 3, 3]) => [1, 3]
|
||||
difference(a, b) {
|
||||
return this.process(a, b, undefined, (freq) => freq < 3);
|
||||
difference<T>(a: T[], b: T[], key: KeySelector<T>) {
|
||||
return this.process(a, b, key, (freq) => freq < 3);
|
||||
}
|
||||
|
||||
// Relative complement. Items from 'a' which are
|
||||
// not also in 'b'.
|
||||
// Set.complement([1, 2, 2], [2, 2, 3]) => [3]
|
||||
complement(a, b) {
|
||||
return this.process(a, b, undefined, (freq) => freq === 1);
|
||||
complement<T>(a: T[], b: T[], key: KeySelector<T>) {
|
||||
return this.process(a, b, key, (freq) => freq === 1);
|
||||
}
|
||||
|
||||
// Returns true if both sets are equivalent, false otherwise.
|
||||
// Set.equals([1, 1, 2], [1, 2, 2]) => true
|
||||
// Set.equals([1, 1, 2], [1, 2, 3]) => false
|
||||
equals(a, b) {
|
||||
equals<T>(a: T[], b: T[], key: KeySelector<T>) {
|
||||
let max = 0;
|
||||
let min = Math.pow(2, 53);
|
||||
const hist = this.process(a, b);
|
||||
const hist = <Histogram<T>>this.process(a, b, key);
|
||||
|
||||
for (const key in hist) {
|
||||
// if (!hist.hasOwnProperty(key)) continue; // Property from object prototype, skip
|
||||
max = Math.max(max, hist[key].freq);
|
||||
min = Math.min(min, hist[key].freq);
|
||||
max = Math.max(max, hist[key].frequency);
|
||||
min = Math.min(min, hist[key].frequency);
|
||||
}
|
||||
return min === 3 && max === 3;
|
||||
}
|
||||
}
|
||||
|
||||
const setManipulator = new SetManipulator();
|
||||
export default setManipulator;
|
||||
export const set = new SetManipulator();
|
||||
9
packages/core/tsconfig.json
Normal file
9
packages/core/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"allowJs": true
|
||||
},
|
||||
"exclude": ["**/__tests__"],
|
||||
"include": ["src/", "index.ts"]
|
||||
}
|
||||
@@ -1,56 +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/>.
|
||||
*/
|
||||
|
||||
const _ignore = "";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* groupBy: "abc" | "year" | "month" | "week" | "none" | undefined,
|
||||
* sortBy: "dateCreated" | "dateDeleted" | "dateEdited" | "dateModified" | "title",
|
||||
* sortDirection: "desc" | "asc"
|
||||
* }} GroupOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {"home" | "notes" | "notebooks" | "tags" | "topics" | "trash" | "favorites" | "reminders"} GroupingKey
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* id: string,
|
||||
* email: string,
|
||||
* isEmailConfirmed: boolean,
|
||||
* mfa: {
|
||||
* isEnabled: boolean,
|
||||
* primaryMethod: string,
|
||||
* secondaryMethod: string,
|
||||
* remainingValidCodes: number
|
||||
* },
|
||||
* subscription: {
|
||||
* appId: 0,
|
||||
* cancelURL: string | null,
|
||||
* expiry: number,
|
||||
* productId: string,
|
||||
* provider: 0 | 1 | 2 | 3,
|
||||
* start: number,
|
||||
* type: 0 | 1 | 2 | 5 | 6 | 7,
|
||||
* updateURL: string | null,
|
||||
* }
|
||||
* }} User
|
||||
*/
|
||||
@@ -1,82 +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/>.
|
||||
*/
|
||||
|
||||
class EventManager {
|
||||
constructor() {
|
||||
this._registry = new Map();
|
||||
}
|
||||
|
||||
unsubscribeAll() {
|
||||
this._registry.clear();
|
||||
}
|
||||
|
||||
subscribeMulti(names, handler, thisArg) {
|
||||
names.forEach((name) => {
|
||||
this.subscribe(name, handler.bind(thisArg));
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(name, handler, once = false) {
|
||||
if (!name || !handler) throw new Error("name and handler are required.");
|
||||
this._registry.set(handler, { name, once });
|
||||
return { unsubscribe: () => this.unsubscribe(name, handler) };
|
||||
}
|
||||
|
||||
subscribeSingle(name, handler) {
|
||||
if (!name || !handler) throw new Error("name and handler are required.");
|
||||
this._registry.forEach((props, handler) => {
|
||||
if (props.name === name) this._registry.delete(handler);
|
||||
});
|
||||
this._registry.set(handler, { name, once: false });
|
||||
return { unsubscribe: () => this.unsubscribe(name, handler) };
|
||||
}
|
||||
|
||||
unsubscribe(_name, handler) {
|
||||
return this._registry.delete(handler);
|
||||
}
|
||||
|
||||
publish(name, ...args) {
|
||||
this._registry.forEach((props, handler) => {
|
||||
if (props.name === name) {
|
||||
handler(...args);
|
||||
if (props.once) this._registry.delete(handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async publishWithResult(name, ...args) {
|
||||
const handlers = [];
|
||||
this._registry.forEach((props, handler) => {
|
||||
if (props.name === name) {
|
||||
handlers.push(handler);
|
||||
if (props.once) this._registry.delete(handler);
|
||||
}
|
||||
});
|
||||
|
||||
if (handlers.length <= 0) return true;
|
||||
return await Promise.all(handlers.map((handler) => handler(...args)));
|
||||
}
|
||||
|
||||
remove(...names) {
|
||||
this._registry.forEach((props, handler) => {
|
||||
if (names.includes(props.name)) this._registry.delete(handler);
|
||||
});
|
||||
}
|
||||
}
|
||||
export default EventManager;
|
||||
@@ -1,362 +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/>.
|
||||
*/
|
||||
|
||||
// Copied from https://github.com/williamkapke/bson-objectid
|
||||
|
||||
const { randomInt } = require("./random");
|
||||
|
||||
var MACHINE_ID = randomInt();
|
||||
var index = (ObjectID.index = randomInt());
|
||||
var pid =
|
||||
(typeof process === "undefined" || typeof process.pid !== "number"
|
||||
? randomInt()
|
||||
: process.pid) % 0xffff;
|
||||
|
||||
/**
|
||||
* Determine if an object is Buffer
|
||||
*
|
||||
* Author: Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
|
||||
* License: MIT
|
||||
*
|
||||
*/
|
||||
var isBuffer = function (obj) {
|
||||
return !!(
|
||||
obj != null &&
|
||||
obj.constructor &&
|
||||
typeof obj.constructor.isBuffer === "function" &&
|
||||
obj.constructor.isBuffer(obj)
|
||||
);
|
||||
};
|
||||
|
||||
// Precomputed hex table enables speedy hex string conversion
|
||||
var hexTable = [];
|
||||
for (var i = 0; i < 256; i++) {
|
||||
hexTable[i] = (i <= 15 ? "0" : "") + i.toString(16);
|
||||
}
|
||||
|
||||
// Regular expression that checks for hex value
|
||||
var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$");
|
||||
|
||||
// Lookup tables
|
||||
var decodeLookup = [];
|
||||
i = 0;
|
||||
while (i < 10) decodeLookup[0x30 + i] = i++;
|
||||
while (i < 16) decodeLookup[0x41 - 10 + i] = decodeLookup[0x61 - 10 + i] = i++;
|
||||
|
||||
/**
|
||||
* Create a new immutable ObjectID instance
|
||||
*
|
||||
* @class Represents the BSON ObjectID type
|
||||
* @param {String|Number} id Can be a 24 byte hex string, 12 byte binary string or a Number.
|
||||
* @return {Object} instance of ObjectID.
|
||||
*/
|
||||
function ObjectID(id) {
|
||||
if (!(this instanceof ObjectID)) return new ObjectID(id);
|
||||
if (id && (id instanceof ObjectID || id._bsontype === "ObjectID")) return id;
|
||||
|
||||
this._bsontype = "ObjectID";
|
||||
|
||||
// The most common usecase (blank id, new objectId instance)
|
||||
if (id == null || typeof id === "number") {
|
||||
// Generate a new id
|
||||
this.id = this.generate(id);
|
||||
// Return the object
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the passed in id is valid
|
||||
var valid = ObjectID.isValid(id);
|
||||
|
||||
// Throw an error if it's not a valid setup
|
||||
if (!valid && id != null) {
|
||||
throw new Error(
|
||||
"Argument passed in must be a single String of 12 bytes or a string of 24 hex characters"
|
||||
);
|
||||
} else if (valid && typeof id === "string" && id.length === 24) {
|
||||
return ObjectID.createFromHexString(id);
|
||||
} else if (id != null && id.length === 12) {
|
||||
// assume 12 byte string
|
||||
this.id = id;
|
||||
} else if (id != null && typeof id.toHexString === "function") {
|
||||
// Duck-typing to support ObjectId from different npm packages
|
||||
return id;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Argument passed in must be a single String of 12 bytes or a string of 24 hex characters"
|
||||
);
|
||||
}
|
||||
}
|
||||
module.exports = ObjectID;
|
||||
ObjectID.default = ObjectID;
|
||||
|
||||
/**
|
||||
* Creates an ObjectID from a second based number, with the rest of the ObjectID zeroed out. Used for comparisons or sorting the ObjectID.
|
||||
*
|
||||
* @param {Number} time an integer number representing a number of seconds.
|
||||
* @return {ObjectID} return the created ObjectID
|
||||
* @api public
|
||||
*/
|
||||
ObjectID.createFromTime = function (time) {
|
||||
time = parseInt(time, 10) % 0xffffffff;
|
||||
return new ObjectID(hex(8, time) + "0000000000000000");
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an ObjectID from a hex string representation of an ObjectID.
|
||||
*
|
||||
* @param {String} hexString create a ObjectID from a passed in 24 byte hexstring.
|
||||
* @return {ObjectID} return the created ObjectID
|
||||
* @api public
|
||||
*/
|
||||
ObjectID.createFromHexString = function (hexString) {
|
||||
// Throw an error if it's not a valid setup
|
||||
if (
|
||||
typeof hexString === "undefined" ||
|
||||
(hexString != null && hexString.length !== 24)
|
||||
) {
|
||||
throw new Error(
|
||||
"Argument passed in must be a single String of 12 bytes or a string of 24 hex characters"
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate lengths
|
||||
var data = "";
|
||||
var i = 0;
|
||||
|
||||
while (i < 24) {
|
||||
data += String.fromCharCode(
|
||||
(decodeLookup[hexString.charCodeAt(i++)] << 4) |
|
||||
decodeLookup[hexString.charCodeAt(i++)]
|
||||
);
|
||||
}
|
||||
|
||||
return new ObjectID(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a value is a valid bson ObjectId
|
||||
*
|
||||
* @param {String} objectid Can be a 24 byte hex string or an instance of ObjectID.
|
||||
* @return {Boolean} return true if the value is a valid bson ObjectID, return false otherwise.
|
||||
* @api public
|
||||
*
|
||||
* THE NATIVE DOCUMENTATION ISN'T CLEAR ON THIS GUY!
|
||||
* http://mongodb.github.io/node-mongodb-native/api-bson-generated/objectid.html#objectid-isvalid
|
||||
*/
|
||||
ObjectID.isValid = function (id) {
|
||||
if (id == null) return false;
|
||||
|
||||
if (typeof id === "number") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof id === "string") {
|
||||
return id.length === 12 || (id.length === 24 && checkForHexRegExp.test(id));
|
||||
}
|
||||
|
||||
if (id instanceof ObjectID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isBuffer(id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Duck-Typing detection of ObjectId like objects
|
||||
if (
|
||||
typeof id.toHexString === "function" &&
|
||||
(id.id instanceof Buffer || typeof id.id === "string")
|
||||
) {
|
||||
return (
|
||||
id.id.length === 12 ||
|
||||
(id.id.length === 24 && checkForHexRegExp.test(id.id))
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
ObjectID.prototype = {
|
||||
constructor: ObjectID,
|
||||
|
||||
/**
|
||||
* Return the ObjectID id as a 24 byte hex string representation
|
||||
*
|
||||
* @return {String} return the 24 byte hex string representation.
|
||||
* @api public
|
||||
*/
|
||||
toHexString: function () {
|
||||
if (!this.id || !this.id.length) {
|
||||
throw new Error(
|
||||
"invalid ObjectId, ObjectId.id must be either a string or a Buffer, but is [" +
|
||||
JSON.stringify(this.id) +
|
||||
"]"
|
||||
);
|
||||
}
|
||||
|
||||
if (this.id.length === 24) {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
if (isBuffer(this.id)) {
|
||||
return this.id.toString("hex");
|
||||
}
|
||||
|
||||
var hexString = "";
|
||||
for (var i = 0; i < this.id.length; i++) {
|
||||
hexString += hexTable[this.id.charCodeAt(i)];
|
||||
}
|
||||
|
||||
return hexString;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compares the equality of this ObjectID with `otherID`.
|
||||
*
|
||||
* @param {Object} otherId ObjectID instance to compare against.
|
||||
* @return {Boolean} the result of comparing two ObjectID's
|
||||
* @api public
|
||||
*/
|
||||
equals: function (otherId) {
|
||||
if (otherId instanceof ObjectID) {
|
||||
return this.toString() === otherId.toString();
|
||||
} else if (
|
||||
typeof otherId === "string" &&
|
||||
ObjectID.isValid(otherId) &&
|
||||
otherId.length === 12 &&
|
||||
isBuffer(this.id)
|
||||
) {
|
||||
return otherId === this.id.toString("binary");
|
||||
} else if (
|
||||
typeof otherId === "string" &&
|
||||
ObjectID.isValid(otherId) &&
|
||||
otherId.length === 24
|
||||
) {
|
||||
return otherId.toLowerCase() === this.toHexString();
|
||||
} else if (
|
||||
typeof otherId === "string" &&
|
||||
ObjectID.isValid(otherId) &&
|
||||
otherId.length === 12
|
||||
) {
|
||||
return otherId === this.id;
|
||||
} else if (
|
||||
otherId != null &&
|
||||
(otherId instanceof ObjectID || otherId.toHexString)
|
||||
) {
|
||||
return otherId.toHexString() === this.toHexString();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the generation date (accurate up to the second) that this ID was generated.
|
||||
*
|
||||
* @return {Date} the generation date
|
||||
* @api public
|
||||
*/
|
||||
getTimestamp: function () {
|
||||
var timestamp = new Date();
|
||||
var time;
|
||||
if (isBuffer(this.id)) {
|
||||
time =
|
||||
this.id[3] |
|
||||
(this.id[2] << 8) |
|
||||
(this.id[1] << 16) |
|
||||
(this.id[0] << 24);
|
||||
} else {
|
||||
time =
|
||||
this.id.charCodeAt(3) |
|
||||
(this.id.charCodeAt(2) << 8) |
|
||||
(this.id.charCodeAt(1) << 16) |
|
||||
(this.id.charCodeAt(0) << 24);
|
||||
}
|
||||
timestamp.setTime(Math.floor(time) * 1000);
|
||||
return timestamp;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate a 12 byte id buffer used in ObjectID's
|
||||
*
|
||||
* @method
|
||||
* @param {number} [time] optional parameter allowing to pass in a second based timestamp.
|
||||
* @return {string} return the 12 byte id buffer string.
|
||||
*/
|
||||
generate: function (time) {
|
||||
if ("number" !== typeof time) {
|
||||
time = ~~(Date.now() / 1000);
|
||||
}
|
||||
|
||||
//keep it in the ring!
|
||||
time = parseInt(time, 10) % 0xffffffff;
|
||||
|
||||
var inc = next();
|
||||
|
||||
return String.fromCharCode(
|
||||
(time >> 24) & 0xff,
|
||||
(time >> 16) & 0xff,
|
||||
(time >> 8) & 0xff,
|
||||
time & 0xff,
|
||||
(MACHINE_ID >> 16) & 0xff,
|
||||
(MACHINE_ID >> 8) & 0xff,
|
||||
MACHINE_ID & 0xff,
|
||||
(pid >> 8) & 0xff,
|
||||
pid & 0xff,
|
||||
(inc >> 16) & 0xff,
|
||||
(inc >> 8) & 0xff,
|
||||
inc & 0xff
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function next() {
|
||||
return (index = (index + 1) % 0xffffff);
|
||||
}
|
||||
|
||||
function hex(length, n) {
|
||||
n = n.toString(16);
|
||||
return n.length === length ? n : "00000000".substring(n.length, length) + n;
|
||||
}
|
||||
|
||||
// function buffer(str) {
|
||||
// var i = 0,
|
||||
// out = [];
|
||||
|
||||
// if (str.length === 24)
|
||||
// for (; i < 24; out.push(parseInt(str[i] + str[i + 1], 16)), i += 2);
|
||||
// else if (str.length === 12) for (; i < 12; out.push(str.charCodeAt(i)), i++);
|
||||
|
||||
// return out;
|
||||
// }
|
||||
|
||||
var inspect =
|
||||
(Symbol && Symbol.for && Symbol.for("nodejs.util.inspect.custom")) ||
|
||||
"inspect";
|
||||
|
||||
/**
|
||||
* Converts to a string representation of this Id.
|
||||
*
|
||||
* @return {String} return the 24 byte hex string representation.
|
||||
* @api private
|
||||
*/
|
||||
ObjectID.prototype[inspect] = function () {
|
||||
return "ObjectID(" + this + ")";
|
||||
};
|
||||
ObjectID.prototype.toJSON = ObjectID.prototype.toHexString;
|
||||
ObjectID.prototype.toString = ObjectID.prototype.toHexString;
|
||||
42
packages/core/vitest.config.ts
Normal file
42
packages/core/vitest.config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
/*
|
||||
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 { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {}
|
||||
});
|
||||
Reference in New Issue
Block a user