Files
notesnook/packages/core/api/database.js

649 lines
17 KiB
JavaScript
Raw Normal View History

import Storage from "../helpers/storage";
2019-11-29 21:28:31 +05:00
import fuzzysearch from "fuzzysearch";
var tfun = require("transfun/transfun.js").tfun;
2020-01-06 14:48:51 +05:00
if (!tfun) {
tfun = global.tfun;
}
2019-12-12 11:49:06 +05:00
import { extractValues, groupBy } from "../utils";
2019-12-14 16:13:00 +05:00
import {
getWeekGroupFromTimestamp,
months,
getLastWeekTimestamp
} from "../utils/date";
import setManipulator from "../utils/set";
2019-11-27 21:58:41 +05:00
const KEYS = {
2019-12-04 15:08:01 +05:00
notes: "notes",
2019-12-12 12:55:19 +05:00
notebooks: "notebooks",
2019-12-21 18:25:07 +05:00
trash: "trash",
2020-01-20 17:27:18 +05:00
tags: "tags",
user: "user"
2019-11-27 21:58:41 +05:00
};
2019-12-12 11:49:06 +05:00
const TYPES = {
note: "note"
};
function checkInitialized() {
if (!this.isInitialized) {
throw new Error(
"Database is not initialized. Make sure to call await init() on startup."
);
}
}
2019-12-12 11:49:06 +05:00
class Database {
constructor(storage) {
this.storage = new Storage(storage);
this.notes = {};
2019-12-04 15:08:01 +05:00
this.notebooks = {};
2019-12-12 12:55:19 +05:00
this.trash = {};
2020-01-20 17:27:18 +05:00
this.tags = {};
this.user = {};
this.isInitialized = false;
}
2019-12-04 17:16:21 +05:00
init() {
return new Promise((resolve, reject) => {
for (let key of extractValues(KEYS)) {
this.storage.read(key).then(data => {
this[key] = data || {};
2020-01-20 17:27:18 +05:00
if (key === KEYS.user) {
this.isInitialized = true;
//TODO use index here
resolve(true);
}
});
}
});
}
getNotes() {
checkInitialized.call(this);
2020-01-03 16:13:19 +05:00
return extractValues(this.notes).reverse();
}
2019-12-17 17:22:25 +05:00
getFavorites() {
2020-02-01 15:23:32 +05:00
return tfun.filter(".favorite === true")([
...this.getNotes(),
...this.getNotebooks()
]);
2019-12-17 17:22:25 +05:00
}
2020-01-03 15:24:54 +05:00
getPinned() {
2020-02-01 15:23:32 +05:00
return tfun.filter(".pinned === true")(this.getNotes());
2020-01-03 15:24:54 +05:00
}
2020-02-01 14:45:20 +05:00
getTag(tag) {
return tfun.filter(`.tags.includes('${tag}')`)(this.getNotes());
}
2019-12-12 11:49:06 +05:00
/**
* @param {string} by One from 'abc', 'month', 'year' or 'week'. Leave it empty for default grouping.
2019-12-18 15:40:49 +05:00
* @param {boolean} special Should only be used in the React app.
2019-12-12 11:49:06 +05:00
*/
2019-12-18 15:40:49 +05:00
groupNotes(by, special = false) {
2020-01-06 16:17:23 +05:00
let notes = !special
2020-02-01 15:23:32 +05:00
? tfun.filter(".pinned === false")(this.getNotes())
2020-01-06 16:17:23 +05:00
: this.getNotes();
2019-12-12 11:49:06 +05:00
switch (by) {
case "abc":
2019-12-18 16:46:54 +05:00
return groupBy(
notes.sort((a, b) => a.title.localeCompare(b.title)),
note => note.title[0].toUpperCase(),
special
);
2019-12-12 11:49:06 +05:00
case "month":
return groupBy(
notes,
2019-12-18 15:40:49 +05:00
note => months[new Date(note.dateCreated).getMonth()],
special
2019-12-12 11:49:06 +05:00
);
case "week":
2019-12-18 15:40:49 +05:00
return groupBy(
notes,
note => getWeekGroupFromTimestamp(note.dateCreated),
special
2019-12-12 11:49:40 +05:00
);
2019-12-12 11:49:06 +05:00
case "year":
2019-12-12 11:49:40 +05:00
return groupBy(
2019-12-12 11:49:06 +05:00
notes,
2019-12-31 17:02:29 +05:00
note => new Date(note.dateCreated).getFullYear().toString(),
2019-12-18 15:40:49 +05:00
special
2019-12-12 11:49:06 +05:00
);
default:
let timestamps = {
recent: getLastWeekTimestamp(7),
lastWeek: getLastWeekTimestamp(7) - 604800000 //seven day timestamp value
};
2019-12-18 15:40:49 +05:00
return groupBy(
notes,
note =>
note.dateCreated >= timestamps.recent
? "Recent"
: note.dateCreated >= timestamps.lastWeek
? "Last week"
: "Older",
special
2019-12-12 11:49:06 +05:00
);
}
}
async addNote(n) {
if (!n) return;
2020-01-13 16:42:41 +05:00
let timestamp = n.dateCreated || Date.now();
let oldNote = this.notes[timestamp];
let note = {
...oldNote,
...n
};
2020-01-28 17:08:53 +05:00
if (isNoteEmpty(note)) {
if (oldNote) await this.deleteNotes(note);
2020-01-13 16:42:41 +05:00
return;
}
note = {
type: TYPES.note,
title: getNoteTitle(note),
content: getNoteContent(note),
pinned: !!note.pinned,
locked: !!note.locked,
notebook: note.notebook || {},
colors: note.colors || [],
2020-02-02 14:36:47 +05:00
tags: note.tags || [],
favorite: !!note.favorite,
headline: getNoteHeadline(note),
dateEditted: Date.now(),
dateCreated: timestamp
};
if (oldNote) {
note.colors = setManipulator.union(oldNote.colors, note.colors);
} else {
2020-02-02 14:36:47 +05:00
await addTags.call(this, note.tags);
}
this.notes[timestamp] = note;
2019-11-27 21:58:41 +05:00
await this.storage.write(KEYS.notes, this.notes);
return timestamp;
}
2020-02-02 14:36:47 +05:00
async addTag(noteId, tag) {
if (!this.notes[noteId])
throw new Error("Couldn't add tag. This note doesn't exist.");
this.notes[noteId].tags.push(tag);
await addTags.call(this, [tag]);
await this.storage.write(KEYS.notes, this.notes);
2019-12-21 18:27:41 +05:00
}
2020-02-02 14:36:47 +05:00
async removeTag(noteId, tag) {
if (!this.notes[noteId])
throw new Error("Couldn't remove tag. This note doesn't exist.");
let tags = this.notes[noteId].tags;
if (tags.indexOf(tag) <= -1)
throw new Error("This note is not tagged by the specified tag.");
this.notes[noteId].tags.splice(tags.indexOf(tag), 1);
await removeTags.call(this, [tag]);
await this.storage.write(KEYS.notes, this.notes);
}
pinNote(id) {
return editItems.call(this, "note", "pinned", id);
2020-02-02 14:36:47 +05:00
}
favoriteNotes(...ids) {
return editItems.call(this, "note", "favorite", ids);
2019-12-21 18:25:07 +05:00
}
2020-01-31 01:39:56 +05:00
async deleteNotes(...noteIds) {
return await deleteItems.call(this, noteIds, KEYS.notes);
}
getNote(id) {
2019-12-05 15:58:11 +05:00
return getItem.call(this, id, KEYS.notes);
}
2020-02-01 15:24:31 +05:00
searchNotes(query, notes = null) {
if (!query) return [];
2020-02-01 15:24:31 +05:00
if (!notes) notes = this.getNotes();
return tfun.filter(v => fuzzysearch(query, v.title + " " + v.content.text))(
2020-02-01 15:24:31 +05:00
notes
2019-11-29 21:28:31 +05:00
);
}
2019-12-31 17:02:29 +05:00
async lockNote(noteId, password) {
if (!this.notes[noteId]) {
throw new Error(`Cannot lock note. Invalid ID: ${noteId} given.`);
}
this.notes[noteId].content = await this.storage.encrypt(
2019-12-31 17:02:29 +05:00
password,
JSON.stringify(this.notes[noteId].content)
2019-12-31 17:02:29 +05:00
);
this.notes[noteId].locked = true;
await this.storage.write(KEYS.notes, this.notes);
return true;
2019-12-12 12:55:19 +05:00
}
2019-12-31 17:02:29 +05:00
async unlockNote(noteId, password, perm = false) {
if (!this.notes[noteId]) {
throw new Error(`Cannot unlock note. Invalid ID: ${noteId} given.`);
}
let decrypted = await this.storage.decrypt(
password,
this.notes[noteId].content
);
2019-12-31 17:02:29 +05:00
if (perm) {
this.notes[noteId].locked = false;
this.notes[noteId].content = JSON.parse(decrypted);
2019-12-31 17:02:29 +05:00
await this.storage.write(KEYS.notes, this.notes);
}
return { ...this.notes[noteId], content: JSON.parse(decrypted) };
2019-12-12 12:55:19 +05:00
}
getNotebooks() {
checkInitialized.call(this);
2019-12-04 15:08:01 +05:00
return extractValues(this.notebooks);
}
2019-12-04 17:16:21 +05:00
2020-02-01 15:23:32 +05:00
searchNotebooks(query) {
if (!query) return [];
return tfun.filter(v => fuzzysearch(query, v.title + " " + v.description))(
this.getNotebooks()
);
}
2019-12-04 15:08:01 +05:00
async addNotebook(notebook) {
2019-12-07 13:33:05 +05:00
if (!notebook || !notebook.title) {
return;
}
if (
extractValues(this.notebooks).findIndex(
2019-12-21 18:30:02 +05:00
nb =>
nb.title === notebook.title && nb.dateCreated !== notebook.dateCreated
2019-12-07 13:33:05 +05:00
) > -1
) {
return;
}
2019-12-04 15:08:01 +05:00
const id = notebook.dateCreated || Date.now();
2019-12-31 17:02:29 +05:00
let topics =
!notebook.topics || notebook.topics.length <= 0 ? [] : notebook.topics; //
if (topics.findIndex(topic => topic.title === "General") <= -1) {
topics.splice(0, 0, makeTopic("General", id));
2019-12-31 17:02:29 +05:00
}
let index = 0;
for (let topic of topics) {
2019-12-07 13:33:05 +05:00
if (
!topic ||
2019-12-31 17:02:29 +05:00
topics.findIndex(t => t.title === (topic || topic.title)) > -1 //check for duplicate
) {
topics.splice(index, 1);
2019-12-07 13:33:05 +05:00
continue;
2019-12-31 17:02:29 +05:00
}
if (typeof topic === "string") {
if (topic.trim().length <= 0) topics.splice(index, 1);
topics[index] = makeTopic(topic, id);
2019-12-31 17:02:29 +05:00
}
index++;
2019-12-04 15:08:01 +05:00
}
2019-12-07 13:33:05 +05:00
2019-12-04 15:08:01 +05:00
this.notebooks[id] = {
2019-12-06 16:53:57 +05:00
type: "notebook",
2019-12-04 15:08:01 +05:00
title: notebook.title,
description: notebook.description,
dateCreated: id,
2019-12-04 15:08:01 +05:00
pinned: notebook.pinned || false,
favorite: notebook.favorite || false,
topics,
totalNotes: 0,
tags: [],
colors: []
};
await this.storage.write(KEYS.notebooks, this.notebooks);
return id;
}
2020-02-02 14:36:47 +05:00
pinNotebook(id) {
return editItems.call(this, "notebook", "pinned", id);
2020-02-02 14:36:47 +05:00
}
favoriteNotebooks(...ids) {
return editItems.call(this, "notebook", "favorite", ids);
2020-02-02 14:36:47 +05:00
}
2019-12-04 17:16:21 +05:00
addTopicToNotebook(notebookId, topic) {
2019-12-07 13:33:05 +05:00
return notebookTopicFn.call(this, notebookId, topic, notebook => {
2020-01-31 01:39:56 +05:00
if (notebook.topics.findIndex(t => t.title === topic) > -1)
return Promise.resolve(false); //check for duplicates
notebook.topics[notebook.topics.length] = makeTopic(topic, notebookId);
2020-01-31 01:39:56 +05:00
return Promise.resolve(true);
2019-12-07 13:33:05 +05:00
});
2019-12-04 17:16:21 +05:00
}
deleteTopicFromNotebook(notebookId, topic) {
2019-12-05 15:58:11 +05:00
return notebookTopicFn.call(this, notebookId, topic, notebook => {
let topicIndex = notebook.topics.findIndex(t => t.title === topic);
2020-01-31 01:39:56 +05:00
if (topicIndex === -1) return Promise.resolve(false);
notebook.topics.splice(topicIndex, 1);
2020-01-31 01:39:56 +05:00
return Promise.resolve(true);
});
2019-12-04 17:16:21 +05:00
}
addNoteToTopic(notebookId, topic, noteId) {
return topicNoteFn.call(
this,
notebookId,
topic,
noteId,
2019-12-07 13:33:05 +05:00
(notebook, topicIndex) => {
2020-01-31 01:39:56 +05:00
if (notebook.topics[topicIndex].notes.includes(noteId)) {
return Promise.resolve(false); //duplicate check
}
notebook.topics[topicIndex].notes.push(noteId);
//increment totalNotes count
notebook.topics[topicIndex].totalNotes++;
notebook.totalNotes++;
//add notebook to the note
this.notes[noteId].notebook = {
2020-01-31 01:39:56 +05:00
id: notebookId,
topic: notebook.topics[topicIndex].title
};
2020-01-31 01:39:56 +05:00
return Promise.resolve(true);
}
);
2019-12-04 17:16:21 +05:00
}
deleteNoteFromTopic(notebookId, topic, noteId) {
return topicNoteFn.call(
this,
notebookId,
topic,
noteId,
2020-01-31 01:39:56 +05:00
(notebook, topicIndex) => {
let index = notebook.topics[topicIndex].notes.indexOf(noteId);
2020-01-31 01:39:56 +05:00
if (index <= -1) return Promise.resolve(false);
notebook.topics[topicIndex].notes.splice(index, 1);
//delete notebook from note
this.notes[noteId].notebook = {};
//decrement totalNotes count
2020-01-06 18:07:17 +05:00
if (notebook.topics[topicIndex].totalNotes > 0)
notebook.topics[topicIndex].totalNotes--;
if (notebook.totalNotes > 0) notebook.totalNotes--;
2020-01-31 01:39:56 +05:00
return Promise.resolve(true);
}
);
2019-12-04 17:16:21 +05:00
}
getTopic(notebookId, topic) {
if (!notebookId || !topic || !this.notebooks[notebookId]) return;
let notebook = this.notebooks[notebookId];
let topicIndex = notebook.topics.findIndex(t => t.title === topic);
if (topicIndex === -1) return;
let nbTopic = notebook.topics[topicIndex];
if (nbTopic.notes.length <= 0) return [];
return nbTopic.notes.map(note => this.getNote(note));
2019-12-04 17:16:21 +05:00
}
getNotebook(id) {
2019-12-05 15:58:11 +05:00
return getItem.call(this, id, KEYS.notebooks);
2019-12-04 17:16:21 +05:00
}
2020-01-31 01:39:56 +05:00
async deleteNotebooks(...notebookIds) {
return await deleteItems.call(this, notebookIds, KEYS.notebooks);
2019-12-07 13:33:05 +05:00
}
async moveNotes(from, to, ...noteIds) {
for (let noteId of noteIds) {
if (!noteId || !to || !to.id || !to.topic) {
throw new Error(`Error: Failed to move note.`);
}
if (!from.id && !from.topic) {
await this.addNoteToTopic(to.id, to.topic, noteId);
} else {
if (from.id === to.id && from.topic === to.topic) {
throw new Error(
"Moving to the same notebook and topic is not possible."
);
}
2020-01-31 01:39:56 +05:00
if (await this.deleteNoteFromTopic(from.id, from.topic, noteId)) {
await this.addNoteToTopic(to.id, to.topic, noteId);
}
2020-01-31 01:39:56 +05:00
}
2019-12-07 13:33:05 +05:00
}
2019-12-04 17:16:21 +05:00
}
2019-12-12 12:55:19 +05:00
async restoreItem(id) {
if (!this.trash.hasOwnProperty(id)) {
2019-12-31 17:02:29 +05:00
throw new Error("Cannot restore: This item is not present in trash.");
2019-12-12 12:55:19 +05:00
}
let type = this.trash[id].dType;
delete this.trash[id].dateDeleted;
delete this.trash[id].dType;
let item = this.trash[id];
this[type][id] = item;
await this.storage.write(type, this[type]);
}
getTrash() {
2019-12-17 17:13:02 +05:00
checkInitialized.call(this);
2019-12-12 12:55:19 +05:00
return extractValues(this.trash).reverse();
}
2019-12-17 17:13:02 +05:00
async clearTrash() {
this[KEYS.trash] = {};
await this.storage.write(KEYS.trash, this[KEYS.trash]);
}
2020-01-20 17:27:18 +05:00
async createUser(user) {
this.user = { ...this.user, ...user };
await this.storage.write(KEYS.user, user);
}
getUser() {
return this.user;
}
2020-02-01 14:45:20 +05:00
getTags() {
return extractValues(this.tags);
}
2019-12-05 15:58:11 +05:00
}
2019-12-04 17:16:21 +05:00
2019-12-05 15:58:11 +05:00
export default Database;
2019-12-04 17:16:21 +05:00
2020-01-31 01:39:56 +05:00
async function deleteItems(ids, key) {
if (!ids || ids.length <= 0 || !this[key] || this[key].length <= 0) {
return false;
2020-01-28 17:08:53 +05:00
}
2020-01-31 01:39:56 +05:00
for (let id of ids) {
let item = key === KEYS.notes ? this.getNote(id) : this.getNotebook(id);
if (!id || !item) continue;
//delete note from the notebook too.
switch (item.type) {
case "note":
2020-01-02 17:00:50 +05:00
if (
item.notebook.hasOwnProperty("topic") &&
2020-01-02 17:00:50 +05:00
!(await this.deleteNoteFromTopic(
2020-01-31 01:39:56 +05:00
item.notebook.id,
2020-01-02 17:00:50 +05:00
item.notebook.topic,
item.dateCreated
))
) {
continue;
}
2020-01-31 01:39:56 +05:00
for (let tag of item.tags) {
this.tags[tag] = {
...this.tags[tag],
count: this.tags[tag].count - 1
};
}
2020-01-31 01:39:56 +05:00
break;
case "notebook":
2020-01-02 17:00:50 +05:00
let skip = false;
for (let topic in item.topics) {
for (let note in topic.notes) {
2020-01-02 17:00:50 +05:00
if (
!(await this.deleteNoteFromTopic(item.dateCreated, topic, note))
) {
skip = true;
break;
}
}
}
if (skip) continue;
2020-01-31 01:39:56 +05:00
break;
2019-12-04 17:16:21 +05:00
}
2020-01-31 01:39:56 +05:00
//put into trash
this[KEYS.trash][item.dateCreated] = this[key][item.dateCreated];
this[KEYS.trash][item.dateCreated]["dateDeleted"] = Date.now();
this[KEYS.trash][item.dateCreated]["dType"] = key;
delete this[key][item.dateCreated];
2019-12-04 17:16:21 +05:00
}
2019-12-12 12:55:19 +05:00
return this.storage
.write(key, this[key])
.then(s =>
this.storage.write(KEYS.trash, this[KEYS.trash]).then(s => true)
);
2019-12-05 15:58:11 +05:00
}
2019-12-04 17:16:21 +05:00
2019-12-05 15:58:11 +05:00
function notebookTopicFn(notebookId, topic, fn) {
2020-01-31 01:39:56 +05:00
if (!notebookId || !topic || !this.notebooks[notebookId])
return Promise.resolve(false);
2019-12-07 13:33:05 +05:00
const notebook = this.notebooks[notebookId];
2019-12-11 13:32:38 +05:00
2020-01-31 01:39:56 +05:00
return fn(notebook).then(res => {
if (res === true) {
this.notebooks[notebookId] = notebook;
return this.storage.write(KEYS.notebooks, this.notebooks).then(s => true);
}
return false;
});
}
2019-12-05 16:17:55 +05:00
function topicNoteFn(notebookId, topic, noteId, fn) {
2020-01-31 01:39:56 +05:00
return notebookTopicFn.call(this, notebookId, topic, notebook => {
let topicIndex = notebook.topics.findIndex(t => t.title === topic);
2020-01-31 01:39:56 +05:00
if (topicIndex === -1 || !this.notes.hasOwnProperty(noteId))
return Promise.resolve(false);
return fn(notebook, topicIndex).then(async res => {
if (res) {
return await this.storage.write(KEYS.notes, this.notes).then(s => true);
} else {
return Promise.resolve(false);
}
});
2019-12-05 16:17:55 +05:00
});
}
2019-12-05 15:58:11 +05:00
function getItem(id, key) {
checkInitialized.call(this);
2019-12-05 15:58:11 +05:00
if (this[key].hasOwnProperty(id)) {
return this[key][id];
}
}
function makeTopic(topic, notebookId) {
return {
2019-12-06 16:53:57 +05:00
type: "topic",
notebookId,
title: topic,
dateCreated: Date.now(),
totalNotes: 0,
notes: []
};
}
2019-12-21 18:27:41 +05:00
async function editItems(type, prop, ...ids) {
if (type === "note" || type === "notebook") {
for (let id of ids) {
2019-12-31 17:02:29 +05:00
let col = type == "note" ? this.notes : this.notebooks;
let func = type == "note" ? this.addNote : this.addNotebook;
2019-12-21 18:27:41 +05:00
if (col[id] === undefined) {
throw new Error(`Wrong ${type} id.`);
}
let state = col[id][prop];
let edit = { [prop]: !state };
2019-12-31 17:02:29 +05:00
await func.call(this, { ...col[id], ...edit });
}
} else {
throw new Error("Invalid type given to pinItem");
2019-12-21 18:27:41 +05:00
}
}
2019-12-21 18:38:18 +05:00
function mergeDedupe(arr) {
return [...new Set([].concat(...arr))];
}
function isNoteEmpty(note) {
return (
!note.content ||
!note.content.delta ||
(!note.locked &&
(!note.title || note.title.trim().length <= 0) &&
(!note.content.text || note.content.text.trim().length <= 0))
);
}
function getNoteHeadline(note) {
if (note.locked) return "";
return (
note.content.text.substring(0, 150) +
(note.content.text.length > 150 ? "..." : "")
);
}
function getNoteTitle(note) {
if (note.title && note.title.length > 0) return note.title.trim();
return note.content.text
.split(" ")
.slice(0, 3)
.join(" ")
.trim();
}
function getNoteContent(note) {
if (note.locked) {
return note.content;
}
return {
text: note.content.text.trim(),
delta: note.content.delta
};
}
2020-02-01 14:45:20 +05:00
2020-02-02 14:36:47 +05:00
async function addTags(tags) {
2020-02-01 14:45:20 +05:00
for (let tag of tags) {
if (!tag || tag.trim().length <= 0) continue;
let oldCount = this.tags[tag] ? this.tags[tag].count : 0;
this.tags[tag] = {
title: tag,
count: oldCount + 1
};
}
2020-02-02 14:36:47 +05:00
await this.storage.write(KEYS.tags, this.tags);
}
async function removeTags(tags) {
for (let tag of tags) {
if (!tag || tag.trim().length <= 0 || !this.tags[tag]) continue;
let oldCount = this.tags[tag].count;
if (oldCount <= 1) {
delete this.tags[tag];
} else {
this.tags[tag] = {
title: tag,
count: oldCount - 1
};
}
}
await this.storage.write(KEYS.tags, this.tags);
2020-02-01 14:45:20 +05:00
}