Skip to content

Instantly share code, notes, and snippets.

@tanishqkancharla
Created March 12, 2024 04:20
Tuple-db ORM
import { isFunction, isSymbol } from "lodash-es";
import type {
KeyValuePair,
ReadOnlyTupleDatabaseClientApi,
TupleTransactionApi,
} from "tuple-database";
import {
InMemoryTupleStorage,
TupleDatabase,
TupleDatabaseClient,
} from "tuple-database";
type TupleReadTransactionApi = ReadOnlyTupleDatabaseClientApi;
type TupleReadWriteTransactionApi = TupleTransactionApi<KeyValuePair>;
type Query<I extends any[], O> = ((
tx: TupleReadTransactionApi,
...args: I
) => O) & { __ADT__: "Query" };
function query<I extends any[], O>(
fn: (tx: TupleReadTransactionApi, ...args: I) => O
): Query<I, O> {
return fn as any;
}
type Mutation<I extends any[], O = void> = ((
tx: TupleReadWriteTransactionApi,
...args: I
) => O) & { __ADT__: "Mutation" };
function mutation<I extends any[], O = void>(
fn: (tx: TupleReadWriteTransactionApi, ...args: I) => O
): Mutation<I, O> {
return fn as any;
}
type AnySchema = {
encode?: Mutation<[any], void>;
decode?: Query<[], any>;
} & {
[key: string]: Query<any[], any> | Mutation<any[], any> | AnySchema;
};
type SchemaToValue<Schema extends AnySchema> = Schema["decode"] extends Query<
[],
infer O
>
? O
: undefined;
type TransactionApi<Schema extends AnySchema> = {
[K in keyof Schema]: Schema[K] extends Query<infer I, infer O>
? (...args: I) => O
: Schema[K] extends Mutation<infer I, infer O>
? (...args: I) => O
: Schema[K] extends AnySchema
? TransactionApi<Schema[K]>
: never;
};
class Database<Schema extends AnySchema> {
private db = new TupleDatabaseClient(
new TupleDatabase(new InMemoryTupleStorage())
);
constructor(public schema: Schema) {}
public transact(transactionFn: (tx: TransactionApi<Schema>) => void) {
const tupleDbTx = this.db.transact();
const schema = this.schema;
const tx = Database.makeTx(schema, tupleDbTx);
transactionFn(tx);
tupleDbTx.commit();
}
private static makeTx<Schema extends AnySchema>(
schema: Schema,
tupleDbTx: TupleReadWriteTransactionApi
): TransactionApi<Schema> {
return new Proxy(
{},
{
get(target, prop, receiver) {
if (isSymbol(prop)) throw new Error(`Unexpected key ${String(prop)}`);
const schemaValue = schema[prop];
if (isFunction(schemaValue)) {
return (...args: any[]) => schemaValue(tupleDbTx, ...args);
} else if (schemaValue) {
return Database.makeTx(
schemaValue,
tupleDbTx.subspace([prop] as any)
);
}
},
}
) as TransactionApi<Schema>;
}
decode() {
return this.schema.decode?.(this.db);
}
scan(args: any) {
return this.db.scan(args);
}
}
type CellSchema<T> = {
get: Query<[], T>;
set: Mutation<[value: T], void>;
encode: Mutation<[value: T], void>;
decode: Query<[], T>;
};
// Assert = T
type CellValue<T> = SchemaToValue<CellSchema<T>>;
function cell<T>(): CellSchema<T> {
return {
encode: mutation((tx, value) => {
tx.set([], value);
}),
decode: query((tx) => {
return tx.get([]);
}),
get: query((tx) => {
return tx.get([]);
}),
set: mutation((tx, value) => {
return tx.set([], value);
}),
};
}
type ObjectSchema<T extends { [key: string]: AnySchema }> = {
[K in keyof T]: T[K];
} & {
encode: Mutation<[value: { [K in keyof T]: SchemaToValue<T[K]> }], void>;
decode: Query<[], { [K in keyof T]: SchemaToValue<T[K]> }>;
};
type TestObjectSchemaToValue = SchemaToValue<
ObjectSchema<{
title: CellSchema<string>;
done: CellSchema<boolean>;
}>
>;
function object<T extends { [key: string]: AnySchema }>(
schemas: T
): ObjectSchema<T> {
return {
...schemas,
encode: mutation((tx, value) => {
for (const key in schemas) {
// TODO: handle error
schemas[key].encode?.(tx.subspace([key]) as any, value[key]);
}
}),
decode: query((tx) => {
const obj: any = {};
for (const key in schemas) {
// TODO: handle error
obj[key] = schemas[key].decode?.(tx.subspace([key]) as any);
}
return obj;
}),
};
}
const todoSchema = object({
title: cell<string>(),
done: cell<boolean>(),
});
type ListSchema<T extends AnySchema> = {
encode: Mutation<[value: T[]], void>;
decode: Query<[], T[]>;
getItem: Query<[index: number], SchemaToValue<T>>;
insert: Mutation<[index: number, value: SchemaToValue<T>], void>;
length: Query<[], number>;
} & {
[index: number]: T;
};
function list<T extends AnySchema>(itemSchema: T): ListSchema<T> {
return new Proxy(
{
getItem: query((tx, index) => {
return tx.get([index]);
}),
insert: mutation((tx, index, value) => {
tx.set(["count"], (tx.get(["count"]) || 0) + 1);
// TODO: idk why the types here aren't working out
const itemSubspace = tx.subspace([index]) as any;
itemSchema.encode?.(itemSubspace, value);
}),
length: query((tx) => {
return tx.get(["count"]) || 0;
}),
encode: mutation((tx, value) => {
for (let i = 0; i < value.length; i++) {
tx.set([i], value[i]);
}
}),
decode: query((tx) => {
const length = tx.get(["count"]) || 0;
const result: T[] = [];
for (let i = 0; i < length; i++) {
result.push(itemSchema.decode?.(tx.subspace([i]) as any) as any);
}
return result;
}),
},
{
get(target, prop, receiver) {
if (isSymbol(prop)) return undefined;
if (target[prop as keyof typeof target])
return target[prop as keyof typeof target];
const index = Number(prop);
if (isNaN(index)) {
throw new Error(`Unexpected key ${prop}`);
}
return itemSchema;
},
}
);
}
const todoListSchema = list(todoSchema);
const db = new Database(todoListSchema);
db.transact((tx) => {
tx.insert(0, {
title: "Groceries",
done: false,
});
});
// [ { title: 'Groceries', done: false } ]
console.log(db.decode());
// [
// { key: [ 0, 'done' ], value: false },
// { key: [ 0, 'title' ], value: 'Groceries' },
// { key: [ 'count' ], value: 1 }
// ]
console.log(db.scan({}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment