Scenario generator for vore roleplay and story ideas.
https://scenario-generator.deliciousreya.net/responses
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
5.0 KiB
132 lines
5.0 KiB
export type QueryDefinition<T extends string> = {
|
|
readonly parameters: {
|
|
readonly [key in T]: { readonly index: number, readonly validator: (value: undefined) => string | number | null }
|
|
}
|
|
readonly query: string
|
|
readonly output: (result: D1Result<object>) => unknown
|
|
}
|
|
export type QueryParameters<DefinitionT extends QueryDefinition<any>> = DefinitionT extends QueryDefinition<infer ParametersT> ? {
|
|
readonly [key in ParametersT]: Exclude<Parameters<DefinitionT['parameters'][key]['validator']>[0], undefined>
|
|
} : never
|
|
export type BoundQuery<ResultT> = {
|
|
readonly statement: D1PreparedStatement,
|
|
readonly transformer: (result: D1Result<object>) => ResultT
|
|
}
|
|
export type QueryOutput<DefinitionT extends QueryDefinition<any>> = ReturnType<DefinitionT["output"]>
|
|
export type PreparedQuery<DefinitionT extends QueryDefinition<any>> = (values: QueryParameters<DefinitionT>) => BoundQuery<QueryOutput<DefinitionT>>
|
|
export type QueryDefinitions = { readonly [key: string]: QueryDefinition<any> }
|
|
export type PreparedQueries<T extends QueryDefinitions> = { readonly [key in keyof T]: PreparedQuery<T[key]> }
|
|
|
|
const QUERY_PARAM_HEURISTIC = /\?(\d+)/g;
|
|
|
|
function parameterIndexes(parameters: QueryDefinition<any>["parameters"]): Set<number> {
|
|
let result = new Set<number>();
|
|
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<number> {
|
|
const result = new Set<number>()
|
|
for (const binding of query.matchAll(QUERY_PARAM_HEURISTIC)) {
|
|
result.add(parseInt(binding[1]))
|
|
}
|
|
return result
|
|
}
|
|
|
|
export function validatedDefinition<T extends QueryDefinition<any>>(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<T extends QueryDefinitions>(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<T extends QueryDefinition<any>>(database: D1Database, definition: T): PreparedQuery<T> {
|
|
const preparedStatement = database.prepare(definition.query);
|
|
return function(values: QueryParameters<T>) {
|
|
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<T>;
|
|
}
|
|
|
|
export function prepareAllQueries<T extends QueryDefinitions>(database: D1Database, q: T): PreparedQueries<T> {
|
|
const result: Partial<PreparedQueries<T>> = {};
|
|
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<T>;
|
|
}
|
|
|
|
export async function runQuery<T>(db: D1Database, query: BoundQuery<T>): Promise<T> {
|
|
const [results] = await db.batch([query.statement]);
|
|
return query.transformer(results as D1Result<object>);
|
|
}
|
|
|
|
export async function batchQueries<T extends [...unknown[]]>(db: D1Database, queries: { readonly [K in keyof T]: BoundQuery<T[K]> }): Promise<T> {
|
|
const results = await db.batch(queries.map(q => q.statement));
|
|
return results.map((result, index) => queries[index].transformer(result as D1Result<object>)) as unknown as T;
|
|
}
|
|
|
|
export class TypedDBWrapper {
|
|
private readonly db: D1Database;
|
|
|
|
constructor(db: D1Database) {
|
|
this.db = db;
|
|
}
|
|
|
|
prepare<T extends QueryDefinition<any>>(query: T): PreparedQuery<T> {
|
|
return prepareQuery(this.db, query);
|
|
}
|
|
|
|
prepareAll<T extends QueryDefinitions>(queries: T): PreparedQueries<T> {
|
|
return prepareAllQueries(this.db, queries);
|
|
}
|
|
|
|
async run<T>(query: BoundQuery<T>): Promise<T> {
|
|
return runQuery(this.db, query);
|
|
}
|
|
|
|
async batch<T extends [...unknown[]]>(...queries: { readonly [K in keyof T]: BoundQuery<T[K]> }): Promise<T> {
|
|
return batchQueries<T>(this.db, queries);
|
|
}
|
|
}
|
|
|
|
// TODO: Use the new run and batch functions to fix the Database class's methods
|
|
|