Skip to content

Commit

Permalink
feat: add worktop/kv submodule
Browse files Browse the repository at this point in the history
- Related #2
  • Loading branch information
lukeed committed Mar 16, 2021
1 parent 31a6703 commit 76f5291
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ node_modules
*-lock.*
*.lock

/kv
/cache
/router
/request
Expand Down
1 change: 1 addition & 0 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ bundle('src/router.ts', pkg.exports['.']);
bundle('src/cache.ts', pkg.exports['./cache']);
bundle('src/request.ts', pkg.exports['./request']);
bundle('src/response.ts', pkg.exports['./response']);
bundle('src/kv.ts', pkg.exports['./kv']);
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"exports": {
".": "./router/index.mjs",
"./kv": "./kv/index.mjs",
"./cache": "./cache/index.mjs",
"./request": "./request/index.mjs",
"./response": "./response/index.mjs",
Expand All @@ -22,7 +23,8 @@
"response",
"request",
"router",
"cache"
"cache",
"kv"
],
"engines": {
"node": ">=12"
Expand Down
41 changes: 41 additions & 0 deletions src/kv.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export namespace KV {
type Value = string | ReadableStream | ArrayBuffer;
type WriteOptions = { expiration?: number; expirationTtl?: number };
type ListOptions = { prefix?: string; limit?: number; cursor?: string };
type GetOptions = 'text' | 'json' | 'arrayBuffer' | 'stream';

interface KeyList {
keys: Array<{ name: string; expiration?: number }>;
list_complete: boolean;
cursor: string;
}

interface Namespace {
get<T>(key: string, type: 'json'): Promise<T>;
get<T>(key: string, type: 'stream'): Promise<ReadableStream>;
get<T>(key: string, type: 'arrayBuffer'): Promise<ArrayBuffer>;
get<T>(key: string, type: 'text'): Promise<string>;
get<T>(key: string, type: GetOptions): Promise<T>;
get<T>(key: string): Promise<string>; // "text"

put(key: string, value: Value, options?: WriteOptions): Promise<void>;
list(options?: ListOptions): Promise<KeyList>;
delete(key: string): Promise<void>;
}
}

export declare class Database<Models, Identifiers extends Record<keyof Models, string> = { [P in keyof Models]: string}> {
constructor(binding: KV.Namespace);
get<K extends keyof Models>(type: K, uid: Identifiers[K], format?: KV.GetOptions): Promise<Models[K] | false>;
put<K extends keyof Models>(type: K, uid: Identifiers[K], value: Models[K], isJSON?: boolean): Promise<boolean>;
del<K extends keyof Models>(type: K, uid: Identifiers[K]): Promise<boolean>;
}

export function read<T>(binding: KV.Namespace, key: string, format?: KV.GetOptions): Promise<T | false>;
export function write<T>(binding: KV.Namespace, key: string, value: T, isJSON?: boolean): Promise<boolean>;
export function remove(binding: KV.Namespace, key: string): Promise<boolean>;

export function until<X extends string>(
toMake: () => X,
toSearch: (val: X) => Promise<unknown | false>
): Promise<X>;
40 changes: 40 additions & 0 deletions src/kv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { KV, Database as DB } from 'worktop/kv';

export function Database<M, I extends Record<keyof M, string> = { [P in keyof M]: string }>(binding: KV.Namespace): DB<M, I> {
var $ = <K extends keyof I>(type: K, uid: I[K]) => `${type}__${uid}`;

return {
get<K extends keyof M>(type: K, uid: I[K], format?: KV.GetOptions) {
return read<M[K]>(binding, $(type, uid), format);
},
put<K extends keyof M>(type: K, uid: I[K], value: M[K], isJSON = true) {
return write<M[K]>(binding, $(type, uid), value, isJSON);
},
del<K extends keyof M>(type: K, uid: I[K]) {
return remove(binding, $(type, uid));
}
};
}

export function read<T>(binding: KV.Namespace, key: string, format: KV.GetOptions = 'json'): Promise<T | false> {
return binding.get<T>(key, format).then(x => x !== void 0 ? x : false);
}

export function write<T=any>(binding: KV.Namespace, key: string, value: T, isJSON: boolean = true): Promise<boolean> {
return binding.put(key, (!isJSON && (typeof value === 'string' || value instanceof ArrayBuffer || value instanceof ReadableStream)) ? value : JSON.stringify(value)).then(() => true, () => false);
}

export function remove(binding: KV.Namespace, key: string): Promise<boolean> {
return binding.delete(key).then(() => true, () => false);
}

export async function until<X extends string>(
toMake: () => X,
toSearch: (val: X) => Promise<unknown | false>
): Promise<X> {
let exists, tmp = '' as X;
while (exists !== false) {
exists = await toSearch(tmp = toMake());
}
return tmp;
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"worktop/cache": ["./cache.d.ts", "./cache.ts"],
"worktop/request": ["./request.d.ts", "./request.ts"],
"worktop/response": ["./response.d.ts", "./response.ts"],
"worktop/kv": ["./kv.d.ts", "./kv.js"],
}
},
"include": [
Expand Down
65 changes: 65 additions & 0 deletions types/check.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as Cache from 'worktop/cache';
import { Database, until } from 'worktop/kv';
import { Router, STATUS_CODES } from 'worktop';
import { reply, ServerResponse } from 'worktop/response';

import type { KV } from 'worktop/kv';
import type { ServerRequest, IncomingCloudflareProperties } from 'worktop/request';
import type { FetchHandler } from 'worktop/response';
import type { Route } from 'worktop';
Expand Down Expand Up @@ -214,3 +216,66 @@ reply(event => {
// ignore me
return fetch(event.request);
});


/**
* WORKTOP/KV
*/

declare namespace Fixed {
type String<N extends number> = { 0: string; length: N } & string;
}

interface IUser {
id: string;
name: string;
age: number;
}

interface IApp {
uid: Fixed.String<11>;
name: string;
}

interface Models {
user: IUser;
app: IApp;
}

interface Identifiers {
user: IUser['id'];
app: IApp['uid'];
}

declare const APPS: KV.Namespace;
declare const len11: Fixed.String<11>;
declare function toUID(): Fixed.String<11>;

const DB1 = new Database<Models>(APPS);
const DB2 = new Database<Models, Identifiers>(APPS);

async function storage() {
// @ts-expect-error - number
await DB1.get('user', 1235678);

// @ts-expect-error - not fixed string
await DB2.get('app', 'asd'); // DB2 is explicit
await DB2.get('app', len11);
await DB1.get('app', 'asd'); // DB1 is guessing

assert<IUser|false>(await DB1.get('user', 'id'));
assert<IApp|false>(await DB2.get('app', len11));

let user: IUser = {
id: 'asd',
name: 'foobar',
age: 123
};

assert<boolean>(await DB1.put('user', user.id, user, true));
assert<boolean>(await DB1.put('user', user.id, user));
assert<boolean>(await DB1.del('user', user.id));

const lookup = (uid: Fixed.String<11>) => DB2.get('app', uid);
assert<Fixed.String<11>>(await until(toUID, lookup));
}

0 comments on commit 76f5291

Please sign in to comment.