@@ -0,0 +1,2 @@ | |||||
*.ts | |||||
tsconfig.json |
@@ -0,0 +1,3 @@ | |||||
# Zeichen - Local Storage Plugin | |||||
Save and load notes in Zeichen using the browser's local storage. |
@@ -0,0 +1,18 @@ | |||||
{ | |||||
"name": "zeichen-plugin-local-storage", | |||||
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>", | |||||
"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" | |||||
} | |||||
} |
@@ -0,0 +1,23 @@ | |||||
export default class LocalStorage<T> { | |||||
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) | |||||
} | |||||
} |
@@ -0,0 +1,55 @@ | |||||
import Storage, { Collection, OutOfSyncError } from '../../core/src/storage' | |||||
import Engine from './Engine' | |||||
export default class LocalStorage<T> implements Storage<T> { | |||||
private readonly engine: Engine<Collection<T>> | |||||
constructor( | |||||
private readonly ownerId: string, | |||||
private readonly storageId: string, | |||||
private readonly getItemId = item => item['id'], | |||||
) { | |||||
this.engine = new Engine<Collection<T>>(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<T> = { | |||||
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<T> = { | |||||
items: newItems, | |||||
lastModifiedBy: this.ownerId, | |||||
lastModifiedAt: new Date(), | |||||
} | |||||
this.engine.replaceCollection(this.storageId, newMeta) | |||||
return oldMeta.items.length !== newItems.length | |||||
} | |||||
} |
@@ -0,0 +1,13 @@ | |||||
import { Plugin } from '../../core/src/plugin' | |||||
import Storage from './Storage' | |||||
type PluginConfig = {} | |||||
const LocalStoragePlugin: Plugin<PluginConfig> = config => ({ | |||||
currentUserId, | |||||
}) => { | |||||
new Storage(currentUserId, 'notes') | |||||
new Storage(currentUserId, 'folders') | |||||
} | |||||
export default LocalStoragePlugin |
@@ -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" | |||||
] | |||||
} |