Skip to content

Commit

Permalink
feat(storage): use Schema interface
Browse files Browse the repository at this point in the history
  • Loading branch information
azu committed Aug 8, 2020
1 parent 6bfb086 commit 2560aae
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 60 deletions.
29 changes: 27 additions & 2 deletions packages/storage/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# @kvs/storage

localstorage for KVS.
Storage-like for KVS.

You can inject Storage object like localStorage, sessionStorage to this storage.

## Install

Expand All @@ -10,7 +12,30 @@ Install with [npm](https://www.npmjs.com/):

## Usage

- [ ] Write usage instructions
```ts
import { kvsStorage } from "@kvs/storage";
(async () => {
type StorageSchema = {
a1: string;
b2: number;
c3: boolean;
};
const storage = await kvsStorage<StorageSchema>({
name: "test",
version: 1,
storage: localStorage
});
await storage.set("a1", "string");
await storage.set("b2", 42);
await storage.set("c3", false);
const a1 = await storage.get("a1");
const b2 = await storage.get("b2");
const c3 = await storage.get("c3");
assert.strictEqual(a1, "string");
assert.strictEqual(b2, 42);
assert.strictEqual(c3, false);
})()
```

## Changelog

Expand Down
77 changes: 41 additions & 36 deletions packages/storage/src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,51 @@
import type { KVS, KVSOptions } from "@kvs/types";
import type { KVS, KVSOptions, StoreNames, StoreValue } from "@kvs/types";
import { JsonValue } from "./JSONValue";

export type KVSStorageKey = string;
export const getItem = <K extends KVSStorageKey>(storage: Storage, key: K) => {
const item = storage.getItem(key);
export const getItem = <Schema extends StorageSchema>(storage: Storage, key: StoreNames<Schema>) => {
const item = storage.getItem(String(key));
return item !== null ? JSON.parse(item) : undefined;
};
export const hasItem = <K extends KVSStorageKey>(storage: Storage, key: K) => {
return storage.getItem(key) !== null;
export const hasItem = <Schema extends StorageSchema>(storage: Storage, key: StoreNames<Schema>) => {
return storage.getItem(String(key)) !== null;
};
export const setItem = <K extends KVSStorageKey, V extends JsonValue | undefined>(
export const setItem = <Schema extends StorageSchema>(
storage: Storage,
key: K,
value: V
key: StoreNames<Schema>,
value: StoreValue<Schema, StoreNames<Schema>> | undefined
) => {
// It is difference with IndexedDB implementation.
// This behavior compatible with localStorage.
if (value === undefined) {
return deleteItem(storage, key);
}
return storage.setItem(key, JSON.stringify(value));
return storage.setItem(String(key), JSON.stringify(value));
};
export const clearItem = (storage: Storage, kvsVersionKey: string) => {
const currentVersion: number | undefined = getItem(storage, kvsVersionKey);
// TODO: kvsVersionKey is special type
const currentVersion: number | undefined = getItem<any>(storage, kvsVersionKey);
// clear all
storage.clear();
// set kvs version again
if (currentVersion !== undefined) {
setItem(storage, kvsVersionKey, currentVersion);
setItem<any>(storage, kvsVersionKey, currentVersion);
}
};
export const deleteItem = <K extends KVSStorageKey>(storage: Storage, key: K) => {
export const deleteItem = <Schema extends StorageSchema>(storage: Storage, key: StoreNames<Schema>): boolean => {
try {
storage.removeItem(key);
storage.removeItem(String(key));
return true;
} catch {
return false;
}
};

export function* createIterator<K extends KVSStorageKey, V extends JsonValue>(
export function* createIterator<Schema extends StorageSchema>(
storage: Storage,
kvsVersionKey: string
): Iterator<[K, V]> {
): Iterator<[StoreNames<Schema>, StoreValue<Schema, StoreNames<Schema>>]> {
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
const key = storage.key(i) as StoreNames<Schema> | undefined;
if (!key) {
continue;
}
Expand All @@ -53,7 +54,7 @@ export function* createIterator<K extends KVSStorageKey, V extends JsonValue>(
continue;
}
const value = getItem(storage, key);
yield [key, value] as [K, V];
yield [key, value];
}
}

Expand All @@ -77,9 +78,10 @@ const openStorage = async ({
storage: Storage;
}) => any;
}) => {
const oldVersion = getItem(storage, kvsVersionKey);
// kvsVersionKey is special type
const oldVersion = getItem<any>(storage, kvsVersionKey);
if (oldVersion === undefined) {
setItem(storage, kvsVersionKey, DEFAULT_KVS_VERSION);
setItem<any>(storage, kvsVersionKey, DEFAULT_KVS_VERSION);
}
// if user set newVersion, upgrade it
if (oldVersion !== version) {
Expand All @@ -95,28 +97,28 @@ const openStorage = async ({
}
return storage;
};
const createStore = <K extends KVSStorageKey, V extends JsonValue>({
const createStore = <Schema extends StorageSchema>({
storage,
kvsVersionKey
}: {
storage: Storage;
kvsVersionKey: string;
}) => {
const store = {
get(key: K): Promise<V | undefined> {
const store: KvsStorage<Schema> = {
get<K extends StoreNames<Schema>>(key: K): Promise<StoreValue<Schema, K> | undefined> {
return Promise.resolve().then(() => {
return getItem(storage, key);
return getItem<Schema>(storage, key);
});
},
has(key: K): Promise<boolean> {
has(key: StoreNames<Schema>): Promise<boolean> {
return Promise.resolve().then(() => {
return hasItem(storage, key);
return hasItem<Schema>(storage, key);
});
},
set(key: K, value: V | undefined): Promise<KVS<K, V>> {
set<K extends StoreNames<Schema>>(key: K, value: StoreValue<Schema, K> | undefined) {
return Promise.resolve()
.then(() => {
return setItem(storage, key, value);
return setItem<Schema>(storage, key, value);
})
.then(() => {
return store;
Expand All @@ -127,17 +129,17 @@ const createStore = <K extends KVSStorageKey, V extends JsonValue>({
return clearItem(storage, kvsVersionKey);
});
},
delete(key: K): Promise<boolean> {
delete(key: StoreNames<Schema>): Promise<boolean> {
return Promise.resolve().then(() => {
return deleteItem(storage, key);
return deleteItem<Schema>(storage, key);
});
},
close(): Promise<void> {
// Noop function
return Promise.resolve();
},
[Symbol.asyncIterator](): AsyncIterator<[K, V]> {
const iterator = createIterator<K, V>(storage, kvsVersionKey);
[Symbol.asyncIterator](): AsyncIterator<[StoreNames<Schema>, StoreValue<Schema, StoreNames<Schema>>]> {
const iterator = createIterator<Schema>(storage, kvsVersionKey);
return {
next() {
return Promise.resolve().then(() => {
Expand All @@ -149,14 +151,17 @@ const createStore = <K extends KVSStorageKey, V extends JsonValue>({
};
return store;
};
export type KvsStorage<K extends KVSStorageKey, V extends JsonValue> = KVS<K, V>;
export type KvsStorageOptions<K extends KVSStorageKey, V extends JsonValue> = KVSOptions<K, V> & {
export type StorageSchema = {
[index: string]: JsonValue;
};
export type KvsStorage<Schema extends StorageSchema> = KVS<Schema>;
export type KvsStorageOptions<Schema extends StorageSchema> = KVSOptions<Schema> & {
kvsVersionKey?: string;
storage: Storage;
};
export const kvsStorage = async <K extends KVSStorageKey, V extends JsonValue>(
options: KvsStorageOptions<K, V>
): Promise<KvsStorage<K, V>> => {
export const kvsStorage = async <Schema extends StorageSchema>(
options: KvsStorageOptions<Schema>
): Promise<KvsStorage<Schema>> => {
const { name, version, upgrade, ...kvStorageOptions } = options;
const kvsVersionKey = kvStorageOptions.kvsVersionKey ?? "__kvs_version__";
const storage = await openStorage({
Expand Down
23 changes: 23 additions & 0 deletions packages/storage/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from "assert";
import { kvsStorage } from "../src";
import { createKVSTestCase } from "@kvs/common-test-case";

Expand Down Expand Up @@ -45,8 +46,30 @@ const deleteAllDB = async () => {
console.error("deleteAllDB", error);
}
};

describe("@kvs/storage", () => {
before(deleteAllDB);
afterEach(deleteAllDB);
kvsTestCase.run();
it("example", async () => {
type StorageSchema = {
a1: string;
b2: number;
c3: boolean;
};
const storage = await kvsStorage<StorageSchema>({
name: databaseName,
version: 2,
storage: localStorage
});
await storage.set("a1", "string");
await storage.set("b2", 42);
await storage.set("c3", false);
const a1 = await storage.get("a1");
const b2 = await storage.get("b2");
const c3 = await storage.get("c3");
assert.strictEqual(a1, "string");
assert.strictEqual(b2, 42);
assert.strictEqual(c3, false);
});
});
85 changes: 63 additions & 22 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,79 @@
export type KVS<K, V> = {
export type StorageSchema = {
[index: string]: any;
};
// https://stackoverflow.com/questions/51465182/typescript-remove-index-signature-using-mapped-types
export type KnownKeys<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K;
} extends { [_ in keyof T]: infer U }
? U
: never;
/**
* Extract known object store names from the DB schema type.
*
* @template DBTypes DB schema type, or unknown if the DB isn't typed.
*/
export type StoreNames<DBTypes extends StorageSchema | unknown> = DBTypes extends StorageSchema
? KnownKeys<DBTypes>
: string;
/**
* Extract database value types from the DB schema type.
*
* @template DBTypes DB schema type, or unknown if the DB isn't typed.
* @template StoreName Names of the object stores to get the types of.
*/
export type StoreValue<
DBTypes extends StorageSchema | unknown,
StoreName extends StoreNames<DBTypes>
> = DBTypes extends StorageSchema ? DBTypes[StoreName] : any;

export type KVS<Schema extends StorageSchema> = {
clear(): Promise<void>;
delete(key: K): Promise<boolean>;
get(key: K): Promise<V | undefined>;
has(key: K): Promise<boolean>;
set(key: K, value: V | undefined): Promise<KVS<K, V>>;
delete(key: StoreNames<Schema>): Promise<boolean>;
get<K extends StoreNames<Schema>>(key: K): Promise<StoreValue<Schema, K> | undefined>;
has(key: StoreNames<Schema>): Promise<boolean>;
set<K extends StoreNames<Schema>>(key: K, value: StoreValue<Schema, K> | undefined): Promise<KVS<Schema>>;
/*
* Close the KVS connection
* DB-like KVS close the connection via this method
* Of course, localStorage-like KVS implement do nothing. It is just noop function
*/
close(): Promise<void>;
} & AsyncIterable<[K, V]>;
export type KVSOptions<K, V> = {
} & AsyncIterable<[StoreNames<Schema>, StoreValue<Schema, StoreNames<Schema>>]>;
export type KVSOptions<Schema extends StorageSchema> = {
name: string;
version: number;
upgrade?({ kvs, oldVersion, newVersion }: { kvs: KVS<K, V>; oldVersion: number; newVersion: number }): Promise<any>;
upgrade?({
kvs,
oldVersion,
newVersion
}: {
kvs: KVS<Schema>;
oldVersion: number;
newVersion: number;
}): Promise<any>;
} & {
// options will be extended
[index: string]: any;
};
export type KVSConstructor<K, V> = (options: KVSOptions<K, V>) => Promise<KVS<K, V>>;
export type KVSConstructor<Schema extends StorageSchema> = (options: KVSOptions<Schema>) => Promise<KVS<Schema>>;
/**
* Sync Version
*/
export type KVSSync<K, V> = {
clear(): void;
delete(key: K): boolean;
get(key: K): V | undefined;
has(key: K): boolean;
set(key: K, value: V | undefined): KVSSync<K, V>;
close(): void;
} & Iterable<[K, V]>;
export type KVSSyncOptions<K, V> = {
name: string;
version: number;
upgrade?({ kvs, oldVersion, newVersion }: { kvs: KVS<K, V>; oldVersion: number; newVersion: number }): any;
};
// export type KVSSync<Schema extends StorageSchema> = {
// clear(): void;
// delete(key: KeyOf<Schema>): boolean;
// get<T extends KeyOf<Schema>>(key: keyof T): Schema[T] | undefined;
// has(key: KeyOf<Schema>): boolean;
// set<T extends KeyOf<Schema>>(key: T, value: Schema[T] | undefined): Schema;
// /*
// * Close the KVS connection
// * DB-like KVS close the connection via this method
// * Of course, localStorage-like KVS implement do nothing. It is just noop function
// */
// close(): void;
// } & Iterable<[KeyOf<Schema>, ValueOf<Schema>]>;
// export type KVSSyncOptions<Schema extends StorageSchema> = {
// name: string;
// version: number;
// upgrade?({ kvs, oldVersion, newVersion }: { kvs: KVS<Schema>; oldVersion: number; newVersion: number }): any;
// };

0 comments on commit 2560aae

Please sign in to comment.