diff --git a/package-lock.json b/package-lock.json index c7c2ef20c6..f66516c558 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3607,7 +3607,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3631,13 +3632,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3654,19 +3657,22 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3797,7 +3803,8 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3811,6 +3818,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3827,6 +3835,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3835,13 +3844,15 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3862,6 +3873,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3950,7 +3962,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3964,6 +3977,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4059,7 +4073,8 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4101,6 +4116,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4122,6 +4138,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4170,13 +4187,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true + "dev": true, + "optional": true } } }, diff --git a/src/main/components/App.tsx b/src/main/components/App.tsx index 4c530eebdf..9bbcd27651 100644 --- a/src/main/components/App.tsx +++ b/src/main/components/App.tsx @@ -1,7 +1,7 @@ import React from 'react' import { inject, observer } from 'mobx-react' import SideNavigator from './SideNavigator' -import NotePage from './NotePage' +import Router from './Router' import AppStore from '../lib/AppStore' import GlobalStyle from './GlobalStyle' import { ThemeProvider } from 'emotion-theming' @@ -32,7 +32,7 @@ class App extends React.Component { {app.dataIsInitialized ? ( <> - + ) : (
Loading data
diff --git a/src/main/components/NotePage/NoteDetail/NoteDetail.tsx b/src/main/components/NotePage/NoteDetail/NoteDetail.tsx index 04fed1535d..932de4007d 100644 --- a/src/main/components/NotePage/NoteDetail/NoteDetail.tsx +++ b/src/main/components/NotePage/NoteDetail/NoteDetail.tsx @@ -3,18 +3,18 @@ import { inject, observer } from 'mobx-react' import { Note } from '../../../types' type NoteDetailProps = { - storageName: string + storageId: string note: Note updateNote: ( - storageName: string, + storageId: string, noteId: string, { content }: { content: string } ) => Promise - removeNote: (storageName: string, noteId: string) => Promise + removeNote: (storageId: string, noteId: string) => Promise } type NoteDetailState = { - prevStorageName: string + prevStorageId: string prevNoteId: string content: string } @@ -26,7 +26,7 @@ export default class NoteDetail extends React.Component< NoteDetailState > { state = { - prevStorageName: '', + prevStorageId: '', prevNoteId: '', content: '' } @@ -36,13 +36,10 @@ export default class NoteDetail extends React.Component< props: NoteDetailProps, state: NoteDetailState ): NoteDetailState { - const { note, storageName } = props - if ( - storageName !== state.prevStorageName || - note._id !== state.prevNoteId - ) { + const { note, storageId } = props + if (storageId !== state.prevStorageId || note._id !== state.prevNoteId) { return { - prevStorageName: storageName, + prevStorageId: storageId, prevNoteId: note._id, content: note.content } @@ -54,7 +51,7 @@ export default class NoteDetail extends React.Component< const { note } = this.props if (note._id !== prevState.prevNoteId && this.queued) { const { content } = prevState - this.saveNote(prevState.prevStorageName, prevState.prevNoteId, { + this.saveNote(prevState.prevStorageId, prevState.prevNoteId, { content }) } @@ -62,8 +59,8 @@ export default class NoteDetail extends React.Component< componentWillUnmount() { if (this.queued) { - const { content, prevStorageName, prevNoteId } = this.state - this.saveNote(prevStorageName, prevNoteId, { + const { content, prevStorageId, prevNoteId } = this.state + this.saveNote(prevStorageId, prevNoteId, { content }) } @@ -89,15 +86,15 @@ export default class NoteDetail extends React.Component< clearTimeout(this.timer) } this.timer = setTimeout(() => { - const { storageName, note } = this.props + const { storageId, note } = this.props const { content } = this.state const {} = this.state - this.saveNote(storageName, note._id, { content }) + this.saveNote(storageId, note._id, { content }) }, 3000) } async saveNote( - storageName: string, + storageId: string, noteId: string, { content }: { content: string } ) { @@ -105,15 +102,15 @@ export default class NoteDetail extends React.Component< this.queued = false const { updateNote } = this.props - await updateNote(storageName, noteId, { + await updateNote(storageId, noteId, { content }) } removeNote = async () => { - const { storageName, note, removeNote } = this.props + const { storageId, note, removeNote } = this.props - await removeNote(storageName, note._id) + await removeNote(storageId, note._id) } render() { diff --git a/src/main/components/NotePage/NotePage.tsx b/src/main/components/NotePage/NotePage.tsx index 4c59d4aa1e..5ad4bc9169 100644 --- a/src/main/components/NotePage/NotePage.tsx +++ b/src/main/components/NotePage/NotePage.tsx @@ -1,12 +1,12 @@ import React from 'react' import { inject, observer } from 'mobx-react' -import pathToRegexp from 'path-to-regexp' import NoteList from './NoteList' import NoteDetail from './NoteDetail' import { RouteStore } from '../../lib/RouteStore' import { DataStore } from '../../lib/db/DataStore' import { withRouter, RouteComponentProps } from 'react-router-dom' import { computed } from 'mobx' +import { storageRegexp, folderRegexp, tagRegexp } from '../../lib/routes' type NotePageProps = { data?: DataStore @@ -15,28 +15,14 @@ type NotePageProps = { type NotePageState = {} -const storageRegexp = pathToRegexp('/storages/:storageName/:rest*', undefined, { - sensitive: true -}) -const folderRegexp = pathToRegexp( - '/storages/:storageName/notes/:rest*', - undefined, - { - sensitive: true - } -) -const tagRegexp = pathToRegexp('/storages/:storageName/tags/:tag', undefined, { - sensitive: true -}) - @inject('data', 'route') @observer class NotePage extends React.Component { @computed get currentStorage() { const { data } = this.props - const { currentStorageName } = this - return data!.storageMap.get(currentStorageName) + const { currentStorageId } = this + return data!.storageMap.get(currentStorageId) } @computed @@ -49,6 +35,12 @@ class NotePage extends React.Component { return storageName } + @computed + get currentStorageId() { + const { data } = this.props + return data!.getStorageId(this.currentStorageName) + } + @computed get currentNoteId() { const { route } = this.props @@ -111,11 +103,11 @@ class NotePage extends React.Component { createNote = async () => { const { data, history } = this.props - const { currentStorageName, currentFolderPath } = this + const { currentStorageId, currentFolderPath } = this const targetFolderPath = currentFolderPath == null ? '/' : currentFolderPath const createdNote = await data!.createNote( - currentStorageName, + currentStorageId, targetFolderPath, { content: ['---', 'title: ', 'tags: ', '---', ''].join('\n') @@ -126,29 +118,24 @@ class NotePage extends React.Component { } updateNote = async ( - storageName: string, + storageId: string, noteId: string, { content }: { content: string } ) => { const { data } = this.props - await data!.updateNote(storageName, noteId, { + await data!.updateNote(storageId, noteId, { content }) } // TODO: Redirect to the next note after deleting selected note - removeNote = async (storageName: string, noteId: string) => { + removeNote = async (storageId: string, noteId: string) => { const { data } = this.props - await data!.removeNote(storageName, noteId) + await data!.removeNote(storageId, noteId) } render() { - const { - currentStorageName, - filteredNotes, - currentNote, - currentNoteId - } = this + const { currentStorageId, filteredNotes, currentNote, currentNoteId } = this return ( <> {
No note selected
) : ( { + render() { + const { route } = this.props + const { pathname } = route! + if (storageRegexp.exec(pathname)) return + return ( + +

Page not found

+

Check the URL or click other link in the left side navigation.

+
+ ) + } +} diff --git a/src/main/components/SideNavigator/SideNavigator.tsx b/src/main/components/SideNavigator/SideNavigator.tsx index 8b21de26dc..b0ea41e1ca 100644 --- a/src/main/components/SideNavigator/SideNavigator.tsx +++ b/src/main/components/SideNavigator/SideNavigator.tsx @@ -19,19 +19,19 @@ export default class SideNavigator extends React.Component { await data!.createStorage(storageName) } - removeStorage = async (storageName: string) => { + removeStorage = async (storageId: string) => { const { data } = this.props - await data!.removeStorage(storageName) + await data!.removeStorage(storageId) } - createFolder = async (storageName: string, folderPath: string) => { + createFolder = async (storageId: string, folderPath: string) => { const { data } = this.props - await data!.createFolder(storageName, folderPath) + await data!.createFolder(storageId, folderPath) } - removeFolder = async (storageName: string, folderPath: string) => { + removeFolder = async (storageId: string, folderPath: string) => { const { data } = this.props - await data!.removeFolder(storageName, folderPath) + await data!.removeFolder(storageId, folderPath) } render() { @@ -41,13 +41,13 @@ export default class SideNavigator extends React.Component { return ( - {storageEntries.map(([name, storage]) => { + {storageEntries.map(([id, storage]) => { const pathname = route!.pathname - const active = `/storages/${name}` === pathname + const active = `/storages/${storage.name}` === pathname return ( Promise createFolder: (storageName: string, folderPath: string) => Promise removeFolder: (storageName: string, folderPath: string) => Promise pathname: string active: boolean + contextMenu?: ContextMenuStore + dialog?: DialogStore } +@inject('contextMenu', 'dialog') @observer class StorageItem extends React.Component { @computed @@ -39,43 +46,88 @@ class StorageItem extends React.Component { } removeStorage = () => { - const { name, removeStorage } = this.props - removeStorage(name) + const { id, removeStorage } = this.props + removeStorage(id) } createFolder = async (folderPath: string) => { - const { name, createFolder } = this.props - await createFolder(name, folderPath) + const { id, createFolder } = this.props + await createFolder(id, folderPath) } removeFolder = async (folderPath: string) => { - const { name, removeFolder } = this.props - await removeFolder(name, folderPath) + const { id, removeFolder } = this.props + await removeFolder(id, folderPath) + } + + openContextMenu = (event: React.MouseEvent) => { + event.preventDefault() + const { contextMenu, dialog, storage } = this.props + const storageName = storage.name + + contextMenu!.open(event, [ + { + type: MenuTypes.Normal, + label: 'New Folder', + onClick: async () => { + dialog!.prompt({ + title: 'Create a Folder', + message: 'Enter the path where do you want to create a folder', + iconType: DialogIconTypes.Question, + defaultValue: '/', + submitButtonLabel: 'Create Folder', + onClose: (value: string | null) => { + if (value == null) return + this.createFolder(value) + } + }) + } + }, + { + type: MenuTypes.Normal, + label: 'Remove Storage', + onClick: async () => { + dialog!.messageBox({ + title: `Remove "${storageName}" storage`, + message: 'All notes and folders will be deleted.', + iconType: DialogIconTypes.Warning, + buttons: ['Remove Storage', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 1, + onClose: (value: number | null) => { + if (value === 0) { + this.removeStorage() + } + } + }) + } + } + ]) } render() { - const { name, pathname, active } = this.props + const { storage, pathname, active } = this.props + const storageName = storage.name return ( - - - {name} + + + {storageName} - {this.folders.map(folder => { const folderPathname = folder.path === '/' - ? `/storages/${name}/notes` - : `/storages/${name}/notes${folder.path}` + ? `/storages/${storageName}/notes` + : `/storages/${storageName}/notes${folder.path}` const folderIsActive = folderPathname === pathname return ( {
    {this.tags.map(tag => { - const tagIsActive = pathname === `/storages/${name}/tags/${tag}` + const tagIsActive = + pathname === `/storages/${storageName}/tags/${tag}` return (
  • {tag} diff --git a/src/main/components/styled.ts b/src/main/components/styled.ts index ccb8490d13..7f3aae6626 100644 --- a/src/main/components/styled.ts +++ b/src/main/components/styled.ts @@ -22,3 +22,7 @@ export const StyledAppContainer = styled.div` } } ` + +export const StyledNotFoundPage = styled.div` + padding: 15px 25px; +` diff --git a/src/main/lib/db/Client.spec.ts b/src/main/lib/db/Client.spec.ts index 89b573026f..14a9bc90c3 100644 --- a/src/main/lib/db/Client.spec.ts +++ b/src/main/lib/db/Client.spec.ts @@ -4,10 +4,11 @@ import { FOLDER_ID_PREFIX } from '../../consts' let clientCount = 0 async function createClient(shouldInit: boolean = true): Promise { - const db = new PouchDB(`dummy${++clientCount}`, { + const id = `dummy${++clientCount}` + const db = new PouchDB(id, { adapter: 'memory' }) - const client = new Client(db) + const client = new Client(db, id, id) if (shouldInit) { await client.init() diff --git a/src/main/lib/db/Client.ts b/src/main/lib/db/Client.ts index e06fd7a6a3..5617897b7c 100644 --- a/src/main/lib/db/Client.ts +++ b/src/main/lib/db/Client.ts @@ -1,7 +1,7 @@ +import semver from 'semver' import { FOLDER_ID_PREFIX, NOTE_ID_PREFIX } from '../../consts' import * as Types from '../../types' -import uuid from 'uuid/v1' -import semver from 'semver' +import uuid from './uuid' export enum ClientErrorTypes { ConflictError = 'ConflictError', @@ -33,7 +33,11 @@ export const metaDataId = 'meta' export default class Client { public initialized: boolean - constructor(private db: PouchDB.Database) {} + constructor( + private db: PouchDB.Database, + public id: string, + public name: string + ) {} getDb() { return this.db diff --git a/src/main/lib/db/ClientManager.spec.ts b/src/main/lib/db/ClientManager.spec.ts index 04ddb39981..29803fe14e 100644 --- a/src/main/lib/db/ClientManager.spec.ts +++ b/src/main/lib/db/ClientManager.spec.ts @@ -11,83 +11,39 @@ describe('ClientManager', () => { }) }) - describe('#getAllClientNames', () => { - it('returns all names of clients', async () => { + describe('#getStorageId', () => { + it('returns storage id for name', async () => { // Given - await manager.addClient('test') + const { id } = await manager.addClient('test') // When - const names = manager.getAllClientNames() + const result = manager.getStorageId('test') // Then - expect(names).toEqual(['default', 'test']) + expect(result).toBe(id) }) - }) - - describe('#setAllClientNames', () => { - it('sets all names of clients', () => { - // When - manager.setAllClientNames(['test']) - - // Then - const names = manager.getAllClientNames() - expect(names).toEqual(['test']) - }) - }) - describe('#addClient', () => { - it('creates new client', async () => { + it('throws if the storage is not registered', () => { // When - const client = await manager.addClient('test') + const run = () => { + manager.getStorageId('test') + } // Then - expect(client).toBeInstanceOf(Client) - const names = manager.getAllClientNames() - expect(names).toEqual(['default', 'test']) + expect(run).toThrow() }) }) describe('#getClient', () => { - it('returns a client', async () => { - await manager.addClient('test') - - const client = manager.getClient('test') as Client - - expect(client).toBeInstanceOf(Client) - }) - - it('throws an error if the client does not exist', () => { - expect(() => { - manager.getClient('test') - }).toThrowError('The client, "test", is not added yet.') - }) - }) - - describe('#removeClient', () => { - it('removes a client', async () => { + it('returns storage client', async () => { // Given - await manager.addClient('test') + const { id } = await manager.addClient('test') // When - await manager.removeClient('test') + const client = manager.getClient(id) // Then - expect(() => { - manager.getClient('test') - }).toThrowError('The client, "test", is not added yet.') - const noteNames = manager.getAllClientNames() - expect(noteNames).toEqual(['default']) - }) - }) - - describe('#init', () => { - it('register all repository', async () => { - // When - await manager.init() - - // Then - const client = manager.getClient('default') - expect(client).toBeDefined() + expect(client).toBeInstanceOf(Client) }) }) }) diff --git a/src/main/lib/db/ClientManager.ts b/src/main/lib/db/ClientManager.ts index 60f75079df..0eab58f6c8 100644 --- a/src/main/lib/db/ClientManager.ts +++ b/src/main/lib/db/ClientManager.ts @@ -1,5 +1,6 @@ import Client from './Client' import PouchDB from './PouchDB' +import uuid from './uuid' export const reservedStorageNameRegex = /[\/?#<>:"\\|?*\x00-\x1F]/ @@ -13,94 +14,102 @@ export interface ClientManagerOptions { adapter?: DBAdapter } -const defaultOptions: ClientManagerOptions = { - adapter: DBAdapter.idb -} +const BOOST_STORAGE_META = 'BOOST_STORAGE_META' -const BOOST_DB_NAMES = 'BOOST_DB_NAMES' -const defaultDBNames = ['default'] +interface SerializedStorageMeta { + id: string + name: string +} export default class ClientManager { - private storage: Storage = localStorage - private clientMap: Map + private webStorage: Storage = localStorage + private storageNameIdMap: Map + private storageIdClientMap: Map private adapter: DBAdapter = DBAdapter.idb constructor(options?: ClientManagerOptions) { options = { - ...defaultOptions, ...options } if (options.storage != null) { - this.storage = options.storage + this.webStorage = options.storage } if (options.adapter != null) { this.adapter = options.adapter } - this.clientMap = new Map() + this.storageIdClientMap = new Map() + this.storageNameIdMap = new Map() } - async addClient(clientName: string) { - this.assertClientName(clientName) - const db = new PouchDB(clientName, { - adapter: this.adapter - }) - const newClient = new Client(db) - - await newClient.init() - - this.clientMap.set(clientName, newClient) - - const clientNameSet = new Set(this.getAllClientNames()) - if (!clientNameSet.has(clientName)) { - clientNameSet.add(clientName) - this.setAllClientNames(clientNameSet) + getStorageId(name: string): string { + if (this.storageNameIdMap.has(name)) { + return this.storageNameIdMap.get(name)! } - - return newClient + throw new Error(`${name} is not registered yet.`) } - getClient(clientName: string): Client { - if (!this.clientMap.has(clientName)) { - throw new Error(`The client, "${clientName}", is not added yet.`) + getClient(clientId: string): Client { + if (clientId == null || !this.storageIdClientMap.has(clientId)) { + throw new Error(`The client id, "${clientId}", is not added yet.`) } - return this.clientMap.get(clientName) as Client + return this.storageIdClientMap.get(clientId) as Client } - getAllClientNames() { - try { - const rawData = this.storage.getItem(BOOST_DB_NAMES) - if (rawData == null) { - return defaultDBNames - } - return [...JSON.parse(rawData)].filter(key => typeof key === 'string') - } catch (error) { - return defaultDBNames - } + save() { + const storageNameIdEntries = [...this.storageNameIdMap.entries()] + const storageMetaList: SerializedStorageMeta[] = storageNameIdEntries.map( + ([name, id]) => ({ + id, + name + }) + ) + const stringifiedStorageMetaList = JSON.stringify(storageMetaList) + + this.webStorage.setItem(BOOST_STORAGE_META, stringifiedStorageMetaList) } - setAllClientNames(names: string[] | Set) { - names = [...names] - this.storage.setItem(BOOST_DB_NAMES, JSON.stringify(names)) + getStorageMetaList() { + const storageNameIdEntries = [...this.storageNameIdMap.entries()] + const storageMetaList: SerializedStorageMeta[] = storageNameIdEntries.map( + ([name, id]) => ({ + id, + name + }) + ) + + return storageMetaList + } + + load() { + let stringifiedStorageMetaList = this.webStorage.getItem(BOOST_STORAGE_META) + if (stringifiedStorageMetaList == null) stringifiedStorageMetaList = '[]' + const storageMetaList: SerializedStorageMeta[] = JSON.parse( + stringifiedStorageMetaList + ) + const storageNameIdEntries = storageMetaList.map( + ({ id, name }) => [name, id] as [string, string] + ) + this.storageNameIdMap = new Map(storageNameIdEntries) } async init() { - const names = this.getAllClientNames() + this.load() + const storageNameIdEntries = [...this.storageNameIdMap.entries()] await Promise.all( - names.map(async name => { - const client = await this.initClient(name) + storageNameIdEntries.map(async ([name, id]) => { + const client = await this.initClient(id, name) if (client != null) { - this.clientMap.set(name, client) + this.storageIdClientMap.set(id, client) } }) ) } - async initClient(clientName: string) { - this.assertClientName(clientName) - const db = new PouchDB(clientName, { + async initClient(clientId: string, clientName: string) { + const db = new PouchDB(clientId, { adapter: this.adapter }) - const newClient = new Client(db) + const newClient = new Client(db, clientId, clientName) try { await newClient.init() @@ -113,18 +122,45 @@ export default class ClientManager { return newClient } - async removeClient(clientName: string) { - const client = this.getClient(clientName) + async removeClient(clientId: string) { + const client = this.getClient(clientId) + const clientName = client.name await client.destroyDB() - const clientNameSet = new Set(this.getAllClientNames()) - clientNameSet.delete(clientName) - this.setAllClientNames(clientNameSet) - return this.clientMap.delete(clientName) + this.storageNameIdMap.delete(clientName) + this.storageIdClientMap.delete(clientId) + } + + async addClient(name: string): Promise { + this.assertClientName(name) + if (this.storageNameIdMap.has(name)) + throw new Error(`${name} is already taken.`) + const storageId = uuid() + const newClient = await this.initClient(storageId, name) + + if (newClient == null) throw new Error('Failed to init new storage client.') + + this.storageIdClientMap.set(storageId, newClient) + this.storageNameIdMap.set(name, storageId) + this.save() + + await newClient.init() + + return newClient } assertClientName(name: string) { if (reservedStorageNameRegex.test(name)) throw new Error('The given client name is invalid.') } + + async destroyAllDB() { + const clientEntries = [...this.storageIdClientMap.entries()] + + await Promise.all( + clientEntries.map(async ([, client]) => { + await client.destroyDB() + }) + ) + } } diff --git a/src/main/lib/db/DataStore.spec.ts b/src/main/lib/db/DataStore.spec.ts index 32302301fe..c79802811a 100644 --- a/src/main/lib/db/DataStore.spec.ts +++ b/src/main/lib/db/DataStore.spec.ts @@ -1,55 +1,49 @@ import { DataStore } from './DataStore' -import Storage from './Storage' import ClientManager, { DBAdapter } from '../db/ClientManager' import MemoryStorage from '../../specs/utils/MemoryStorage' -import PouchDB from '../db/PouchDB' -export async function prepare(): Promise { - const manager = new ClientManager({ - storage: new MemoryStorage(), - adapter: DBAdapter.memory - }) - - await manager.init() - await manager.addClient('test') - - return manager -} - -// TODO: Implement tests for all apis describe('DataStore', () => { - afterEach(async () => { - const db = new PouchDB('test', { - adapter: 'memory' + let manager: ClientManager + beforeEach(async () => { + manager = new ClientManager({ + storage: new MemoryStorage(), + adapter: DBAdapter.memory }) - await db.destroy() }) + afterEach(async () => { + await manager.destroyAllDB() + }) + describe('constructor', () => { it('uses optional manager', () => { - const manager = new ClientManager() + // When const data = new DataStore({ manager }) + // Then expect(data.manager).toBe(manager) }) }) describe('initStorage', () => { - it('initializes a storage instance', async () => { - const manager = await prepare() + it('inits manager', async () => { + // Given const data = new DataStore({ manager }) + manager.init = jest.fn() + // When await data.init() - expect(data.storageMap.get('test')).not.toBeUndefined() + // Then + expect(manager.init).toBeCalled() }) it('sets notes and folders to the storage instance', async () => { - const manager = await prepare() - const client = await manager.getClient('test') + // Given + const client = await manager.addClient('test') const note = await client.createNote('/', { content: 'test' }) @@ -57,11 +51,11 @@ describe('DataStore', () => { manager }) + // When await data.init() - expect(data.storageMap.get('test')).not.toBeUndefined() expect( - (data.storageMap.get('test') as Storage).noteMap.get(note._id) + data.storageMap.get(client.id)!.noteMap.get(note._id) ).toMatchObject({ content: 'test' }) @@ -71,20 +65,16 @@ describe('DataStore', () => { describe('createNote', () => { it('creates a note', async () => { // Given - const manager = await prepare() - const data = new DataStore({ - manager - }) + const { data, storageId } = await prepare(manager) await data.init() // When - const note = await data.createNote('test', '/', { + const note = await data.createNote(storageId, '/', { content: 'test' }) - expect(data.storageMap.get('test')).not.toBeUndefined() expect( - (data.storageMap.get('test') as Storage).noteMap.get(note._id) + data.storageMap.get(storageId)!.noteMap.get(note._id) ).toMatchObject({ content: 'test' }) @@ -94,20 +84,15 @@ describe('DataStore', () => { describe('updateFolder', () => { it('sets a folder', async () => { // Given - const manager = await prepare() - const data = new DataStore({ - manager - }) - await data.init() - await data.createFolder('test', '/test', {}) + const { data, storageId } = await prepare(manager) + await data.createFolder(storageId, '/test', {}) // When - await data.updateFolder('test', '/test', {}) + await data.updateFolder(storageId, '/test', {}) // THen - expect(data.storageMap.get('test')).not.toBeUndefined() expect( - (data.storageMap.get('test') as Storage).folderMap.get('folder:/test') + data.storageMap.get(storageId)!.folderMap.get('folder:/test') ).toMatchObject({ _id: 'folder:/test' }) @@ -117,20 +102,30 @@ describe('DataStore', () => { describe('removeFolder', () => { it('removes a folder', async () => { // Given - const manager = await prepare() - const data = new DataStore({ - manager - }) - await data.init() - await data.createFolder('test', '/test', {}) + const { data, storageId } = await prepare(manager) + await data.createFolder(storageId, '/test', {}) // When - await data.removeFolder('test', '/test') + await data.removeFolder(storageId, '/test') // Then expect( - (data.storageMap.get('test') as Storage).folderMap.has('folder:/test') + data.storageMap.get(storageId)!.folderMap.has('folder:/test') ).toBe(false) }) }) }) + +async function prepare(manager: ClientManager) { + const data = new DataStore({ + manager + }) + const client = await manager.addClient('test') + const storageId = client.id + await data.init() + + return { + data, + storageId + } +} diff --git a/src/main/lib/db/DataStore.ts b/src/main/lib/db/DataStore.ts index 3717e627ba..74d6237726 100644 --- a/src/main/lib/db/DataStore.ts +++ b/src/main/lib/db/DataStore.ts @@ -22,21 +22,21 @@ export class DataStore { async init() { await this.manager.init() - const clientNames = this.manager.getAllClientNames() + const storageMetaList = this.manager.getStorageMetaList() await Promise.all( - clientNames.map(async name => { - const storage = await this.initStorage(name) - this.addStorageToMap(name, storage) + storageMetaList.map(async ({ id, name }) => { + const storage = await this.initStorage(id, name) + this.addStorageToMap(id, storage) }) ) } - async initStorage(name: string): Promise { - const client = this.manager.getClient(name) + async initStorage(storageId: string, name: string): Promise { + const client = this.manager.getClient(storageId) const { folders, notes } = await client.getAllData() - const storage = new Storage() + const storage = new Storage(name) storage.addNote(...notes) storage.addFolder(...folders) @@ -45,82 +45,86 @@ export class DataStore { async createStorage(name: string) { const client = await this.manager.addClient(name) - + const { id } = client const { folders, notes } = await client.getAllData() - const storage = new Storage() + const storage = new Storage(name) storage.addNote(...notes) storage.addFolder(...folders) - this.addStorageToMap(name, storage) + this.addStorageToMap(id, storage) } - async removeStorage(name: string) { - await this.manager.removeClient(name) + async removeStorage(id: string) { + await this.manager.removeClient(id) - this.removeStorageFromMap(name) + this.removeStorageFromMap(id) + } + + getStorageId(name: string) { + return this.manager.getStorageId(name) } async createFolder( - name: string, + id: string, path: string, folder: Types.EditableFolderProps = {} ): Promise { - const client = this.manager.getClient(name) + const client = this.manager.getClient(id) const createdFolder = await client.createFolder(path, folder) - this.assertStorageExists(name) - const storage = this.storageMap.get(name)! + this.assertStorageExists(id) + const storage = this.storageMap.get(id)! storage.addFolder(createdFolder) return createdFolder } async updateFolder( - name: string, + id: string, path: string, folder: Partial ): Promise { - const client = this.manager.getClient(name) + const client = this.manager.getClient(id) const updatedFolder = await client.updateFolder(path, folder) - this.assertStorageExists(name) - const storage = this.storageMap.get(name)! + this.assertStorageExists(id) + const storage = this.storageMap.get(id)! storage.addFolder(updatedFolder) return updatedFolder } - async removeFolder(name: string, path: string): Promise { - const client = this.manager.getClient(name) + async removeFolder(id: string, path: string): Promise { + const client = this.manager.getClient(id) await client.removeFolder(path) - this.assertStorageExists(name) - const storage = this.storageMap.get(name)! + this.assertStorageExists(id) + const storage = this.storageMap.get(id)! storage.removeFolder(path) } async createNote( - name: string, + storageId: string, path: string, note: Partial ): Promise { - const client = this.manager.getClient(name) + const client = this.manager.getClient(storageId) const createdNote = await client.createNote(path, note) - this.assertStorageExists(name) - const storage = this.storageMap.get(name) as Storage + this.assertStorageExists(storageId) + const storage = this.storageMap.get(storageId) as Storage storage.addNote(createdNote) return createdNote } async updateNote( - name: string, - id: string, + storageId: string, + noteId: string, note: Partial ): Promise { - const client = this.manager.getClient(name) + const client = this.manager.getClient(storageId) if (note.content != null) { const { title, tags } = getMetaData(note.content) note = { @@ -129,10 +133,10 @@ export class DataStore { ...note } } - const updatedNote = await client.updateNote(id, note) + const updatedNote = await client.updateNote(noteId, note) - this.assertStorageExists(name) - const storage = this.storageMap.get(name) as Storage + this.assertStorageExists(storageId) + const storage = this.storageMap.get(storageId) as Storage storage.addNote(updatedNote) return updatedNote diff --git a/src/main/lib/db/Storage.spec.ts b/src/main/lib/db/Storage.spec.ts index f886349f20..ac89486ca2 100644 --- a/src/main/lib/db/Storage.spec.ts +++ b/src/main/lib/db/Storage.spec.ts @@ -334,7 +334,7 @@ describe('Storage', () => { it('updates biding after tags changed', () => { // Given - const storage = new Storage() + const storage = new Storage('') const now = new Date() storage.addNote({ _id: 'note:test', diff --git a/src/main/lib/db/Storage.ts b/src/main/lib/db/Storage.ts index a71df535c9..d900ae3497 100644 --- a/src/main/lib/db/Storage.ts +++ b/src/main/lib/db/Storage.ts @@ -4,9 +4,14 @@ import { FOLDER_ID_PREFIX } from '../../consts' import { difference } from 'ramda' export default class Storage { + @observable name: string @observable folderMap: Map = new Map() @observable noteMap: Map = new Map() + constructor(name?: string) { + this.name = name == null ? '' : name + } + /** * FIXME: We should use ObservableSet here. But it is not available yet. * https://github.com/mobxjs/mobx/pull/1592 diff --git a/src/main/lib/db/uuid.ts b/src/main/lib/db/uuid.ts new file mode 100644 index 0000000000..990ee5dc79 --- /dev/null +++ b/src/main/lib/db/uuid.ts @@ -0,0 +1,5 @@ +import uuidV1 from 'uuid/v1' + +export default function uuid(): string { + return uuidV1() +} diff --git a/src/main/lib/routes.ts b/src/main/lib/routes.ts new file mode 100644 index 0000000000..e2bcc60397 --- /dev/null +++ b/src/main/lib/routes.ts @@ -0,0 +1,25 @@ +import pathToRegexp from 'path-to-regexp' + +export const storageRegexp = pathToRegexp( + '/storages/:storageName/:rest*', + undefined, + { + sensitive: true + } +) + +export const folderRegexp = pathToRegexp( + '/storages/:storageName/notes/:rest*', + undefined, + { + sensitive: true + } +) + +export const tagRegexp = pathToRegexp( + '/storages/:storageName/tags/:tag', + undefined, + { + sensitive: true + } +)