init.
This commit is contained in:
commit
fc701bec68
9 changed files with 366 additions and 0 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"deno.enable": true
|
||||
}
|
6
deno.json
Normal file
6
deno.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"fmt": {
|
||||
"useTabs": true,
|
||||
"lineWidth": 120
|
||||
}
|
||||
}
|
18
deno.lock
generated
Normal file
18
deno.lock
generated
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"version": "3",
|
||||
"remote": {
|
||||
"https://deno.land/x/zod@v3.22.4/ZodError.ts": "4de18ff525e75a0315f2c12066b77b5c2ae18c7c15ef7df7e165d63536fdf2ea",
|
||||
"https://deno.land/x/zod@v3.22.4/errors.ts": "5285922d2be9700cc0c70c95e4858952b07ae193aa0224be3cbd5cd5567eabef",
|
||||
"https://deno.land/x/zod@v3.22.4/external.ts": "a6cfbd61e9e097d5f42f8a7ed6f92f93f51ff927d29c9fbaec04f03cbce130fe",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/enumUtil.ts": "54efc393cc9860e687d8b81ff52e980def00fa67377ad0bf8b3104f8a5bf698c",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/errorUtil.ts": "7a77328240be7b847af6de9189963bd9f79cab32bbc61502a9db4fe6683e2ea7",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/parseUtil.ts": "f791e6e65a0340d85ad37d26cd7a3ba67126cd9957eac2b7163162155283abb1",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/partialUtil.ts": "998c2fe79795257d4d1cf10361e74492f3b7d852f61057c7c08ac0a46488b7e7",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/typeAliases.ts": "0fda31a063c6736fc3cf9090dd94865c811dfff4f3cb8707b932bf937c6f2c3e",
|
||||
"https://deno.land/x/zod@v3.22.4/helpers/util.ts": "8baf19b19b2fca8424380367b90364b32503b6b71780269a6e3e67700bb02774",
|
||||
"https://deno.land/x/zod@v3.22.4/index.ts": "d27aabd973613985574bc31f39e45cb5d856aa122ef094a9f38a463b8ef1a268",
|
||||
"https://deno.land/x/zod@v3.22.4/locales/en.ts": "a7a25cd23563ccb5e0eed214d9b31846305ddbcdb9c5c8f508b108943366ab4c",
|
||||
"https://deno.land/x/zod@v3.22.4/mod.ts": "64e55237cb4410e17d968cd08975566059f27638ebb0b86048031b987ba251c4",
|
||||
"https://deno.land/x/zod@v3.22.4/types.ts": "724185522fafe43ee56a52333958764c8c8cd6ad4effa27b42651df873fc151e"
|
||||
}
|
||||
}
|
62
examples/demo.ts
Executable file
62
examples/demo.ts
Executable file
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env -S deno run -A --unstable-kv
|
||||
|
||||
import { EntryFor, Schema, Store } from "../mod.ts";
|
||||
|
||||
async function main() {
|
||||
// define nodes and open store.
|
||||
const nodes = new Schema({
|
||||
user: {
|
||||
"name": "string",
|
||||
"posts": ["many", "post"],
|
||||
},
|
||||
post: {
|
||||
"content": "string",
|
||||
"upvotes": "number",
|
||||
"author": ["one", "user"],
|
||||
},
|
||||
});
|
||||
const store = await Store.open(nodes, "/tmp/feur.kv");
|
||||
type User = EntryFor<typeof store, "user">;
|
||||
|
||||
{
|
||||
// insert data.
|
||||
const bob = await store.insert("user", {
|
||||
name: "bob",
|
||||
posts: store.empty_collection("post"),
|
||||
});
|
||||
|
||||
const post_a = await store.insert("post", {
|
||||
content: "Lorem",
|
||||
upvotes: 5,
|
||||
author: bob,
|
||||
});
|
||||
await (await bob.get("posts")).add(post_a);
|
||||
|
||||
const post_b = await store.insert("post", {
|
||||
content: "Ipsum",
|
||||
upvotes: 3,
|
||||
author: bob,
|
||||
});
|
||||
await (await bob.get("posts")).add(post_b);
|
||||
}
|
||||
|
||||
{
|
||||
// prints
|
||||
let bob = null as User | null;
|
||||
for await (const user of store.all("user")) if (await user.get("name") === "bob") bob = user;
|
||||
if (bob === null) throw new Error("Bob not found");
|
||||
|
||||
console.log({ name: await bob.get("name") });
|
||||
|
||||
const posts = await bob.get("posts");
|
||||
for await (const post of posts) {
|
||||
console.log({
|
||||
author: await (await post.get("author")).get("name"),
|
||||
content: await post.get("content"),
|
||||
upvotes: await post.get("upvotes"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) await main();
|
1
mod.ts
Normal file
1
mod.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./src/lib.ts";
|
85
src/entry.ts
Normal file
85
src/entry.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts";
|
||||
import { QueriedFor, QueriedOfField, Table } from "./typing.ts";
|
||||
import { Store } from "./store.ts";
|
||||
import { StoredFor } from "./typing.ts";
|
||||
|
||||
export class Entry<T extends Table<T>, S extends keyof T> {
|
||||
public readonly store;
|
||||
public readonly schema;
|
||||
public readonly id;
|
||||
|
||||
public constructor(store: Store<T>, schema: S, id: string) {
|
||||
this.store = store;
|
||||
this.schema = schema;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public async get<F extends keyof T[S]>(field: F): Promise<QueriedOfField<T, S, F>> {
|
||||
if (!await this.store.entry_exists(this.schema, this.id)) throw new Error("Accessing a deleted entry");
|
||||
const str = (value: unknown) => z.string().parse(value);
|
||||
const content = await this.store.read_field(this.schema, this.id, field);
|
||||
const kind = this.store.nodes.table[this.schema][field];
|
||||
if (kind === "string") return z.string().parse(content) as QueriedFor<T, T[S][F]>;
|
||||
if (kind === "number") return z.number().parse(content) as QueriedFor<T, T[S][F]>;
|
||||
if (kind === "boolean") return z.boolean().parse(content) as QueriedFor<T, T[S][F]>;
|
||||
if (kind[0] === "maybe") {
|
||||
if (content === null) return null as QueriedFor<T, T[S][F]>;
|
||||
else return new Entry(this.store, kind[1], str(content)) as QueriedFor<T, T[S][F]>;
|
||||
}
|
||||
if (kind[0] === "one") return new Entry(this.store, kind[1], str(content)) as QueriedFor<T, T[S][F]>;
|
||||
if (kind[0] === "many") return new Collection(this.store, kind[1], str(content)) as QueriedFor<T, T[S][F]>;
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
|
||||
public async set<F extends keyof T[S]>(field: F, value: QueriedFor<T, T[S][F]>) {
|
||||
let raw: string | number | boolean | null;
|
||||
if (value instanceof Collection) raw = value.id;
|
||||
else if (value instanceof Entry) raw = value.id;
|
||||
else raw = value;
|
||||
await this.store.write_field(this.schema, this.id, field, raw as StoredFor<T, T[S][F]>);
|
||||
}
|
||||
|
||||
public async delete() {
|
||||
const schema = this.store.nodes.table[this.schema];
|
||||
for (const field in schema) {
|
||||
const kind = schema[field];
|
||||
if (Array.isArray(kind) && kind[0] === "many") {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const collection = await this.get(field) as Collection<T, any>;
|
||||
for await (const value of collection) this.store.remove_relation(collection.id, value.id);
|
||||
await this.store.delete_relations(collection.id);
|
||||
}
|
||||
await this.store.delete_field(this.schema, this.id, field);
|
||||
}
|
||||
await this.store.delete_entry(this.schema, this.id);
|
||||
}
|
||||
}
|
||||
|
||||
export class Collection<T extends Table<T>, S extends keyof T> {
|
||||
public readonly store;
|
||||
public readonly schema;
|
||||
public readonly id;
|
||||
|
||||
public constructor(store: Store<T>, schema: S, collection_id: string) {
|
||||
this.store = store;
|
||||
this.schema = schema;
|
||||
this.id = collection_id;
|
||||
}
|
||||
|
||||
public async *[Symbol.asyncIterator]() {
|
||||
for await (const id_ of this.store.list_relations(this.id)) {
|
||||
const id = z.string().parse(id_);
|
||||
const entry = new Entry(this.store, this.schema, id);
|
||||
if (await this.store.entry_exists(this.schema, id)) yield entry;
|
||||
else await this.remove(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public async add(entry: Entry<T, S>) {
|
||||
await this.store.add_relation(this.id, entry.id);
|
||||
}
|
||||
|
||||
public async remove(entry: Entry<T, S>) {
|
||||
await this.store.remove_relation(this.id, entry.id);
|
||||
}
|
||||
}
|
3
src/lib.ts
Normal file
3
src/lib.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { Collection, Entry } from "./entry.ts";
|
||||
export { Schema, Store } from "./store.ts";
|
||||
export type { EntryFor, Field, Property, QueriedFor, QueriedOf, Relation, Structure, Table } from "./typing.ts";
|
129
src/store.ts
Normal file
129
src/store.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
import z from "https://deno.land/x/zod@v3.22.4/index.ts";
|
||||
import { Collection, Entry } from "./entry.ts";
|
||||
import { QueriedOf, StoredOfField, Table } from "./typing.ts";
|
||||
|
||||
export class Schema<T extends Table<T>> {
|
||||
public readonly table;
|
||||
|
||||
constructor(table: T) {
|
||||
this.table = table;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
note : storing values :
|
||||
|
||||
for each field :
|
||||
- field is string | number | boolean :
|
||||
[prefix, 'entries', schema, id] <- id
|
||||
[prefix, 'fields', schema, id, field] <- value
|
||||
|
||||
- field is relation one :
|
||||
[prefix, 'entries', schema, id] <- id
|
||||
[prefix, 'fields', schema, id, field] <- id_to
|
||||
|
||||
- field is relation maybe :
|
||||
[prefix, 'entries', schema, id] <- id
|
||||
[prefix, 'fields', schema, id, field] <- id_to | null
|
||||
|
||||
- field is relation many :
|
||||
[prefix, 'entries', schema, id] <- id
|
||||
[prefix, 'fields', schema, id, field] <- id_relation
|
||||
[prefix, 'relations', id_relation, id_to] <- id_to
|
||||
*/
|
||||
|
||||
export class Store<T extends Table<T>> {
|
||||
public readonly nodes;
|
||||
public readonly kv;
|
||||
public readonly prefix;
|
||||
|
||||
public constructor(nodes: Schema<T>, kv: Deno.Kv, prefix: string) {
|
||||
this.nodes = nodes;
|
||||
this.kv = kv;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
public static async open<T extends Table<T>>(nodes: Schema<T>, path: string, prefix: string = "store") {
|
||||
const kv = await Deno.openKv(path);
|
||||
return new Store(nodes, kv, prefix);
|
||||
}
|
||||
|
||||
public empty_collection<S extends keyof T>(produces: S) {
|
||||
return new Collection(this, produces, this.new_id());
|
||||
}
|
||||
|
||||
public async get<S extends keyof T>(schema: S, id: string) {
|
||||
const exists = await this.entry_exists(schema, id);
|
||||
if (!exists) return null;
|
||||
return new Entry(this, schema, id);
|
||||
}
|
||||
|
||||
public async insert<S extends keyof T, F extends keyof T[S]>(schema: S, values: QueriedOf<T, S>) {
|
||||
const entry = new Entry(this, schema, this.new_id());
|
||||
for (const key in values) {
|
||||
const value = values[key];
|
||||
await entry.set(key, value);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
public async *all<S extends keyof T>(schema: S) {
|
||||
for await (const id of this.list_entries(schema)) {
|
||||
yield new Entry(this, schema, z.string().parse(id));
|
||||
}
|
||||
}
|
||||
|
||||
public new_id() {
|
||||
return `${Math.random()}`;
|
||||
}
|
||||
|
||||
public async write_field<S extends keyof T, F extends keyof T[S]>(
|
||||
schema: S,
|
||||
id: string,
|
||||
field: F,
|
||||
value: StoredOfField<T, S, F>,
|
||||
) {
|
||||
await this.kv.set([this.prefix, "fields", schema, id, field], value);
|
||||
await this.kv.set([this.prefix, "entries", schema, id], id);
|
||||
}
|
||||
|
||||
public async read_field<S extends keyof T, F extends keyof T[S]>(schema: S, id: string, field: F) {
|
||||
const result = await this.kv.get([this.prefix, "fields", schema, id, field]);
|
||||
return result.value;
|
||||
}
|
||||
|
||||
public async delete_field<S extends keyof T, F extends keyof T[S]>(schema: S, id: string, field: F) {
|
||||
await this.kv.delete([this.prefix, "fields", schema, id, field]);
|
||||
}
|
||||
|
||||
public async entry_exists<S extends keyof T>(schema: S, id: string) {
|
||||
const result = await this.kv.get([this.prefix, "entries", schema, id]);
|
||||
return result !== null;
|
||||
}
|
||||
|
||||
public async delete_entry<S extends keyof T>(schema: S, id: string) {
|
||||
await this.kv.delete([this.prefix, "entries", schema, id]);
|
||||
}
|
||||
|
||||
public async *list_entries<S extends keyof T>(schema: S) {
|
||||
const prefix = [this.prefix, "entries", schema];
|
||||
for await (const entry of this.kv.list({ prefix })) yield entry.value;
|
||||
}
|
||||
|
||||
public async add_relation(id_relation: string, id_to: string) {
|
||||
await this.kv.set([this.prefix, "relations", id_relation, id_to], id_to);
|
||||
}
|
||||
|
||||
public async remove_relation(id_relation: string, id_to: string) {
|
||||
await this.kv.delete([this.prefix, "relations", id_relation, id_to]);
|
||||
}
|
||||
|
||||
public async *list_relations(id_relation: string) {
|
||||
const prefix = [this.prefix, "relations", id_relation];
|
||||
for await (const entry of this.kv.list({ prefix })) yield entry.value;
|
||||
}
|
||||
|
||||
public async delete_relations(id_relation: string) {
|
||||
await this.kv.delete([this.prefix, "relations", id_relation]);
|
||||
}
|
||||
}
|
59
src/typing.ts
Normal file
59
src/typing.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { Collection, Entry } from "./entry.ts";
|
||||
import { Store } from "./store.ts";
|
||||
|
||||
export type Table<Self extends Table<Self>> = {
|
||||
[Name: string]: Structure<Self>;
|
||||
};
|
||||
|
||||
export type Structure<T extends Table<T>> = {
|
||||
[name: string]: Property<T>;
|
||||
};
|
||||
|
||||
export type Property<T extends Table<T>> =
|
||||
| Field
|
||||
| Relation<T>;
|
||||
|
||||
export type Field = "string" | "number" | "boolean";
|
||||
export type Relation<T extends Table<T>> =
|
||||
| ["maybe", keyof T]
|
||||
| ["one", keyof T]
|
||||
| ["many", keyof T];
|
||||
|
||||
export type QueriedFor<
|
||||
T extends Table<T>,
|
||||
P extends Property<T>,
|
||||
> = P extends "string" ? string
|
||||
: P extends "number" ? number
|
||||
: P extends "boolean" ? boolean
|
||||
: P[0] extends "maybe" ? null | Entry<T, P[1]>
|
||||
: P[0] extends "one" ? Entry<T, P[1]>
|
||||
: Collection<T, P[1]>;
|
||||
|
||||
export type StoredFor<
|
||||
T extends Table<T>,
|
||||
P extends Property<T>,
|
||||
> = P extends "string" ? string
|
||||
: P extends "number" ? number
|
||||
: P extends "boolean" ? boolean
|
||||
: P[0] extends "maybe" ? null | string
|
||||
: P[0] extends "one" ? string
|
||||
: string;
|
||||
|
||||
export type QueriedOf<
|
||||
T extends Table<T>,
|
||||
S extends keyof T,
|
||||
> = { [P in keyof T[S]]: QueriedFor<T, T[S][P]> };
|
||||
|
||||
export type QueriedOfField<
|
||||
T extends Table<T>,
|
||||
S extends keyof T,
|
||||
F extends keyof T[S],
|
||||
> = QueriedOf<T, S>[F];
|
||||
|
||||
export type StoredOfField<
|
||||
T extends Table<T>,
|
||||
S extends keyof T,
|
||||
F extends keyof T[S],
|
||||
> = { [P in keyof T[S]]: StoredFor<T, T[S][P]> }[F];
|
||||
|
||||
export type EntryFor<S extends Store<any>, T extends keyof S["nodes"]["table"]> = Entry<S["nodes"]["table"], T>;
|
Loading…
Add table
Add a link
Reference in a new issue