commit 6983443f63334697fa640693f8be724c4d5a61b1 Author: TheoryOfNekomata Date: Mon Nov 30 11:59:21 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..f6ba95d --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Zeichen - Remote Storage Plugin + +Save and load notes in Zeichen using an external API. diff --git a/package.json b/package.json new file mode 100644 index 0000000..bc8749a --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "zeichen-plugin-remote-storage", + "author": "TheoryOfNekomata ", + "version": "0.1.0", + "description": "Save and load notes in Zeichen using an external API.", + "keywords": [ + "zeichen", + "plugin", + "storage", + "API" + ], + "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..c094f9e --- /dev/null +++ b/src/Engine.ts @@ -0,0 +1,70 @@ +import { Deserializer, Serializer } from '../../core/src/storage' + +const CREATED = 201 +const NO_CONTENT = 204 +const NOT_FOUND = 404 +const GONE = 410 + +export default class RemoteStorage { + constructor( + private readonly baseUrl: string, + private readonly getItemId = item => item['id'], + private readonly serializers: Map = new Map([ + ['*/*', JSON.stringify], + ['application/json', JSON.stringify], + ['text/json', JSON.stringify], + ]), + private readonly deserializers: Map = new Map([ + ['*/*', JSON.parse], + ['application/json', JSON.parse], + ['text/json', JSON.parse], + ]), + ) {} + + async getCollection(collectionId: string) { + const response = await window.fetch([this.baseUrl, collectionId].join('/')) + const contentType = response.headers.get('content-type') + const { [contentType]: deserializer = this.deserializers.get('*/*'), } = Object.fromEntries(this.deserializers.entries()) + const payload = await response.text() + return deserializer(payload) + } + + async setItem(collectionId: string, item: U, contentType = 'application/json') { + const { [contentType]: serializer = this.serializers.get('application/json'), } = Object.fromEntries(this.serializers.entries()) + const response = await window.fetch([this.baseUrl, collectionId, this.getItemId(item)].join('/'), { + method: 'put', + body: serializer(item), + }) + // resource is created + if (response.status === CREATED) { + return + } + if (100 <= response.status && response.status <= 399) { + console.warn(`Expected response is ${CREATED}, got ${response.status}.`) + return + } + throw new Error(response.statusText) + } + + async removeItem(collectionId: string, item: U) { + const response = await window.fetch([this.baseUrl, collectionId, this.getItemId(item)].join('/'), { + method: 'delete', + }) + // resource is deleted + if (response.status === NO_CONTENT) { + return true + } + // resource is already deleted + if (response.status === NOT_FOUND || response.status === GONE) { + return false + } + if (100 <= response.status && response.status <= 399) { + console.warn(`Expected response is ${NO_CONTENT}, got ${response.status}.`) + // assume there's a change in the collection + return true + } + throw new Error(response.statusText) + } + + // TODO do removeCollection for account closing? +} diff --git a/src/Storage.ts b/src/Storage.ts new file mode 100644 index 0000000..a9a37f1 --- /dev/null +++ b/src/Storage.ts @@ -0,0 +1,27 @@ +import Storage, { Collection } from '../../core/src/storage' +import Engine from './Engine' + +export default class RemoteStorage implements Storage { + private readonly engine: Engine> + + constructor( + private ownerId: string, + private baseUrl: string, + private collectionId: string, + ) { + this.engine = new Engine(baseUrl) + } + + async deleteItem(item: T) { + return this.engine.removeItem(this.collectionId, item) + } + + async queryItems() { + const response = await this.engine.getCollection(this.collectionId) as Collection; + return response.items; + } + + async saveItem(newItem: T) { + return this.engine.setItem(this.collectionId, newItem) + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..873b4d5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,15 @@ +import { Plugin } from '../../core/src/plugin' +import Storage from './Storage' + +type PluginConfig = { + baseUrl: string, +} + +const RemoteStoragePlugin: Plugin = config => ({ + currentUserId, +}) => { + new Storage(currentUserId, config.baseUrl, 'notes') + new Storage(currentUserId, config.baseUrl, 'folders') +} + +export default RemoteStoragePlugin 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" + ] +}