mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
core: migrate all database writes to sqlite
This commit is contained in:
57
packages/core/__benches__/relations.bench.ts
Normal file
57
packages/core/__benches__/relations.bench.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { bench, describe } from "vitest";
|
||||||
|
import { databaseTest } from "../__tests__/utils";
|
||||||
|
|
||||||
|
describe("relations", async () => {
|
||||||
|
const db = await databaseTest();
|
||||||
|
// const totalNotebooks = 10;
|
||||||
|
|
||||||
|
// let parentNotebookId: string | undefined = undefined;
|
||||||
|
// for (let i = 1; i <= 100; ++i) {
|
||||||
|
// const id = await db.notebooks.add({ title: `notebook-somethign-${i}` });
|
||||||
|
// if (parentNotebookId)
|
||||||
|
// await db.relations.add(
|
||||||
|
// { id: parentNotebookId, type: "notebook" },
|
||||||
|
// { id, type: "notebook" }
|
||||||
|
// );
|
||||||
|
// parentNotebookId = id;
|
||||||
|
// for (let j = 1; j <= 100; ++j) {
|
||||||
|
// await db.relations.add(
|
||||||
|
// { type: "notebook", id },
|
||||||
|
// { id: `${j * i}-note`, type: "note" }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
// const id2 = await db.notebooks.add({ title: `notebook-somethign` });
|
||||||
|
|
||||||
|
console.log(await db.notebooks.totalNotes("6516a04a35a073f359e7e801"));
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// await db.relations.from({ id: "8-note", type: "note" }, "notebook").unlink()
|
||||||
|
// );
|
||||||
|
|
||||||
|
// bench("get some relations from 10k relations", async () => {
|
||||||
|
// await db.notebooks.totalNotes("6516a04a35a073f359e7e801");
|
||||||
|
// });
|
||||||
|
});
|
||||||
@@ -68,11 +68,12 @@ test("add invalid note", () =>
|
|||||||
expect(db.notes.add({ hello: "world" })).rejects.toThrow();
|
expect(db.notes.add({ hello: "world" })).rejects.toThrow();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("add note", () =>
|
test.only("add note", () =>
|
||||||
noteTest().then(async ({ db, id }) => {
|
noteTest().then(async ({ db, id }) => {
|
||||||
const note = db.notes.note(id);
|
const note = await db.notes.note$(id);
|
||||||
expect(note).toBeDefined();
|
expect(note).toBeDefined();
|
||||||
expect(await note?.content()).toStrictEqual(TEST_NOTE.content.data);
|
const content = await db.content.get(note!.contentId!);
|
||||||
|
expect(content!.data).toStrictEqual(TEST_NOTE.content.data);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
test("get note content", () =>
|
test("get note content", () =>
|
||||||
|
|||||||
BIN
packages/core/nn.db
Normal file
BIN
packages/core/nn.db
Normal file
Binary file not shown.
449
packages/core/package-lock.json
generated
449
packages/core/package-lock.json
generated
@@ -20,6 +20,7 @@
|
|||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"katex": "0.16.2",
|
"katex": "0.16.2",
|
||||||
|
"kysely": "^0.26.3",
|
||||||
"linkedom": "^0.14.17",
|
"linkedom": "^0.14.17",
|
||||||
"liqe": "^1.13.0",
|
"liqe": "^1.13.0",
|
||||||
"mime-db": "1.52.0",
|
"mime-db": "1.52.0",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@notesnook/crypto": "file:../crypto",
|
"@notesnook/crypto": "file:../crypto",
|
||||||
|
"@types/better-sqlite3": "^7.6.5",
|
||||||
"@types/event-source-polyfill": "^1.0.1",
|
"@types/event-source-polyfill": "^1.0.1",
|
||||||
"@types/html-to-text": "^9.0.0",
|
"@types/html-to-text": "^9.0.0",
|
||||||
"@types/katex": "^0.16.2",
|
"@types/katex": "^0.16.2",
|
||||||
@@ -40,6 +42,7 @@
|
|||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.5",
|
||||||
"@vitest/coverage-v8": "^0.34.1",
|
"@vitest/coverage-v8": "^0.34.1",
|
||||||
"abortcontroller-polyfill": "^1.7.3",
|
"abortcontroller-polyfill": "^1.7.3",
|
||||||
|
"better-sqlite3": "^8.6.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"event-source-polyfill": "^1.0.31",
|
"event-source-polyfill": "^1.0.31",
|
||||||
@@ -2173,6 +2176,15 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/better-sqlite3": {
|
||||||
|
"version": "7.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.9.tgz",
|
||||||
|
"integrity": "sha512-FvktcujPDj9XKMJQWFcl2vVl7OdRIqsSRX9b0acWwTmwLK9CF2eqo/FRcmMLNpugKoX/avA6pb7TorDLmpgTnQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "4.3.14",
|
"version": "4.3.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.14.tgz",
|
||||||
@@ -2444,6 +2456,57 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/better-sqlite3": {
|
||||||
|
"version": "8.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.7.0.tgz",
|
||||||
|
"integrity": "sha512-99jZU4le+f3G6aIl6PmmV0cxUIWqKieHxsiF7G34CVFiE+/UabpYqkU0NJIkY/96mQKikHeBjtR27vFfs5JpEw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
|
"prebuild-install": "^7.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bindings": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^5.5.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/boolbase": {
|
"node_modules/boolbase": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
@@ -2457,6 +2520,30 @@
|
|||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cac": {
|
"node_modules/cac": {
|
||||||
"version": "6.7.14",
|
"version": "6.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
@@ -2523,6 +2610,12 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/combined-stream": {
|
"node_modules/combined-stream": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2667,6 +2760,21 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decompress-response": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-response": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deep-eql": {
|
"node_modules/deep-eql": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
|
||||||
@@ -2679,6 +2787,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-extend": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deepmerge": {
|
"node_modules/deepmerge": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -2694,6 +2811,15 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/diff-sequences": {
|
"node_modules/diff-sequences": {
|
||||||
"version": "29.6.3",
|
"version": "29.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
|
||||||
@@ -2773,6 +2899,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/end-of-stream": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
@@ -2840,6 +2975,15 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expand-template": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fetch-cookie": {
|
"node_modules/fetch-cookie": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
@@ -2848,6 +2992,12 @@
|
|||||||
"tough-cookie": "^4.0.0"
|
"tough-cookie": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-uri-to-path": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2861,25 +3011,17 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fs-constants": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/get-func-name": {
|
"node_modules/get-func-name": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||||
@@ -2889,6 +3031,12 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/github-from-package": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||||
|
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3032,6 +3180,26 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3046,6 +3214,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ini": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/is-alphabetical": {
|
"node_modules/is-alphabetical": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3252,6 +3426,14 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kysely": {
|
||||||
|
"version": "0.26.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.26.3.tgz",
|
||||||
|
"integrity": "sha512-yWSgGi9bY13b/W06DD2OCDDHQmq1kwTGYlQ4wpZkMOJqMGCstVCFIvxCCVG4KfY1/3G0MhDAcZsip/Lw8/vJWw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/leac": {
|
"node_modules/leac": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3360,6 +3542,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mimic-response": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3371,6 +3565,21 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mkdirp-classic": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/mlly": {
|
"node_modules/mlly": {
|
||||||
"version": "1.6.1",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz",
|
||||||
@@ -3397,6 +3606,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/napi-build-utils": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/nearley": {
|
"node_modules/nearley": {
|
||||||
"version": "2.20.1",
|
"version": "2.20.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3421,6 +3636,18 @@
|
|||||||
"version": "2.20.3",
|
"version": "2.20.3",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-abi": {
|
||||||
|
"version": "3.56.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz",
|
||||||
|
"integrity": "sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.3.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.6.7",
|
"version": "2.6.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3644,6 +3871,32 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prebuild-install": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^2.0.0",
|
||||||
|
"expand-template": "^2.0.3",
|
||||||
|
"github-from-package": "0.0.0",
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"mkdirp-classic": "^0.5.3",
|
||||||
|
"napi-build-utils": "^1.0.1",
|
||||||
|
"node-abi": "^3.3.0",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"rc": "^1.2.7",
|
||||||
|
"simple-get": "^4.0.0",
|
||||||
|
"tar-fs": "^2.0.0",
|
||||||
|
"tunnel-agent": "^0.6.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prebuild-install": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-format": {
|
"node_modules/pretty-format": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
|
||||||
@@ -3678,6 +3931,16 @@
|
|||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pump": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"end-of-stream": "^1.1.0",
|
||||||
|
"once": "^1.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3708,12 +3971,41 @@
|
|||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"deep-extend": "^0.6.0",
|
||||||
|
"ini": "~1.3.0",
|
||||||
|
"minimist": "^1.2.0",
|
||||||
|
"strip-json-comments": "~2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rc": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/refractor": {
|
"node_modules/refractor": {
|
||||||
"version": "4.8.1",
|
"version": "4.8.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3781,6 +4073,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3849,6 +4161,51 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-concat": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/simple-get": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"decompress-response": "^6.0.0",
|
||||||
|
"once": "^1.3.1",
|
||||||
|
"simple-concat": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3889,6 +4246,24 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-json-comments": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/strip-literal": {
|
"node_modules/strip-literal": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
|
||||||
@@ -3917,6 +4292,34 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tar-fs": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/test-exclude": {
|
"node_modules/test-exclude": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3992,6 +4395,18 @@
|
|||||||
"version": "2.4.1",
|
"version": "2.4.1",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
|
"node_modules/tunnel-agent": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-detect": {
|
"node_modules/type-detect": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||||
@@ -4026,6 +4441,12 @@
|
|||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/v8-to-istanbul": {
|
"node_modules/v8-to-istanbul": {
|
||||||
"version": "9.2.0",
|
"version": "9.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@notesnook/crypto": "file:../crypto",
|
"@notesnook/crypto": "file:../crypto",
|
||||||
|
"@types/better-sqlite3": "^7.6.5",
|
||||||
"@types/event-source-polyfill": "^1.0.1",
|
"@types/event-source-polyfill": "^1.0.1",
|
||||||
"@types/html-to-text": "^9.0.0",
|
"@types/html-to-text": "^9.0.0",
|
||||||
"@types/katex": "^0.16.2",
|
"@types/katex": "^0.16.2",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.5",
|
||||||
"@vitest/coverage-v8": "^0.34.1",
|
"@vitest/coverage-v8": "^0.34.1",
|
||||||
"abortcontroller-polyfill": "^1.7.3",
|
"abortcontroller-polyfill": "^1.7.3",
|
||||||
|
"better-sqlite3": "^8.6.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"event-source-polyfill": "^1.0.31",
|
"event-source-polyfill": "^1.0.31",
|
||||||
@@ -54,6 +56,7 @@
|
|||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"katex": "0.16.2",
|
"katex": "0.16.2",
|
||||||
|
"kysely": "^0.26.3",
|
||||||
"linkedom": "^0.14.17",
|
"linkedom": "^0.14.17",
|
||||||
"liqe": "^1.13.0",
|
"liqe": "^1.13.0",
|
||||||
"mime-db": "1.52.0",
|
"mime-db": "1.52.0",
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ import {
|
|||||||
import TokenManager from "./token-manager";
|
import TokenManager from "./token-manager";
|
||||||
import { Attachment } from "../types";
|
import { Attachment } from "../types";
|
||||||
import { Settings } from "../collections/settings";
|
import { Settings } from "../collections/settings";
|
||||||
|
import { DatabaseAccessor, DatabaseSchema, createDatabase } from "../database";
|
||||||
|
import { Kysely, SqliteDriver, Transaction } from "kysely";
|
||||||
|
import BetterSQLite3 from "better-sqlite3";
|
||||||
|
|
||||||
type EventSourceConstructor = new (
|
type EventSourceConstructor = new (
|
||||||
uri: string,
|
uri: string,
|
||||||
@@ -111,6 +114,32 @@ class Database {
|
|||||||
return this.options.compressor;
|
return this.options.compressor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _sql?: Kysely<DatabaseSchema>;
|
||||||
|
sql: DatabaseAccessor = () => {
|
||||||
|
if (this._transaction) return this._transaction;
|
||||||
|
|
||||||
|
if (!this._sql)
|
||||||
|
throw new Error(
|
||||||
|
"Database not initialized. Did you forget to call db.init()?"
|
||||||
|
);
|
||||||
|
return this._sql;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _transaction?: Transaction<DatabaseSchema>;
|
||||||
|
transaction = (
|
||||||
|
executor: (tr: Transaction<DatabaseSchema>) => void | Promise<void>
|
||||||
|
) => {
|
||||||
|
if (this._transaction) return executor(this._transaction);
|
||||||
|
return this.sql()
|
||||||
|
.transaction()
|
||||||
|
.execute(async (tr) => {
|
||||||
|
this._transaction = tr;
|
||||||
|
await executor(tr);
|
||||||
|
this._transaction = undefined;
|
||||||
|
})
|
||||||
|
.finally(() => (this._transaction = undefined));
|
||||||
|
};
|
||||||
|
|
||||||
private options?: Options;
|
private options?: Options;
|
||||||
EventSource?: EventSourceConstructor;
|
EventSource?: EventSourceConstructor;
|
||||||
eventSource?: EventSource | null;
|
eventSource?: EventSource | null;
|
||||||
@@ -144,6 +173,7 @@ class Database {
|
|||||||
reminders = new Reminders(this);
|
reminders = new Reminders(this);
|
||||||
relations = new Relations(this);
|
relations = new Relations(this);
|
||||||
notes = new Notes(this);
|
notes = new Notes(this);
|
||||||
|
|
||||||
// constructor() {
|
// constructor() {
|
||||||
// this.sseMutex = new Mutex();
|
// this.sseMutex = new Mutex();
|
||||||
// // this.lastHeartbeat = undefined; // { local: 0, server: 0 };
|
// // this.lastHeartbeat = undefined; // { local: 0, server: 0 };
|
||||||
@@ -170,8 +200,8 @@ class Database {
|
|||||||
this
|
this
|
||||||
);
|
);
|
||||||
EV.subscribe(EVENTS.attachmentDeleted, async (attachment: Attachment) => {
|
EV.subscribe(EVENTS.attachmentDeleted, async (attachment: Attachment) => {
|
||||||
await this.fs().cancel(attachment.metadata.hash, "upload");
|
await this.fs().cancel(attachment.hash, "upload");
|
||||||
await this.fs().cancel(attachment.metadata.hash, "download");
|
await this.fs().cancel(attachment.hash, "download");
|
||||||
});
|
});
|
||||||
EV.subscribe(EVENTS.userLoggedOut, async () => {
|
EV.subscribe(EVENTS.userLoggedOut, async () => {
|
||||||
await this.monographs.clear();
|
await this.monographs.clear();
|
||||||
@@ -179,6 +209,10 @@ class Database {
|
|||||||
this.disconnectSSE();
|
this.disconnectSSE();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._sql = await createDatabase(
|
||||||
|
new SqliteDriver({ database: BetterSQLite3("nn.db") })
|
||||||
|
);
|
||||||
|
|
||||||
await this._validate();
|
await this._validate();
|
||||||
|
|
||||||
await this.initCollections();
|
await this.initCollections();
|
||||||
|
|||||||
@@ -66,21 +66,21 @@ class Collector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const itemType in SYNC_COLLECTIONS_MAP) {
|
// for (const itemType in SYNC_COLLECTIONS_MAP) {
|
||||||
const collectionKey =
|
// const collectionKey =
|
||||||
SYNC_COLLECTIONS_MAP[itemType as keyof typeof SYNC_COLLECTIONS_MAP];
|
// SYNC_COLLECTIONS_MAP[itemType as keyof typeof SYNC_COLLECTIONS_MAP];
|
||||||
const collection = this.db[collectionKey].collection;
|
// const collection = this.db[collectionKey].collection;
|
||||||
for (const chunk of collection.iterateSync(chunkSize)) {
|
// for (const chunk of collection.iterateSync(chunkSize)) {
|
||||||
const items = await this.prepareChunk(
|
// const items = await this.prepareChunk(
|
||||||
chunk,
|
// chunk,
|
||||||
lastSyncedTimestamp,
|
// lastSyncedTimestamp,
|
||||||
isForceSync,
|
// isForceSync,
|
||||||
key
|
// key
|
||||||
);
|
// );
|
||||||
if (!items) continue;
|
// if (!items) continue;
|
||||||
yield { items, type: itemType as keyof typeof SYNC_COLLECTIONS_MAP };
|
// yield { items, type: itemType as keyof typeof SYNC_COLLECTIONS_MAP };
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepareChunk(
|
async prepareChunk(
|
||||||
|
|||||||
@@ -29,21 +29,18 @@ import {
|
|||||||
isWebClip
|
isWebClip
|
||||||
} from "../utils/filename";
|
} from "../utils/filename";
|
||||||
import { Cipher, DataFormat, SerializedKey } from "@notesnook/crypto";
|
import { Cipher, DataFormat, SerializedKey } from "@notesnook/crypto";
|
||||||
import { CachedCollection } from "../database/cached-collection";
|
|
||||||
import { Output } from "../interfaces";
|
import { Output } from "../interfaces";
|
||||||
import { Attachment, AttachmentMetadata, isDeleted } from "../types";
|
import { Attachment } from "../types";
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
|
import { isCipher } from "../database/crypto";
|
||||||
|
|
||||||
export class Attachments implements ICollection {
|
export class Attachments implements ICollection {
|
||||||
name = "attachments";
|
name = "attachments";
|
||||||
key: Cipher<"base64"> | null = null;
|
key: Cipher<"base64"> | null = null;
|
||||||
readonly collection: CachedCollection<"attachments", Attachment>;
|
readonly collection: SQLCollection<"attachments", Attachment>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new CachedCollection(
|
this.collection = new SQLCollection(db.sql, "attachments", db.eventManager);
|
||||||
db.storage,
|
|
||||||
"attachments",
|
|
||||||
db.eventManager
|
|
||||||
);
|
|
||||||
this.key = null;
|
this.key = null;
|
||||||
|
|
||||||
EV.subscribe(
|
EV.subscribe(
|
||||||
@@ -60,15 +57,15 @@ export class Attachments implements ICollection {
|
|||||||
eventData: Record<string, unknown>;
|
eventData: Record<string, unknown>;
|
||||||
}) => {
|
}) => {
|
||||||
if (!success || !eventData || !eventData.readOnDownload) return;
|
if (!success || !eventData || !eventData.readOnDownload) return;
|
||||||
const attachment = this.attachment(filename);
|
const attachment = await this.attachment(filename);
|
||||||
if (!attachment || !attachment.metadata) return;
|
if (!attachment) return;
|
||||||
|
|
||||||
const src = await this.read(filename, getOutputType(attachment));
|
const src = await this.read(filename, getOutputType(attachment));
|
||||||
if (!src) return;
|
if (!src) return;
|
||||||
|
|
||||||
EV.publish(EVENTS.mediaAttachmentDownloaded, {
|
EV.publish(EVENTS.mediaAttachmentDownloaded, {
|
||||||
groupId,
|
groupId,
|
||||||
hash: attachment.metadata.hash,
|
hash: attachment.hash,
|
||||||
attachmentType: getAttachmentType(attachment),
|
attachmentType: getAttachmentType(attachment),
|
||||||
src
|
src
|
||||||
});
|
});
|
||||||
@@ -86,7 +83,7 @@ export class Attachments implements ICollection {
|
|||||||
filename: string;
|
filename: string;
|
||||||
error: string;
|
error: string;
|
||||||
}) => {
|
}) => {
|
||||||
const attachment = this.attachment(filename);
|
const attachment = await this.attachment(filename);
|
||||||
if (!attachment) return;
|
if (!attachment) return;
|
||||||
if (success) await this.markAsUploaded(attachment.id);
|
if (success) await this.markAsUploaded(attachment.id);
|
||||||
else
|
else
|
||||||
@@ -104,54 +101,50 @@ export class Attachments implements ICollection {
|
|||||||
|
|
||||||
async add(
|
async add(
|
||||||
item: Partial<
|
item: Partial<
|
||||||
Omit<Attachment, "key" | "metadata"> & {
|
Omit<Attachment, "key" | "encryptionKey"> & {
|
||||||
key: SerializedKey;
|
key: SerializedKey;
|
||||||
}
|
}
|
||||||
> & {
|
>
|
||||||
metadata: Partial<AttachmentMetadata> & { hash: string };
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
if (!item) return console.error("attachment cannot be undefined");
|
if (!item) return console.error("attachment cannot be undefined");
|
||||||
if (!item.metadata.hash) throw new Error("Please provide attachment hash.");
|
if (!item.hash) throw new Error("Please provide attachment hash.");
|
||||||
|
|
||||||
const oldAttachment = this.all.find(
|
const oldAttachment = await this.attachment(item.hash);
|
||||||
(a) => a.metadata.hash === item.metadata?.hash
|
|
||||||
);
|
|
||||||
const id = oldAttachment?.id || getId();
|
const id = oldAttachment?.id || getId();
|
||||||
|
|
||||||
const encryptedKey = item.key
|
const encryptedKey = item.key
|
||||||
? await this.encryptKey(item.key)
|
? JSON.stringify(await this.encryptKey(item.key))
|
||||||
: oldAttachment?.key;
|
: oldAttachment?.encryptionKey;
|
||||||
const attachment = {
|
const attachment = {
|
||||||
...oldAttachment,
|
...oldAttachment,
|
||||||
...oldAttachment?.metadata,
|
|
||||||
...item,
|
...item,
|
||||||
key: encryptedKey
|
encryptionKey: encryptedKey
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
iv,
|
iv,
|
||||||
length,
|
size,
|
||||||
alg,
|
alg,
|
||||||
hash,
|
hash,
|
||||||
hashType,
|
hashType,
|
||||||
filename,
|
filename,
|
||||||
|
mimeType,
|
||||||
salt,
|
salt,
|
||||||
type,
|
|
||||||
chunkSize,
|
chunkSize,
|
||||||
key
|
encryptionKey
|
||||||
} = attachment;
|
} = attachment;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!iv ||
|
!iv ||
|
||||||
!length ||
|
!size ||
|
||||||
!alg ||
|
!alg ||
|
||||||
!hash ||
|
!hash ||
|
||||||
!hashType ||
|
!hashType ||
|
||||||
!filename ||
|
// !filename ||
|
||||||
|
// !mimeType ||
|
||||||
!salt ||
|
!salt ||
|
||||||
!chunkSize ||
|
!chunkSize ||
|
||||||
!key
|
!encryptionKey
|
||||||
) {
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
"Attachment is invalid because all properties are required:",
|
"Attachment is invalid because all properties are required:",
|
||||||
@@ -161,27 +154,33 @@ export class Attachments implements ICollection {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.collection.add({
|
await this.collection.upsert({
|
||||||
type: "attachment",
|
type: "attachment",
|
||||||
id,
|
id,
|
||||||
iv,
|
iv,
|
||||||
salt,
|
salt,
|
||||||
length,
|
size,
|
||||||
alg,
|
alg,
|
||||||
key,
|
encryptionKey,
|
||||||
chunkSize,
|
chunkSize,
|
||||||
metadata: {
|
|
||||||
hash,
|
filename:
|
||||||
hashType,
|
filename ||
|
||||||
filename: getFileNameWithExtension(filename, type),
|
getFileNameWithExtension(
|
||||||
type: type || "application/octet-stream"
|
filename || hash,
|
||||||
},
|
mimeType || "application/octet-stream"
|
||||||
|
),
|
||||||
|
hash,
|
||||||
|
hashType,
|
||||||
|
mimeType: mimeType || "application/octet-stream",
|
||||||
|
|
||||||
dateCreated: attachment.dateCreated || Date.now(),
|
dateCreated: attachment.dateCreated || Date.now(),
|
||||||
dateModified: attachment.dateModified || Date.now(),
|
dateModified: attachment.dateModified || Date.now(),
|
||||||
dateUploaded: attachment.dateUploaded,
|
dateUploaded: attachment.dateUploaded,
|
||||||
dateDeleted: undefined,
|
|
||||||
failed: attachment.failed
|
failed: attachment.failed
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateKey() {
|
async generateKey() {
|
||||||
@@ -189,7 +188,9 @@ export class Attachments implements ICollection {
|
|||||||
return await this.db.crypto().generateRandomKey();
|
return await this.db.crypto().generateRandomKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptKey(key: Cipher<"base64">): Promise<SerializedKey | null> {
|
async decryptKey(keyJSON: string): Promise<SerializedKey | null> {
|
||||||
|
const key = JSON.parse(keyJSON);
|
||||||
|
if (!isCipher(key)) return null;
|
||||||
const encryptionKey = await this._getEncryptionKey();
|
const encryptionKey = await this._getEncryptionKey();
|
||||||
const plainData = await this.db.storage().decrypt(encryptionKey, key);
|
const plainData = await this.db.storage().decrypt(encryptionKey, key);
|
||||||
if (!plainData) return null;
|
if (!plainData) return null;
|
||||||
@@ -197,8 +198,8 @@ export class Attachments implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remove(hashOrId: string, localOnly: boolean) {
|
async remove(hashOrId: string, localOnly: boolean) {
|
||||||
const attachment = this.attachment(hashOrId);
|
const attachment = await this.attachment(hashOrId);
|
||||||
if (!attachment || !attachment.metadata) return false;
|
if (!attachment) return false;
|
||||||
|
|
||||||
if (!localOnly && !(await this.canDetach(attachment)))
|
if (!localOnly && !(await this.canDetach(attachment)))
|
||||||
throw new Error("This attachment is inside a locked note.");
|
throw new Error("This attachment is inside a locked note.");
|
||||||
@@ -206,116 +207,115 @@ export class Attachments implements ICollection {
|
|||||||
if (
|
if (
|
||||||
await this.db
|
await this.db
|
||||||
.fs()
|
.fs()
|
||||||
.deleteFile(
|
.deleteFile(attachment.hash, localOnly || !attachment.dateUploaded)
|
||||||
attachment.metadata.hash,
|
|
||||||
localOnly || !attachment.dateUploaded
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
if (!localOnly) {
|
if (!localOnly) {
|
||||||
await this.detach(attachment);
|
await this.detach(attachment);
|
||||||
}
|
}
|
||||||
await this.collection.remove(attachment.id);
|
await this.collection.softDelete([attachment.id]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async detach(attachment: Attachment) {
|
async detach(attachment: Attachment) {
|
||||||
for (const note of this.db.relations.from(attachment, "note").resolved()) {
|
for (const note of await this.db.relations
|
||||||
|
.from(attachment, "note")
|
||||||
|
.resolve()) {
|
||||||
if (!note || !note.contentId) continue;
|
if (!note || !note.contentId) continue;
|
||||||
await this.db.content.removeAttachments(note.contentId, [
|
await this.db.content.removeAttachments(note.contentId, [
|
||||||
attachment.metadata.hash
|
attachment.hash
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async canDetach(attachment: Attachment) {
|
private async canDetach(attachment: Attachment) {
|
||||||
return this.db.relations
|
return (await this.db.relations.from(attachment, "note").resolve()).every(
|
||||||
.from(attachment, "note")
|
(note) => !note.locked
|
||||||
.resolved()
|
);
|
||||||
.every((note) => !note.locked);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ofNote(
|
async ofNote(
|
||||||
noteId: string,
|
noteId: string,
|
||||||
...types: ("files" | "images" | "webclips" | "all")[]
|
...types: ("files" | "images" | "webclips" | "all")[]
|
||||||
): Attachment[] {
|
): Promise<Attachment[]> {
|
||||||
const noteAttachments = this.db.relations
|
const noteAttachments = await this.db.relations
|
||||||
.from({ type: "note", id: noteId }, "attachment")
|
.from({ type: "note", id: noteId }, "attachment")
|
||||||
.resolved();
|
.resolve();
|
||||||
|
|
||||||
if (types.includes("all")) return noteAttachments;
|
if (types.includes("all")) return noteAttachments;
|
||||||
|
|
||||||
return noteAttachments.filter((a) => {
|
return noteAttachments.filter((a) => {
|
||||||
if (isImage(a.metadata.type) && types.includes("images")) return true;
|
if (isImage(a.mimeType) && types.includes("images")) return true;
|
||||||
else if (isWebClip(a.metadata.type) && types.includes("webclips"))
|
else if (isWebClip(a.mimeType) && types.includes("webclips")) return true;
|
||||||
return true;
|
|
||||||
else if (types.includes("files")) return true;
|
else if (types.includes("files")) return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
exists(hash: string) {
|
async exists(hash: string) {
|
||||||
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
return !!(await this.attachment(hash));
|
||||||
return !!attachment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async read<TOutputFormat extends DataFormat>(
|
async read<TOutputFormat extends DataFormat>(
|
||||||
hash: string,
|
hash: string,
|
||||||
outputType: TOutputFormat
|
outputType: TOutputFormat
|
||||||
): Promise<Output<TOutputFormat> | undefined> {
|
): Promise<Output<TOutputFormat> | undefined> {
|
||||||
const attachment = this.all.find((a) => a.metadata.hash === hash);
|
const attachment = await this.attachment(hash);
|
||||||
if (!attachment) return;
|
if (!attachment) return;
|
||||||
|
|
||||||
const key = await this.decryptKey(attachment.key);
|
const key = await this.decryptKey(attachment.encryptionKey);
|
||||||
if (!key) return;
|
if (!key) return;
|
||||||
const data = await this.db
|
const data = await this.db.fs().readEncrypted(attachment.hash, key, {
|
||||||
.fs()
|
chunkSize: attachment.chunkSize,
|
||||||
.readEncrypted(attachment.metadata.hash, key, {
|
iv: attachment.iv,
|
||||||
chunkSize: attachment.chunkSize,
|
salt: attachment.salt,
|
||||||
iv: attachment.iv,
|
size: attachment.size,
|
||||||
salt: attachment.salt,
|
alg: attachment.alg,
|
||||||
length: attachment.length,
|
outputType
|
||||||
alg: attachment.alg,
|
});
|
||||||
outputType
|
|
||||||
});
|
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
outputType === "base64"
|
outputType === "base64"
|
||||||
? dataurl.fromObject({
|
? dataurl.fromObject({
|
||||||
type: attachment.metadata.type,
|
mimeType: attachment.mimeType,
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
: data
|
: data
|
||||||
) as Output<TOutputFormat>;
|
) as Output<TOutputFormat>;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment(hashOrId: string) {
|
async attachment(hashOrId: string): Promise<Attachment | undefined> {
|
||||||
return this.all.find(
|
return await this.db
|
||||||
(a) => a.id === hashOrId || a.metadata?.hash === hashOrId
|
.sql()
|
||||||
);
|
.selectFrom("attachments")
|
||||||
|
.selectAll()
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([eb("id", "==", hashOrId), eb("hash", "==", hashOrId)])
|
||||||
|
)
|
||||||
|
.where("deleted", "is", null)
|
||||||
|
.$narrowType<Attachment>()
|
||||||
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
markAsUploaded(id: string) {
|
markAsUploaded(id: string) {
|
||||||
const attachment = this.attachment(id);
|
return this.collection.update([id], {
|
||||||
if (!attachment) return;
|
dateUploaded: Date.now(),
|
||||||
attachment.dateUploaded = Date.now();
|
failed: null
|
||||||
attachment.failed = undefined;
|
});
|
||||||
return this.collection.update(attachment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(id: string) {
|
reset(id: string) {
|
||||||
const attachment = this.attachment(id);
|
return this.collection.update([id], {
|
||||||
if (!attachment) return;
|
dateUploaded: null
|
||||||
attachment.dateUploaded = undefined;
|
});
|
||||||
return this.collection.update(attachment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
markAsFailed(id: string, reason: string) {
|
markAsFailed(id: string, reason: string) {
|
||||||
const attachment = this.attachment(id);
|
return this.collection.update([id], {
|
||||||
if (!attachment) return;
|
dateUploaded: null,
|
||||||
attachment.failed = reason;
|
failed: reason
|
||||||
return this.collection.update(attachment);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(
|
async save(
|
||||||
@@ -325,7 +325,7 @@ export class Attachments implements ICollection {
|
|||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const hashResult = await this.db.fs().hashBase64(data);
|
const hashResult = await this.db.fs().hashBase64(data);
|
||||||
if (!hashResult) return;
|
if (!hashResult) return;
|
||||||
if (this.exists(hashResult.hash)) return hashResult.hash;
|
if (await this.exists(hashResult.hash)) return hashResult.hash;
|
||||||
|
|
||||||
const key = await this.generateKey();
|
const key = await this.generateKey();
|
||||||
const { hash, hashType, ...encryptionMetadata } = await this.db
|
const { hash, hashType, ...encryptionMetadata } = await this.db
|
||||||
@@ -335,27 +335,23 @@ export class Attachments implements ICollection {
|
|||||||
await this.add({
|
await this.add({
|
||||||
...encryptionMetadata,
|
...encryptionMetadata,
|
||||||
key,
|
key,
|
||||||
metadata: {
|
|
||||||
filename: filename || hash,
|
filename: filename || hash,
|
||||||
hash,
|
hash,
|
||||||
hashType,
|
hashType,
|
||||||
type: mimeType || "application/octet-stream"
|
mimeType: mimeType || "application/octet-stream"
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadMedia(noteId: string, hashesToLoad?: string[]) {
|
async downloadMedia(noteId: string, hashesToLoad?: string[]) {
|
||||||
let attachments = this.ofNote(noteId, "images", "webclips");
|
let attachments = await this.ofNote(noteId, "images", "webclips");
|
||||||
if (hashesToLoad)
|
if (hashesToLoad)
|
||||||
attachments = attachments.filter((a) =>
|
attachments = attachments.filter((a) => hasItem(hashesToLoad, a.hash));
|
||||||
hasItem(hashesToLoad, a.metadata.hash)
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.db.fs().queueDownloads(
|
await this.db.fs().queueDownloads(
|
||||||
attachments.map((a) => ({
|
attachments.map((a) => ({
|
||||||
filename: a.metadata.hash,
|
filename: a.hash,
|
||||||
metadata: a.metadata,
|
|
||||||
chunkSize: a.chunkSize
|
chunkSize: a.chunkSize
|
||||||
})),
|
})),
|
||||||
noteId,
|
noteId,
|
||||||
@@ -375,65 +371,60 @@ export class Attachments implements ICollection {
|
|||||||
const isDeleted = await this.db.fs().deleteFile(attachment.metadata.hash);
|
const isDeleted = await this.db.fs().deleteFile(attachment.metadata.hash);
|
||||||
if (!isDeleted) continue;
|
if (!isDeleted) continue;
|
||||||
|
|
||||||
await this.collection.remove(attachment.id);
|
await this.collection.softDelete(attachment.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get pending() {
|
// get pending() {
|
||||||
return this.all.filter(
|
// return this.all.filter(
|
||||||
(attachment) => !attachment.dateUploaded || attachment.dateUploaded <= 0
|
// (attachment) =>
|
||||||
);
|
// (!attachment.dateUploaded || attachment.dateUploaded <= 0) &&
|
||||||
}
|
// this.db.relations.to(attachment, "note").length > 0
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
get uploaded() {
|
// get uploaded() {
|
||||||
return this.all.filter((attachment) => !!attachment.dateUploaded);
|
// return this.all.filter((attachment) => !!attachment.dateUploaded);
|
||||||
}
|
// }
|
||||||
|
|
||||||
get syncable() {
|
// get syncable() {
|
||||||
return this.collection
|
// return this.collection
|
||||||
.raw()
|
// .raw()
|
||||||
.filter(
|
// .filter(
|
||||||
(attachment) => isDeleted(attachment) || !!attachment.dateUploaded
|
// (attachment) => isDeleted(attachment) || !!attachment.dateUploaded
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
get deleted() {
|
// get deleted() {
|
||||||
return this.all.filter((attachment) => !!attachment.dateDeleted);
|
// return this.all.filter((attachment) => !!attachment.dateDeleted);
|
||||||
}
|
// }
|
||||||
|
|
||||||
get images() {
|
// get images() {
|
||||||
return this.all.filter(
|
// return this.all.filter((attachment) => isImage(attachment.metadata.type));
|
||||||
(attachment) => attachment.metadata && isImage(attachment.metadata.type)
|
// }
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get webclips() {
|
// get webclips() {
|
||||||
return this.all.filter(
|
// return this.all.filter((attachment) => isWebClip(attachment.metadata.type));
|
||||||
(attachment) => attachment.metadata && isWebClip(attachment.metadata.type)
|
// }
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get media() {
|
// get media() {
|
||||||
return this.all.filter(
|
// return this.all.filter(
|
||||||
(attachment) =>
|
// (attachment) =>
|
||||||
attachment.metadata &&
|
// isImage(attachment.metadata.type) || isWebClip(attachment.metadata.type)
|
||||||
(isImage(attachment.metadata.type) ||
|
// );
|
||||||
isWebClip(attachment.metadata.type))
|
// }
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get files() {
|
// get files() {
|
||||||
return this.all.filter(
|
// return this.all.filter(
|
||||||
(attachment) =>
|
// (attachment) =>
|
||||||
attachment.metadata &&
|
// !isImage(attachment.metadata.type) &&
|
||||||
!isImage(attachment.metadata.type) &&
|
// !isWebClip(attachment.metadata.type)
|
||||||
!isWebClip(attachment.metadata.type)
|
// );
|
||||||
);
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
get all() {
|
// get all() {
|
||||||
return this.collection.items();
|
// return this.collection.items();
|
||||||
}
|
// }
|
||||||
|
|
||||||
private async encryptKey(key: SerializedKey) {
|
private async encryptKey(key: SerializedKey) {
|
||||||
const encryptionKey = await this._getEncryptionKey();
|
const encryptionKey = await this._getEncryptionKey();
|
||||||
@@ -454,15 +445,15 @@ export class Attachments implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getOutputType(attachment: Attachment): DataFormat {
|
export function getOutputType(attachment: Attachment): DataFormat {
|
||||||
if (attachment.metadata.type === "application/vnd.notesnook.web-clip")
|
if (attachment.mimeType === "application/vnd.notesnook.web-clip")
|
||||||
return "text";
|
return "text";
|
||||||
else if (attachment.metadata.type.startsWith("image/")) return "base64";
|
else if (attachment.mimeType.startsWith("image/")) return "base64";
|
||||||
return "uint8array";
|
return "uint8array";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAttachmentType(attachment: Attachment) {
|
function getAttachmentType(attachment: Attachment) {
|
||||||
if (attachment.metadata.type === "application/vnd.notesnook.web-clip")
|
if (attachment.mimeType === "application/vnd.notesnook.web-clip")
|
||||||
return "webclip";
|
return "webclip";
|
||||||
else if (attachment.metadata.type.startsWith("image/")) return "image";
|
else if (attachment.mimeType.startsWith("image/")) return "image";
|
||||||
else return "generic";
|
else return "generic";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
import { getId } from "../utils/id";
|
import { getId } from "../utils/id";
|
||||||
import { Color, MaybeDeletedItem } from "../types";
|
import { Color } from "../types";
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { CachedCollection } from "../database/cached-collection";
|
|
||||||
import { Tags } from "./tags";
|
import { Tags } from "./tags";
|
||||||
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
|
|
||||||
export const DefaultColors: Record<string, string> = {
|
export const DefaultColors: Record<string, string> = {
|
||||||
red: "#f44336",
|
red: "#f44336",
|
||||||
@@ -36,13 +36,9 @@ export const DefaultColors: Record<string, string> = {
|
|||||||
|
|
||||||
export class Colors implements ICollection {
|
export class Colors implements ICollection {
|
||||||
name = "colors";
|
name = "colors";
|
||||||
readonly collection: CachedCollection<"colors", Color>;
|
readonly collection: SQLCollection<"colors", Color>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new CachedCollection(
|
this.collection = new SQLCollection(db.sql, "colors", db.eventManager);
|
||||||
db.storage,
|
|
||||||
"colors",
|
|
||||||
db.eventManager
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -53,27 +49,27 @@ export class Colors implements ICollection {
|
|||||||
return this.collection.get(id);
|
return this.collection.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async merge(remoteColor: MaybeDeletedItem<Color>) {
|
// async merge(remoteColor: MaybeDeletedItem<Color>) {
|
||||||
if (!remoteColor) return;
|
// if (!remoteColor) return;
|
||||||
|
|
||||||
const localColor = this.collection.get(remoteColor.id);
|
// const localColor = this.collection.get(remoteColor.id);
|
||||||
if (!localColor || remoteColor.dateModified > localColor.dateModified)
|
// if (!localColor || remoteColor.dateModified > localColor.dateModified)
|
||||||
await this.collection.add(remoteColor);
|
// await this.collection.add(remoteColor);
|
||||||
}
|
// }
|
||||||
|
|
||||||
async add(item: Partial<Color>) {
|
async add(item: Partial<Color>) {
|
||||||
if (item.remote)
|
if (item.remote)
|
||||||
throw new Error("Please use db.colors.merge to merge remote colors.");
|
throw new Error("Please use db.colors.merge to merge remote colors.");
|
||||||
|
|
||||||
const id = item.id || getId(item.dateCreated);
|
const id = item.id || getId(item.dateCreated);
|
||||||
const oldColor = this.color(id);
|
const oldColor = await this.color(id);
|
||||||
|
|
||||||
item.title = item.title ? Tags.sanitize(item.title) : item.title;
|
item.title = item.title ? Tags.sanitize(item.title) : item.title;
|
||||||
if (!item.title && !oldColor?.title) throw new Error("Title is required.");
|
if (!item.title && !oldColor?.title) throw new Error("Title is required.");
|
||||||
if (!item.colorCode && !oldColor?.colorCode)
|
if (!item.colorCode && !oldColor?.colorCode)
|
||||||
throw new Error("Color code is required.");
|
throw new Error("Color code is required.");
|
||||||
|
|
||||||
const color: Color = {
|
await this.collection.upsert({
|
||||||
id,
|
id,
|
||||||
dateCreated: item.dateCreated || oldColor?.dateCreated || Date.now(),
|
dateCreated: item.dateCreated || oldColor?.dateCreated || Date.now(),
|
||||||
dateModified: item.dateModified || oldColor?.dateModified || Date.now(),
|
dateModified: item.dateModified || oldColor?.dateModified || Date.now(),
|
||||||
@@ -81,34 +77,35 @@ export class Colors implements ICollection {
|
|||||||
colorCode: item.colorCode || oldColor?.colorCode || "",
|
colorCode: item.colorCode || oldColor?.colorCode || "",
|
||||||
type: "color",
|
type: "color",
|
||||||
remote: false
|
remote: false
|
||||||
};
|
});
|
||||||
await this.collection.add(color);
|
return id;
|
||||||
return color.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
// get raw() {
|
||||||
return this.collection.raw();
|
// return this.collection.raw();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// get all(): Color[] {
|
||||||
|
// return this.collection.items();
|
||||||
|
// }
|
||||||
|
|
||||||
|
async remove(...ids: string[]) {
|
||||||
|
await this.db.transaction(async () => {
|
||||||
|
await this.db.relations.unlinkOfType("color", ids);
|
||||||
|
await this.collection.softDelete(ids);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get all(): Color[] {
|
// async delete(id: string) {
|
||||||
return this.collection.items();
|
// await this.collection.delete(id);
|
||||||
}
|
// await this.db.relations.cleanup();
|
||||||
|
// }
|
||||||
async remove(id: string) {
|
|
||||||
await this.collection.remove(id);
|
|
||||||
await this.db.relations.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(id: string) {
|
|
||||||
await this.collection.delete(id);
|
|
||||||
await this.db.relations.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
exists(id: string) {
|
exists(id: string) {
|
||||||
return this.collection.exists(id);
|
return this.collection.exists(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
find(idOrTitle: string) {
|
// find(idOrTitle: string) {
|
||||||
return this.all.find((t) => t.title === idOrTitle || t.id === idOrTitle);
|
// return this.all.find((t) => t.title === idOrTitle || t.id === idOrTitle);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,12 @@ import {
|
|||||||
ContentItem,
|
ContentItem,
|
||||||
ContentType,
|
ContentType,
|
||||||
EncryptedContentItem,
|
EncryptedContentItem,
|
||||||
MaybeDeletedItem,
|
|
||||||
UnencryptedContentItem,
|
UnencryptedContentItem,
|
||||||
isDeleted
|
isDeleted
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { IndexedCollection } from "../database/indexed-collection";
|
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { getOutputType } from "./attachments";
|
import { getOutputType } from "./attachments";
|
||||||
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
|
|
||||||
export const EMPTY_CONTENT = (noteId: string): UnencryptedContentItem => ({
|
export const EMPTY_CONTENT = (noteId: string): UnencryptedContentItem => ({
|
||||||
noteId,
|
noteId,
|
||||||
@@ -48,27 +47,15 @@ export const EMPTY_CONTENT = (noteId: string): UnencryptedContentItem => ({
|
|||||||
|
|
||||||
export class Content implements ICollection {
|
export class Content implements ICollection {
|
||||||
name = "content";
|
name = "content";
|
||||||
readonly collection: IndexedCollection<"content", ContentItem>;
|
readonly collection: SQLCollection<"content", ContentItem>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new IndexedCollection(
|
this.collection = new SQLCollection(db.sql, "content", db.eventManager);
|
||||||
db.storage,
|
|
||||||
"content",
|
|
||||||
db.eventManager
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.collection.init();
|
await this.collection.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async merge(content: MaybeDeletedItem<ContentItem>) {
|
|
||||||
return await this.collection.addItem(
|
|
||||||
isDeleted(content) || !isUnencryptedContent(content)
|
|
||||||
? content
|
|
||||||
: await this.extractAttachments(content)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async add(content: Partial<ContentItem>) {
|
async add(content: Partial<ContentItem>) {
|
||||||
if (typeof content.data === "object") {
|
if (typeof content.data === "object") {
|
||||||
if ("data" in content.data && typeof content.data.data === "string")
|
if ("data" in content.data && typeof content.data.data === "string")
|
||||||
@@ -106,7 +93,7 @@ export class Content implements ICollection {
|
|||||||
conflicted: content.conflicted,
|
conflicted: content.conflicted,
|
||||||
dateResolved: content.dateResolved
|
dateResolved: content.dateResolved
|
||||||
};
|
};
|
||||||
await this.collection.addItem(
|
await this.collection.upsert(
|
||||||
isUnencryptedContent(contentItem)
|
isUnencryptedContent(contentItem)
|
||||||
? await this.extractAttachments(contentItem)
|
? await this.extractAttachments(contentItem)
|
||||||
: contentItem
|
: contentItem
|
||||||
@@ -133,29 +120,46 @@ export class Content implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async raw(id: string) {
|
async raw(id: string) {
|
||||||
const content = await this.collection.getItem(id);
|
const content = await this.collection.get(id);
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id: string) {
|
remove(...ids: string[]) {
|
||||||
if (!id) return;
|
return this.collection.softDelete(ids);
|
||||||
return this.collection.removeItem(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
multi(ids: string[]) {
|
removeByNoteId(...ids: string[]) {
|
||||||
return this.collection.getItems(ids);
|
return this.db
|
||||||
|
.sql()
|
||||||
|
.replaceInto("content")
|
||||||
|
.columns(["id", "dateModified", "deleted"])
|
||||||
|
.expression((eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom("content")
|
||||||
|
.where("noteId", "in", ids)
|
||||||
|
.select((eb) => [
|
||||||
|
"content.id",
|
||||||
|
eb.lit(Date.now()).as("dateModified"),
|
||||||
|
eb.lit(1).as("deleted")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// multi(ids: string[]) {
|
||||||
|
// return this.collection.getItems(ids);
|
||||||
|
// }
|
||||||
|
|
||||||
exists(id: string) {
|
exists(id: string) {
|
||||||
return this.collection.exists(id);
|
return this.collection.exists(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async all() {
|
// async all() {
|
||||||
return Object.values(
|
// return Object.values(
|
||||||
await this.collection.getItems(this.collection.indexer.indices)
|
// await this.collection.getItems(this.collection.indexer.indices)
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
insertMedia(contentItem: UnencryptedContentItem) {
|
insertMedia(contentItem: UnencryptedContentItem) {
|
||||||
return this.insert(contentItem, async (hashes) => {
|
return this.insert(contentItem, async (hashes) => {
|
||||||
@@ -183,17 +187,16 @@ export class Content implements ICollection {
|
|||||||
const content = getContentFromData(contentItem.type, contentItem.data);
|
const content = getContentFromData(contentItem.type, contentItem.data);
|
||||||
if (!content) return contentItem;
|
if (!content) return contentItem;
|
||||||
contentItem.data = await content.insertMedia(async (hashes) => {
|
contentItem.data = await content.insertMedia(async (hashes) => {
|
||||||
const attachments = hashes.reduce((attachments, hash) => {
|
const attachments: Attachment[] = [];
|
||||||
const attachment = this.db.attachments.attachment(hash);
|
for (const hash of hashes) {
|
||||||
if (!attachment) return attachments;
|
const attachment = await this.db.attachments.attachment(hash);
|
||||||
|
if (!attachment) continue;
|
||||||
attachments.push(attachment);
|
attachments.push(attachment);
|
||||||
return attachments;
|
}
|
||||||
}, [] as Attachment[]);
|
|
||||||
|
|
||||||
await this.db.fs().queueDownloads(
|
await this.db.fs().queueDownloads(
|
||||||
attachments.map((a) => ({
|
attachments.map((a) => ({
|
||||||
filename: a.metadata.hash,
|
filename: a.hash,
|
||||||
metadata: a.metadata,
|
|
||||||
chunkSize: a.chunkSize
|
chunkSize: a.chunkSize
|
||||||
})),
|
})),
|
||||||
groupId,
|
groupId,
|
||||||
@@ -203,11 +206,11 @@ export class Content implements ICollection {
|
|||||||
const sources: Record<string, string> = {};
|
const sources: Record<string, string> = {};
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
const src = await this.db.attachments.read(
|
const src = await this.db.attachments.read(
|
||||||
attachment.metadata.hash,
|
attachment.hash,
|
||||||
getOutputType(attachment)
|
getOutputType(attachment)
|
||||||
);
|
);
|
||||||
if (!src) continue;
|
if (!src) continue;
|
||||||
sources[attachment.metadata.hash] = src;
|
sources[attachment.hash] = src;
|
||||||
}
|
}
|
||||||
return sources;
|
return sources;
|
||||||
});
|
});
|
||||||
@@ -243,16 +246,16 @@ export class Content implements ICollection {
|
|||||||
this.db.attachments.save
|
this.db.attachments.save
|
||||||
);
|
);
|
||||||
|
|
||||||
const noteAttachments = this.db.relations
|
const noteAttachments = await this.db.relations
|
||||||
.from({ type: "note", id: contentItem.noteId }, "attachment")
|
.from({ type: "note", id: contentItem.noteId }, "attachment")
|
||||||
.resolved();
|
.resolve();
|
||||||
|
|
||||||
const toDelete = noteAttachments.filter((attachment) => {
|
const toDelete = noteAttachments.filter((attachment) => {
|
||||||
return hashes.every((hash) => hash !== attachment.metadata.hash);
|
return hashes.every((hash) => hash !== attachment.hash);
|
||||||
});
|
});
|
||||||
|
|
||||||
const toAdd = hashes.filter((hash) => {
|
const toAdd = hashes.filter((hash) => {
|
||||||
return hash && noteAttachments.every((a) => hash !== a.metadata.hash);
|
return hash && noteAttachments.every((a) => hash !== a.hash);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const attachment of toDelete) {
|
for (const attachment of toDelete) {
|
||||||
@@ -266,7 +269,7 @@ export class Content implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const hash of toAdd) {
|
for (const hash of toAdd) {
|
||||||
const attachment = this.db.attachments.attachment(hash);
|
const attachment = await this.db.attachments.attachment(hash);
|
||||||
if (!attachment) continue;
|
if (!attachment) continue;
|
||||||
await this.db.relations.add(
|
await this.db.relations.add(
|
||||||
{
|
{
|
||||||
@@ -284,24 +287,24 @@ export class Content implements ICollection {
|
|||||||
return contentItem;
|
return contentItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup() {
|
// async cleanup() {
|
||||||
const indices = this.collection.indexer.indices;
|
// const indices = this.collection.indexer.indices;
|
||||||
await this.db.notes.init();
|
// await this.db.notes.init();
|
||||||
const notes = this.db.notes.all;
|
// const notes = this.db.notes.all;
|
||||||
if (!notes.length && indices.length > 0) return [];
|
// if (!notes.length && indices.length > 0) return [];
|
||||||
const ids = [];
|
// const ids = [];
|
||||||
for (const contentId of indices) {
|
// for (const contentId of indices) {
|
||||||
const noteIndex = notes.findIndex((note) => note.contentId === contentId);
|
// const noteIndex = notes.findIndex((note) => note.contentId === contentId);
|
||||||
const isOrphaned = noteIndex === -1;
|
// const isOrphaned = noteIndex === -1;
|
||||||
if (isOrphaned) {
|
// if (isOrphaned) {
|
||||||
ids.push(contentId);
|
// ids.push(contentId);
|
||||||
await this.collection.deleteItem(contentId);
|
// await this.collection.deleteItem(contentId);
|
||||||
} else if (notes[noteIndex].localOnly) {
|
// } else if (notes[noteIndex].localOnly) {
|
||||||
ids.push(contentId);
|
// ids.push(contentId);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return ids;
|
// return ids;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isUnencryptedContent(
|
export function isUnencryptedContent(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { isCipher } from "../database/crypto";
|
import { isCipher } from "../database/crypto";
|
||||||
import { IndexedCollection } from "../database/indexed-collection";
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
import { HistorySession, isDeleted } from "../types";
|
import { HistorySession, isDeleted } from "../types";
|
||||||
import { makeSessionContentId } from "../utils/id";
|
import { makeSessionContentId } from "../utils/id";
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
@@ -29,13 +29,9 @@ export class NoteHistory implements ICollection {
|
|||||||
name = "notehistory";
|
name = "notehistory";
|
||||||
versionsLimit = 100;
|
versionsLimit = 100;
|
||||||
sessionContent = new SessionContent(this.db);
|
sessionContent = new SessionContent(this.db);
|
||||||
private readonly collection: IndexedCollection<"notehistory", HistorySession>;
|
private readonly collection: SQLCollection<"notehistory", HistorySession>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new IndexedCollection(
|
this.collection = new SQLCollection(db.sql, "notehistory", db.eventManager);
|
||||||
db.storage,
|
|
||||||
"notehistory",
|
|
||||||
db.eventManager
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -43,10 +39,6 @@ export class NoteHistory implements ICollection {
|
|||||||
await this.sessionContent.init();
|
await this.sessionContent.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async merge(item: HistorySession) {
|
|
||||||
await this.collection.addItem(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(noteId: string) {
|
async get(noteId: string) {
|
||||||
if (!noteId) return [];
|
if (!noteId) return [];
|
||||||
|
|
||||||
@@ -67,7 +59,7 @@ export class NoteHistory implements ICollection {
|
|||||||
content: NoteContent<boolean>
|
content: NoteContent<boolean>
|
||||||
) {
|
) {
|
||||||
sessionId = `${noteId}_${sessionId}`;
|
sessionId = `${noteId}_${sessionId}`;
|
||||||
const oldSession = await this.collection.getItem(sessionId);
|
const oldSession = await this.collection.get(sessionId);
|
||||||
|
|
||||||
if (oldSession && isDeleted(oldSession)) return;
|
if (oldSession && isDeleted(oldSession)) return;
|
||||||
|
|
||||||
@@ -82,7 +74,7 @@ export class NoteHistory implements ICollection {
|
|||||||
locked
|
locked
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.collection.addItem(session);
|
await this.collection.upsert(session);
|
||||||
await this.sessionContent.add(sessionId, content, locked);
|
await this.sessionContent.add(sessionId, content, locked);
|
||||||
await this.cleanup(noteId);
|
await this.cleanup(noteId);
|
||||||
|
|
||||||
@@ -104,19 +96,18 @@ export class NoteHistory implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async content(sessionId: string) {
|
async content(sessionId: string) {
|
||||||
const session = await this.collection.getItem(sessionId);
|
const session = await this.collection.get(sessionId);
|
||||||
if (!session || isDeleted(session)) return;
|
if (!session || isDeleted(session)) return;
|
||||||
return await this.sessionContent.get(session.sessionContentId);
|
return await this.sessionContent.get(session.sessionContentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(sessionId: string) {
|
async remove(sessionId: string) {
|
||||||
const session = await this.collection.getItem(sessionId);
|
const session = await this.collection.get(sessionId);
|
||||||
if (!session || isDeleted(session)) return;
|
if (!session || isDeleted(session)) return;
|
||||||
await this._remove(session);
|
await this._remove(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearSessions(noteId: string) {
|
async clearSessions(...noteIds: string[]) {
|
||||||
if (!noteId) return;
|
|
||||||
const history = await this.get(noteId);
|
const history = await this.get(noteId);
|
||||||
for (const item of history) {
|
for (const item of history) {
|
||||||
await this._remove(item);
|
await this._remove(item);
|
||||||
@@ -124,12 +115,12 @@ export class NoteHistory implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _remove(session: HistorySession) {
|
private async _remove(session: HistorySession) {
|
||||||
await this.collection.deleteItem(session.id);
|
await this.collection.delete(session.id);
|
||||||
await this.sessionContent.remove(session.sessionContentId);
|
await this.sessionContent.remove(session.sessionContentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(sessionId: string) {
|
async restore(sessionId: string) {
|
||||||
const session = await this.collection.getItem(sessionId);
|
const session = await this.collection.get(sessionId);
|
||||||
if (!session || isDeleted(session)) return;
|
if (!session || isDeleted(session)) return;
|
||||||
|
|
||||||
const content = await this.sessionContent.get(session.sessionContentId);
|
const content = await this.sessionContent.get(session.sessionContentId);
|
||||||
@@ -153,14 +144,14 @@ export class NoteHistory implements ICollection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async all() {
|
// async all() {
|
||||||
return this.getSessions(this.collection.indexer.indices);
|
// return this.getSessions(this.collection.indexer.indices);
|
||||||
}
|
// }
|
||||||
|
|
||||||
private async getSessions(sessionIds: string[]): Promise<HistorySession[]> {
|
// private async getSessions(sessionIds: string[]): Promise<HistorySession[]> {
|
||||||
const items = await this.collection.getItems(sessionIds);
|
// const items = await this.collection.getItems(sessionIds);
|
||||||
return Object.values(items).filter(
|
// return Object.values(items).filter(
|
||||||
(a) => !isDeleted(a)
|
// (a) => !isDeleted(a)
|
||||||
) as HistorySession[];
|
// ) as HistorySession[];
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,25 +17,20 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createNotebookModel } from "../models/notebook";
|
|
||||||
import { getId } from "../utils/id";
|
import { getId } from "../utils/id";
|
||||||
import { CachedCollection } from "../database/cached-collection";
|
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { BaseTrashItem, Notebook, TrashOrItem, isTrashItem } from "../types";
|
import { Notebook, TrashOrItem, isTrashItem } from "../types";
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
|
|
||||||
export class Notebooks implements ICollection {
|
export class Notebooks implements ICollection {
|
||||||
name = "notebooks";
|
name = "notebooks";
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
collection: CachedCollection<"notebooks", TrashOrItem<Notebook>>;
|
collection: SQLCollection<"notebooks", TrashOrItem<Notebook>>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new CachedCollection(
|
this.collection = new SQLCollection(db.sql, "notebooks", db.eventManager);
|
||||||
db.storage,
|
|
||||||
"notebooks",
|
|
||||||
db.eventManager
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -51,7 +46,7 @@ export class Notebooks implements ICollection {
|
|||||||
|
|
||||||
//TODO reliably and efficiently check for duplicates.
|
//TODO reliably and efficiently check for duplicates.
|
||||||
const id = notebookArg.id || getId();
|
const id = notebookArg.id || getId();
|
||||||
const oldNotebook = this.collection.get(id);
|
const oldNotebook = await this.notebook(id);
|
||||||
|
|
||||||
if (oldNotebook && isTrashItem(oldNotebook))
|
if (oldNotebook && isTrashItem(oldNotebook))
|
||||||
throw new Error("Cannot modify trashed notebooks.");
|
throw new Error("Cannot modify trashed notebooks.");
|
||||||
@@ -64,7 +59,7 @@ export class Notebooks implements ICollection {
|
|||||||
if (!mergedNotebook.title)
|
if (!mergedNotebook.title)
|
||||||
throw new Error("Notebook must contain a title.");
|
throw new Error("Notebook must contain a title.");
|
||||||
|
|
||||||
const notebook: Notebook = {
|
await this.collection.upsert({
|
||||||
id,
|
id,
|
||||||
type: "notebook",
|
type: "notebook",
|
||||||
title: mergedNotebook.title,
|
title: mergedNotebook.title,
|
||||||
@@ -74,79 +69,87 @@ export class Notebooks implements ICollection {
|
|||||||
dateCreated: mergedNotebook.dateCreated || Date.now(),
|
dateCreated: mergedNotebook.dateCreated || Date.now(),
|
||||||
dateModified: mergedNotebook.dateModified || Date.now(),
|
dateModified: mergedNotebook.dateModified || Date.now(),
|
||||||
dateEdited: Date.now()
|
dateEdited: Date.now()
|
||||||
};
|
});
|
||||||
|
|
||||||
await this.collection.add(notebook);
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
// get raw() {
|
||||||
return this.collection.raw();
|
// return this.collection.raw();
|
||||||
}
|
// }
|
||||||
|
|
||||||
get all() {
|
// get all() {
|
||||||
return this.collection.items((note) =>
|
// return this.collection.items((note) =>
|
||||||
isTrashItem(note) ? undefined : note
|
// isTrashItem(note) ? undefined : note
|
||||||
) as Notebook[];
|
// ) as Notebook[];
|
||||||
}
|
// }
|
||||||
|
|
||||||
get pinned() {
|
// get pinned() {
|
||||||
return this.all.filter((item) => item.pinned === true);
|
// return this.all.filter((item) => item.pinned === true);
|
||||||
}
|
// }
|
||||||
|
|
||||||
get trashed() {
|
// get trashed() {
|
||||||
return this.raw.filter((item) =>
|
// return this.raw.filter((item) =>
|
||||||
isTrashItem(item)
|
// isTrashItem(item)
|
||||||
) as BaseTrashItem<Notebook>[];
|
// ) as BaseTrashItem<Notebook>[];
|
||||||
}
|
// }
|
||||||
|
|
||||||
async pin(...ids: string[]) {
|
async pin(...ids: string[]) {
|
||||||
for (const id of ids) {
|
await this.collection.update(ids, { pinned: true });
|
||||||
if (!this.exists(id)) continue;
|
|
||||||
await this.add({ id, pinned: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unpin(...ids: string[]) {
|
async unpin(...ids: string[]) {
|
||||||
for (const id of ids) {
|
await this.collection.update(ids, { pinned: false });
|
||||||
if (!this.exists(id)) continue;
|
|
||||||
await this.add({ id, pinned: false });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
totalNotes(id: string) {
|
async totalNotes(id: string) {
|
||||||
let count = 0;
|
const result = await this.db
|
||||||
const subNotebooks = this.db.relations.from(
|
.sql()
|
||||||
{ type: "notebook", id },
|
.withRecursive(`subNotebooks(id)`, (eb) =>
|
||||||
"notebook"
|
eb
|
||||||
);
|
.selectNoFrom((eb) => eb.val(id).as("id"))
|
||||||
for (const notebook of subNotebooks) {
|
.unionAll((eb) =>
|
||||||
count += this.totalNotes(notebook.to.id);
|
eb
|
||||||
}
|
.selectFrom(["relations", "subNotebooks"])
|
||||||
count += this.db.relations.from({ type: "notebook", id }, "note").length;
|
.select("relations.toId as id")
|
||||||
return count;
|
.where("toType", "==", "notebook")
|
||||||
|
.where("fromType", "==", "notebook")
|
||||||
|
.whereRef("fromId", "==", "subNotebooks.id")
|
||||||
|
.where("toId", "not in", this.db.trash.cache)
|
||||||
|
.$narrowType<{ id: string }>()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.selectFrom("relations")
|
||||||
|
.where("toType", "==", "note")
|
||||||
|
.where("fromType", "==", "notebook")
|
||||||
|
.where("fromId", "in", (eb) =>
|
||||||
|
eb.selectFrom("subNotebooks").select("subNotebooks.id")
|
||||||
|
)
|
||||||
|
.where("toId", "not in", this.db.trash.cache)
|
||||||
|
.select((eb) => eb.fn.count<number>("id").as("totalNotes"))
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
if (!result) return 0;
|
||||||
|
return result.totalNotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
notebook(idOrNotebook: string | Notebook) {
|
async notebook(id: string) {
|
||||||
const notebook =
|
const notebook = await this.collection.get(id);
|
||||||
typeof idOrNotebook === "string"
|
|
||||||
? this.collection.get(idOrNotebook)
|
|
||||||
: idOrNotebook;
|
|
||||||
if (!notebook || isTrashItem(notebook)) return;
|
if (!notebook || isTrashItem(notebook)) return;
|
||||||
return createNotebookModel(notebook, this.db);
|
return notebook;
|
||||||
}
|
}
|
||||||
|
|
||||||
exists(id: string) {
|
exists(id: string) {
|
||||||
return this.collection.exists(id);
|
return this.collection.exists(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async remove(...ids: string[]) {
|
||||||
|
await this.db.trash.add("notebook", ids);
|
||||||
|
}
|
||||||
|
|
||||||
async delete(...ids: string[]) {
|
async delete(...ids: string[]) {
|
||||||
for (const id of ids) {
|
await this.db.transaction(async () => {
|
||||||
const notebook = this.collection.get(id);
|
await this.db.relations.unlinkOfType("notebook", ids);
|
||||||
if (!notebook || isTrashItem(notebook)) continue;
|
await this.collection.softDelete(ids);
|
||||||
await this.collection.remove(id);
|
});
|
||||||
await this.db.shortcuts?.remove(id);
|
|
||||||
await this.db.trash?.add(notebook);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createNoteModel } from "../models/note";
|
|
||||||
import { getId } from "../utils/id";
|
import { getId } from "../utils/id";
|
||||||
import { getContentFromData } from "../content-types";
|
import { getContentFromData } from "../content-types";
|
||||||
import { NEWLINE_STRIP_REGEX, formatTitle } from "../utils/title-format";
|
import { NEWLINE_STRIP_REGEX, formatTitle } from "../utils/title-format";
|
||||||
@@ -26,17 +25,11 @@ import { Tiptap } from "../content-types/tiptap";
|
|||||||
import { EMPTY_CONTENT, isUnencryptedContent } from "./content";
|
import { EMPTY_CONTENT, isUnencryptedContent } from "./content";
|
||||||
import { CHECK_IDS, checkIsUserPremium } from "../common";
|
import { CHECK_IDS, checkIsUserPremium } from "../common";
|
||||||
import { buildFromTemplate } from "../utils/templates";
|
import { buildFromTemplate } from "../utils/templates";
|
||||||
import {
|
import { Note, TrashOrItem, isTrashItem, isDeleted } from "../types";
|
||||||
Note,
|
|
||||||
TrashOrItem,
|
|
||||||
isTrashItem,
|
|
||||||
isDeleted,
|
|
||||||
BaseTrashItem
|
|
||||||
} from "../types";
|
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { CachedCollection } from "../database/cached-collection";
|
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
import { NoteContent } from "./session-content";
|
import { NoteContent } from "./session-content";
|
||||||
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
|
|
||||||
type ExportOptions = {
|
type ExportOptions = {
|
||||||
format: "html" | "md" | "txt" | "md-frontmatter";
|
format: "html" | "md" | "txt" | "md-frontmatter";
|
||||||
@@ -50,17 +43,15 @@ export class Notes implements ICollection {
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
collection: CachedCollection<"notes", TrashOrItem<Note>>;
|
collection: SQLCollection<"notes", TrashOrItem<Note>>;
|
||||||
|
totalNotes = 0;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new CachedCollection(
|
this.collection = new SQLCollection(db.sql, "notes", db.eventManager);
|
||||||
db.storage,
|
|
||||||
"notes",
|
|
||||||
db.eventManager
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.collection.init();
|
await this.collection.init();
|
||||||
|
this.totalNotes = await this.collection.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
async add(
|
async add(
|
||||||
@@ -70,9 +61,7 @@ export class Notes implements ICollection {
|
|||||||
throw new Error("Please use db.notes.merge to merge remote notes.");
|
throw new Error("Please use db.notes.merge to merge remote notes.");
|
||||||
|
|
||||||
const id = item.id || getId();
|
const id = item.id || getId();
|
||||||
const oldNote = this.collection.get(id);
|
const oldNote = await this.note(id);
|
||||||
if (oldNote && isTrashItem(oldNote))
|
|
||||||
throw new Error("Cannot modify trashed notes.");
|
|
||||||
|
|
||||||
const note = {
|
const note = {
|
||||||
...oldNote,
|
...oldNote,
|
||||||
@@ -110,10 +99,10 @@ export class Notes implements ICollection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteTitle = this.getNoteTitle(note, oldNote, note.headline);
|
const noteTitle = await this.getNoteTitle(note, oldNote, note.headline);
|
||||||
if (oldNote && oldNote.title !== noteTitle) note.dateEdited = Date.now();
|
if (oldNote && oldNote.title !== noteTitle) note.dateEdited = Date.now();
|
||||||
|
|
||||||
await this.collection.add({
|
await this.collection.upsert({
|
||||||
id,
|
id,
|
||||||
contentId: note.contentId,
|
contentId: note.contentId,
|
||||||
type: "note",
|
type: "note",
|
||||||
@@ -136,54 +125,61 @@ export class Notes implements ICollection {
|
|||||||
dateModified: note.dateModified || Date.now()
|
dateModified: note.dateModified || Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!oldNote) this.totalNotes++;
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
note(idOrNote: string | Note) {
|
async note(idOrNote: string) {
|
||||||
if (!idOrNote) return;
|
const note = await this.collection.get(idOrNote);
|
||||||
const note =
|
if (!note || isTrashItem(note) || isDeleted(note)) return;
|
||||||
typeof idOrNote === "object" ? idOrNote : this.collection.get(idOrNote);
|
return note;
|
||||||
if (!note || isTrashItem(note)) return;
|
|
||||||
return createNoteModel(note, this.db);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
// note(idOrNote: string | Note) {
|
||||||
return this.collection.raw();
|
// if (!idOrNote) return;
|
||||||
}
|
// const note =
|
||||||
|
// typeof idOrNote === "object" ? idOrNote : this.collection.get(idOrNote);
|
||||||
|
// if (!note || isTrashItem(note)) return;
|
||||||
|
// return createNoteModel(note, this.db);
|
||||||
|
// }
|
||||||
|
|
||||||
get all() {
|
// get raw() {
|
||||||
return this.collection.items((note) =>
|
// return this.collection.raw();
|
||||||
isTrashItem(note) ? undefined : note
|
// }
|
||||||
) as Note[];
|
|
||||||
}
|
|
||||||
|
|
||||||
isTrashed(id: string) {
|
// get all() {
|
||||||
return this.raw.find((item) => item.id === id && isTrashItem(item));
|
// return this.collection.items((note) =>
|
||||||
}
|
// isTrashItem(note) ? undefined : note
|
||||||
|
// ) as Note[];
|
||||||
|
// }
|
||||||
|
|
||||||
get trashed() {
|
// isTrashed(id: string) {
|
||||||
return this.raw.filter((item) =>
|
// return this.raw.find((item) => item.id === id && isTrashItem(item));
|
||||||
isTrashItem(item)
|
// }
|
||||||
) as BaseTrashItem<Note>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
get pinned() {
|
// get trashed() {
|
||||||
return this.all.filter((item) => item.pinned === true);
|
// return this.raw.filter((item) =>
|
||||||
}
|
// isTrashItem(item)
|
||||||
|
// ) as BaseTrashItem<Note>[];
|
||||||
|
// }
|
||||||
|
|
||||||
get conflicted() {
|
// get pinned() {
|
||||||
return this.all.filter((item) => item.conflicted === true);
|
// return this.all.filter((item) => item.pinned === true);
|
||||||
}
|
// }
|
||||||
|
|
||||||
get favorites() {
|
// get conflicted() {
|
||||||
return this.all.filter((item) => item.favorite === true);
|
// return this.all.filter((item) => item.conflicted === true);
|
||||||
}
|
// }
|
||||||
|
|
||||||
get locked(): Note[] {
|
// get favorites() {
|
||||||
return this.all.filter(
|
// return this.all.filter((item) => item.favorite === true);
|
||||||
(item) => !isTrashItem(item) && item.locked === true
|
// }
|
||||||
) as Note[];
|
|
||||||
}
|
// get locked(): Note[] {
|
||||||
|
// return this.all.filter(
|
||||||
|
// (item) => !isTrashItem(item) && item.locked === true
|
||||||
|
// ) as Note[];
|
||||||
|
// }
|
||||||
|
|
||||||
exists(id: string) {
|
exists(id: string) {
|
||||||
return this.collection.exists(id);
|
return this.collection.exists(id);
|
||||||
@@ -202,7 +198,7 @@ export class Notes implements ICollection {
|
|||||||
if (format !== "txt" && !(await checkIsUserPremium(CHECK_IDS.noteExport)))
|
if (format !== "txt" && !(await checkIsUserPremium(CHECK_IDS.noteExport)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const note = this.note(id);
|
const note = await this.note(id);
|
||||||
if (!note) return false;
|
if (!note) return false;
|
||||||
|
|
||||||
if (!options.contentItem) {
|
if (!options.contentItem) {
|
||||||
@@ -239,15 +235,15 @@ export class Notes implements ICollection {
|
|||||||
return options?.disableTemplate
|
return options?.disableTemplate
|
||||||
? contentString
|
? contentString
|
||||||
: buildFromTemplate(format, {
|
: buildFromTemplate(format, {
|
||||||
...note.data,
|
...note,
|
||||||
content: contentString
|
content: contentString
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async duplicate(...ids: string[]) {
|
async duplicate(...ids: string[]) {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const note = this.collection.get(id);
|
const note = await this.note(id);
|
||||||
if (!note || isTrashItem(note)) continue;
|
if (!note) continue;
|
||||||
|
|
||||||
const content = note.contentId
|
const content = note.contentId
|
||||||
? await this.db.content.raw(note.contentId)
|
? await this.db.content.raw(note.contentId)
|
||||||
@@ -274,13 +270,16 @@ export class Notes implements ICollection {
|
|||||||
});
|
});
|
||||||
if (!duplicateId) return;
|
if (!duplicateId) return;
|
||||||
|
|
||||||
for (const notebook of this.db.relations
|
for (const notebook of await this.db.relations
|
||||||
.to(note, "notebook")
|
.to(note, "notebook")
|
||||||
.resolved()) {
|
.get()) {
|
||||||
await this.db.relations.add(notebook, {
|
await this.db.relations.add(
|
||||||
id: duplicateId,
|
{ type: "notebook", id: notebook },
|
||||||
type: "note"
|
{
|
||||||
});
|
id: duplicateId,
|
||||||
|
type: "note"
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return duplicateId;
|
return duplicateId;
|
||||||
@@ -288,24 +287,17 @@ export class Notes implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _delete(moveToTrash = true, ...ids: string[]) {
|
private async _delete(moveToTrash = true, ...ids: string[]) {
|
||||||
for (const id of ids) {
|
if (moveToTrash) {
|
||||||
const item = this.collection.get(id);
|
await this.db.trash.add("note", ids);
|
||||||
if (!item) continue;
|
} else {
|
||||||
const itemData = clone(item);
|
await this.db.transaction(async () => {
|
||||||
|
await this.db.relations.unlinkOfType("note", ids);
|
||||||
if (moveToTrash && !isTrashItem(itemData))
|
await this.collection.softDelete(ids);
|
||||||
await this.db.trash.add(itemData);
|
await this.db.content.removeByNoteId(...ids);
|
||||||
else {
|
});
|
||||||
await this.db.relations.unlinkAll(item, "tag");
|
|
||||||
await this.db.relations.unlinkAll(item, "color");
|
|
||||||
await this.db.relations.unlinkAll(item, "attachment");
|
|
||||||
await this.db.relations.unlinkAll(item, "notebook");
|
|
||||||
|
|
||||||
await this.collection.remove(id);
|
|
||||||
if (itemData.contentId)
|
|
||||||
await this.db.content.remove(itemData.contentId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.totalNotes = Math.max(0, this.totalNotes - ids.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addToNotebook(notebookId: string, ...noteIds: string[]) {
|
async addToNotebook(notebookId: string, ...noteIds: string[]) {
|
||||||
@@ -318,24 +310,31 @@ export class Notes implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async removeFromNotebook(notebookId: string, ...noteIds: string[]) {
|
async removeFromNotebook(notebookId: string, ...noteIds: string[]) {
|
||||||
for (const noteId of noteIds) {
|
await this.db.transaction(async () => {
|
||||||
await this.db.relations.unlink(
|
for (const noteId of noteIds) {
|
||||||
{ id: notebookId, type: "notebook" },
|
await this.db.relations.unlink(
|
||||||
{ type: "note", id: noteId }
|
{ id: notebookId, type: "notebook" },
|
||||||
);
|
{ type: "note", id: noteId }
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFromAllNotebooks(...noteIds: string[]) {
|
async removeFromAllNotebooks(...noteIds: string[]) {
|
||||||
for (const noteId of noteIds) {
|
await this.db.transaction(async () => {
|
||||||
await this.db.relations.unlinkAll(
|
for (const noteId of noteIds) {
|
||||||
{ type: "note", id: noteId },
|
await this.db.relations
|
||||||
"notebook"
|
.to({ type: "note", id: noteId }, "notebook")
|
||||||
);
|
.unlink();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNoteTitle(note: Partial<Note>, oldNote?: Note, headline?: string) {
|
private async getNoteTitle(
|
||||||
|
note: Partial<Note>,
|
||||||
|
oldNote?: Note,
|
||||||
|
headline?: string
|
||||||
|
) {
|
||||||
if (note.title && note.title.trim().length > 0) {
|
if (note.title && note.title.trim().length > 0) {
|
||||||
return note.title.replace(NEWLINE_STRIP_REGEX, " ");
|
return note.title.replace(NEWLINE_STRIP_REGEX, " ");
|
||||||
} else if (
|
} else if (
|
||||||
@@ -352,7 +351,7 @@ export class Notes implements ICollection {
|
|||||||
this.db.settings.getDateFormat(),
|
this.db.settings.getDateFormat(),
|
||||||
this.db.settings.getTimeFormat(),
|
this.db.settings.getTimeFormat(),
|
||||||
headline?.split(" ").splice(0, 10).join(" "),
|
headline?.split(" ").splice(0, 10).join(" "),
|
||||||
this.collection.count()
|
this.totalNotes
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,197 +17,113 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CachedCollection } from "../database/cached-collection";
|
|
||||||
import { makeId } from "../utils/id";
|
import { makeId } from "../utils/id";
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
import { Relation, ItemMap, ItemReference, MaybeDeletedItem } from "../types";
|
import {
|
||||||
|
Relation,
|
||||||
|
ItemMap,
|
||||||
|
ItemReference,
|
||||||
|
ValueOf,
|
||||||
|
MaybeDeletedItem
|
||||||
|
} from "../types";
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
type RelationsArray<TType extends keyof ItemMap> = Relation[] & {
|
import { DatabaseAccessor, DatabaseSchema } from "../database";
|
||||||
resolved: (limit?: number) => ItemMap[TType][];
|
import { SelectQueryBuilder } from "kysely";
|
||||||
has: (id: string) => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Relations implements ICollection {
|
export class Relations implements ICollection {
|
||||||
name = "relations";
|
name = "relations";
|
||||||
readonly collection: CachedCollection<"relations", Relation>;
|
readonly collection: SQLCollection<"relations", Relation>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new CachedCollection(
|
this.collection = new SQLCollection(db.sql, "relations", db.eventManager);
|
||||||
db.storage,
|
|
||||||
"relations",
|
|
||||||
db.eventManager
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
async init() {
|
||||||
return this.collection.init();
|
// return this.collection.init();
|
||||||
}
|
|
||||||
|
|
||||||
async merge(relation: MaybeDeletedItem<Relation>) {
|
|
||||||
await this.collection.add(relation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async add(from: ItemReference, to: ItemReference) {
|
async add(from: ItemReference, to: ItemReference) {
|
||||||
if (
|
await this.collection.upsert({
|
||||||
this.all.find(
|
|
||||||
(a) =>
|
|
||||||
compareItemReference(a.from, from) && compareItemReference(a.to, to)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const relation: Relation = {
|
|
||||||
id: generateId(from, to),
|
id: generateId(from, to),
|
||||||
type: "relation",
|
type: "relation",
|
||||||
dateCreated: Date.now(),
|
dateCreated: Date.now(),
|
||||||
dateModified: Date.now(),
|
dateModified: Date.now(),
|
||||||
from: { id: from.id, type: from.type },
|
fromId: from.id,
|
||||||
to: { id: to.id, type: to.type }
|
fromType: from.type,
|
||||||
};
|
toId: to.id,
|
||||||
|
toType: to.type
|
||||||
await this.collection.add(relation);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
from<TType extends keyof ItemMap>(
|
from<TType extends keyof RelatableTable>(
|
||||||
reference: ItemReference,
|
reference: ItemReference,
|
||||||
type: TType
|
type: TType
|
||||||
): RelationsArray<TType> {
|
) {
|
||||||
const relations =
|
return new RelationsArray(
|
||||||
type === "note" || type === "notebook"
|
this.db.sql,
|
||||||
? this.all.filter(
|
this.db.trash.cache,
|
||||||
(a) =>
|
reference,
|
||||||
compareItemReference(a.from, reference) &&
|
type,
|
||||||
a.to.type === type &&
|
"from"
|
||||||
!this.db.trash.exists(a.to.id)
|
);
|
||||||
)
|
|
||||||
: this.all.filter(
|
|
||||||
(a) => compareItemReference(a.from, reference) && a.to.type === type
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.defineProperties(relations, {
|
|
||||||
resolved: {
|
|
||||||
writable: false,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: false,
|
|
||||||
value: (limit?: number) =>
|
|
||||||
this.resolve(limit ? relations.slice(0, limit) : relations, "to")
|
|
||||||
},
|
|
||||||
has: {
|
|
||||||
writable: false,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: false,
|
|
||||||
value: (id: string) => relations.some((rel) => rel.to.id === id)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return relations as RelationsArray<TType>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
to<TType extends keyof ItemMap>(
|
to<TType extends keyof RelatableTable>(
|
||||||
reference: ItemReference,
|
reference: ItemReference,
|
||||||
type: TType
|
type: TType
|
||||||
): RelationsArray<TType> {
|
) {
|
||||||
const relations =
|
return new RelationsArray(
|
||||||
type === "note" || type === "notebook"
|
this.db.sql,
|
||||||
? this.all.filter(
|
this.db.trash.cache,
|
||||||
(a) =>
|
reference,
|
||||||
compareItemReference(a.to, reference) &&
|
type,
|
||||||
a.from.type === type &&
|
"to"
|
||||||
!this.db.trash.exists(a.from.id)
|
);
|
||||||
)
|
|
||||||
: this.all.filter(
|
|
||||||
(a) => compareItemReference(a.to, reference) && a.from.type === type
|
|
||||||
);
|
|
||||||
Object.defineProperties(relations, {
|
|
||||||
resolved: {
|
|
||||||
writable: false,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: false,
|
|
||||||
value: (limit?: number) =>
|
|
||||||
this.resolve(limit ? relations.slice(0, limit) : relations, "from")
|
|
||||||
},
|
|
||||||
has: {
|
|
||||||
writable: false,
|
|
||||||
enumerable: false,
|
|
||||||
configurable: false,
|
|
||||||
value: (id: string) => relations.some((rel) => rel.from.id === id)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return relations as RelationsArray<TType>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
// get raw() {
|
||||||
return this.collection.raw();
|
// return this.collection.raw();
|
||||||
}
|
// }
|
||||||
|
|
||||||
get all(): Relation[] {
|
// get all(): Relation[] {
|
||||||
return this.collection.items();
|
// return this.collection.items();
|
||||||
}
|
// }
|
||||||
|
|
||||||
relation(id: string) {
|
// relation(id: string) {
|
||||||
return this.collection.get(id);
|
// return this.collection.get(id);
|
||||||
}
|
// }
|
||||||
|
|
||||||
async remove(...ids: string[]) {
|
async remove(...ids: string[]) {
|
||||||
for (const id of ids) {
|
await this.collection.softDelete(ids);
|
||||||
await this.collection.remove(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unlink(from: ItemReference, to: ItemReference) {
|
unlink(from: ItemReference, to: ItemReference) {
|
||||||
const relation = this.all.find(
|
return this.remove(generateId(from, to));
|
||||||
(a) =>
|
|
||||||
compareItemReference(a.from, from) && compareItemReference(a.to, to)
|
|
||||||
);
|
|
||||||
if (!relation) return;
|
|
||||||
|
|
||||||
await this.remove(relation.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async unlinkAll(to: ItemReference, type?: keyof ItemMap) {
|
async unlinkOfType(type: keyof RelatableTable, ids?: string[]) {
|
||||||
for (const relation of this.all.filter(
|
await this.db
|
||||||
(a) => compareItemReference(a.to, to) && (!type || a.from.type === type)
|
.sql()
|
||||||
)) {
|
.replaceInto("relations")
|
||||||
await this.remove(relation.id);
|
.columns(["id", "dateModified", "deleted"])
|
||||||
}
|
.expression((eb) =>
|
||||||
}
|
eb
|
||||||
|
.selectFrom("relations")
|
||||||
private resolve(relations: Relation[], resolveType: "from" | "to") {
|
.where((eb) =>
|
||||||
const items = [];
|
eb.or([eb("fromType", "==", type), eb("toType", "==", type)])
|
||||||
for (const relation of relations) {
|
)
|
||||||
const reference = resolveType === "from" ? relation.from : relation.to;
|
.$if(ids !== undefined && ids.length > 0, (eb) =>
|
||||||
let item = null;
|
eb.where((eb) =>
|
||||||
switch (reference.type) {
|
eb.or([eb("fromId", "in", ids), eb("toId", "in", ids)])
|
||||||
case "tag":
|
)
|
||||||
item = this.db.tags.tag(reference.id);
|
)
|
||||||
break;
|
.select((eb) => [
|
||||||
case "color":
|
"relations.id",
|
||||||
item = this.db.colors.color(reference.id);
|
eb.lit(Date.now()).as("dateModified"),
|
||||||
break;
|
eb.lit(1).as("deleted")
|
||||||
case "reminder":
|
])
|
||||||
item = this.db.reminders.reminder(reference.id);
|
)
|
||||||
break;
|
.execute();
|
||||||
case "note": {
|
|
||||||
const note = this.db.notes.note(reference.id);
|
|
||||||
if (!note) continue;
|
|
||||||
item = note.data;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "notebook": {
|
|
||||||
const notebook = this.db.notebooks.notebook(reference.id);
|
|
||||||
if (!notebook) continue;
|
|
||||||
item = notebook.data;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "attachment": {
|
|
||||||
const attachment = this.db.attachments.attachment(reference.id);
|
|
||||||
if (!attachment) continue;
|
|
||||||
item = attachment;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item) items.push(item);
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,3 +145,132 @@ function generateId(a: ItemReference, b: ItemReference) {
|
|||||||
const str = `${a.id}${b.id}${a.type}${b.type}`;
|
const str = `${a.id}${b.id}${a.type}${b.type}`;
|
||||||
return makeId(str);
|
return makeId(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TABLE_MAP = {
|
||||||
|
note: "notes",
|
||||||
|
notebook: "notebooks",
|
||||||
|
reminder: "reminders",
|
||||||
|
tag: "tags",
|
||||||
|
color: "colors",
|
||||||
|
attachment: "attachments"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type RelatableTable = typeof TABLE_MAP;
|
||||||
|
|
||||||
|
class RelationsArray<TType extends keyof RelatableTable> {
|
||||||
|
private table: ValueOf<RelatableTable> = TABLE_MAP[this.type];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly sql: DatabaseAccessor,
|
||||||
|
private readonly trashIds: string[],
|
||||||
|
private readonly reference: ItemReference,
|
||||||
|
private readonly type: TType,
|
||||||
|
private readonly direction: "from" | "to"
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async resolve(limit?: number): Promise<ItemMap[TType][]> {
|
||||||
|
const items = await this.sql()
|
||||||
|
.selectFrom(this.table)
|
||||||
|
.where("id", "in", (b) =>
|
||||||
|
b
|
||||||
|
.selectFrom("relations")
|
||||||
|
.$call((eb) =>
|
||||||
|
this.buildRelationsQuery()(
|
||||||
|
eb as SelectQueryBuilder<DatabaseSchema, "relations", unknown>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.$if(limit !== undefined && limit > 0, (b) => b.limit(limit!))
|
||||||
|
.selectAll()
|
||||||
|
// TODO: check if we need to index deleted field.
|
||||||
|
.where("deleted", "is", null)
|
||||||
|
.execute();
|
||||||
|
return items as unknown as ItemMap[TType][];
|
||||||
|
}
|
||||||
|
|
||||||
|
async unlink() {
|
||||||
|
await this.sql()
|
||||||
|
.replaceInto("relations")
|
||||||
|
.columns(["id", "dateModified", "deleted"])
|
||||||
|
.expression((eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom("relations")
|
||||||
|
.$call(this.buildRelationsQuery())
|
||||||
|
.clearSelect()
|
||||||
|
.select((eb) => [
|
||||||
|
"relations.id",
|
||||||
|
eb.lit(Date.now()).as("dateModified"),
|
||||||
|
eb.lit(1).as("deleted")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get() {
|
||||||
|
const ids = await this.sql()
|
||||||
|
.selectFrom("relations")
|
||||||
|
.$call(this.buildRelationsQuery())
|
||||||
|
.execute();
|
||||||
|
return ids.map((i) => i.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async count() {
|
||||||
|
const result = await this.sql()
|
||||||
|
.selectFrom("relations")
|
||||||
|
.$call(this.buildRelationsQuery())
|
||||||
|
.clearSelect()
|
||||||
|
.select((b) => b.fn.count<number>("relations.id").as("count"))
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (!result) return 0;
|
||||||
|
return result.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async has(id: string) {
|
||||||
|
const result = await this.sql()
|
||||||
|
.selectFrom("relations")
|
||||||
|
.$call(this.buildRelationsQuery())
|
||||||
|
.clearSelect()
|
||||||
|
.where(this.direction === "from" ? "toId" : "fromId", "==", id)
|
||||||
|
.select((b) => b.fn.count<number>("id").as("count"))
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (!result) return false;
|
||||||
|
return result.count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an optimized query for obtaining relations based on the given
|
||||||
|
* parameters. The resulting query uses a covering index (the most
|
||||||
|
* optimizable index) for obtaining relations.
|
||||||
|
*/
|
||||||
|
private buildRelationsQuery() {
|
||||||
|
return (
|
||||||
|
builder: SelectQueryBuilder<DatabaseSchema, "relations", unknown>
|
||||||
|
) => {
|
||||||
|
if (this.direction === "to") {
|
||||||
|
return builder
|
||||||
|
.where("fromType", "==", this.type)
|
||||||
|
.where("toType", "==", this.reference.type)
|
||||||
|
.where("toId", "==", this.reference.id)
|
||||||
|
.$if(
|
||||||
|
(this.type === "note" || this.type === "notebook") &&
|
||||||
|
this.trashIds.length > 0,
|
||||||
|
(b) => b.where("fromId", "not in", this.trashIds)
|
||||||
|
)
|
||||||
|
.select("relations.fromId as id")
|
||||||
|
.$narrowType<{ id: string }>();
|
||||||
|
} else {
|
||||||
|
return builder
|
||||||
|
.where("toType", "==", this.type)
|
||||||
|
.where("fromType", "==", this.reference.type)
|
||||||
|
.where("fromId", "==", this.reference.id)
|
||||||
|
.$if(
|
||||||
|
(this.type === "note" || this.type === "notebook") &&
|
||||||
|
this.trashIds.length > 0,
|
||||||
|
(b) => b.where("toId", "not in", this.trashIds)
|
||||||
|
)
|
||||||
|
.select("relations.toId as id")
|
||||||
|
.$narrowType<{ id: string }>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ import isYesterday from "dayjs/plugin/isYesterday";
|
|||||||
import { TimeFormat, formatDate } from "../utils/date";
|
import { TimeFormat, formatDate } from "../utils/date";
|
||||||
import { getId } from "../utils/id";
|
import { getId } from "../utils/id";
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
import { CachedCollection } from "../database/cached-collection";
|
|
||||||
import { Reminder } from "../types";
|
import { Reminder } from "../types";
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
|
|
||||||
dayjs.extend(isTomorrow);
|
dayjs.extend(isTomorrow);
|
||||||
dayjs.extend(isSameOrBefore);
|
dayjs.extend(isSameOrBefore);
|
||||||
@@ -36,13 +36,9 @@ dayjs.extend(isToday);
|
|||||||
|
|
||||||
export class Reminders implements ICollection {
|
export class Reminders implements ICollection {
|
||||||
name = "reminders";
|
name = "reminders";
|
||||||
readonly collection: CachedCollection<"reminders", Reminder>;
|
readonly collection: SQLCollection<"reminders", Reminder>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new CachedCollection(
|
this.collection = new SQLCollection(db.sql, "reminders", db.eventManager);
|
||||||
db.storage,
|
|
||||||
"reminders",
|
|
||||||
db.eventManager
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@@ -65,7 +61,7 @@ export class Reminders implements ICollection {
|
|||||||
if (!reminder.date || !reminder.title)
|
if (!reminder.date || !reminder.title)
|
||||||
throw new Error("date and title are required in a reminder.");
|
throw new Error("date and title are required in a reminder.");
|
||||||
|
|
||||||
await this.collection.add({
|
await this.collection.upsert({
|
||||||
id,
|
id,
|
||||||
type: "reminder",
|
type: "reminder",
|
||||||
dateCreated: reminder.dateCreated || Date.now(),
|
dateCreated: reminder.dateCreated || Date.now(),
|
||||||
@@ -81,16 +77,16 @@ export class Reminders implements ICollection {
|
|||||||
disabled: reminder.disabled,
|
disabled: reminder.disabled,
|
||||||
snoozeUntil: reminder.snoozeUntil
|
snoozeUntil: reminder.snoozeUntil
|
||||||
});
|
});
|
||||||
return reminder.id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
// get raw() {
|
||||||
return this.collection.raw();
|
// return this.collection.raw();
|
||||||
}
|
// }
|
||||||
|
|
||||||
get all() {
|
// get all() {
|
||||||
return this.collection.items();
|
// return this.collection.items();
|
||||||
}
|
// }
|
||||||
|
|
||||||
exists(itemId: string) {
|
exists(itemId: string) {
|
||||||
return this.collection.exists(itemId);
|
return this.collection.exists(itemId);
|
||||||
@@ -101,9 +97,7 @@ export class Reminders implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remove(...reminderIds: string[]) {
|
async remove(...reminderIds: string[]) {
|
||||||
for (const id of reminderIds) {
|
await this.collection.softDelete(reminderIds);
|
||||||
await this.collection.remove(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import { tinyToTiptap } from "../migrations";
|
|||||||
import { makeSessionContentId } from "../utils/id";
|
import { makeSessionContentId } from "../utils/id";
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
import { isCipher } from "../database/crypto";
|
import { isCipher } from "../database/crypto";
|
||||||
import { IndexedCollection } from "../database/indexed-collection";
|
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { ContentType, SessionContentItem, isDeleted } from "../types";
|
import { ContentType, SessionContentItem, isDeleted } from "../types";
|
||||||
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
|
|
||||||
export type NoteContent<TLocked extends boolean> = {
|
export type NoteContent<TLocked extends boolean> = {
|
||||||
data: TLocked extends true ? Cipher<"base64"> : string;
|
data: TLocked extends true ? Cipher<"base64"> : string;
|
||||||
@@ -33,13 +33,13 @@ export type NoteContent<TLocked extends boolean> = {
|
|||||||
|
|
||||||
export class SessionContent implements ICollection {
|
export class SessionContent implements ICollection {
|
||||||
name = "sessioncontent";
|
name = "sessioncontent";
|
||||||
private readonly collection: IndexedCollection<
|
private readonly collection: SQLCollection<
|
||||||
"sessioncontent",
|
"sessioncontent",
|
||||||
SessionContentItem
|
SessionContentItem
|
||||||
>;
|
>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new IndexedCollection(
|
this.collection = new SQLCollection(
|
||||||
db.storage,
|
db.sql,
|
||||||
"sessioncontent",
|
"sessioncontent",
|
||||||
db.eventManager
|
db.eventManager
|
||||||
);
|
);
|
||||||
@@ -49,10 +49,6 @@ export class SessionContent implements ICollection {
|
|||||||
await this.collection.init();
|
await this.collection.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async merge(item: SessionContentItem) {
|
|
||||||
await this.collection.addItem(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
async add<TLocked extends boolean>(
|
async add<TLocked extends boolean>(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
content: NoteContent<TLocked>,
|
content: NoteContent<TLocked>,
|
||||||
@@ -64,7 +60,7 @@ export class SessionContent implements ICollection {
|
|||||||
? content.data
|
? content.data
|
||||||
: await this.db.compressor().compress(content.data);
|
: await this.db.compressor().compress(content.data);
|
||||||
|
|
||||||
await this.collection.addItem({
|
await this.collection.upsert({
|
||||||
type: "sessioncontent",
|
type: "sessioncontent",
|
||||||
id: makeSessionContentId(sessionId),
|
id: makeSessionContentId(sessionId),
|
||||||
data,
|
data,
|
||||||
@@ -78,7 +74,7 @@ export class SessionContent implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(sessionContentId: string) {
|
async get(sessionContentId: string) {
|
||||||
const session = await this.collection.getItem(sessionContentId);
|
const session = await this.collection.get(sessionContentId);
|
||||||
if (!session || isDeleted(session)) return;
|
if (!session || isDeleted(session)) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -93,7 +89,7 @@ export class SessionContent implements ICollection {
|
|||||||
tinyToTiptap(await this.db.compressor().decompress(session.data))
|
tinyToTiptap(await this.db.compressor().decompress(session.data))
|
||||||
);
|
);
|
||||||
session.contentType = "tiptap";
|
session.contentType = "tiptap";
|
||||||
await this.collection.addItem(session);
|
await this.collection.upsert(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -106,13 +102,13 @@ export class SessionContent implements ICollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remove(sessionContentId: string) {
|
async remove(sessionContentId: string) {
|
||||||
await this.collection.deleteItem(sessionContentId);
|
await this.collection.delete(sessionContentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async all() {
|
// async all() {
|
||||||
const indices = this.collection.indexer.indices;
|
// const indices = this.collection.indexer.indices;
|
||||||
const items = await this.collection.getItems(indices);
|
// const items = await this.collection.getItems(indices);
|
||||||
|
|
||||||
return Object.values(items);
|
// return Object.values(items);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ import {
|
|||||||
TrashCleanupInterval
|
TrashCleanupInterval
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
import { CachedCollection } from "../database/cached-collection";
|
|
||||||
import { TimeFormat } from "../utils/date";
|
import { TimeFormat } from "../utils/date";
|
||||||
|
import { SQLCachedCollection } from "../database/sql-cached-collection";
|
||||||
|
|
||||||
const DEFAULT_GROUP_OPTIONS = (key: GroupingKey) =>
|
const DEFAULT_GROUP_OPTIONS = (key: GroupingKey) =>
|
||||||
({
|
({
|
||||||
@@ -66,12 +66,12 @@ const defaultSettings: SettingItemMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class Settings implements ICollection {
|
export class Settings implements ICollection {
|
||||||
name = "settingsv2";
|
name = "settings";
|
||||||
readonly collection: CachedCollection<"settingsv2", SettingItem>;
|
readonly collection: SQLCachedCollection<"settings", SettingItem>;
|
||||||
constructor(db: Database) {
|
constructor(db: Database) {
|
||||||
this.collection = new CachedCollection(
|
this.collection = new SQLCachedCollection(
|
||||||
db.storage,
|
db.sql,
|
||||||
"settingsv2",
|
"settings",
|
||||||
db.eventManager
|
db.eventManager
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -80,19 +80,19 @@ export class Settings implements ICollection {
|
|||||||
return this.collection.init();
|
return this.collection.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
// get raw() {
|
||||||
return this.collection.raw();
|
// return this.collection.raw();
|
||||||
}
|
// }
|
||||||
|
|
||||||
private async set<TKey extends keyof SettingItemMap>(
|
private async set<TKey extends keyof SettingItemMap>(
|
||||||
key: TKey,
|
key: TKey,
|
||||||
value: SettingItemMap[TKey]
|
value: SettingItemMap[TKey]
|
||||||
) {
|
) {
|
||||||
const id = makeId(key);
|
const id = makeId(key);
|
||||||
const oldItem = this.collection.get(id);
|
const oldItem = await this.collection.get(id);
|
||||||
if (oldItem && oldItem.key !== key) throw new Error("Key conflict.");
|
if (oldItem && oldItem.key !== key) throw new Error("Key conflict.");
|
||||||
|
|
||||||
await this.collection.add({
|
await this.collection.upsert({
|
||||||
id,
|
id,
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -18,20 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { CachedCollection } from "../database/cached-collection";
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
import { Notebook, Shortcut, Tag, Topic } from "../types";
|
import { Shortcut } from "../types";
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
|
|
||||||
const ALLOWED_SHORTCUT_TYPES = ["notebook", "topic", "tag"];
|
const ALLOWED_SHORTCUT_TYPES = ["notebook", "topic", "tag"];
|
||||||
export class Shortcuts implements ICollection {
|
export class Shortcuts implements ICollection {
|
||||||
name = "shortcuts";
|
name = "shortcuts";
|
||||||
readonly collection: CachedCollection<"shortcuts", Shortcut>;
|
readonly collection: SQLCollection<"shortcuts", Shortcut>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new CachedCollection(
|
this.collection = new SQLCollection(db.sql, "shortcuts", db.eventManager);
|
||||||
db.storage,
|
|
||||||
"shortcuts",
|
|
||||||
db.eventManager
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -45,11 +41,15 @@ export class Shortcuts implements ICollection {
|
|||||||
"Please use db.shortcuts.merge to merge remote shortcuts."
|
"Please use db.shortcuts.merge to merge remote shortcuts."
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shortcut.item && !ALLOWED_SHORTCUT_TYPES.includes(shortcut.item.type))
|
if (
|
||||||
|
shortcut.itemId &&
|
||||||
|
shortcut.itemType &&
|
||||||
|
!ALLOWED_SHORTCUT_TYPES.includes(shortcut.itemType)
|
||||||
|
)
|
||||||
throw new Error("Cannot create a shortcut for this type of item.");
|
throw new Error("Cannot create a shortcut for this type of item.");
|
||||||
|
|
||||||
const oldShortcut = shortcut.item
|
const oldShortcut = shortcut.itemId
|
||||||
? this.shortcut(shortcut.item.id)
|
? this.shortcut(shortcut.itemId)
|
||||||
: shortcut.id
|
: shortcut.id
|
||||||
? this.shortcut(shortcut.id)
|
? this.shortcut(shortcut.id)
|
||||||
: null;
|
: null;
|
||||||
@@ -64,65 +64,72 @@ export class Shortcuts implements ICollection {
|
|||||||
|
|
||||||
const id = shortcut.id || shortcut.item.id;
|
const id = shortcut.id || shortcut.item.id;
|
||||||
|
|
||||||
await this.collection.add({
|
await this.collection.upsert({
|
||||||
id,
|
id,
|
||||||
type: "shortcut",
|
type: "shortcut",
|
||||||
item: shortcut.item,
|
itemId: shortcut.itemId,
|
||||||
|
itemType: shortcut.itemType,
|
||||||
|
// item: shortcut.item,
|
||||||
dateCreated: shortcut.dateCreated || Date.now(),
|
dateCreated: shortcut.dateCreated || Date.now(),
|
||||||
dateModified: shortcut.dateModified || Date.now(),
|
dateModified: shortcut.dateModified || Date.now(),
|
||||||
sortIndex: this.collection.count()
|
sortIndex: await this.collection.count()
|
||||||
});
|
});
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
// get raw() {
|
||||||
return this.collection.raw();
|
// return this.collection.raw();
|
||||||
}
|
// }
|
||||||
|
|
||||||
get all() {
|
// get all() {
|
||||||
return this.collection.items();
|
// return this.collection.items();
|
||||||
}
|
// }
|
||||||
|
|
||||||
get resolved() {
|
async get() {
|
||||||
return this.all.reduce((prev, shortcut) => {
|
// return this.all.reduce((prev, shortcut) => {
|
||||||
const {
|
// const {
|
||||||
item: { id }
|
// item: { id }
|
||||||
} = shortcut;
|
// } = shortcut;
|
||||||
|
// let item: Notebook | Topic | Tag | null | undefined = null;
|
||||||
let item: Notebook | Topic | Tag | null | undefined = null;
|
// switch (shortcut.item.type) {
|
||||||
switch (shortcut.item.type) {
|
// case "notebook": {
|
||||||
case "notebook": {
|
// const notebook = this.db.notebooks.notebook(id);
|
||||||
const notebook = this.db.notebooks.notebook(id);
|
// item = notebook ? notebook.data : null;
|
||||||
item = notebook ? notebook.data : null;
|
// break;
|
||||||
break;
|
// }
|
||||||
}
|
// case "tag":
|
||||||
case "tag":
|
// item = this.db.tags.tag(id);
|
||||||
item = this.db.tags.tag(id);
|
// break;
|
||||||
break;
|
// }
|
||||||
}
|
// if (item) prev.push(item);
|
||||||
if (item) prev.push(item);
|
// return prev;
|
||||||
return prev;
|
// }, [] as (Notebook | Topic | Tag)[]);
|
||||||
}, [] as (Notebook | Topic | Tag)[]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exists(id: string) {
|
exists(id: string) {
|
||||||
return !!this.shortcut(id);
|
return this.collection.exists(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcut(id: string) {
|
shortcut(id: string) {
|
||||||
return this.all.find(
|
return this.collection.get(id);
|
||||||
(shortcut) => shortcut.item.id === id || shortcut.id === id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(...shortcutIds: string[]) {
|
async remove(...shortcutIds: string[]) {
|
||||||
const shortcuts = this.all.filter(
|
await this.collection.softDelete(shortcutIds);
|
||||||
(shortcut) =>
|
// await this.db
|
||||||
shortcutIds.includes(shortcut.item.id) ||
|
// .sql()
|
||||||
shortcutIds.includes(shortcut.id)
|
// .deleteFrom("shortcuts")
|
||||||
);
|
// .where((eb) =>
|
||||||
for (const { id } of shortcuts) {
|
// eb.or([eb("id", "in", shortcutIds), eb("itemId", "in", shortcutIds)])
|
||||||
await this.collection.remove(id);
|
// )
|
||||||
}
|
// .execute();
|
||||||
|
// const shortcuts = this.all.filter(
|
||||||
|
// (shortcut) =>
|
||||||
|
// shortcutIds.includes(shortcut.item.id) ||
|
||||||
|
// shortcutIds.includes(shortcut.id)
|
||||||
|
// );
|
||||||
|
// for (const { id } of shortcuts) {
|
||||||
|
// await this.collection.remove(id);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getId } from "../utils/id";
|
import { getId } from "../utils/id";
|
||||||
import { CachedCollection } from "../database/cached-collection";
|
import { Tag } from "../types";
|
||||||
import { MaybeDeletedItem, Tag } from "../types";
|
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import { ICollection } from "./collection";
|
import { ICollection } from "./collection";
|
||||||
|
import { SQLCollection } from "../database/sql-collection";
|
||||||
|
|
||||||
export class Tags implements ICollection {
|
export class Tags implements ICollection {
|
||||||
name = "tags";
|
name = "tags";
|
||||||
readonly collection: CachedCollection<"tags", Tag>;
|
readonly collection: SQLCollection<"tags", Tag>;
|
||||||
constructor(private readonly db: Database) {
|
constructor(private readonly db: Database) {
|
||||||
this.collection = new CachedCollection(db.storage, "tags", db.eventManager);
|
this.collection = new SQLCollection(db.sql, "tags", db.eventManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@@ -38,58 +38,46 @@ export class Tags implements ICollection {
|
|||||||
return this.collection.get(id);
|
return this.collection.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
find(idOrTitle: string) {
|
// find(idOrTitle: string) {
|
||||||
return this.all.find(
|
// return this.all.find(
|
||||||
(tag) => tag.title === idOrTitle || tag.id === idOrTitle
|
// (tag) => tag.title === idOrTitle || tag.id === idOrTitle
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
async merge(remoteTag: MaybeDeletedItem<Tag>) {
|
|
||||||
if (!remoteTag) return;
|
|
||||||
|
|
||||||
const localTag = this.collection.get(remoteTag.id);
|
|
||||||
if (!localTag || remoteTag.dateModified > localTag.dateModified)
|
|
||||||
await this.collection.add(remoteTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
async add(item: Partial<Tag>) {
|
async add(item: Partial<Tag>) {
|
||||||
if (item.remote)
|
if (item.remote)
|
||||||
throw new Error("Please use db.tags.merge to merge remote tags.");
|
throw new Error("Please use db.tags.merge to merge remote tags.");
|
||||||
|
|
||||||
const id = item.id || getId(item.dateCreated);
|
const id = item.id || getId(item.dateCreated);
|
||||||
const oldTag = this.tag(id);
|
const oldTag = await this.tag(id);
|
||||||
|
|
||||||
item.title = item.title ? Tags.sanitize(item.title) : item.title;
|
item.title = item.title ? Tags.sanitize(item.title) : item.title;
|
||||||
if (!item.title && !oldTag?.title) throw new Error("Title is required.");
|
if (!item.title && !oldTag?.title) throw new Error("Title is required.");
|
||||||
|
|
||||||
const tag: Tag = {
|
await this.collection.upsert({
|
||||||
id,
|
id,
|
||||||
dateCreated: item.dateCreated || oldTag?.dateCreated || Date.now(),
|
dateCreated: item.dateCreated || oldTag?.dateCreated || Date.now(),
|
||||||
dateModified: item.dateModified || oldTag?.dateModified || Date.now(),
|
dateModified: item.dateModified || oldTag?.dateModified || Date.now(),
|
||||||
title: item.title || oldTag?.title || "",
|
title: item.title || oldTag?.title || "",
|
||||||
type: "tag",
|
type: "tag",
|
||||||
remote: false
|
remote: false
|
||||||
};
|
});
|
||||||
await this.collection.add(tag);
|
return id;
|
||||||
return tag.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get raw() {
|
// get raw() {
|
||||||
return this.collection.raw();
|
// return this.collection.raw();
|
||||||
}
|
// }
|
||||||
|
|
||||||
get all() {
|
// get all() {
|
||||||
return this.collection.items();
|
// return this.collection.items();
|
||||||
}
|
// }
|
||||||
|
|
||||||
async remove(id: string) {
|
async remove(...ids: string[]) {
|
||||||
await this.collection.remove(id);
|
await this.db.transaction(async () => {
|
||||||
await this.db.relations.cleanup();
|
await this.db.relations.unlinkOfType("tag", ids);
|
||||||
}
|
await this.collection.softDelete(ids);
|
||||||
|
});
|
||||||
async delete(id: string) {
|
|
||||||
await this.collection.delete(id);
|
|
||||||
await this.db.relations.cleanup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exists(id: string) {
|
exists(id: string) {
|
||||||
|
|||||||
@@ -19,23 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import Database from "../api";
|
import Database from "../api";
|
||||||
import {
|
|
||||||
BaseTrashItem,
|
|
||||||
Note,
|
|
||||||
Notebook,
|
|
||||||
TrashItem,
|
|
||||||
isTrashItem
|
|
||||||
} from "../types";
|
|
||||||
|
|
||||||
function toTrashItem<T extends Note | Notebook>(item: T): BaseTrashItem<T> {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
id: item.id,
|
|
||||||
type: "trash",
|
|
||||||
itemType: item.type,
|
|
||||||
dateDeleted: Date.now()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Trash {
|
export default class Trash {
|
||||||
collections = ["notes", "notebooks"] as const;
|
collections = ["notes", "notebooks"] as const;
|
||||||
@@ -44,96 +27,122 @@ export default class Trash {
|
|||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.cleanup();
|
await this.cleanup();
|
||||||
this.cache = this.all.map((t) => t.id);
|
const result = await this.db
|
||||||
|
.sql()
|
||||||
|
.selectNoFrom((eb) => [
|
||||||
|
eb
|
||||||
|
.selectFrom("notes")
|
||||||
|
.where("type", "==", "trash")
|
||||||
|
.select("id")
|
||||||
|
.as("id"),
|
||||||
|
eb
|
||||||
|
.selectFrom("notebooks")
|
||||||
|
.where("type", "==", "trash")
|
||||||
|
.select("id")
|
||||||
|
.as("id")
|
||||||
|
])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
this.cache = result.reduce((ids, item) => {
|
||||||
|
if (item.id) ids.push(item.id);
|
||||||
|
return ids;
|
||||||
|
}, [] as string[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup() {
|
async cleanup() {
|
||||||
const now = dayjs().unix();
|
|
||||||
const duration = this.db.settings.getTrashCleanupInterval();
|
const duration = this.db.settings.getTrashCleanupInterval();
|
||||||
if (duration === -1 || !duration) return;
|
if (duration === -1 || !duration) return;
|
||||||
for (const item of this.all) {
|
|
||||||
if (
|
const maxMs = dayjs().subtract(duration, "days").toDate().getTime();
|
||||||
isTrashItem(item) &&
|
const expiredItems = await this.db
|
||||||
item.dateDeleted &&
|
.sql()
|
||||||
dayjs(item.dateDeleted).add(duration, "days").unix() > now
|
.selectNoFrom((eb) => [
|
||||||
)
|
eb
|
||||||
continue;
|
.selectFrom("notes")
|
||||||
await this.delete(item.id);
|
.where("type", "==", "trash")
|
||||||
}
|
.where("dateDeleted", "<=", maxMs)
|
||||||
|
.select("id")
|
||||||
|
.as("noteId"),
|
||||||
|
eb
|
||||||
|
.selectFrom("notebooks")
|
||||||
|
.where("type", "==", "trash")
|
||||||
|
.where("dateDeleted", "<=", maxMs)
|
||||||
|
.select("id")
|
||||||
|
.as("notebookId")
|
||||||
|
])
|
||||||
|
.execute();
|
||||||
|
const { noteIds, notebookIds } = expiredItems.reduce(
|
||||||
|
(ids, item) => {
|
||||||
|
if (item.noteId) ids.noteIds.push(item.noteId);
|
||||||
|
if (item.notebookId) ids.notebookIds.push(item.notebookId);
|
||||||
|
return ids;
|
||||||
|
},
|
||||||
|
{ noteIds: [] as string[], notebookIds: [] as string[] }
|
||||||
|
);
|
||||||
|
await this.delete("note", noteIds);
|
||||||
|
await this.delete("note", notebookIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
get all(): TrashItem[] {
|
async add(type: "note" | "notebook", ids: string[]) {
|
||||||
const trashItems: TrashItem[] = [];
|
if (type === "note") {
|
||||||
for (const key of this.collections) {
|
await this.db.notes.collection.update(ids, {
|
||||||
const collection = this.db[key];
|
type: "trash",
|
||||||
trashItems.push(...collection.trashed);
|
itemType: "note",
|
||||||
|
dateDeleted: Date.now()
|
||||||
|
});
|
||||||
|
} else if (type === "notebook") {
|
||||||
|
await this.db.notebooks.collection.update(ids, {
|
||||||
|
type: "trash",
|
||||||
|
itemType: "notebook",
|
||||||
|
dateDeleted: Date.now()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return trashItems;
|
this.cache.push(...ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getItem(id: string) {
|
async delete(type: "note" | "notebook", ids: string[]) {
|
||||||
for (const key of this.collections) {
|
if (type === "note") {
|
||||||
const collection = this.db[key].collection;
|
await this.db.content.removeByNoteId(...ids);
|
||||||
const item = collection.get(id);
|
await this.db.noteHistory.clearSessions(...ids);
|
||||||
if (item && isTrashItem(item)) return [item, collection] as const;
|
await this.db.notes.delete(...ids);
|
||||||
|
} else if (type === "notebook") {
|
||||||
|
await this.db.relations.unlinkOfType("notebook", ids);
|
||||||
|
await this.db.notebooks.delete(...ids);
|
||||||
}
|
}
|
||||||
return [] as const;
|
ids.forEach((id) => this.cache.splice(this.cache.indexOf(id), 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
async add(item: Note | Notebook) {
|
async restore(type: "note" | "notebook", ids: string[]) {
|
||||||
if (item.type === "note") {
|
if (type === "note") {
|
||||||
await this.db.notes.collection.update(toTrashItem(item));
|
await this.db.notes.collection.update(ids, {
|
||||||
} else if (item.type === "notebook") {
|
type: "note",
|
||||||
await this.db.notebooks.collection.update(toTrashItem(item));
|
dateDeleted: null,
|
||||||
}
|
itemType: null
|
||||||
this.cache.push(item.id);
|
});
|
||||||
}
|
} else {
|
||||||
|
await this.db.notebooks.collection.update(ids, {
|
||||||
async delete(...ids: string[]) {
|
type: "notebook",
|
||||||
for (const id of ids) {
|
dateDeleted: null,
|
||||||
const [item, collection] = this.getItem(id);
|
itemType: null
|
||||||
if (!item || !collection) continue;
|
});
|
||||||
if (item.itemType === "note") {
|
|
||||||
if (item.contentId) await this.db.content.remove(item.contentId);
|
|
||||||
await this.db.noteHistory.clearSessions(id);
|
|
||||||
} else if (item.itemType === "notebook") {
|
|
||||||
await this.db.relations.unlinkAll({ type: "notebook", id: item.id });
|
|
||||||
}
|
|
||||||
await collection.remove(id);
|
|
||||||
this.cache.splice(this.cache.indexOf(id), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async restore(...ids: string[]) {
|
|
||||||
for (const id of ids) {
|
|
||||||
const [item] = this.getItem(id);
|
|
||||||
if (!item) continue;
|
|
||||||
if (item.itemType === "note") {
|
|
||||||
await this.db.notes.collection.update({ ...item, type: "note" });
|
|
||||||
} else if (item.itemType === "notebook") {
|
|
||||||
await this.db.notebooks.collection.update({
|
|
||||||
...item,
|
|
||||||
type: "notebook"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.cache.splice(this.cache.indexOf(id), 1);
|
|
||||||
}
|
}
|
||||||
|
ids.forEach((id) => this.cache.splice(this.cache.indexOf(id), 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear() {
|
async clear() {
|
||||||
for (const item of this.all) {
|
// for (const item of this.all) {
|
||||||
await this.delete(item.id);
|
// await this.delete(item.id);
|
||||||
}
|
// }
|
||||||
this.cache = [];
|
this.cache = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
synced(id: string) {
|
// synced(id: string) {
|
||||||
const [item] = this.getItem(id);
|
// // const [item] = this.getItem(id);
|
||||||
if (item && item.itemType === "note") {
|
// if (item && item.itemType === "note") {
|
||||||
const { contentId } = item;
|
// const { contentId } = item;
|
||||||
return !contentId || this.db.content.exists(contentId);
|
// return !contentId || this.db.content.exists(contentId);
|
||||||
} else return true;
|
// } else return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -25,13 +25,12 @@ import {
|
|||||||
StorageAccessor
|
StorageAccessor
|
||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
import { DataFormat, SerializedKey } from "@notesnook/crypto/dist/src/types";
|
import { DataFormat, SerializedKey } from "@notesnook/crypto/dist/src/types";
|
||||||
import { AttachmentMetadata } from "../types";
|
|
||||||
import { EV, EVENTS } from "../common";
|
import { EV, EVENTS } from "../common";
|
||||||
|
|
||||||
export type FileStorageAccessor = () => FileStorage;
|
export type FileStorageAccessor = () => FileStorage;
|
||||||
export type DownloadableFile = {
|
export type DownloadableFile = {
|
||||||
filename: string;
|
filename: string;
|
||||||
metadata: AttachmentMetadata;
|
// metadata: AttachmentMetadata;
|
||||||
chunkSize: number;
|
chunkSize: number;
|
||||||
};
|
};
|
||||||
export type QueueItem = DownloadableFile & {
|
export type QueueItem = DownloadableFile & {
|
||||||
@@ -57,7 +56,7 @@ export class FileStorage {
|
|||||||
this.downloads.set(groupId, files);
|
this.downloads.set(groupId, files);
|
||||||
|
|
||||||
for (const file of files as QueueItem[]) {
|
for (const file of files as QueueItem[]) {
|
||||||
const { filename, metadata, chunkSize } = file;
|
const { filename, chunkSize } = file;
|
||||||
if (await this.exists(filename)) {
|
if (await this.exists(filename)) {
|
||||||
current++;
|
current++;
|
||||||
EV.publish(EVENTS.fileDownloaded, {
|
EV.publish(EVENTS.fileDownloaded, {
|
||||||
@@ -71,7 +70,6 @@ export class FileStorage {
|
|||||||
|
|
||||||
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
||||||
const { execute, cancel } = this.fs.downloadFile(filename, {
|
const { execute, cancel } = this.fs.downloadFile(filename, {
|
||||||
metadata,
|
|
||||||
url,
|
url,
|
||||||
chunkSize,
|
chunkSize,
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
@@ -106,11 +104,10 @@ export class FileStorage {
|
|||||||
this.uploads.set(groupId, files);
|
this.uploads.set(groupId, files);
|
||||||
|
|
||||||
for (const file of files as QueueItem[]) {
|
for (const file of files as QueueItem[]) {
|
||||||
const { filename, chunkSize, metadata } = file;
|
const { filename, chunkSize } = file;
|
||||||
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
||||||
const { execute, cancel } = this.fs.uploadFile(filename, {
|
const { execute, cancel } = this.fs.uploadFile(filename, {
|
||||||
chunkSize,
|
chunkSize,
|
||||||
metadata,
|
|
||||||
url,
|
url,
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
@@ -142,21 +139,15 @@ export class FileStorage {
|
|||||||
this.uploads.delete(groupId);
|
this.uploads.delete(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadFile(
|
async downloadFile(groupId: string, filename: string, chunkSize: number) {
|
||||||
groupId: string,
|
|
||||||
filename: string,
|
|
||||||
chunkSize: number,
|
|
||||||
metadata: AttachmentMetadata
|
|
||||||
) {
|
|
||||||
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
const url = `${hosts.API_HOST}/s3?name=${filename}`;
|
||||||
const token = await this.tokenManager.getAccessToken();
|
const token = await this.tokenManager.getAccessToken();
|
||||||
const { execute, cancel } = this.fs.downloadFile(filename, {
|
const { execute, cancel } = this.fs.downloadFile(filename, {
|
||||||
metadata,
|
|
||||||
url,
|
url,
|
||||||
chunkSize,
|
chunkSize,
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
this.downloads.set(groupId, [{ cancel, filename, chunkSize, metadata }]);
|
this.downloads.set(groupId, [{ cancel, filename, chunkSize }]);
|
||||||
const result = await execute();
|
const result = await execute();
|
||||||
this.downloads.delete(groupId);
|
this.downloads.delete(groupId);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
191
packages/core/src/database/index.ts
Normal file
191
packages/core/src/database/index.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Migrator,
|
||||||
|
Kysely,
|
||||||
|
SqliteAdapter,
|
||||||
|
SqliteIntrospector,
|
||||||
|
SqliteQueryCompiler,
|
||||||
|
sql,
|
||||||
|
Driver,
|
||||||
|
KyselyPlugin,
|
||||||
|
PluginTransformQueryArgs,
|
||||||
|
PluginTransformResultArgs,
|
||||||
|
QueryResult,
|
||||||
|
UnknownRow,
|
||||||
|
RootOperationNode,
|
||||||
|
OperationNodeTransformer,
|
||||||
|
ValueNode,
|
||||||
|
PrimitiveValueListNode,
|
||||||
|
Transaction,
|
||||||
|
ColumnType
|
||||||
|
} from "kysely";
|
||||||
|
import {
|
||||||
|
Attachment,
|
||||||
|
Color,
|
||||||
|
ContentItem,
|
||||||
|
HistorySession,
|
||||||
|
Note,
|
||||||
|
Notebook,
|
||||||
|
Relation,
|
||||||
|
Reminder,
|
||||||
|
SessionContentItem,
|
||||||
|
SettingItem,
|
||||||
|
Shortcut,
|
||||||
|
Tag,
|
||||||
|
TrashOrItem,
|
||||||
|
ValueOf
|
||||||
|
} from "../types";
|
||||||
|
import { NNMigrationProvider } from "./migrations";
|
||||||
|
|
||||||
|
// type FilteredKeys<T, U> = {
|
||||||
|
// [P in keyof T]: T[P] extends U ? P : never;
|
||||||
|
// }[keyof T];
|
||||||
|
type SQLiteValue<T> = T extends string | number | boolean | Array<number>
|
||||||
|
? T
|
||||||
|
: T extends object | Array<any>
|
||||||
|
? ColumnType<T, string, string>
|
||||||
|
: never;
|
||||||
|
export type SQLiteItem<T> = {
|
||||||
|
[P in keyof T]?: T[P] | null;
|
||||||
|
} & { id: string };
|
||||||
|
|
||||||
|
export interface DatabaseSchema {
|
||||||
|
notes: SQLiteItem<TrashOrItem<Note>>; //| SQLiteItem<BaseTrashItem<Note>>;
|
||||||
|
content: SQLiteItem<ContentItem>;
|
||||||
|
relations: SQLiteItem<Relation>;
|
||||||
|
notebooks: SQLiteItem<TrashOrItem<Notebook>>;
|
||||||
|
attachments: SQLiteItem<Attachment>;
|
||||||
|
tags: SQLiteItem<Tag>;
|
||||||
|
colors: SQLiteItem<Color>;
|
||||||
|
reminders: SQLiteItem<Reminder>;
|
||||||
|
settings: SQLiteItem<SettingItem>;
|
||||||
|
notehistory: SQLiteItem<HistorySession>;
|
||||||
|
sessioncontent: SQLiteItem<SessionContentItem>;
|
||||||
|
shortcuts: SQLiteItem<Shortcut>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AsyncOrSyncResult<Async extends boolean, Response> = Async extends true
|
||||||
|
? Promise<Response>
|
||||||
|
: Response;
|
||||||
|
|
||||||
|
export interface DatabaseCollection<T, Async extends boolean> {
|
||||||
|
clear(): Promise<void>;
|
||||||
|
init(): Promise<void>;
|
||||||
|
upsert(item: T): Promise<void>;
|
||||||
|
softDelete(ids: string[]): Promise<void>;
|
||||||
|
delete(ids: string[]): Promise<void>;
|
||||||
|
exists(id: string): AsyncOrSyncResult<Async, boolean>;
|
||||||
|
count(): AsyncOrSyncResult<Async, number>;
|
||||||
|
get(id: string): AsyncOrSyncResult<Async, T | undefined>;
|
||||||
|
put(items: (T | undefined)[]): Promise<void>;
|
||||||
|
update(ids: string[], partial: Partial<T>): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DatabaseAccessor = () =>
|
||||||
|
| Kysely<DatabaseSchema>
|
||||||
|
| Transaction<DatabaseSchema>;
|
||||||
|
|
||||||
|
type FilterBooleanProperties<T> = keyof {
|
||||||
|
[K in keyof T as T[K] extends boolean | undefined | null ? K : never]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
type BooleanFields = ValueOf<{
|
||||||
|
[D in keyof DatabaseSchema]: FilterBooleanProperties<DatabaseSchema[D]>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const BooleanProperties: BooleanFields[] = [
|
||||||
|
"compressed",
|
||||||
|
"conflicted",
|
||||||
|
"deleted",
|
||||||
|
"disabled",
|
||||||
|
"favorite",
|
||||||
|
"localOnly",
|
||||||
|
"locked",
|
||||||
|
"migrated",
|
||||||
|
"pinned",
|
||||||
|
"readonly",
|
||||||
|
"remote",
|
||||||
|
"synced"
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function createDatabase(driver: Driver) {
|
||||||
|
const db = new Kysely<DatabaseSchema>({
|
||||||
|
dialect: {
|
||||||
|
createAdapter: () => new SqliteAdapter(),
|
||||||
|
createDriver: () => driver,
|
||||||
|
createIntrospector: (db) => new SqliteIntrospector(db),
|
||||||
|
createQueryCompiler: () => new SqliteQueryCompiler()
|
||||||
|
},
|
||||||
|
plugins: [new SqliteBooleanPlugin()]
|
||||||
|
});
|
||||||
|
|
||||||
|
const migrator = new Migrator({
|
||||||
|
db,
|
||||||
|
provider: new NNMigrationProvider()
|
||||||
|
});
|
||||||
|
|
||||||
|
await sql`PRAGMA journal_mode = WAL`.execute(db);
|
||||||
|
await sql`PRAGMA synchronous = normal`.execute(db);
|
||||||
|
|
||||||
|
await migrator.migrateToLatest();
|
||||||
|
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SqliteBooleanPlugin implements KyselyPlugin {
|
||||||
|
readonly #transformer = new SqliteBooleanTransformer();
|
||||||
|
|
||||||
|
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
|
||||||
|
return this.#transformer.transformNode(args.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformResult(
|
||||||
|
args: PluginTransformResultArgs
|
||||||
|
): Promise<QueryResult<UnknownRow>> {
|
||||||
|
for (const row of args.result.rows) {
|
||||||
|
for (const key of BooleanProperties) {
|
||||||
|
const value = row[key];
|
||||||
|
row[key] = value === 1 ? true : false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(args.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqliteBooleanTransformer extends OperationNodeTransformer {
|
||||||
|
transformValue(node: ValueNode): ValueNode {
|
||||||
|
return {
|
||||||
|
...super.transformValue(node),
|
||||||
|
value: typeof node.value === "boolean" ? (node.value ? 1 : 0) : node.value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected transformPrimitiveValueList(
|
||||||
|
node: PrimitiveValueListNode
|
||||||
|
): PrimitiveValueListNode {
|
||||||
|
return {
|
||||||
|
...super.transformPrimitiveValueList(node),
|
||||||
|
values: node.values.map((value) =>
|
||||||
|
typeof value === "boolean" ? (value ? 1 : 0) : value
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
218
packages/core/src/database/migrations.ts
Normal file
218
packages/core/src/database/migrations.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CreateTableBuilder, Migration, MigrationProvider, sql } from "kysely";
|
||||||
|
|
||||||
|
export class NNMigrationProvider implements MigrationProvider {
|
||||||
|
async getMigrations(): Promise<Record<string, Migration>> {
|
||||||
|
return {
|
||||||
|
"1": {
|
||||||
|
async up(db) {
|
||||||
|
await db.schema
|
||||||
|
.createTable("notes")
|
||||||
|
.modifyEnd(sql`without rowid`)
|
||||||
|
.$call(addBaseColumns)
|
||||||
|
.addColumn("title", "text")
|
||||||
|
.addColumn("headline", "text")
|
||||||
|
.addColumn("contentId", "text")
|
||||||
|
.addColumn("pinned", "boolean")
|
||||||
|
.addColumn("locked", "boolean")
|
||||||
|
.addColumn("favorite", "boolean")
|
||||||
|
.addColumn("localOnly", "boolean")
|
||||||
|
.addColumn("conflicted", "boolean")
|
||||||
|
.addColumn("readonly", "boolean")
|
||||||
|
.addColumn("dateEdited", "integer")
|
||||||
|
.addColumn("dateDeleted", "integer")
|
||||||
|
.addColumn("itemType", "text")
|
||||||
|
.addForeignKeyConstraint(
|
||||||
|
"note_has_content",
|
||||||
|
["contentId"],
|
||||||
|
"content",
|
||||||
|
["id"],
|
||||||
|
(b) => b.onDelete("restrict").onUpdate("restrict")
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("content")
|
||||||
|
.modifyEnd(sql`without rowid`)
|
||||||
|
.$call(addBaseColumns)
|
||||||
|
.addColumn("noteId", "text")
|
||||||
|
.addColumn("data", "text")
|
||||||
|
.addColumn("localOnly", "boolean")
|
||||||
|
.addColumn("conflicted", "text")
|
||||||
|
.addColumn("sessionId", "text")
|
||||||
|
.addColumn("dateEdited", "integer")
|
||||||
|
.addColumn("dateResolved", "integer")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("notebooks")
|
||||||
|
.modifyEnd(sql`without rowid`)
|
||||||
|
.$call(addBaseColumns)
|
||||||
|
.addColumn("title", "text")
|
||||||
|
.addColumn("description", "text")
|
||||||
|
.addColumn("dateEdited", "text")
|
||||||
|
.addColumn("pinned", "boolean")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("tags")
|
||||||
|
.modifyEnd(sql`without rowid`)
|
||||||
|
.$call(addBaseColumns)
|
||||||
|
.addColumn("title", "text")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("colors")
|
||||||
|
.modifyEnd(sql`without rowid`)
|
||||||
|
.$call(addBaseColumns)
|
||||||
|
.addColumn("title", "text")
|
||||||
|
.addColumn("colorCode", "text")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("relations")
|
||||||
|
.modifyEnd(sql`without rowid`)
|
||||||
|
.$call(addBaseColumns)
|
||||||
|
.addColumn("fromType", "text")
|
||||||
|
.addColumn("fromId", "text")
|
||||||
|
.addColumn("toType", "text")
|
||||||
|
.addColumn("toId", "text")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("shortcuts")
|
||||||
|
.modifyEnd(sql`without rowid`)
|
||||||
|
.$call(addBaseColumns)
|
||||||
|
.addColumn("sortIndex", "integer")
|
||||||
|
.addColumn("itemId", "text")
|
||||||
|
.addColumn("itemType", "text")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("reminders")
|
||||||
|
.modifyEnd(sql`without rowid`)
|
||||||
|
.$call(addBaseColumns)
|
||||||
|
.addColumn("title", "text")
|
||||||
|
.addColumn("description", "text")
|
||||||
|
.addColumn("priority", "text")
|
||||||
|
.addColumn("date", "integer")
|
||||||
|
.addColumn("mode", "text")
|
||||||
|
.addColumn("recurringMode", "text")
|
||||||
|
.addColumn("selectedDays", "blob")
|
||||||
|
.addColumn("localOnly", "boolean")
|
||||||
|
.addColumn("disabled", "boolean")
|
||||||
|
.addColumn("snoozeUntil", "integer")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable("attachments")
|
||||||
|
.modifyEnd(sql`without rowid`)
|
||||||
|
.$call(addBaseColumns)
|
||||||
|
.addColumn("iv", "text")
|
||||||
|
.addColumn("salt", "text")
|
||||||
|
.addColumn("size", "integer")
|
||||||
|
.addColumn("alg", "text")
|
||||||
|
.addColumn("encryptionKey", "text")
|
||||||
|
.addColumn("chunkSize", "integer")
|
||||||
|
.addColumn("hash", "text", (c) => c.unique())
|
||||||
|
.addColumn("hashType", "text")
|
||||||
|
.addColumn("mimeType", "text")
|
||||||
|
.addColumn("filename", "text")
|
||||||
|
.addColumn("dateDeleted", "integer")
|
||||||
|
.addColumn("dateUploaded", "integer")
|
||||||
|
.addColumn("failed", "text")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("relation_from_general")
|
||||||
|
.on("relations")
|
||||||
|
.columns(["fromType", "toType", "fromId"])
|
||||||
|
.where("toType", "!=", "note")
|
||||||
|
.where("toType", "!=", "notebook")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("relation_to_general")
|
||||||
|
.on("relations")
|
||||||
|
.columns(["fromType", "toType", "toId"])
|
||||||
|
.where("fromType", "!=", "note")
|
||||||
|
.where("fromType", "!=", "notebook")
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("relation_from_note_notebook")
|
||||||
|
.on("relations")
|
||||||
|
.columns(["fromType", "toType", "fromId", "toId"])
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([
|
||||||
|
eb("toType", "==", "note"),
|
||||||
|
eb("toType", "==", "notebook")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("relation_to_note_notebook")
|
||||||
|
.on("relations")
|
||||||
|
.columns(["fromType", "toType", "toId", "fromId"])
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([
|
||||||
|
eb("fromType", "==", "note"),
|
||||||
|
eb("fromType", "==", "notebook")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("note_type")
|
||||||
|
.on("notes")
|
||||||
|
.columns(["type"])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("notebook_type")
|
||||||
|
.on("notebooks")
|
||||||
|
.columns(["type"])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex("attachment_hash")
|
||||||
|
.on("attachments")
|
||||||
|
.column("hash")
|
||||||
|
.execute();
|
||||||
|
},
|
||||||
|
async down(db) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addBaseColumns = <T extends string, C extends string = never>(
|
||||||
|
builder: CreateTableBuilder<T, C>
|
||||||
|
) => {
|
||||||
|
return builder
|
||||||
|
.addColumn("id", "text", (c) => c.primaryKey().unique().notNull())
|
||||||
|
.addColumn("type", "text")
|
||||||
|
.addColumn("dateModified", "integer")
|
||||||
|
.addColumn("dateCreated", "integer")
|
||||||
|
.addColumn("synced", "boolean")
|
||||||
|
.addColumn("deleted", "boolean");
|
||||||
|
};
|
||||||
169
packages/core/src/database/sql-cached-collection.ts
Normal file
169
packages/core/src/database/sql-cached-collection.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MaybeDeletedItem, isDeleted } from "../types";
|
||||||
|
import EventManager from "../utils/event-manager";
|
||||||
|
import { DatabaseAccessor, DatabaseCollection, DatabaseSchema } from ".";
|
||||||
|
import { SQLCollection } from "./sql-collection";
|
||||||
|
|
||||||
|
export class SQLCachedCollection<
|
||||||
|
TCollectionType extends keyof DatabaseSchema,
|
||||||
|
T extends DatabaseSchema[TCollectionType] = DatabaseSchema[TCollectionType]
|
||||||
|
> implements DatabaseCollection<T, false>
|
||||||
|
{
|
||||||
|
private collection: SQLCollection<TCollectionType, T>;
|
||||||
|
private cache = new Map<string, MaybeDeletedItem<T>>();
|
||||||
|
// private cachedItems?: T[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
sql: DatabaseAccessor,
|
||||||
|
type: TCollectionType,
|
||||||
|
eventManager: EventManager
|
||||||
|
) {
|
||||||
|
this.collection = new SQLCollection(sql, type, eventManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.collection.init();
|
||||||
|
// const data = await this.collection.indexer.readMulti(
|
||||||
|
// this.collection.indexer.indices
|
||||||
|
// );
|
||||||
|
// this.cache = new Map(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// async add(item: MaybeDeletedItem<T>) {
|
||||||
|
// await this.collection.addItem(item);
|
||||||
|
// this.cache.set(item.id, item);
|
||||||
|
// this.invalidateCache();
|
||||||
|
// }
|
||||||
|
|
||||||
|
async clear() {
|
||||||
|
await this.collection.clear();
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsert(item: T) {
|
||||||
|
await this.collection.upsert(item);
|
||||||
|
this.cache.set(item.id, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(ids: string[]) {
|
||||||
|
ids.forEach((id) => this.cache.delete(id));
|
||||||
|
await this.collection.delete(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(ids: string[]) {
|
||||||
|
ids.forEach((id) =>
|
||||||
|
this.cache.set(id, {
|
||||||
|
id,
|
||||||
|
deleted: true,
|
||||||
|
dateModified: Date.now()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await this.collection.softDelete(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
exists(id: string) {
|
||||||
|
const item = this.cache.get(id);
|
||||||
|
return !!item && !isDeleted(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
count(): number {
|
||||||
|
return this.cache.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: string): T | undefined {
|
||||||
|
const item = this.cache.get(id);
|
||||||
|
if (!item || isDeleted(item)) return;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(items: (T | undefined)[]): Promise<void> {
|
||||||
|
await this.collection.put(items);
|
||||||
|
for (const item of items) {
|
||||||
|
if (item) this.cache.set(item.id, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(ids: string[], partial: Partial<T>): Promise<void> {
|
||||||
|
await this.collection.update(ids, partial);
|
||||||
|
for (const id of ids) {
|
||||||
|
const item = this.cache.get(id);
|
||||||
|
if (!item) continue;
|
||||||
|
this.cache.set(id, { ...item, ...partial, dateModified: Date.now() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// has(id: string) {
|
||||||
|
// return this.cache.has(id);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// count() {
|
||||||
|
// return this.cache.size;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// get(id: string) {
|
||||||
|
// const item = this.cache.get(id);
|
||||||
|
// if (!item || isDeleted(item)) return;
|
||||||
|
// return item;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getRaw(id: string) {
|
||||||
|
// const item = this.cache.get(id);
|
||||||
|
// return item;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// raw() {
|
||||||
|
// return Array.from(this.cache.values());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// items(map?: (item: T) => T | undefined) {
|
||||||
|
// if (this.cachedItems && this.cachedItems.length === this.cache.size)
|
||||||
|
// return this.cachedItems;
|
||||||
|
|
||||||
|
// this.cachedItems = [];
|
||||||
|
// this.cache.forEach((value) => {
|
||||||
|
// if (isDeleted(value)) return;
|
||||||
|
// const mapped = map ? map(value) : value;
|
||||||
|
// if (!mapped) return;
|
||||||
|
// this.cachedItems?.push(mapped);
|
||||||
|
// });
|
||||||
|
// this.cachedItems.sort((a, b) => b.dateCreated - a.dateCreated);
|
||||||
|
// return this.cachedItems;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async setItems(items: (MaybeDeletedItem<T> | undefined)[]) {
|
||||||
|
// await this.collection.setItems(items);
|
||||||
|
// for (const item of items) {
|
||||||
|
// if (item) {
|
||||||
|
// this.cache.set(item.id, item);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.invalidateCache();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// *iterateSync(chunkSize: number) {
|
||||||
|
// yield* chunkedIterate(Array.from(this.cache.values()), chunkSize);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// invalidateCache() {
|
||||||
|
// this.cachedItems = undefined;
|
||||||
|
// }
|
||||||
|
}
|
||||||
148
packages/core/src/database/sql-collection.ts
Normal file
148
packages/core/src/database/sql-collection.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the Notesnook project (https://notesnook.com/)
|
||||||
|
|
||||||
|
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EVENTS } from "../common";
|
||||||
|
import { isDeleted } from "../types";
|
||||||
|
import EventManager from "../utils/event-manager";
|
||||||
|
import {
|
||||||
|
DatabaseAccessor,
|
||||||
|
DatabaseCollection,
|
||||||
|
DatabaseSchema,
|
||||||
|
SQLiteItem
|
||||||
|
} from ".";
|
||||||
|
|
||||||
|
export class SQLCollection<
|
||||||
|
TCollectionType extends keyof DatabaseSchema,
|
||||||
|
T extends DatabaseSchema[TCollectionType] = DatabaseSchema[TCollectionType]
|
||||||
|
> implements DatabaseCollection<SQLiteItem<T>, true>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly db: DatabaseAccessor,
|
||||||
|
private readonly type: TCollectionType,
|
||||||
|
private readonly eventManager: EventManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async clear() {
|
||||||
|
await this.db().deleteFrom(this.type).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {}
|
||||||
|
|
||||||
|
async upsert(item: SQLiteItem<T>) {
|
||||||
|
if (!item.id) throw new Error("The item must contain the id field.");
|
||||||
|
if (!item.deleted) item.dateCreated = item.dateCreated || Date.now();
|
||||||
|
this.eventManager.publish(EVENTS.databaseUpdated, item.id, item);
|
||||||
|
|
||||||
|
// if item is newly synced, remote will be true.
|
||||||
|
if (!item.remote) {
|
||||||
|
item.dateModified = Date.now();
|
||||||
|
item.synced = false;
|
||||||
|
}
|
||||||
|
// the item has become local now, so remove the flags
|
||||||
|
delete item.remote;
|
||||||
|
|
||||||
|
await this.db()
|
||||||
|
.replaceInto<keyof DatabaseSchema>(this.type)
|
||||||
|
.values(item)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDelete(ids: string[]) {
|
||||||
|
this.eventManager.publish(EVENTS.databaseUpdated, ids);
|
||||||
|
await this.db()
|
||||||
|
.replaceInto<keyof DatabaseSchema>(this.type)
|
||||||
|
.values(
|
||||||
|
ids.map((id) => ({
|
||||||
|
id,
|
||||||
|
deleted: true,
|
||||||
|
dateModified: Date.now()
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(ids: string[]) {
|
||||||
|
this.eventManager.publish(EVENTS.databaseUpdated, ids);
|
||||||
|
await this.db()
|
||||||
|
.deleteFrom<keyof DatabaseSchema>(this.type)
|
||||||
|
.where("id", "in", ids)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(id: string) {
|
||||||
|
const { count } =
|
||||||
|
(await this.db()
|
||||||
|
.selectFrom<keyof DatabaseSchema>(this.type)
|
||||||
|
.select((a) => a.fn.count<number>("id").as("count"))
|
||||||
|
.where("id", "==", id)
|
||||||
|
.limit(1)
|
||||||
|
.executeTakeFirst()) || {};
|
||||||
|
|
||||||
|
return count !== undefined && count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async count() {
|
||||||
|
const { count } =
|
||||||
|
(await this.db()
|
||||||
|
.selectFrom<keyof DatabaseSchema>(this.type)
|
||||||
|
.select((a) => a.fn.count<number>("id").as("count"))
|
||||||
|
.where("deleted", "is", null)
|
||||||
|
.executeTakeFirst()) || {};
|
||||||
|
return count || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: string) {
|
||||||
|
const item = await this.db()
|
||||||
|
.selectFrom<keyof DatabaseSchema>(this.type)
|
||||||
|
.selectAll()
|
||||||
|
.where("id", "==", id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (!item || isDeleted(item)) return;
|
||||||
|
return item as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async put(items: (SQLiteItem<T> | undefined)[]) {
|
||||||
|
const entries = items.reduce((array, item) => {
|
||||||
|
if (!item) return array;
|
||||||
|
if (!item.remote) {
|
||||||
|
item.dateModified = Date.now();
|
||||||
|
item.synced = false;
|
||||||
|
}
|
||||||
|
delete item.remote;
|
||||||
|
array.push(item);
|
||||||
|
return array;
|
||||||
|
}, [] as SQLiteItem<T>[]);
|
||||||
|
|
||||||
|
await this.db()
|
||||||
|
.replaceInto<keyof DatabaseSchema>(this.type)
|
||||||
|
.values(entries)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(ids: string[], partial: Partial<SQLiteItem<T>>) {
|
||||||
|
await this.db()
|
||||||
|
.updateTable<keyof DatabaseSchema>(this.type)
|
||||||
|
.where("id", "in", ids)
|
||||||
|
.set({
|
||||||
|
...partial,
|
||||||
|
dateModified: Date.now()
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,14 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Cipher, DataFormat, SerializedKey } from "@notesnook/crypto";
|
import { Cipher, DataFormat, SerializedKey } from "@notesnook/crypto";
|
||||||
import { AttachmentMetadata } from "./types";
|
|
||||||
|
|
||||||
export type Output<TOutputFormat extends DataFormat> =
|
export type Output<TOutputFormat extends DataFormat> =
|
||||||
TOutputFormat extends Omit<DataFormat, "uint8array"> ? string : Uint8Array;
|
TOutputFormat extends Omit<DataFormat, "uint8array"> ? string : Uint8Array;
|
||||||
export type FileEncryptionMetadata = {
|
export type FileEncryptionMetadata = {
|
||||||
chunkSize: number;
|
chunkSize: number;
|
||||||
iv: string;
|
iv: string;
|
||||||
length: number;
|
size: number;
|
||||||
salt: string;
|
salt: string;
|
||||||
alg: string;
|
alg: string;
|
||||||
};
|
};
|
||||||
@@ -74,7 +73,7 @@ export interface ICompressor {
|
|||||||
|
|
||||||
export type RequestOptions = {
|
export type RequestOptions = {
|
||||||
url: string;
|
url: string;
|
||||||
metadata?: AttachmentMetadata;
|
// metadata?: AttachmentMetadata;
|
||||||
chunkSize: number;
|
chunkSize: number;
|
||||||
headers: { Authorization: string };
|
headers: { Authorization: string };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -298,10 +298,17 @@ const migrations: Migration[] = [
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
shortcut: (item) => {
|
shortcut: (item) => {
|
||||||
if (item.item.type === "topic") {
|
if (item.item?.type === "topic") {
|
||||||
item.item = { type: "notebook", id: item.item.id };
|
item.item = { type: "notebook", id: item.item.id };
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.item) {
|
||||||
|
item.itemId = item.item.id;
|
||||||
|
item.itemType = item.item.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete item.item;
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
settings: async (item, db) => {
|
settings: async (item, db) => {
|
||||||
if (item.trashCleanupInterval)
|
if (item.trashCleanupInterval)
|
||||||
@@ -336,6 +343,16 @@ const migrations: Migration[] = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
},
|
||||||
|
relation: (item) => {
|
||||||
|
item.fromId = item.from!.id;
|
||||||
|
item.fromType = item.from!.type;
|
||||||
|
item.toId = item.to!.id;
|
||||||
|
item.toType = item.to!.type;
|
||||||
|
|
||||||
|
delete item.to;
|
||||||
|
delete item.from;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export interface BaseItem<TType extends ItemType> {
|
|||||||
migrated?: boolean;
|
migrated?: boolean;
|
||||||
remote?: boolean;
|
remote?: boolean;
|
||||||
synced?: boolean;
|
synced?: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NotebookReference = {
|
export type NotebookReference = {
|
||||||
@@ -164,6 +165,9 @@ export interface Note extends BaseItem<"note"> {
|
|||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
|
|
||||||
dateEdited: number;
|
dateEdited: number;
|
||||||
|
|
||||||
|
dateDeleted: null;
|
||||||
|
itemType: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Notebook extends BaseItem<"notebook"> {
|
export interface Notebook extends BaseItem<"notebook"> {
|
||||||
@@ -171,6 +175,10 @@ export interface Notebook extends BaseItem<"notebook"> {
|
|||||||
description?: string;
|
description?: string;
|
||||||
dateEdited: number;
|
dateEdited: number;
|
||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
|
|
||||||
|
dateDeleted: null;
|
||||||
|
itemType: null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated only kept here for migration purposes.
|
* @deprecated only kept here for migration purposes.
|
||||||
*/
|
*/
|
||||||
@@ -191,6 +199,9 @@ export interface Topic extends BaseItem<"topic"> {
|
|||||||
notes?: string[];
|
notes?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes
|
||||||
|
*/
|
||||||
export type AttachmentMetadata = {
|
export type AttachmentMetadata = {
|
||||||
hash: string;
|
hash: string;
|
||||||
hashType: string;
|
hashType: string;
|
||||||
@@ -201,15 +212,32 @@ export type AttachmentMetadata = {
|
|||||||
export interface Attachment extends BaseItem<"attachment"> {
|
export interface Attachment extends BaseItem<"attachment"> {
|
||||||
iv: string;
|
iv: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
length: number;
|
|
||||||
alg: string;
|
alg: string;
|
||||||
key: Cipher<"base64">;
|
|
||||||
chunkSize: number;
|
chunkSize: number;
|
||||||
metadata: AttachmentMetadata;
|
|
||||||
dateUploaded?: number;
|
dateUploaded?: number;
|
||||||
failed?: string;
|
failed?: string;
|
||||||
dateDeleted?: number;
|
dateDeleted?: number;
|
||||||
|
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
hash: string;
|
||||||
|
hashType: string;
|
||||||
|
mimeType: string;
|
||||||
|
encryptionKey: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes
|
||||||
|
*/
|
||||||
|
key?: Cipher<"base64">;
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes
|
||||||
|
*/
|
||||||
|
length?: number;
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes
|
||||||
|
*/
|
||||||
|
metadata?: AttachmentMetadata;
|
||||||
/**
|
/**
|
||||||
* @deprecated only kept here for migration purposes
|
* @deprecated only kept here for migration purposes
|
||||||
*/
|
*/
|
||||||
@@ -244,14 +272,31 @@ export type ItemReference = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface Relation extends BaseItem<"relation"> {
|
export interface Relation extends BaseItem<"relation"> {
|
||||||
from: ItemReference;
|
fromId: string;
|
||||||
to: ItemReference;
|
fromType: keyof ItemMap;
|
||||||
|
toId: string;
|
||||||
|
toType: keyof ItemMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes
|
||||||
|
*/
|
||||||
|
from?: ItemReference;
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes
|
||||||
|
*/
|
||||||
|
to?: ItemReference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes
|
||||||
|
*/
|
||||||
type BaseShortcutReference = {
|
type BaseShortcutReference = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes
|
||||||
|
*/
|
||||||
type TagNotebookShortcutReference = BaseShortcutReference & {
|
type TagNotebookShortcutReference = BaseShortcutReference & {
|
||||||
type: "tag" | "notebook";
|
type: "tag" | "notebook";
|
||||||
};
|
};
|
||||||
@@ -265,7 +310,14 @@ type TopicShortcutReference = BaseShortcutReference & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface Shortcut extends BaseItem<"shortcut"> {
|
export interface Shortcut extends BaseItem<"shortcut"> {
|
||||||
item: TopicShortcutReference | TagNotebookShortcutReference;
|
itemId: string;
|
||||||
|
itemType: "tag" | "notebook";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated only kept here for migration purposes
|
||||||
|
*/
|
||||||
|
item?: TopicShortcutReference | TagNotebookShortcutReference;
|
||||||
|
|
||||||
sortIndex: number;
|
sortIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,17 +428,14 @@ export type TrashOrItem<T extends BaseItem<"note" | "notebook">> =
|
|||||||
|
|
||||||
export type BaseTrashItem<TItem extends BaseItem<"note" | "notebook">> =
|
export type BaseTrashItem<TItem extends BaseItem<"note" | "notebook">> =
|
||||||
BaseItem<"trash"> & {
|
BaseItem<"trash"> & {
|
||||||
title: string;
|
|
||||||
itemType: TItem["type"];
|
itemType: TItem["type"];
|
||||||
dateDeleted: number;
|
dateDeleted: number;
|
||||||
} & Omit<TItem, "id" | "type">;
|
} & Omit<TItem, "id" | "type" | "dateDeleted" | "itemType">;
|
||||||
|
|
||||||
export type TrashItem = BaseTrashItem<Note> | BaseTrashItem<Notebook>;
|
export type TrashItem = BaseTrashItem<Note> | BaseTrashItem<Notebook>;
|
||||||
|
|
||||||
export function isDeleted<T extends BaseItem<ItemType>>(
|
export function isDeleted(item: object): item is DeletedItem {
|
||||||
item: MaybeDeletedItem<T>
|
return "deleted" in item && !!item.deleted;
|
||||||
): item is DeletedItem {
|
|
||||||
return "deleted" in item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTrashItem(item: MaybeDeletedItem<Item>): item is TrashItem {
|
export function isTrashItem(item: MaybeDeletedItem<Item>): item is TrashItem {
|
||||||
|
|||||||
@@ -27,10 +27,14 @@ export default defineConfig({
|
|||||||
coverage: {
|
coverage: {
|
||||||
reporter: ["text", "html"]
|
reporter: ["text", "html"]
|
||||||
},
|
},
|
||||||
|
exclude: ["__benches__/**/*.bench.ts"],
|
||||||
include: [
|
include: [
|
||||||
...(IS_E2E ? ["__e2e__/**/*.test.{js,ts}"] : []),
|
...(IS_E2E ? ["__e2e__/**/*.test.{js,ts}"] : []),
|
||||||
"__tests__/**/*.test.{js,ts}",
|
"__tests__/**/*.test.{js,ts}",
|
||||||
"src/**/*.test.{js,ts}"
|
"src/**/*.test.{js,ts}"
|
||||||
]
|
],
|
||||||
|
benchmark: {
|
||||||
|
include: ["__benches__/**/*.bench.ts"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user