This commit is contained in:
JOLIMAITRE Matthieu 2024-05-29 03:57:05 +02:00
commit fc701bec68
9 changed files with 366 additions and 0 deletions

85
src/entry.ts Normal file
View 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
View 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
View 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
View 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>;