Revert "feat: impl fully indexed & searchable data stores"

This reverts commit 56297da18b.
This commit is contained in:
thecodrr
2020-11-16 15:00:52 +05:00
parent fd0d84b3ab
commit c81c0feda8
12 changed files with 152 additions and 292 deletions

View File

@@ -2,7 +2,7 @@ import {
StorageInterface, StorageInterface,
notebookTest, notebookTest,
TEST_NOTEBOOK, TEST_NOTEBOOK,
TEST_NOTE, TEST_NOTE
} from "./utils"; } from "./utils";
beforeEach(async () => { beforeEach(async () => {
@@ -26,7 +26,7 @@ test("search all notebooks", () =>
notebookTest({ notebookTest({
...TEST_NOTEBOOK, ...TEST_NOTEBOOK,
title: "I will be searched.", title: "I will be searched.",
description: "searched description", description: "searched description"
}).then(({ db }) => { }).then(({ db }) => {
let filtered = db.notebooks.filter("searhed"); let filtered = db.notebooks.filter("searhed");
expect(filtered.length).toBeGreaterThan(0); expect(filtered.length).toBeGreaterThan(0);
@@ -54,10 +54,16 @@ test("unpin a notebook", () =>
test("delete a notebook", () => test("delete a notebook", () =>
notebookTest().then(async ({ db, id }) => { notebookTest().then(async ({ db, id }) => {
let noteId = await db.notes.add(TEST_NOTE); let noteId = await db.notes.add(TEST_NOTE);
await db.notebooks.notebook(id).topics.topic("General").add(noteId); await db.notebooks
expect(db.notebooks.notebook(id).topics.topic("General").has(noteId)).toBe( .notebook(id)
true .topics.topic("General")
); .add(noteId);
expect(
db.notebooks
.notebook(id)
.topics.topic("General")
.has(noteId)
).toBe(true);
let note = db.notes.note(noteId); let note = db.notes.note(noteId);
expect(note.notebook.id).toBe(id); expect(note.notebook.id).toBe(id);
await db.notebooks.delete(id); await db.notebooks.delete(id);

View File

@@ -79,12 +79,13 @@ test("delete a topic", () =>
})); }));
test("delete note from edited topic", () => test("delete note from edited topic", () =>
notebookTest().then(async ({ db, id }) => { notebookTest().then(async ({ id }) =>
const noteId = await db.notes.add(TEST_NOTE); noteTest().then(async ({ db, id: noteId }) => {
let topics = db.notebooks.notebook(id).topics; let topics = db.notebooks.notebook(id).topics;
await topics.add("Home"); await topics.add("Home");
let topic = topics.topic("Home"); let topic = topics.topic("Home");
await db.notes.move({ id, topic: topic._topic.title }, noteId); await db.notes.move({ id, topic: topic._topic.title }, noteId);
await topics.add({ id: topic._topic.id, title: "Hello22" }); await topics.add({ id: topic._topic.id, title: "Hello22" });
await db.notes.delete(noteId); await db.notes.delete(noteId);
})); })
));

View File

@@ -1,5 +1,4 @@
import fuzzysearch from "fuzzysearch"; import fuzzysearch from "fuzzysearch";
import sm from "../utils/set";
var tfun = require("transfun/transfun.js").tfun; var tfun = require("transfun/transfun.js").tfun;
if (!tfun) { if (!tfun) {
tfun = global.tfun; tfun = global.tfun;
@@ -15,22 +14,23 @@ export default class Lookup {
} }
notes(notes, query) { notes(notes, query) {
return sm.union( let contentIds = this._db.content._collection.search.searchDocs(query);
this._db.content._collection.search.search(query, { let noteIds = this._db.notes._collection.search.searchDocs(query);
map: (elem) => { return notes.filter((note) => {
return notes.find((note) => note.contentId === elem); return (
}, contentIds.findIndex((content) => note.id === content.noteId) > -1 ||
}), noteIds.findIndex((n) => n.id === note.id) > -1
this._db.notes._collection.search.search(query)
); );
});
} }
notebooks(array, query) { notebooks(array, query) {
return this._db.notebooks._collection.search.search(query, { const notebooksIds = this._db.notebooks._collection.search.searchDocs(
map: (elem) => { query
return array.find((nb) => nb.id === elem); );
}, return tfun.filter(
}); (nb) => notebooksIds.findIndex((notebook) => notebook.id === nb.id) > -1
)(array);
} }
topics(array, query) { topics(array, query) {

View File

@@ -44,7 +44,11 @@ export default class Content extends Collection {
return this._collection.removeItem(id); return this._collection.removeItem(id);
} }
multi(ids) {
return this._collection.getItems(ids);
}
all() { all() {
return this._collection.getItems(); return this._collection.getItems(this._collection.indexer.indices);
} }
} }

View File

@@ -41,6 +41,7 @@ export default class Notebooks extends Collection {
description: notebook.description, description: notebook.description,
dateCreated: notebook.dateCreated, dateCreated: notebook.dateCreated,
pinned: !!notebook.pinned, pinned: !!notebook.pinned,
favorite: !!notebook.favorite,
topics: notebook.topics || [], topics: notebook.topics || [],
totalNotes: 0, totalNotes: 0,
}; };

View File

@@ -34,23 +34,26 @@ export default class Backup {
(key) => !invalidKeys.some((t) => t === key) (key) => !invalidKeys.some((t) => t === key)
); );
let data = Object.fromEntries(await this._db.context.readMulti(keys)); const db = Object.fromEntries(await this._db.context.readMulti(keys));
db.h = md5.hex(JSON.stringify(db));
db.ht = "md5";
if (encrypt) { if (encrypt) {
const key = await this._db.user.key(); const key = await this._db.user.key();
data = await this._db.context.encrypt(key, JSON.stringify(data)); return JSON.stringify({
type,
date: Date.now(),
data: await this._db.context.encrypt(key, JSON.stringify(db)),
});
} }
// save backup time // save backup time
await this._db.context.write("lastBackupTime", Date.now()); await this._db.context.write("lastBackupTime", Date.now());
return JSON.stringify({ return JSON.stringify({
version: 2,
type, type,
date: Date.now(), date: Date.now(),
data, data: db,
hash: md5.hex(JSON.stringify(data)),
hash_type: "md5",
}); });
} }
@@ -72,26 +75,16 @@ export default class Backup {
db = JSON.parse(await this._db.context.decrypt(key, db)); db = JSON.parse(await this._db.context.decrypt(key, db));
} }
if (!this._verify(backup)) if (!this._verify(db))
throw new Error("Backup file has been tempered, aborting..."); throw new Error("Backup file has been tempered, aborting...");
// TODO add a proper restoration system.
// for (let key in db) {
// let value = db[key];
// if (value && value.dateEdited) {
// value.dateEdited = Date.now();
// }
// const oldValue = await this._db.context.read(oldValue); for (let key in db) {
let value = db[key];
// let finalValue = oldValue || value; if (value && value.dateEdited) {
// if (typeof value === "object") { value.dateEdited = Date.now();
// finalValue = Array.isArray(value) }
// ? [...value, ...oldValue] await this._db.context.write(key, value);
// : { ...value, ...oldValue }; }
// }
// await this._db.context.write(key, finalValue);
// }
} }
_validate(backup) { _validate(backup) {
@@ -103,11 +96,14 @@ export default class Backup {
); );
} }
_verify(backup) { _verify(db) {
const { hash, hash_type, data: db } = backup; const hash = db.h;
const hash_type = db.ht;
delete db.h;
delete db.ht;
switch (hash_type) { switch (hash_type) {
case "md5": { case "md5": {
return hash === md5.hex(JSON.stringify(db)); return hash == md5.hex(JSON.stringify(db));
} }
default: { default: {
return false; return false;

View File

@@ -1,36 +1,45 @@
import Storage from "./storage"; import Indexer from "./indexer";
import HyperSearch from "hypersearch";
import { getSchema } from "./schemas";
import PersistentCachedMap from "./persistentcachedmap";
import sort from "fast-sort"; import sort from "fast-sort";
import { EV } from "../common";
import IndexedCollection from "./indexed-collection"; import IndexedCollection from "./indexed-collection";
export default class CachedCollection extends IndexedCollection { export default class CachedCollection extends IndexedCollection {
constructor(context, type) {
super(context, type);
}
async init() { async init() {
const index = new PersistentCachedMap(`${this.type}Index`, this.storage); await super.init();
const store = new PersistentCachedMap(`${this.type}Store`, this.storage); const data = await this.indexer.readMulti(this.indexer.indices);
await index.init(); this.map = new Map(data);
await store.init(); }
this.search = new HyperSearch({
schema: getSchema(this.type), async clear() {
tokenizer: "forward", await super.clear();
index, this.map.clear();
store, }
});
async updateItem(item, index = true) {
await super.updateItem(item, index);
this.map.set(item.id, item);
EV.publish("db:write", item);
} }
exists(id) { exists(id) {
const item = this.getItem(id); return this.map.has(id) && !this.map.get(id).deleted;
return item && !item.deleted; }
getItem(id) {
return this.map.get(id);
} }
getRaw() { getRaw() {
return Array.from(this.search.getAllDocs()); return Array.from(this.map.values());
} }
getItems(sortFn = (u) => u.dateCreated) { getItems(sortFn = (u) => u.dateCreated) {
let items = []; let items = [];
this.search.options.store.forEach((value) => { this.map.forEach((value) => {
if (!value || value.deleted) return; if (!value || value.deleted) return;
items[items.length] = value; items[items.length] = value;
}); });

View File

@@ -1,78 +1,77 @@
import Storage from "./storage"; import Indexer from "./indexer";
import HyperSearch from "hypersearch"; import HyperSearch from "hypersearch";
import { getSchema } from "./schemas"; import { getSchema } from "./schemas";
import PersistentCachedMap from "./persistentcachedmap";
import PersistentMap from "./persistentmap";
import sort from "fast-sort";
export default class IndexedCollection { export default class IndexedCollection {
/** constructor(context, type) {
* this.indexer = new Indexer(context, type);
* @param {Storage} storage
* @param {string} type
*/
constructor(storage, type) {
this.type = type; this.type = type;
this.storage = storage;
}
clear() {
return this.search.clear();
}
async init() {
const index = new PersistentCachedMap(`${this.type}Index`, this.storage);
const store = new PersistentMap(`${this.type}Store`, this.storage);
await index.init();
await store.init();
this.search = new HyperSearch({ this.search = new HyperSearch({
schema: getSchema(this.type), schema: getSchema(type),
tokenizer: "forward", tokenizer: "forward",
index,
store,
}); });
} }
async addItem(item) { clear() {
const exists = this.exists(item.id); return this.indexer.clear();
if (!exists) item.dateCreated = item.dateCreated || Date.now();
await this._upsertItem(item);
} }
/** async init() {
* @protected await this.indexer.init();
*/ const index = await this.indexer.read(`${this.type}-index`);
async _upsertItem(item) { if (index) this.search.import(index);
}
async addItem(item) {
const exists = await this.exists(item.id);
if (!exists) item.dateCreated = item.dateCreated || Date.now();
await this.updateItem(item);
if (!exists) {
await this.indexer.index(item.id);
}
}
async updateItem(item, index = true) {
if (!item.id) throw new Error("The item must contain the id field."); if (!item.id) throw new Error("The item must contain the id field.");
// if item is newly synced, remote will be true. // if item is newly synced, remote will be true.
item.dateEdited = item.remote ? item.dateEdited : Date.now(); item.dateEdited = item.remote ? item.dateEdited : Date.now();
// the item has become local now, so remove the flag. // the item has become local now, so remove the flag.
delete item.remote; delete item.remote;
const exists = this.exists(item.id); await this.indexer.write(item.id, item);
if (!exists) await this.search.addDoc(item);
else await this.search.updateDoc(item.id, item); if (index && (this.type === "notes" || this.type === "notebooks")) {
this.search.addDoc(item);
this.indexer.write(`${this.type}-index`, this.search.export());
}
} }
async removeItem(id) { async removeItem(id) {
await this.search.remove(id); await this.updateItem(
await this._upsertItem({ {
id, id,
deleted: true, deleted: true,
dateCreated: Date.now(), dateCreated: Date.now(),
dateEdited: Date.now(), dateEdited: Date.now(),
}); },
false
);
if (this.type === "notes" || this.type === "notebooks")
this.search.remove(id);
} }
async exists(id) { exists(id) {
const item = await this.getItem(id); return this.indexer.exists(id);
return item && !item.deleted;
} }
getItem(id) { getItem(id) {
return this.search.getById(id); return this.indexer.read(id);
} }
async getItems() { async getItems(indices) {
return await this.search.getAllDocs(); const data = await this.indexer.readMulti(indices);
return data.reduce((total, current) => {
total.push(current[1]);
return total;
}, []);
} }
} }

View File

@@ -11,7 +11,7 @@ export default class Indexer extends Storage {
this.indices = (await this.read(this.type, true)) || []; this.indices = (await this.read(this.type, true)) || [];
} }
exists(key) { async exists(key) {
return this.indices.includes(key); return this.indices.includes(key);
} }
@@ -30,10 +30,7 @@ export default class Indexer extends Storage {
} }
async clear() { async clear() {
for (var i = 0; i < this.indices.length; ++i) {
let key = this.indices[i];
await this.remove(key);
}
this.indices = []; this.indices = [];
await super.clear();
} }
} }

View File

@@ -1,53 +0,0 @@
import Storage from "./storage";
import Indexer from "./indexer";
export default class PersistentCachedMap {
/**
*
* @param {string} key
* @param {Storage} storage
*/
constructor(key, storage) {
this.key = key;
this.indexer = new Indexer(storage, key);
}
async init() {
await this.indexer.init();
const data = await this.indexer.readMulti(this.indexer.indices);
this.map = new Map(data);
}
async set(key, value) {
this.map.set(key, value);
await this.indexer.write(key, value);
await this.indexer.index(key);
}
async delete(key) {
await this.indexer.remove(key);
await this.indexer.deindex(key);
return this.map.delete(key);
}
get(key) {
return this.map.get(key);
}
has(key) {
return this.map.has(key);
}
async clear() {
await this.indexer.clear();
this.map.clear();
}
values() {
return this.map.values();
}
forEach(callbackFn) {
return this.map.forEach(callbackFn);
}
}

View File

@@ -1,48 +0,0 @@
import Indexer from "./indexer";
import Storage from "./storage";
export default class PersistentMap {
/**
*
* @param {string} key
* @param {Storage} storage
*/
constructor(key, storage) {
this.key = key;
this.indexer = new Indexer(storage, key);
}
init() {
return this.indexer.init();
}
async set(key, value) {
await this.indexer.write(key, value);
await this.indexer.index(key);
}
async delete(key) {
await this.indexer.remove(key);
await this.indexer.deindex(key);
}
get(key) {
return this.indexer.read(key);
}
has(key) {
return this.indexer.exists(key);
}
async clear() {
await this.indexer.clear();
}
async values() {
const data = await this.indexer.readMulti(this.indexer.indices);
return data.reduce((total, current) => {
total.push(current[1]);
return total;
}, []);
}
}

View File

@@ -1,46 +1,22 @@
const { getContentFromData } = require("../contenttypes"); const { getContentFromData } = require("../contenttypes");
const { qclone } = require("qclone");
const onlyStore = { store: true, index: false };
const indexAndStore = { store: true, index: true };
const asId = { asId: true };
const basicItem = {
id: asId,
deleted: onlyStore,
dateCreated: onlyStore,
dateEdited: onlyStore,
type: onlyStore,
};
const schemas = { const schemas = {
content: { content: {
...basicItem, id: { asId: true, store: false },
noteId: onlyStore, noteId: { store: true, index: false },
conflicted: onlyStore,
resolved: onlyStore,
data: { data: {
resolve: (doc) => { resolve: (doc) => {
if (!doc.data || doc.data.iv) return ""; if (doc.data.iv) return "";
const content = getContentFromData(doc.type, doc.data); const content = getContentFromData(doc.type, doc.data);
return content._text; return content._text;
}, },
store: true, store: false,
index: true, index: true,
}, },
}, },
notes: { notes: {
...basicItem, id: { asId: true, store: false },
locked: onlyStore, title: true,
colors: onlyStore,
tags: onlyStore,
conflicted: onlyStore,
contentId: onlyStore,
pinned: onlyStore,
favorite: onlyStore,
title: indexAndStore,
headline: onlyStore,
notebook: onlyStore,
// pinned: { // pinned: {
// resolve: (doc) => (doc.pinned ? "pinned:true" : "pinned:false"), // resolve: (doc) => (doc.pinned ? "pinned:true" : "pinned:false"),
// index: true, // index: true,
@@ -49,46 +25,18 @@ const schemas = {
// }, // },
}, },
notebooks: { notebooks: {
...basicItem, id: { asId: true, store: false },
title: indexAndStore, title: true,
description: indexAndStore, description: true,
totalNotes: onlyStore,
pinned: onlyStore,
topics: { topics: {
resolve: (doc) => { resolve: (doc) => {
if (!doc.topics) return ""; if (!doc.topics) return "";
return doc.topics.map((v) => v.title || v).join(" "); return doc.topics.map((v) => v.title || v).join(" ");
}, },
...indexAndStore,
}, },
}, },
colors: {
...basicItem,
merge: ["tags"],
},
tags: {
...basicItem,
noteIds: onlyStore,
deletedIds: onlyStore,
title: indexAndStore,
},
trash: {
id: asId,
dateDeleted: onlyStore,
itemType: onlyStore,
itemId: onlyStore,
merge: ["notes", "notebooks"],
},
}; };
export function getSchema(type) { export function getSchema(type) {
let schema = qclone(schemas[type]); return schemas[type];
if (schema.merge) {
const mergeSchemas = schema.merge;
delete schema.merge;
mergeSchemas.forEach((key) => {
schema = { ...schema, ...schemas[key] };
});
}
return schema;
} }