commit fc701bec680dd73be29c72164f47ee87fac540c7 Author: JOLIMAITRE Matthieu Date: Wed May 29 03:57:05 2024 +0200 init. diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cbac569 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..92b61b1 --- /dev/null +++ b/deno.json @@ -0,0 +1,6 @@ +{ + "fmt": { + "useTabs": true, + "lineWidth": 120 + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..ef78be9 --- /dev/null +++ b/deno.lock @@ -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" + } +} diff --git a/examples/demo.ts b/examples/demo.ts new file mode 100755 index 0000000..f306504 --- /dev/null +++ b/examples/demo.ts @@ -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; + + { + // 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(); diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..8f7c8a1 --- /dev/null +++ b/mod.ts @@ -0,0 +1 @@ +export * from "./src/lib.ts"; diff --git a/src/entry.ts b/src/entry.ts new file mode 100644 index 0000000..cf78611 --- /dev/null +++ b/src/entry.ts @@ -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, S extends keyof T> { + public readonly store; + public readonly schema; + public readonly id; + + public constructor(store: Store, schema: S, id: string) { + this.store = store; + this.schema = schema; + this.id = id; + } + + public async get(field: F): Promise> { + 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; + if (kind === "number") return z.number().parse(content) as QueriedFor; + if (kind === "boolean") return z.boolean().parse(content) as QueriedFor; + if (kind[0] === "maybe") { + if (content === null) return null as QueriedFor; + else return new Entry(this.store, kind[1], str(content)) as QueriedFor; + } + if (kind[0] === "one") return new Entry(this.store, kind[1], str(content)) as QueriedFor; + if (kind[0] === "many") return new Collection(this.store, kind[1], str(content)) as QueriedFor; + throw new Error("Unreachable"); + } + + public async set(field: F, value: QueriedFor) { + 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); + } + + 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; + 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, S extends keyof T> { + public readonly store; + public readonly schema; + public readonly id; + + public constructor(store: Store, 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) { + await this.store.add_relation(this.id, entry.id); + } + + public async remove(entry: Entry) { + await this.store.remove_relation(this.id, entry.id); + } +} diff --git a/src/lib.ts b/src/lib.ts new file mode 100644 index 0000000..f072a0b --- /dev/null +++ b/src/lib.ts @@ -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"; diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..8622ffa --- /dev/null +++ b/src/store.ts @@ -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> { + 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> { + public readonly nodes; + public readonly kv; + public readonly prefix; + + public constructor(nodes: Schema, kv: Deno.Kv, prefix: string) { + this.nodes = nodes; + this.kv = kv; + this.prefix = prefix; + } + + public static async open>(nodes: Schema, path: string, prefix: string = "store") { + const kv = await Deno.openKv(path); + return new Store(nodes, kv, prefix); + } + + public empty_collection(produces: S) { + return new Collection(this, produces, this.new_id()); + } + + public async get(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(schema: S, values: QueriedOf) { + 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(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( + schema: S, + id: string, + field: F, + value: StoredOfField, + ) { + 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(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(schema: S, id: string, field: F) { + await this.kv.delete([this.prefix, "fields", schema, id, field]); + } + + public async entry_exists(schema: S, id: string) { + const result = await this.kv.get([this.prefix, "entries", schema, id]); + return result !== null; + } + + public async delete_entry(schema: S, id: string) { + await this.kv.delete([this.prefix, "entries", schema, id]); + } + + public async *list_entries(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]); + } +} diff --git a/src/typing.ts b/src/typing.ts new file mode 100644 index 0000000..eafc993 --- /dev/null +++ b/src/typing.ts @@ -0,0 +1,59 @@ +import { Collection, Entry } from "./entry.ts"; +import { Store } from "./store.ts"; + +export type Table> = { + [Name: string]: Structure; +}; + +export type Structure> = { + [name: string]: Property; +}; + +export type Property> = + | Field + | Relation; + +export type Field = "string" | "number" | "boolean"; +export type Relation> = + | ["maybe", keyof T] + | ["one", keyof T] + | ["many", keyof T]; + +export type QueriedFor< + T extends Table, + P extends Property, +> = P extends "string" ? string + : P extends "number" ? number + : P extends "boolean" ? boolean + : P[0] extends "maybe" ? null | Entry + : P[0] extends "one" ? Entry + : Collection; + +export type StoredFor< + T extends Table, + P extends Property, +> = 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, + S extends keyof T, +> = { [P in keyof T[S]]: QueriedFor }; + +export type QueriedOfField< + T extends Table, + S extends keyof T, + F extends keyof T[S], +> = QueriedOf[F]; + +export type StoredOfField< + T extends Table, + S extends keyof T, + F extends keyof T[S], +> = { [P in keyof T[S]]: StoredFor }[F]; + +export type EntryFor, T extends keyof S["nodes"]["table"]> = Entry;