commit 2fb6af99ba99525cf9ed62849992b4495381a7b5 Author: TheoryOfNekomata Date: Mon Nov 30 11:57:33 2020 +0800 Separate project Extract project from monorepo. diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..bf193c2 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +*.ts +tsconfig.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..621a780 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Zeichen - Local Storage Plugin + +Save and load notes in Zeichen using the browser's local storage. diff --git a/package.json b/package.json new file mode 100644 index 0000000..3ed48cb --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "zeichen-plugin-local-storage", + "author": "TheoryOfNekomata ", + "version": "0.1.0", + "description": "Save and load notes in Zeichen using the browser's local storage.", + "keywords": [ + "zeichen", + "plugin", + "storage", + "local" + ], + "devDependencies": { + "typescript": "^4.0.3" + }, + "scripts": { + "build": "tsc **/*.ts" + } +} diff --git a/src/Engine.ts b/src/Engine.ts new file mode 100644 index 0000000..002f81d --- /dev/null +++ b/src/Engine.ts @@ -0,0 +1,23 @@ +export default class LocalStorage { + constructor( + private readonly source: Storage, + private readonly serializer: (t: T) => string = JSON.stringify, + private readonly deserializer: (s: string) => T = JSON.parse, + ) {} + + getCollection(collectionId: string) { + const raw = this.source.getItem(collectionId) + if (raw !== null) { + return this.deserializer(raw) + } + return null + } + + replaceCollection(collectionId: string, collectionData: T) { + this.source.setItem(collectionId, this.serializer(collectionData)) + } + + removeCollection(collectionId: string) { + this.source.removeItem(collectionId) + } +} diff --git a/src/Storage.ts b/src/Storage.ts new file mode 100644 index 0000000..62bd7df --- /dev/null +++ b/src/Storage.ts @@ -0,0 +1,55 @@ +import Storage, { Collection, OutOfSyncError } from '../../core/src/storage' +import Engine from './Engine' + +export default class LocalStorage implements Storage { + private readonly engine: Engine> + + constructor( + private readonly ownerId: string, + private readonly storageId: string, + private readonly getItemId = item => item['id'], + ) { + this.engine = new Engine>(window.localStorage) + } + + private getMeta() { + const oldMeta = this.engine.getCollection(this.storageId) + if (oldMeta === null) { + throw new OutOfSyncError() + } + return oldMeta + } + + private existenceCheck(newItem: T) { + return oldItem => this.getItemId(oldItem) === this.getItemId(newItem) + } + + async queryItems() { + return this.getMeta().items + } + + async saveItem(newItem: T) { + const oldMeta = this.getMeta() + const isExistingItem = oldMeta.items.some(this.existenceCheck(newItem)) + const newMeta: Collection = { + items: isExistingItem + ? oldMeta.items.map(oldItem => this.existenceCheck(newItem)(oldItem) ? newItem : oldItem) + : [...oldMeta.items, newItem], + lastModifiedBy: this.ownerId, + lastModifiedAt: new Date(), + } + this.engine.replaceCollection(this.storageId, newMeta) + } + + async deleteItem(newItem: T) { + const oldMeta = this.getMeta() + const newItems = oldMeta.items.filter(oldItem => !this.existenceCheck(newItem)(oldItem)) + const newMeta: Collection = { + items: newItems, + lastModifiedBy: this.ownerId, + lastModifiedAt: new Date(), + } + this.engine.replaceCollection(this.storageId, newMeta) + return oldMeta.items.length !== newItems.length + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e25e74d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,13 @@ +import { Plugin } from '../../core/src/plugin' +import Storage from './Storage' + +type PluginConfig = {} + +const LocalStoragePlugin: Plugin = config => ({ + currentUserId, +}) => { + new Storage(currentUserId, 'notes') + new Storage(currentUserId, 'folders') +} + +export default LocalStoragePlugin diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..18c72e3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "declaration": true, + "declarationDir": "./dist", + "sourceMap": true, + "strict": false + }, + "exclude": [ + "node_modules", + "**/*.test.ts" + ], + "include": [ + "**/*.ts" + ] +}