mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 06:59:31 +01:00
ci: add more tests and general fixes
This commit is contained in:
@@ -37,7 +37,7 @@ test("get delta of note", () =>
|
|||||||
|
|
||||||
test("delete note", () =>
|
test("delete note", () =>
|
||||||
noteTest().then(async ({ db, id }) => {
|
noteTest().then(async ({ db, id }) => {
|
||||||
let { id: notebookId } = await db.notebooks.add(TEST_NOTEBOOK);
|
let notebookId = await db.notebooks.add(TEST_NOTEBOOK);
|
||||||
let topics = db.notebooks.notebook(notebookId).topics;
|
let topics = db.notebooks.notebook(notebookId).topics;
|
||||||
let topic = topics.topic("General");
|
let topic = topics.topic("General");
|
||||||
await topic.add(id);
|
await topic.add(id);
|
||||||
@@ -160,7 +160,7 @@ test("favorite note", () =>
|
|||||||
|
|
||||||
test("add note to topic", () =>
|
test("add note to topic", () =>
|
||||||
noteTest().then(async ({ db, id }) => {
|
noteTest().then(async ({ db, id }) => {
|
||||||
let { id: notebookId } = await db.notebooks.add({ title: "Hello" });
|
let notebookId = await db.notebooks.add({ title: "Hello" });
|
||||||
let topics = db.notebooks.notebook(notebookId).topics;
|
let topics = db.notebooks.notebook(notebookId).topics;
|
||||||
await topics.add("Home");
|
await topics.add("Home");
|
||||||
let topic = topics.topic("Home");
|
let topic = topics.topic("Home");
|
||||||
@@ -175,7 +175,7 @@ test("add note to topic", () =>
|
|||||||
|
|
||||||
test("duplicate note to topic should not be added", () =>
|
test("duplicate note to topic should not be added", () =>
|
||||||
noteTest().then(async ({ db, id }) => {
|
noteTest().then(async ({ db, id }) => {
|
||||||
let { id: notebookId } = await db.notebooks.add({ title: "Hello" });
|
let notebookId = await db.notebooks.add({ title: "Hello" });
|
||||||
let topics = db.notebooks.notebook(notebookId).topics;
|
let topics = db.notebooks.notebook(notebookId).topics;
|
||||||
await topics.add("Home");
|
await topics.add("Home");
|
||||||
let topic = topics.topic("Home");
|
let topic = topics.topic("Home");
|
||||||
@@ -186,14 +186,14 @@ test("duplicate note to topic should not be added", () =>
|
|||||||
|
|
||||||
test("move note", (done) =>
|
test("move note", (done) =>
|
||||||
noteTest().then(async ({ db, id }) => {
|
noteTest().then(async ({ db, id }) => {
|
||||||
let { id: notebookId } = await db.notebooks.add({ title: "Hello" });
|
let notebookId = await db.notebooks.add({ title: "Hello" });
|
||||||
let topics = db.notebooks.notebook(notebookId).topics;
|
let topics = db.notebooks.notebook(notebookId).topics;
|
||||||
await topics.add("Home");
|
await topics.add("Home");
|
||||||
let topic = topics.topic("Home");
|
let topic = topics.topic("Home");
|
||||||
await topic.add(id);
|
await topic.add(id);
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
let { id: notebookId2 } = await db.notebooks.add({ title: "Hello2" });
|
let notebookId2 = await db.notebooks.add({ title: "Hello2" });
|
||||||
await db.notebooks.notebook(notebookId2).topics.add("Home2");
|
await db.notebooks.notebook(notebookId2).topics.add("Home2");
|
||||||
await db.notes.move({ id: notebookId2, topic: "Home2" }, id);
|
await db.notes.move({ id: notebookId2, topic: "Home2" }, id);
|
||||||
let note = db.notes.note(id);
|
let note = db.notes.note(id);
|
||||||
@@ -207,7 +207,7 @@ test("move note", (done) =>
|
|||||||
|
|
||||||
test("moving note to same notebook and topic should do nothing", () =>
|
test("moving note to same notebook and topic should do nothing", () =>
|
||||||
noteTest().then(async ({ db, id }) => {
|
noteTest().then(async ({ db, id }) => {
|
||||||
const { id: notebookId } = await db.notebooks.add({ title: "Hello" });
|
const notebookId = await db.notebooks.add({ title: "Hello" });
|
||||||
let topics = db.notebooks.notebook(notebookId).topics;
|
let topics = db.notebooks.notebook(notebookId).topics;
|
||||||
await topics.add("Home");
|
await topics.add("Home");
|
||||||
let topic = topics.topic("Home");
|
let topic = topics.topic("Home");
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
noteTest,
|
noteTest,
|
||||||
notebookTest,
|
notebookTest,
|
||||||
TEST_NOTE,
|
TEST_NOTE,
|
||||||
TEST_NOTEBOOK
|
TEST_NOTEBOOK,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
|
||||||
beforeEach(() => StorageInterface.clear());
|
beforeEach(() => StorageInterface.clear());
|
||||||
@@ -22,11 +22,8 @@ test("permanently delete a note", () =>
|
|||||||
|
|
||||||
test("restore a deleted note", () =>
|
test("restore a deleted note", () =>
|
||||||
noteTest().then(async ({ db, id }) => {
|
noteTest().then(async ({ db, id }) => {
|
||||||
let { id: nbId } = await db.notebooks.add(TEST_NOTEBOOK);
|
let nbId = await db.notebooks.add(TEST_NOTEBOOK);
|
||||||
await db.notebooks
|
await db.notebooks.notebook(nbId).topics.topic("General").add(id);
|
||||||
.notebook(nbId)
|
|
||||||
.topics.topic("General")
|
|
||||||
.add(id);
|
|
||||||
await db.notes.delete(id);
|
await db.notes.delete(id);
|
||||||
await db.trash.restore(db.trash.all[0].id);
|
await db.trash.restore(db.trash.all[0].id);
|
||||||
expect(db.trash.all.length).toBe(0);
|
expect(db.trash.all.length).toBe(0);
|
||||||
@@ -34,12 +31,9 @@ test("restore a deleted note", () =>
|
|||||||
expect(note).toBeDefined();
|
expect(note).toBeDefined();
|
||||||
expect(await note.text()).toBe(TEST_NOTE.content.text);
|
expect(await note.text()).toBe(TEST_NOTE.content.text);
|
||||||
expect(await note.delta()).toStrictEqual(TEST_NOTE.content.delta);
|
expect(await note.delta()).toStrictEqual(TEST_NOTE.content.delta);
|
||||||
expect(
|
expect(db.notebooks.notebook(nbId).topics.topic("General").has(id)).toBe(
|
||||||
db.notebooks
|
true
|
||||||
.notebook(nbId)
|
);
|
||||||
.topics.topic("General")
|
|
||||||
.has(id)
|
|
||||||
).toBe(true);
|
|
||||||
expect(db.notes.note(id).notebook.id).toBe(nbId);
|
expect(db.notes.note(id).notebook.id).toBe(nbId);
|
||||||
expect(db.notes.note(id).notebook.topic).toBe("General");
|
expect(db.notes.note(id).notebook.topic).toBe("General");
|
||||||
}));
|
}));
|
||||||
@@ -70,15 +64,12 @@ test("restore a deleted locked note", () =>
|
|||||||
|
|
||||||
test("restore a deleted note that's in a deleted notebook", () =>
|
test("restore a deleted note that's in a deleted notebook", () =>
|
||||||
noteTest().then(async ({ db, id }) => {
|
noteTest().then(async ({ db, id }) => {
|
||||||
let { id: nbId } = await db.notebooks.add(TEST_NOTEBOOK);
|
let nbId = await db.notebooks.add(TEST_NOTEBOOK);
|
||||||
await db.notebooks
|
await db.notebooks.notebook(nbId).topics.topic("General").add(id);
|
||||||
.notebook(nbId)
|
|
||||||
.topics.topic("General")
|
|
||||||
.add(id);
|
|
||||||
await db.notes.delete(id);
|
await db.notes.delete(id);
|
||||||
await db.notebooks.delete(nbId);
|
await db.notebooks.delete(nbId);
|
||||||
const deletedNote = db.trash.all.find(
|
const deletedNote = db.trash.all.find(
|
||||||
v => v.itemId.includes(id) && v.type === "note"
|
(v) => v.itemId.includes(id) && v.type === "note"
|
||||||
);
|
);
|
||||||
await db.trash.restore(deletedNote.id);
|
await db.trash.restore(deletedNote.id);
|
||||||
let note = db.notes.note(id);
|
let note = db.notes.note(id);
|
||||||
@@ -89,10 +80,7 @@ test("restore a deleted note that's in a deleted 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
|
await db.notebooks.notebook(id).topics.topic("General").add(noteId);
|
||||||
.notebook(id)
|
|
||||||
.topics.topic("General")
|
|
||||||
.add(noteId);
|
|
||||||
await db.notebooks.delete(id);
|
await db.notebooks.delete(id);
|
||||||
expect(db.notebooks.notebook(id).data.deleted).toBe(true);
|
expect(db.notebooks.notebook(id).data.deleted).toBe(true);
|
||||||
expect(db.notes.note(noteId).notebook).toStrictEqual({});
|
expect(db.notes.note(noteId).notebook).toStrictEqual({});
|
||||||
@@ -101,10 +89,7 @@ test("delete a notebook", () =>
|
|||||||
test("restore a deleted notebook", () =>
|
test("restore a deleted 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
|
await db.notebooks.notebook(id).topics.topic("General").add(noteId);
|
||||||
.notebook(id)
|
|
||||||
.topics.topic("General")
|
|
||||||
.add(noteId);
|
|
||||||
await db.notebooks.delete(id);
|
await db.notebooks.delete(id);
|
||||||
await db.trash.restore(db.trash.all[0].id);
|
await db.trash.restore(db.trash.all[0].id);
|
||||||
let notebook = db.notebooks.notebook(id);
|
let notebook = db.notebooks.notebook(id);
|
||||||
@@ -116,14 +101,11 @@ test("restore a deleted notebook", () =>
|
|||||||
test("restore a notebook that has deleted notes", () =>
|
test("restore a notebook that has deleted notes", () =>
|
||||||
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
|
await db.notebooks.notebook(id).topics.topic("General").add(noteId);
|
||||||
.notebook(id)
|
|
||||||
.topics.topic("General")
|
|
||||||
.add(noteId);
|
|
||||||
await db.notebooks.delete(id);
|
await db.notebooks.delete(id);
|
||||||
await db.notes.delete(noteId);
|
await db.notes.delete(noteId);
|
||||||
const deletedNotebook = db.trash.all.find(
|
const deletedNotebook = db.trash.all.find(
|
||||||
v => v.itemId.includes(id) && v.type === "notebook"
|
(v) => v.itemId.includes(id) && v.type === "notebook"
|
||||||
);
|
);
|
||||||
await db.trash.restore(deletedNotebook.id);
|
await db.trash.restore(deletedNotebook.id);
|
||||||
let notebook = db.notebooks.notebook(id);
|
let notebook = db.notebooks.notebook(id);
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { getLastWeekTimestamp } from "../../utils/date";
|
|||||||
const TEST_NOTEBOOK = {
|
const TEST_NOTEBOOK = {
|
||||||
title: "Test Notebook",
|
title: "Test Notebook",
|
||||||
description: "Test Description",
|
description: "Test Description",
|
||||||
topics: ["hello", "hello", " "]
|
topics: ["hello", "hello", " "],
|
||||||
};
|
};
|
||||||
|
|
||||||
const TEST_NOTEBOOK2 = {
|
const TEST_NOTEBOOK2 = {
|
||||||
title: "Test Notebook 2",
|
title: "Test Notebook 2",
|
||||||
description: "Test Description 2",
|
description: "Test Description 2",
|
||||||
topics: ["Home2"]
|
topics: ["Home2"],
|
||||||
};
|
};
|
||||||
|
|
||||||
function databaseTest() {
|
function databaseTest() {
|
||||||
@@ -20,23 +20,23 @@ function databaseTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notebookTest = (notebook = TEST_NOTEBOOK) =>
|
const notebookTest = (notebook = TEST_NOTEBOOK) =>
|
||||||
databaseTest().then(async db => {
|
databaseTest().then(async (db) => {
|
||||||
let nb = await db.notebooks.add(notebook);
|
let id = await db.notebooks.add(notebook);
|
||||||
return { db, id: nb ? nb.id : undefined };
|
return { db, id };
|
||||||
});
|
});
|
||||||
|
|
||||||
var TEST_NOTE = {
|
var TEST_NOTE = {
|
||||||
content: {
|
content: {
|
||||||
delta: { ops: [{ type: "insert", text: "I am a delta" }] },
|
delta: { ops: [{ type: "insert", text: "I am a delta" }] },
|
||||||
text: "I am a text"
|
text: "I am a text",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const LONG_TEXT =
|
const LONG_TEXT =
|
||||||
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
|
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
|
||||||
|
|
||||||
const noteTest = (note = TEST_NOTE) =>
|
const noteTest = (note = TEST_NOTE) =>
|
||||||
databaseTest().then(async db => {
|
databaseTest().then(async (db) => {
|
||||||
let id = await db.notes.add(note);
|
let id = await db.notes.add(note);
|
||||||
return { db, id };
|
return { db, id };
|
||||||
});
|
});
|
||||||
@@ -47,12 +47,12 @@ const groupedTest = (type, special = false) =>
|
|||||||
await db.notes.add({
|
await db.notes.add({
|
||||||
...TEST_NOTE,
|
...TEST_NOTE,
|
||||||
title: "Some title",
|
title: "Some title",
|
||||||
dateCreated: getLastWeekTimestamp() - 604800000
|
dateCreated: getLastWeekTimestamp() - 604800000,
|
||||||
});
|
});
|
||||||
await db.notes.add({
|
await db.notes.add({
|
||||||
...TEST_NOTE,
|
...TEST_NOTE,
|
||||||
title: "Some title and title title",
|
title: "Some title and title title",
|
||||||
dateCreated: getLastWeekTimestamp() - 604800000 * 2
|
dateCreated: getLastWeekTimestamp() - 604800000 * 2,
|
||||||
});
|
});
|
||||||
let grouped = db.notes.group(type, special);
|
let grouped = db.notes.group(type, special);
|
||||||
if (special) {
|
if (special) {
|
||||||
@@ -77,5 +77,5 @@ export {
|
|||||||
TEST_NOTEBOOK,
|
TEST_NOTEBOOK,
|
||||||
TEST_NOTEBOOK2,
|
TEST_NOTEBOOK2,
|
||||||
TEST_NOTE,
|
TEST_NOTE,
|
||||||
LONG_TEXT
|
LONG_TEXT,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
/**
|
|
||||||
* GENERAL PROCESS:
|
|
||||||
* make a get request to server with current lastSyncedTimestamp
|
|
||||||
* parse the response. the response should contain everything that user has on the server
|
|
||||||
* decrypt the response
|
|
||||||
* merge everything into the database and look for conflicts
|
|
||||||
* send the conflicts (if any) to the end-user for resolution
|
|
||||||
* once the conflicts have been resolved, send the updated data back to the server
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MERGING:
|
|
||||||
* Locally, get everything that was editted/added after the lastSyncedTimestamp
|
|
||||||
* Run forEach loop on the server response.
|
|
||||||
* Add items that do not exist in the local collections
|
|
||||||
* Remove items (without asking) that need to be removed
|
|
||||||
* Update items that were editted before the lastSyncedTimestamp
|
|
||||||
* Try to merge items that were edited after the lastSyncedTimestamp
|
|
||||||
* Items in which the content has changed, send them for conflict resolution
|
|
||||||
* Otherwise, keep the most recently updated copy.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CONFLICTS:
|
|
||||||
* Syncing should pause until all the conflicts have been resolved
|
|
||||||
* And then it should continue.
|
|
||||||
*/
|
|
||||||
import Database from "./index";
|
|
||||||
import { HOST, HEADERS } from "../utils/constants";
|
|
||||||
var tfun = require("transfun/transfun.js").tfun;
|
|
||||||
if (!tfun) {
|
|
||||||
tfun = global.tfun;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Sync {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Database} db
|
|
||||||
*/
|
|
||||||
constructor(db) {
|
|
||||||
this.db = db;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _fetch(lastSyncedTimestamp) {
|
|
||||||
let token = await this.db.user.token();
|
|
||||||
if (!token) throw new Error("You are not logged in");
|
|
||||||
let response = await fetch(`${HOST}sync?lst=${lastSyncedTimestamp}`, {
|
|
||||||
headers: { ...HEADERS, Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
//TODO decrypt the response.
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async throwOnConflicts() {
|
|
||||||
let hasConflicts = await this.db.context.read("hasConflicts");
|
|
||||||
if (hasConflicts) {
|
|
||||||
const mergeConflictError = new Error(
|
|
||||||
"Merge conflicts detected. Please resolve all conflicts to continue syncing."
|
|
||||||
);
|
|
||||||
mergeConflictError.code = "MERGE_CONFLICT";
|
|
||||||
throw mergeConflictError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async start() {
|
|
||||||
let user = await this.db.user.get();
|
|
||||||
if (!user) throw new Error("You need to login to sync.");
|
|
||||||
|
|
||||||
await this.db.conflicts.recalculate();
|
|
||||||
await this.throwOnConflicts();
|
|
||||||
|
|
||||||
let lastSyncedTimestamp = user.lastSynced || 0;
|
|
||||||
let serverResponse = await this._fetch(lastSyncedTimestamp);
|
|
||||||
|
|
||||||
// we prepare local data before merging so we always have correct data
|
|
||||||
const prepare = new Prepare(this.db, user);
|
|
||||||
const data = await prepare.get(lastSyncedTimestamp);
|
|
||||||
|
|
||||||
// merge the server response
|
|
||||||
const merger = new Merger(this.db, lastSyncedTimestamp);
|
|
||||||
const mergeResult = await merger.merge(serverResponse);
|
|
||||||
await this.throwOnConflicts();
|
|
||||||
// send the data back to server
|
|
||||||
await this._send(data);
|
|
||||||
|
|
||||||
// update our lastSynced time
|
|
||||||
if (mergeResult || !areAllEmpty(data))
|
|
||||||
await this.db.user.set({ lastSynced: data.lastSynced });
|
|
||||||
}
|
|
||||||
|
|
||||||
async _send(data) {
|
|
||||||
//TODO encrypt the payload
|
|
||||||
let token = await this.db.user.token();
|
|
||||||
if (!token) return;
|
|
||||||
let response = await fetch(`${HOST}sync`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { ...HEADERS, Authorization: `Bearer ${token}` },
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
return response.ok;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Merger {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Database} db
|
|
||||||
*/
|
|
||||||
constructor(db, lastSynced) {
|
|
||||||
this._db = db;
|
|
||||||
this._lastSynced = lastSynced;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _mergeItem(remoteItem, get, add) {
|
|
||||||
let localItem = await get(remoteItem.id);
|
|
||||||
remoteItem = { ...JSON.parse(remoteItem.data), remote: true };
|
|
||||||
if (!localItem || remoteItem.dateEdited > localItem.dateEdited) {
|
|
||||||
await add(remoteItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _mergeArray(array, get, set) {
|
|
||||||
return Promise.all(
|
|
||||||
array.map(async (item) => await this._mergeItem(item, get, set))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _mergeItemWithConflicts(remoteItem, get, add, resolve) {
|
|
||||||
let localItem = await get(remoteItem.id);
|
|
||||||
if (!localItem) {
|
|
||||||
await add({ ...JSON.parse(remoteItem.data), remote: true });
|
|
||||||
} else if (localItem.resolved) {
|
|
||||||
await add({ ...localItem, resolved: false });
|
|
||||||
} else if (localItem.dateEdited > this._lastSynced) {
|
|
||||||
// we have a conflict
|
|
||||||
await resolve(localItem, JSON.parse(remoteItem.data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _mergeArrayWithConflicts(array, get, set, resolve) {
|
|
||||||
return Promise.all(
|
|
||||||
array.map(
|
|
||||||
async (item) =>
|
|
||||||
await this._mergeItemWithConflicts(item, get, set, resolve)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async merge(serverResponse) {
|
|
||||||
const {
|
|
||||||
notes,
|
|
||||||
synced,
|
|
||||||
notebooks,
|
|
||||||
delta,
|
|
||||||
text,
|
|
||||||
tags,
|
|
||||||
colors,
|
|
||||||
trash,
|
|
||||||
} = serverResponse;
|
|
||||||
|
|
||||||
if (synced || areAllEmpty(serverResponse)) return false;
|
|
||||||
|
|
||||||
await this._mergeArray(
|
|
||||||
notes,
|
|
||||||
(id) => this._db.notes.note(id),
|
|
||||||
(item) => this._db.notes.add(item)
|
|
||||||
);
|
|
||||||
await this._mergeArray(
|
|
||||||
notebooks,
|
|
||||||
(id) => this._db.notebooks.notebook(id),
|
|
||||||
(item) => this._db.notebooks.add(item)
|
|
||||||
);
|
|
||||||
|
|
||||||
await this._mergeArrayWithConflicts(
|
|
||||||
delta,
|
|
||||||
(id) => this._db.delta.raw(id),
|
|
||||||
(item) => this._db.delta.add(item),
|
|
||||||
async (local, remote) => {
|
|
||||||
await this._db.delta.add({ ...local, conflicted: remote });
|
|
||||||
await this._db.notes.add({ id: local.noteId, conflicted: true });
|
|
||||||
await this._db.context.write("hasConflicts", true);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await this._mergeArray(
|
|
||||||
text,
|
|
||||||
(id) => this._db.text.raw(id),
|
|
||||||
(item) => this._db.text.add(item)
|
|
||||||
);
|
|
||||||
|
|
||||||
await this._mergeArray(
|
|
||||||
tags,
|
|
||||||
(id) => this._db.tags.tag(id),
|
|
||||||
(item) => this._db.tags.merge(item)
|
|
||||||
);
|
|
||||||
|
|
||||||
await this._mergeArray(
|
|
||||||
colors,
|
|
||||||
(id) => this._db.colors.tag(id),
|
|
||||||
(item) => this._db.colors.merge(item)
|
|
||||||
);
|
|
||||||
|
|
||||||
await this._mergeArray(
|
|
||||||
trash,
|
|
||||||
() => undefined,
|
|
||||||
(item) => this._db.trash.add(item)
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Prepare {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {Database} db
|
|
||||||
* @param {Object} user
|
|
||||||
* @param {Number} lastSyncedTimestamp
|
|
||||||
*/
|
|
||||||
constructor(db, user) {
|
|
||||||
this._db = db;
|
|
||||||
this._user = user;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(lastSyncedTimestamp) {
|
|
||||||
this._lastSyncedTimestamp = lastSyncedTimestamp;
|
|
||||||
return {
|
|
||||||
notes: this._prepareForServer(this._db.notes.raw),
|
|
||||||
notebooks: this._prepareForServer(this._db.notebooks.raw),
|
|
||||||
delta: this._prepareForServer(await this._db.delta.all()),
|
|
||||||
text: this._prepareForServer(await this._db.text.all()),
|
|
||||||
tags: this._prepareForServer(this._db.tags.raw),
|
|
||||||
colors: this._prepareForServer(this._db.colors.raw),
|
|
||||||
trash: this._prepareForServer(this._db.trash.raw),
|
|
||||||
lastSynced: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_prepareForServer(array) {
|
|
||||||
return tfun
|
|
||||||
.filter((item) => item.dateEdited > this._lastSyncedTimestamp)
|
|
||||||
.map((item) => ({
|
|
||||||
id: item.id,
|
|
||||||
data: JSON.stringify(item),
|
|
||||||
}))(array);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function areAllEmpty(obj) {
|
|
||||||
const arrays = Object.values(obj).filter((v) => v.length !== undefined);
|
|
||||||
return arrays.every((array) => array.length === 0);
|
|
||||||
}
|
|
||||||
123
packages/core/api/sync/__tests__/merger.test.js
Normal file
123
packages/core/api/sync/__tests__/merger.test.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import Merger from "../merger";
|
||||||
|
import {
|
||||||
|
StorageInterface,
|
||||||
|
databaseTest,
|
||||||
|
noteTest,
|
||||||
|
TEST_NOTE,
|
||||||
|
TEST_NOTEBOOK,
|
||||||
|
} from "../../../__tests__/utils";
|
||||||
|
import { tagsCollectionParams, mainCollectionParams } from "./utils";
|
||||||
|
|
||||||
|
const emptyServerResponse = {
|
||||||
|
notes: [],
|
||||||
|
notebooks: [],
|
||||||
|
delta: [],
|
||||||
|
text: [],
|
||||||
|
tags: [],
|
||||||
|
colors: [],
|
||||||
|
trash: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const testItem = { id: "someId", dateEdited: 2 };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
StorageInterface.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("server response with all arrays empty should cause early return", async () => {
|
||||||
|
const merger = new Merger();
|
||||||
|
const result = await merger.merge(emptyServerResponse);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("null server response should return false", async () => {
|
||||||
|
const merger = new Merger();
|
||||||
|
const result = await merger.merge();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
mainCollectionParams("notes", "note", TEST_NOTE),
|
||||||
|
mainCollectionParams("notebooks", "notebook", TEST_NOTEBOOK),
|
||||||
|
tagsCollectionParams("tags", "someTag"),
|
||||||
|
tagsCollectionParams("colors", "red"),
|
||||||
|
];
|
||||||
|
|
||||||
|
describe.each(tests)(
|
||||||
|
"general %s syncing tests",
|
||||||
|
(collection, add, edit, get) => {
|
||||||
|
test(`merge ${collection} into empty database`, () =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
const merger = new Merger(db, 0);
|
||||||
|
const result = await merger.merge({
|
||||||
|
[collection]: [{ id: testItem.id, data: JSON.stringify(testItem) }],
|
||||||
|
synced: false,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(db[collection].all[0].id).toStrictEqual(testItem.id);
|
||||||
|
expect(db[collection].all[0].dateEdited).toStrictEqual(
|
||||||
|
testItem.dateEdited
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test(`merge local and remote ${collection}`, () =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
const merger = new Merger(db, 0);
|
||||||
|
const item = await add(db);
|
||||||
|
item.title = "Google";
|
||||||
|
const result = await merger.merge({
|
||||||
|
[collection]: [{ id: item.id, data: JSON.stringify(item) }],
|
||||||
|
synced: false,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(db[collection].all.length).toBe(1);
|
||||||
|
expect(db[collection].all[0]).toStrictEqual(item);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test(`local ${collection} are more updated than remote ones`, () =>
|
||||||
|
databaseTest().then(async (db) => {
|
||||||
|
const merger = new Merger(db, 0);
|
||||||
|
const item = await add(db);
|
||||||
|
await edit(db, item);
|
||||||
|
item.title = "Google";
|
||||||
|
const result = await merger.merge({
|
||||||
|
[collection]: [{ id: item.id, data: JSON.stringify(item) }],
|
||||||
|
synced: false,
|
||||||
|
});
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(db[collection].all.length).toBe(1);
|
||||||
|
expect(db[collection].all[0]).toStrictEqual(get(db, item));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
test("local delta updated after lastSyncedTimestamp should cause merge conflict", () => {
|
||||||
|
return noteTest().then(async ({ db, id }) => {
|
||||||
|
const content = {
|
||||||
|
text: "my name is abdullah",
|
||||||
|
delta: { ops: [{ insert: "my name is abdullah" }] },
|
||||||
|
};
|
||||||
|
const deltaId = db.notes.note(id).data.content.delta;
|
||||||
|
const merger = new Merger(db, 200);
|
||||||
|
const result = await merger.merge({
|
||||||
|
delta: [
|
||||||
|
{
|
||||||
|
id: deltaId,
|
||||||
|
data: JSON.stringify({
|
||||||
|
id: deltaId,
|
||||||
|
noteId: id,
|
||||||
|
data: JSON.stringify(content.delta),
|
||||||
|
dateEdited: 2919,
|
||||||
|
conflicted: false,
|
||||||
|
resolved: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const localDelta = await db.delta.raw(deltaId);
|
||||||
|
expect(localDelta.conflicted.id).toBe(deltaId);
|
||||||
|
expect(localDelta.conflicted.noteId).toBe(id);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(await db.context.read("hasConflicts")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
91
packages/core/api/sync/__tests__/prepare.test.js
Normal file
91
packages/core/api/sync/__tests__/prepare.test.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import StorageInterface from "../../../__mocks__/storage.mock";
|
||||||
|
import Prepare from "../prepare";
|
||||||
|
import {
|
||||||
|
noteTest,
|
||||||
|
TEST_NOTE,
|
||||||
|
TEST_NOTEBOOK,
|
||||||
|
databaseTest,
|
||||||
|
} from "../../../__tests__/utils";
|
||||||
|
|
||||||
|
function getMainCollectionParams(name, testItem) {
|
||||||
|
return [
|
||||||
|
name,
|
||||||
|
(db, collection) => db[collection].add(testItem),
|
||||||
|
(db, collection) =>
|
||||||
|
db[collection].add({
|
||||||
|
...testItem,
|
||||||
|
id: Math.random().toString(),
|
||||||
|
remote: true,
|
||||||
|
dateEdited: 1,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagsCollectionParams(name, testItem) {
|
||||||
|
return [
|
||||||
|
name,
|
||||||
|
(db, collection) => db[collection].add(testItem + Math.random(), 2),
|
||||||
|
(db, collection) =>
|
||||||
|
db[collection]._collection.addItem({
|
||||||
|
title: testItem + MAX_ITEMS + 1,
|
||||||
|
noteIds: [2],
|
||||||
|
deletedIds: [],
|
||||||
|
id: Math.random().toString(),
|
||||||
|
remote: true,
|
||||||
|
dateEdited: 1,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_ITEMS = 5;
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
getMainCollectionParams("notes", TEST_NOTE),
|
||||||
|
getMainCollectionParams("notebooks", TEST_NOTEBOOK),
|
||||||
|
getTagsCollectionParams("tags", "someTag"),
|
||||||
|
getTagsCollectionParams("colors", "red"),
|
||||||
|
getMainCollectionParams("trash", {
|
||||||
|
id: 2141,
|
||||||
|
type: "note",
|
||||||
|
title: "someTitle",
|
||||||
|
}),
|
||||||
|
getMainCollectionParams("delta", { ops: [{ insert: "true" }] }),
|
||||||
|
getMainCollectionParams("text", "true"),
|
||||||
|
];
|
||||||
|
|
||||||
|
describe.each(tests)("%s preparation", (collection, add, addExtra) => {
|
||||||
|
beforeEach(() => {
|
||||||
|
StorageInterface.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`prepare ${collection} when user has never synced before`, () => {
|
||||||
|
return databaseTest().then(async (db) => {
|
||||||
|
await Promise.all(
|
||||||
|
Array(MAX_ITEMS)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => add(db, collection))
|
||||||
|
);
|
||||||
|
const prepare = new Prepare(db);
|
||||||
|
const data = await prepare.get(0);
|
||||||
|
expect(data[collection].length).toBe(MAX_ITEMS);
|
||||||
|
expect(data[collection].every((item) => !!item.data)).toBeTruthy();
|
||||||
|
expect(data.lastSynced).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`prepare ${collection} when user has synced before`, () => {
|
||||||
|
return databaseTest().then(async (db) => {
|
||||||
|
await Promise.all(
|
||||||
|
Array(MAX_ITEMS)
|
||||||
|
.fill(0)
|
||||||
|
.map(() => add(db, collection))
|
||||||
|
);
|
||||||
|
await addExtra(db, collection);
|
||||||
|
const prepare = new Prepare(db);
|
||||||
|
const data = await prepare.get(10);
|
||||||
|
expect(data[collection].length).toBe(MAX_ITEMS);
|
||||||
|
expect(data[collection].every((item) => !!item.data)).toBeTruthy();
|
||||||
|
expect(data.lastSynced).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
102
packages/core/api/sync/__tests__/sync.test.js
Normal file
102
packages/core/api/sync/__tests__/sync.test.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
//import User from "../../models/user";
|
||||||
|
import { enableFetchMocks, disableFetchMocks } from "jest-fetch-mock";
|
||||||
|
import StorageInterface from "../../../__mocks__/storage.mock";
|
||||||
|
//import Sync from "../sync";
|
||||||
|
//import Prepare from "../prepare";
|
||||||
|
import { databaseTest, TEST_NOTE } from "../../../__tests__/utils";
|
||||||
|
|
||||||
|
const SUCCESS_LOGIN_RESPONSE = {
|
||||||
|
access_token: "access_token",
|
||||||
|
refresh_token: "refresh_token",
|
||||||
|
payload: {
|
||||||
|
username: "thecodrr",
|
||||||
|
email: process.env.EMAIL,
|
||||||
|
lastSynced: 0,
|
||||||
|
},
|
||||||
|
scopes: "sync",
|
||||||
|
expiry: 36000,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
enableFetchMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.resetMocks();
|
||||||
|
StorageInterface.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("syncing when user is not logged in should throw", () =>
|
||||||
|
databaseTest().then((db) => {
|
||||||
|
expect(db.sync()).rejects.toThrow("You need to login to sync.");
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("sync without merge conflicts, cause merge conflicts, resolve them and then resync", () => {
|
||||||
|
return databaseTest().then(async (db) => {
|
||||||
|
// 1. login
|
||||||
|
fetchMock.mockResponseOnce(JSON.stringify(SUCCESS_LOGIN_RESPONSE));
|
||||||
|
await db.user.login("username", "password");
|
||||||
|
|
||||||
|
// 2. create local note
|
||||||
|
const noteId = await db.notes.add(TEST_NOTE);
|
||||||
|
|
||||||
|
// 3. start sync
|
||||||
|
fetchMock
|
||||||
|
.once(JSON.stringify({ notes: [], synced: false }))
|
||||||
|
.once(JSON.stringify({}), { status: 200 });
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const user = await db.user.get();
|
||||||
|
expect(user.lastSynced).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
/////// CAUSE MERGE CONFLICT! ///////
|
||||||
|
// 4. edit the note's content
|
||||||
|
await db.notes.add({
|
||||||
|
id: noteId,
|
||||||
|
content: { text: "i am a text", delta: { ops: [{ insert: "text" }] } },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. sync again and expect conflicts
|
||||||
|
const deltaId = db.notes.note(noteId).data.content.delta;
|
||||||
|
const delta = {
|
||||||
|
id: deltaId,
|
||||||
|
data: JSON.stringify({
|
||||||
|
id: deltaId,
|
||||||
|
dateEdited: Date.now(),
|
||||||
|
conflicted: false,
|
||||||
|
data: { ops: [{ insert: "text" }] },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMock
|
||||||
|
.once(JSON.stringify({ notes: [], delta: [delta], synced: false }))
|
||||||
|
.once(JSON.stringify({}), { status: 200 });
|
||||||
|
|
||||||
|
await expect(db.sync()).rejects.toThrow(
|
||||||
|
"Merge conflicts detected. Please resolve all conflicts to continue syncing."
|
||||||
|
);
|
||||||
|
|
||||||
|
let rawDelta = await db.delta.raw(deltaId);
|
||||||
|
expect(rawDelta.conflicted.id).toBe(deltaId);
|
||||||
|
expect(rawDelta.conflicted.data).toBeTruthy();
|
||||||
|
|
||||||
|
// 6. Resolve conflicts
|
||||||
|
await db.notes.add({
|
||||||
|
id: noteId,
|
||||||
|
conflicted: false,
|
||||||
|
content: {
|
||||||
|
text: "i am a text",
|
||||||
|
delta: { data: { ops: [{ insert: "text" }] }, resolved: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
rawDelta = await db.delta.raw(deltaId);
|
||||||
|
expect(rawDelta.conflicted).toBe(false);
|
||||||
|
//expect(rawDelta.resolved).toBe(true);
|
||||||
|
|
||||||
|
// 7. Resync (no conflicts should appear)
|
||||||
|
fetchMock
|
||||||
|
.once(JSON.stringify({ notes: [], delta: [delta], synced: false }))
|
||||||
|
.once(JSON.stringify({}), { status: 200 });
|
||||||
|
await expect(db.sync()).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
35
packages/core/api/sync/__tests__/utils.js
Normal file
35
packages/core/api/sync/__tests__/utils.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
function mainCollectionParams(collection, itemKey, item) {
|
||||||
|
async function addItem(db) {
|
||||||
|
const id = await db[collection].add(item);
|
||||||
|
return db[collection][itemKey](id).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editItem(db, item) {
|
||||||
|
await db[collection].add({ ...item, title: "dobido" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItem(db, item) {
|
||||||
|
return db[collection][itemKey](item.id).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [collection, addItem, editItem, getItem];
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagsCollectionParams(collection, item) {
|
||||||
|
async function addItem(db) {
|
||||||
|
const id = await db[collection].add(item, 20);
|
||||||
|
return db[collection].tag(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editItem(db) {
|
||||||
|
await db[collection].add(item, 240);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItem(db, item) {
|
||||||
|
return db[collection].tag(item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [collection, addItem, editItem, getItem];
|
||||||
|
}
|
||||||
|
|
||||||
|
export { tagsCollectionParams, mainCollectionParams };
|
||||||
11
packages/core/api/sync/__tests__/utils.test.js
Normal file
11
packages/core/api/sync/__tests__/utils.test.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { areAllEmpty } from "../utils";
|
||||||
|
|
||||||
|
test("return true if all array items in object are empty", () => {
|
||||||
|
const result = areAllEmpty({ a: [], b: [], c: true, f: 214 });
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("return false if any array item in object is not empty", () => {
|
||||||
|
const result = areAllEmpty({ a: [2, 3, 4], b: [], c: true, f: 214 });
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
109
packages/core/api/sync/index.js
Normal file
109
packages/core/api/sync/index.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* GENERAL PROCESS:
|
||||||
|
* make a get request to server with current lastSyncedTimestamp
|
||||||
|
* parse the response. the response should contain everything that user has on the server
|
||||||
|
* decrypt the response
|
||||||
|
* merge everything into the database and look for conflicts
|
||||||
|
* send the conflicts (if any) to the end-user for resolution
|
||||||
|
* once the conflicts have been resolved, send the updated data back to the server
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MERGING:
|
||||||
|
* Locally, get everything that was editted/added after the lastSyncedTimestamp
|
||||||
|
* Run forEach loop on the server response.
|
||||||
|
* Add items that do not exist in the local collections
|
||||||
|
* Remove items (without asking) that need to be removed
|
||||||
|
* Update items that were editted before the lastSyncedTimestamp
|
||||||
|
* Try to merge items that were edited after the lastSyncedTimestamp
|
||||||
|
* Items in which the content has changed, send them for conflict resolution
|
||||||
|
* Otherwise, keep the most recently updated copy.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CONFLICTS:
|
||||||
|
* Syncing should pause until all the conflicts have been resolved
|
||||||
|
* And then it should continue.
|
||||||
|
*/
|
||||||
|
import Database from "../index";
|
||||||
|
import { HOST, HEADERS } from "../../utils/constants";
|
||||||
|
import Prepare from "./prepare";
|
||||||
|
import Merger from "./merger";
|
||||||
|
import { areAllEmpty } from "./utils";
|
||||||
|
var tfun = require("transfun/transfun.js").tfun;
|
||||||
|
if (!tfun) {
|
||||||
|
tfun = global.tfun;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Sync {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Database} db
|
||||||
|
*/
|
||||||
|
constructor(db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetch(lastSyncedTimestamp) {
|
||||||
|
let token = await this.db.user.token();
|
||||||
|
if (!token) throw new Error("You are not logged in");
|
||||||
|
let response = await fetch(`${HOST}sync?lst=${lastSyncedTimestamp}`, {
|
||||||
|
headers: { ...HEADERS, Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
//TODO decrypt the response.
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async throwOnConflicts(lastSynced) {
|
||||||
|
let hasConflicts = await this.db.context.read("hasConflicts");
|
||||||
|
if (hasConflicts) {
|
||||||
|
if (lastSynced) {
|
||||||
|
await this.db.user.set({ lastSynced });
|
||||||
|
}
|
||||||
|
const mergeConflictError = new Error(
|
||||||
|
"Merge conflicts detected. Please resolve all conflicts to continue syncing."
|
||||||
|
);
|
||||||
|
mergeConflictError.code = "MERGE_CONFLICT";
|
||||||
|
throw mergeConflictError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
let user = await this.db.user.get();
|
||||||
|
if (!user) throw new Error("You need to login to sync.");
|
||||||
|
|
||||||
|
await this.db.conflicts.recalculate();
|
||||||
|
await this.throwOnConflicts();
|
||||||
|
|
||||||
|
let lastSyncedTimestamp = user.lastSynced || 0;
|
||||||
|
let serverResponse = await this._fetch(lastSyncedTimestamp);
|
||||||
|
|
||||||
|
// we prepare local data before merging so we always have correct data
|
||||||
|
const prepare = new Prepare(this.db, user);
|
||||||
|
const data = await prepare.get(lastSyncedTimestamp);
|
||||||
|
|
||||||
|
// merge the server response
|
||||||
|
const merger = new Merger(this.db, lastSyncedTimestamp);
|
||||||
|
const mergeResult = await merger.merge(serverResponse);
|
||||||
|
await this.throwOnConflicts(data.lastSynced);
|
||||||
|
|
||||||
|
// send the data back to server
|
||||||
|
await this._send(data);
|
||||||
|
|
||||||
|
// update our lastSynced time
|
||||||
|
if (mergeResult || !areAllEmpty(data))
|
||||||
|
await this.db.user.set({ lastSynced: data.lastSynced });
|
||||||
|
}
|
||||||
|
|
||||||
|
async _send(data) {
|
||||||
|
//TODO encrypt the payload
|
||||||
|
let token = await this.db.user.token();
|
||||||
|
if (!token) return;
|
||||||
|
let response = await fetch(`${HOST}sync`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...HEADERS, Authorization: `Bearer ${token}` },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
packages/core/api/sync/merger.js
Normal file
120
packages/core/api/sync/merger.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import Database from "../index";
|
||||||
|
import { areAllEmpty } from "./utils";
|
||||||
|
|
||||||
|
class Merger {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Database} db
|
||||||
|
*/
|
||||||
|
constructor(db, lastSynced) {
|
||||||
|
this._db = db;
|
||||||
|
this._lastSynced = lastSynced;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _mergeItem(remoteItem, get, add) {
|
||||||
|
let localItem = await get(remoteItem.id);
|
||||||
|
remoteItem = { ...JSON.parse(remoteItem.data), remote: true };
|
||||||
|
if (!localItem || remoteItem.dateEdited > localItem.dateEdited) {
|
||||||
|
await add(remoteItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _mergeArray(array, get, set) {
|
||||||
|
if (!array) return;
|
||||||
|
return Promise.all(
|
||||||
|
array.map(async (item) => await this._mergeItem(item, get, set))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _mergeItemWithConflicts(remoteItem, get, add, resolve) {
|
||||||
|
let localItem = await get(remoteItem.id);
|
||||||
|
|
||||||
|
remoteItem = { ...JSON.parse(remoteItem.data), remote: true };
|
||||||
|
|
||||||
|
if (!localItem || remoteItem.dateEdited > localItem.dateEdited) {
|
||||||
|
await add(remoteItem);
|
||||||
|
} /* else if (localItem.resolved) {
|
||||||
|
await add({ ...localItem, resolved: false });
|
||||||
|
} */ else if (
|
||||||
|
localItem.dateEdited > this._lastSynced
|
||||||
|
) {
|
||||||
|
// we have a conflict
|
||||||
|
await resolve(localItem, remoteItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _mergeArrayWithConflicts(array, get, set, resolve) {
|
||||||
|
if (!array) return;
|
||||||
|
return Promise.all(
|
||||||
|
array.map(
|
||||||
|
async (item) =>
|
||||||
|
await this._mergeItemWithConflicts(item, get, set, resolve)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async merge(serverResponse) {
|
||||||
|
if (!serverResponse) return false;
|
||||||
|
const {
|
||||||
|
notes,
|
||||||
|
synced,
|
||||||
|
notebooks,
|
||||||
|
delta,
|
||||||
|
text,
|
||||||
|
tags,
|
||||||
|
colors,
|
||||||
|
trash,
|
||||||
|
} = serverResponse;
|
||||||
|
|
||||||
|
if (synced || areAllEmpty(serverResponse)) return false;
|
||||||
|
|
||||||
|
await this._mergeArray(
|
||||||
|
notes,
|
||||||
|
(id) => this._db.notes.note(id),
|
||||||
|
(item) => this._db.notes.add(item)
|
||||||
|
);
|
||||||
|
await this._mergeArray(
|
||||||
|
notebooks,
|
||||||
|
(id) => this._db.notebooks.notebook(id),
|
||||||
|
(item) => this._db.notebooks.add(item)
|
||||||
|
);
|
||||||
|
|
||||||
|
await this._mergeArrayWithConflicts(
|
||||||
|
delta,
|
||||||
|
(id) => this._db.delta.raw(id),
|
||||||
|
(item) => this._db.delta.add(item),
|
||||||
|
async (local, remote) => {
|
||||||
|
await this._db.delta.add({ ...local, conflicted: remote });
|
||||||
|
await this._db.notes.add({ id: local.noteId, conflicted: true });
|
||||||
|
await this._db.context.write("hasConflicts", true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await this._mergeArray(
|
||||||
|
text,
|
||||||
|
(id) => this._db.text.raw(id),
|
||||||
|
(item) => this._db.text.add(item)
|
||||||
|
);
|
||||||
|
|
||||||
|
await this._mergeArray(
|
||||||
|
tags,
|
||||||
|
(id) => this._db.tags.tag(id),
|
||||||
|
(item) => this._db.tags.merge(item)
|
||||||
|
);
|
||||||
|
|
||||||
|
await this._mergeArray(
|
||||||
|
colors,
|
||||||
|
(id) => this._db.colors.tag(id),
|
||||||
|
(item) => this._db.colors.merge(item)
|
||||||
|
);
|
||||||
|
|
||||||
|
await this._mergeArray(
|
||||||
|
trash,
|
||||||
|
() => undefined,
|
||||||
|
(item) => this._db.trash.add(item)
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Merger;
|
||||||
35
packages/core/api/sync/prepare.js
Normal file
35
packages/core/api/sync/prepare.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Database from "../index";
|
||||||
|
|
||||||
|
class Prepare {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Database} db
|
||||||
|
*/
|
||||||
|
constructor(db) {
|
||||||
|
this._db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(lastSyncedTimestamp) {
|
||||||
|
this._lastSyncedTimestamp = lastSyncedTimestamp;
|
||||||
|
return {
|
||||||
|
notes: this._prepareForServer(this._db.notes.raw),
|
||||||
|
notebooks: this._prepareForServer(this._db.notebooks.raw),
|
||||||
|
delta: this._prepareForServer(await this._db.delta.all()),
|
||||||
|
text: this._prepareForServer(await this._db.text.all()),
|
||||||
|
tags: this._prepareForServer(this._db.tags.raw),
|
||||||
|
colors: this._prepareForServer(this._db.colors.raw),
|
||||||
|
trash: this._prepareForServer(this._db.trash.raw),
|
||||||
|
lastSynced: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepareForServer(array) {
|
||||||
|
return tfun
|
||||||
|
.filter((item) => item.dateEdited > this._lastSyncedTimestamp)
|
||||||
|
.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
data: JSON.stringify(item),
|
||||||
|
}))(array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Prepare;
|
||||||
6
packages/core/api/sync/utils.js
Normal file
6
packages/core/api/sync/utils.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
function areAllEmpty(obj) {
|
||||||
|
const arrays = Object.values(obj).filter((v) => v.length !== undefined);
|
||||||
|
return arrays.every((array) => array.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { areAllEmpty };
|
||||||
@@ -18,7 +18,7 @@ export default class Content {
|
|||||||
id,
|
id,
|
||||||
data: content.data || content,
|
data: content.data || content,
|
||||||
conflicted: content.conflicted || false,
|
conflicted: content.conflicted || false,
|
||||||
resolved: !!content.resolved,
|
//resolved: !!content.resolved,
|
||||||
dateEdited: content.dateEdited,
|
dateEdited: content.dateEdited,
|
||||||
dateCreated: content.dateCreated,
|
dateCreated: content.dateCreated,
|
||||||
remote: content.remote,
|
remote: content.remote,
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ export default class Notebooks {
|
|||||||
|
|
||||||
async add(notebookArg) {
|
async add(notebookArg) {
|
||||||
if (!notebookArg) throw new Error("Notebook cannot be undefined or null.");
|
if (!notebookArg) throw new Error("Notebook cannot be undefined or null.");
|
||||||
|
|
||||||
|
if (notebookArg.remote) {
|
||||||
|
return await this._collection.addItem(notebookArg);
|
||||||
|
}
|
||||||
|
|
||||||
//TODO reliably and efficiently check for duplicates.
|
//TODO reliably and efficiently check for duplicates.
|
||||||
const id = notebookArg.id || getId();
|
const id = notebookArg.id || getId();
|
||||||
let oldNotebook = this._collection.getItem(id);
|
let oldNotebook = this._collection.getItem(id);
|
||||||
@@ -37,7 +42,7 @@ export default class Notebooks {
|
|||||||
|
|
||||||
let notebook = {
|
let notebook = {
|
||||||
...oldNotebook,
|
...oldNotebook,
|
||||||
...notebookArg
|
...notebookArg,
|
||||||
};
|
};
|
||||||
|
|
||||||
notebook = {
|
notebook = {
|
||||||
@@ -49,7 +54,7 @@ export default class Notebooks {
|
|||||||
pinned: !!notebook.pinned,
|
pinned: !!notebook.pinned,
|
||||||
favorite: !!notebook.favorite,
|
favorite: !!notebook.favorite,
|
||||||
topics: notebook.topics || [],
|
topics: notebook.topics || [],
|
||||||
totalNotes: 0
|
totalNotes: 0,
|
||||||
};
|
};
|
||||||
if (!oldNotebook) {
|
if (!oldNotebook) {
|
||||||
notebook.topics.splice(0, 0, "General");
|
notebook.topics.splice(0, 0, "General");
|
||||||
@@ -60,7 +65,7 @@ export default class Notebooks {
|
|||||||
if (!oldNotebook) {
|
if (!oldNotebook) {
|
||||||
await this.notebook(notebook).topics.add(...notebook.topics);
|
await this.notebook(notebook).topics.add(...notebook.topics);
|
||||||
}
|
}
|
||||||
return notebook;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
get raw() {
|
||||||
@@ -68,7 +73,7 @@ export default class Notebooks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get all() {
|
get all() {
|
||||||
return sort(this._collection.getAllItems()).desc(t => t.pinned);
|
return sort(this._collection.getAllItems()).desc((t) => t.pinned);
|
||||||
}
|
}
|
||||||
|
|
||||||
get pinned() {
|
get pinned() {
|
||||||
@@ -100,7 +105,7 @@ export default class Notebooks {
|
|||||||
|
|
||||||
filter(query) {
|
filter(query) {
|
||||||
if (!query) return [];
|
if (!query) return [];
|
||||||
let queryFn = v => fuzzysearch(query, v.title + " " + v.description);
|
let queryFn = (v) => fuzzysearch(query, v.title + " " + v.description);
|
||||||
if (query instanceof Function) queryFn = query;
|
if (query instanceof Function) queryFn = query;
|
||||||
return tfun.filter(queryFn)(this.all);
|
return tfun.filter(queryFn)(this.all);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ export default class Tags {
|
|||||||
}
|
}
|
||||||
|
|
||||||
notes(tag) {
|
notes(tag) {
|
||||||
const tagItem = this.all.find(t => t.title === tag);
|
const tagItem = this.all.find((t) => t.title === tag);
|
||||||
if (!tagItem) return [];
|
if (!tagItem) return [];
|
||||||
return tagItem.noteIds;
|
return tagItem.noteIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
tag(id) {
|
tag(id) {
|
||||||
const tagItem = this.all.find(t => t.id === id);
|
const tagItem = this.all.find((t) => t.id === id);
|
||||||
if (!tagItem) return;
|
if (!tagItem) return;
|
||||||
return tagItem;
|
return tagItem;
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export default class Tags {
|
|||||||
await this._collection.addItem(tag);
|
await this._collection.addItem(tag);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const oldTag = this.all.find(t => t.id === tag.id);
|
const oldTag = this.all.find((t) => t.id === tag.id);
|
||||||
if (!oldTag) return await this._collection.addItem(tag);
|
if (!oldTag) return await this._collection.addItem(tag);
|
||||||
|
|
||||||
const deletedIds = set.union(oldTag.deletedIds, tag.deletedIds);
|
const deletedIds = set.union(oldTag.deletedIds, tag.deletedIds);
|
||||||
@@ -45,14 +45,14 @@ export default class Tags {
|
|||||||
...oldTag,
|
...oldTag,
|
||||||
noteIds,
|
noteIds,
|
||||||
dateEdited,
|
dateEdited,
|
||||||
deletedIds
|
deletedIds,
|
||||||
};
|
};
|
||||||
await this._collection.addItem(tag);
|
await this._collection.addItem(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
async add(tagTitle, noteId) {
|
async add(tagTitle, noteId) {
|
||||||
if (!tagTitle || !noteId) return;
|
if (!tagTitle || !noteId) return;
|
||||||
const oldTag = this.all.find(t => t.title === tagTitle) || {};
|
const oldTag = this.all.find((t) => t.title === tagTitle) || {};
|
||||||
|
|
||||||
let tag = { ...oldTag, title: tagTitle };
|
let tag = { ...oldTag, title: tagTitle };
|
||||||
let id = tag.id || getId();
|
let id = tag.id || getId();
|
||||||
@@ -62,10 +62,11 @@ export default class Tags {
|
|||||||
id,
|
id,
|
||||||
title: tag.title,
|
title: tag.title,
|
||||||
noteIds: [...notes, noteId],
|
noteIds: [...notes, noteId],
|
||||||
deletedIds
|
deletedIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this._collection.addItem(tag);
|
await this._collection.addItem(tag);
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
get raw() {
|
||||||
@@ -78,7 +79,7 @@ export default class Tags {
|
|||||||
|
|
||||||
async remove(tagTitle, noteId) {
|
async remove(tagTitle, noteId) {
|
||||||
if (!tagTitle || !noteId) return;
|
if (!tagTitle || !noteId) return;
|
||||||
let tag = this.all.find(t => t.title === tagTitle);
|
let tag = this.all.find((t) => t.title === tagTitle);
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
tag = qclone(tag);
|
tag = qclone(tag);
|
||||||
const noteIndex = tag.noteIds.indexOf(noteId);
|
const noteIndex = tag.noteIds.indexOf(noteId);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"@babel/plugin-transform-runtime": "^7.7.4",
|
"@babel/plugin-transform-runtime": "^7.7.4",
|
||||||
"@babel/preset-env": "^7.7.4",
|
"@babel/preset-env": "^7.7.4",
|
||||||
"@babel/runtime": "^7.7.4",
|
"@babel/runtime": "^7.7.4",
|
||||||
|
"@types/jest": "^25.2.1",
|
||||||
"babel-jest": "^24.9.0",
|
"babel-jest": "^24.9.0",
|
||||||
"babel-polyfill": "^6.26.0",
|
"babel-polyfill": "^6.26.0",
|
||||||
"babel-preset-env": "^1.7.0",
|
"babel-preset-env": "^1.7.0",
|
||||||
|
|||||||
@@ -823,6 +823,16 @@
|
|||||||
"@types/istanbul-reports" "^1.1.1"
|
"@types/istanbul-reports" "^1.1.1"
|
||||||
"@types/yargs" "^13.0.0"
|
"@types/yargs" "^13.0.0"
|
||||||
|
|
||||||
|
"@jest/types@^25.2.6":
|
||||||
|
version "25.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jest/types/-/types-25.2.6.tgz#c12f44af9bed444438091e4b59e7ed05f8659cb6"
|
||||||
|
integrity sha512-myJTTV37bxK7+3NgKc4Y/DlQ5q92/NOwZsZ+Uch7OXdElxOg61QYc72fPYNAjlvbnJ2YvbXLamIsa9tj48BmyQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/istanbul-lib-coverage" "^2.0.0"
|
||||||
|
"@types/istanbul-reports" "^1.1.1"
|
||||||
|
"@types/yargs" "^15.0.0"
|
||||||
|
chalk "^3.0.0"
|
||||||
|
|
||||||
"@types/babel__core@^7.1.0":
|
"@types/babel__core@^7.1.0":
|
||||||
version "7.1.3"
|
version "7.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30"
|
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.3.tgz#e441ea7df63cd080dfcd02ab199e6d16a735fc30"
|
||||||
@@ -856,6 +866,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.3.0"
|
"@babel/types" "^7.3.0"
|
||||||
|
|
||||||
|
"@types/color-name@^1.1.1":
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||||
|
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||||
|
|
||||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
|
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
|
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
|
||||||
@@ -876,6 +891,14 @@
|
|||||||
"@types/istanbul-lib-coverage" "*"
|
"@types/istanbul-lib-coverage" "*"
|
||||||
"@types/istanbul-lib-report" "*"
|
"@types/istanbul-lib-report" "*"
|
||||||
|
|
||||||
|
"@types/jest@^25.2.1":
|
||||||
|
version "25.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-25.2.1.tgz#9544cd438607955381c1bdbdb97767a249297db5"
|
||||||
|
integrity sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA==
|
||||||
|
dependencies:
|
||||||
|
jest-diff "^25.2.1"
|
||||||
|
pretty-format "^25.2.1"
|
||||||
|
|
||||||
"@types/stack-utils@^1.0.1":
|
"@types/stack-utils@^1.0.1":
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||||
@@ -893,6 +916,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/yargs-parser" "*"
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
|
"@types/yargs@^15.0.0":
|
||||||
|
version "15.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.4.tgz#7e5d0f8ca25e9d5849f2ea443cf7c402decd8299"
|
||||||
|
integrity sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==
|
||||||
|
dependencies:
|
||||||
|
"@types/yargs-parser" "*"
|
||||||
|
|
||||||
abab@^2.0.0:
|
abab@^2.0.0:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
|
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
|
||||||
@@ -956,6 +986,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
|
||||||
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
|
integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
|
||||||
|
|
||||||
|
ansi-regex@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
|
||||||
|
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
|
||||||
|
|
||||||
ansi-styles@^2.2.1:
|
ansi-styles@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
|
||||||
@@ -968,6 +1003,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-convert "^1.9.0"
|
color-convert "^1.9.0"
|
||||||
|
|
||||||
|
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
||||||
|
version "4.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359"
|
||||||
|
integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==
|
||||||
|
dependencies:
|
||||||
|
"@types/color-name" "^1.1.1"
|
||||||
|
color-convert "^2.0.1"
|
||||||
|
|
||||||
anymatch@^2.0.0:
|
anymatch@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
|
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
|
||||||
@@ -1718,6 +1761,14 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2:
|
|||||||
escape-string-regexp "^1.0.5"
|
escape-string-regexp "^1.0.5"
|
||||||
supports-color "^5.3.0"
|
supports-color "^5.3.0"
|
||||||
|
|
||||||
|
chalk@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
|
||||||
|
integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==
|
||||||
|
dependencies:
|
||||||
|
ansi-styles "^4.1.0"
|
||||||
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
chownr@^1.1.1:
|
chownr@^1.1.1:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
|
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142"
|
||||||
@@ -1772,11 +1823,23 @@ color-convert@^1.9.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color-name "1.1.3"
|
color-name "1.1.3"
|
||||||
|
|
||||||
|
color-convert@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||||
|
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
|
||||||
|
dependencies:
|
||||||
|
color-name "~1.1.4"
|
||||||
|
|
||||||
color-name@1.1.3:
|
color-name@1.1.3:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||||
|
|
||||||
|
color-name@~1.1.4:
|
||||||
|
version "1.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||||
|
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||||
|
|
||||||
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||||
version "1.0.8"
|
version "1.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||||
@@ -1976,6 +2039,11 @@ diff-sequences@^24.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
|
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5"
|
||||||
integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
|
integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==
|
||||||
|
|
||||||
|
diff-sequences@^25.2.6:
|
||||||
|
version "25.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd"
|
||||||
|
integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==
|
||||||
|
|
||||||
domexception@^1.0.1:
|
domexception@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
|
||||||
@@ -2370,6 +2438,11 @@ has-flag@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||||
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
|
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
|
||||||
|
|
||||||
|
has-flag@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||||
|
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||||
|
|
||||||
has-symbols@^1.0.0, has-symbols@^1.0.1:
|
has-symbols@^1.0.0, has-symbols@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
|
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
|
||||||
@@ -2774,6 +2847,16 @@ jest-diff@^24.9.0:
|
|||||||
jest-get-type "^24.9.0"
|
jest-get-type "^24.9.0"
|
||||||
pretty-format "^24.9.0"
|
pretty-format "^24.9.0"
|
||||||
|
|
||||||
|
jest-diff@^25.2.1:
|
||||||
|
version "25.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-25.2.6.tgz#a6d70a9ab74507715ea1092ac513d1ab81c1b5e7"
|
||||||
|
integrity sha512-KuadXImtRghTFga+/adnNrv9s61HudRMR7gVSbP35UKZdn4IK2/0N0PpGZIqtmllK9aUyye54I3nu28OYSnqOg==
|
||||||
|
dependencies:
|
||||||
|
chalk "^3.0.0"
|
||||||
|
diff-sequences "^25.2.6"
|
||||||
|
jest-get-type "^25.2.6"
|
||||||
|
pretty-format "^25.2.6"
|
||||||
|
|
||||||
jest-docblock@^24.3.0:
|
jest-docblock@^24.3.0:
|
||||||
version "24.9.0"
|
version "24.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
|
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
|
||||||
@@ -2828,6 +2911,11 @@ jest-get-type@^24.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
|
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e"
|
||||||
integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==
|
integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==
|
||||||
|
|
||||||
|
jest-get-type@^25.2.6:
|
||||||
|
version "25.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-25.2.6.tgz#0b0a32fab8908b44d508be81681487dbabb8d877"
|
||||||
|
integrity sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==
|
||||||
|
|
||||||
jest-haste-map@^24.9.0:
|
jest-haste-map@^24.9.0:
|
||||||
version "24.9.0"
|
version "24.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
|
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
|
||||||
@@ -3794,6 +3882,16 @@ pretty-format@^24.9.0:
|
|||||||
ansi-styles "^3.2.0"
|
ansi-styles "^3.2.0"
|
||||||
react-is "^16.8.4"
|
react-is "^16.8.4"
|
||||||
|
|
||||||
|
pretty-format@^25.2.1, pretty-format@^25.2.6:
|
||||||
|
version "25.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.2.6.tgz#542a1c418d019bbf1cca2e3620443bc1323cb8d7"
|
||||||
|
integrity sha512-DEiWxLBaCHneffrIT4B+TpMvkV9RNvvJrd3lY9ew1CEQobDzEXmYT1mg0hJhljZty7kCc10z13ohOFAE8jrUDg==
|
||||||
|
dependencies:
|
||||||
|
"@jest/types" "^25.2.6"
|
||||||
|
ansi-regex "^5.0.0"
|
||||||
|
ansi-styles "^4.0.0"
|
||||||
|
react-is "^16.12.0"
|
||||||
|
|
||||||
private@^0.1.6:
|
private@^0.1.6:
|
||||||
version "0.1.8"
|
version "0.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
|
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
|
||||||
@@ -3860,6 +3958,11 @@ rc@^1.2.7:
|
|||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
strip-json-comments "~2.0.1"
|
strip-json-comments "~2.0.1"
|
||||||
|
|
||||||
|
react-is@^16.12.0:
|
||||||
|
version "16.13.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
react-is@^16.8.4:
|
react-is@^16.8.4:
|
||||||
version "16.12.0"
|
version "16.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
||||||
@@ -4452,6 +4555,13 @@ supports-color@^6.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag "^3.0.0"
|
has-flag "^3.0.0"
|
||||||
|
|
||||||
|
supports-color@^7.1.0:
|
||||||
|
version "7.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
|
||||||
|
integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==
|
||||||
|
dependencies:
|
||||||
|
has-flag "^4.0.0"
|
||||||
|
|
||||||
symbol-tree@^3.2.2:
|
symbol-tree@^3.2.2:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||||
|
|||||||
Reference in New Issue
Block a user