export type QueryDefinition = { readonly parameters: { readonly [key in T]: { readonly index: number, readonly validator: (value: undefined) => string | number | null } } readonly query: string readonly output: (result: D1Result) => unknown } export type QueryParameters> = DefinitionT extends QueryDefinition ? { readonly [key in ParametersT]: Exclude[0], undefined> } : never export type BoundQuery = { readonly statement: D1PreparedStatement, readonly transformer: (result: D1Result) => ResultT } export type QueryOutput> = ReturnType export type PreparedQuery> = (values: QueryParameters) => BoundQuery> export type QueryDefinitions = { readonly [key: string]: QueryDefinition } export type PreparedQueries = { readonly [key in keyof T]: PreparedQuery } const QUERY_PARAM_HEURISTIC = /\?(\d+)/g; function parameterIndexes(parameters: QueryDefinition["parameters"]): Set { let result = new Set(); for (const key of Object.keys(parameters)) { const value = parameters[key].index; if (result.has(value)) { throw Error(`found duplicate index ${value}`) } result.add(value) } return result; } function queryBindingIndexes(query: string): Set { const result = new Set() for (const binding of query.matchAll(QUERY_PARAM_HEURISTIC)) { result.add(parseInt(binding[1])) } return result } export function validatedDefinition>(definition: T): T { const queryBindings = queryBindingIndexes(definition.query); const parameters = parameterIndexes(definition.parameters) const missing = Array.from(queryBindings).filter(v => !parameters.has(v)) const extra = Array.from(parameters).filter(v => !queryBindings.has(v)) if (missing.length + extra.length > 0) { if (missing.length > 0) { if (extra.length > 0) { throw Error(`missing definitions for ${missing.map(v => `?${v}`).join(', ')} and don't need definitions for ${extra.map(v => `?${v}`).join(', ')}`) } else { throw Error(`missing definitions for ${missing.map(v => `?${v}`).join(', ')} `) } } else { throw Error(`don't need definitions for ${extra.map(v => `?${v}`).join(', ')}`) } } return definition; } export function validatedDefinitions(definitions: T): T { for (const key of Object.keys(definitions) as (keyof T & string)[]) { try { validatedDefinition(definitions[key]); } catch (e) { throw Error(`when validating definition for ${key}: ${e}`) } } return definitions; } export function prepareQuery>(database: D1Database, definition: T): PreparedQuery { const preparedStatement = database.prepare(definition.query); return function(values: QueryParameters) { const bindings: unknown[] = new Array(Array.from(parameterIndexes(definition.parameters)).reduce((a, b) => Math.max(a, b), 0)); for (const key of Object.keys(definition.parameters)) { bindings[definition.parameters[key].index - 1] = definition.parameters[key].validator(values[key]); } return { statement: preparedStatement.bind(...bindings), transformer: definition.output }; } as PreparedQuery; } export function prepareAllQueries(database: D1Database, q: T): PreparedQueries { const result: Partial> = {}; for (const key of Object.keys(q) as (keyof T & string)[]) { try { result[key] = prepareQuery(database, q[key]) } catch (e) { throw Error(`when preparing ${key}: ${e}`) } } return result as PreparedQueries; } export async function runQuery(db: D1Database, query: BoundQuery): Promise { const [results] = await db.batch([query.statement]); return query.transformer(results as D1Result); } export async function batchQueries(db: D1Database, queries: { readonly [K in keyof T]: BoundQuery }): Promise { const results = await db.batch(queries.map(q => q.statement)); return results.map((result, index) => queries[index].transformer(result as D1Result)) as unknown as T; } export class TypedDBWrapper { private readonly db: D1Database; constructor(db: D1Database) { this.db = db; } prepare>(query: T): PreparedQuery { return prepareQuery(this.db, query); } prepareAll(queries: T): PreparedQueries { return prepareAllQueries(this.db, queries); } async run(query: BoundQuery): Promise { return runQuery(this.db, query); } async batch(...queries: { readonly [K in keyof T]: BoundQuery }): Promise { return batchQueries(this.db, queries); } } // TODO: Use the new run and batch functions to fix the Database class's methods