Created
March 12, 2024 04:20
Tuple-db ORM
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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