import { type AliasableExpression, type ColumnBuilderCallback, type ColumnDataType, type Compilable, type CompiledQuery, type DatabaseIntrospector, type Dialect, type Expression, type Kysely, type KyselyPlugin, type OnModifyForeignAction, type QueryResult, type RawBuilder, type SelectQueryBuilder, type SelectQueryNode, type Simplify, sql, } from "kysely"; import type { BaseIntrospector, BaseIntrospectorConfig } from "./BaseIntrospector"; import type { Constructor, DB } from "core"; import { KyselyPluginRunner } from "data/plugins/KyselyPluginRunner"; import type { Field } from "data/fields/Field"; export type QB = SelectQueryBuilder; export type IndexMetadata = { name: string; table: string; isUnique: boolean; columns: { name: string; order: number }[]; }; export interface SelectQueryBuilderExpression extends AliasableExpression { get isSelectQueryBuilder(): true; toOperationNode(): SelectQueryNode; } export type SchemaResponse = [string, ColumnDataType, ColumnBuilderCallback] | undefined; const FieldSpecTypes = [ "text", "integer", "real", "blob", "date", "datetime", "timestamp", "boolean", "json", ] as const; export type FieldSpec = { type: (typeof FieldSpecTypes)[number]; name: string; nullable?: boolean; dflt?: any; unique?: boolean; primary?: boolean; references?: string; onDelete?: OnModifyForeignAction; onUpdate?: OnModifyForeignAction; }; export type IndexSpec = { name: string; columns: string[]; unique?: boolean; }; export type DbFunctions = { jsonObjectFrom(expr: SelectQueryBuilderExpression): RawBuilder | null>; jsonArrayFrom(expr: SelectQueryBuilderExpression): RawBuilder[]>; jsonBuildObject>>( obj: O, ): RawBuilder< Simplify<{ [K in keyof O]: O[K] extends Expression ? V : never; }> >; }; export type ConnQuery = CompiledQuery | Compilable; export type ConnQueryResult = T extends CompiledQuery ? QueryResult : T extends Compilable ? QueryResult : never; export type ConnQueryResults = { [K in keyof T]: ConnQueryResult; }; const CONN_SYMBOL = Symbol.for("bknd:connection"); export type Features = { batching: boolean; softscans: boolean; }; export abstract class Connection { abstract name: string; protected initialized = false; protected pluginRunner: KyselyPluginRunner; protected readonly supported: Partial = { batching: false, softscans: true, }; kysely: Kysely; client!: Client; constructor( kysely: Kysely, public fn: Partial = {}, protected plugins: KyselyPlugin[] = [], ) { this.kysely = kysely; this[CONN_SYMBOL] = true; this.pluginRunner = new KyselyPluginRunner(plugins); } // @todo: consider moving constructor logic here, required by sqlocal async init(): Promise { this.initialized = true; } /** * This is a helper function to manage Connection classes * coming from different places * @param conn */ static isConnection(conn: unknown): conn is Connection { if (!conn) return false; return conn[CONN_SYMBOL] === true; } getIntrospector(): BaseIntrospector { return this.kysely.introspection as any; } supports(feature: keyof typeof this.supported): boolean { return this.supported[feature] ?? false; } async ping(): Promise { const res = await sql`SELECT 1`.execute(this.kysely); return res.rows.length > 0; } protected async transformResultRows(result: any[]): Promise { return await this.pluginRunner.transformResultRows(result); } /** * Execute a query and return the result including all metadata * returned from the dialect. */ async executeQueries(...qbs: O): Promise> { return Promise.all(qbs.map(async (qb) => await this.kysely.executeQuery(qb))) as any; } async executeQuery(qb: O): Promise> { const res = await this.executeQueries(qb); return res[0] as any; } protected getCompiled(...qbs: ConnQuery[]): CompiledQuery[] { return qbs.map((qb) => { if ("compile" in qb) { return qb.compile(); } return qb; }); } protected async withTransformedRows< Key extends string = "rows", O extends { [K in Key]: any[] }[] = [], >(result: O, _key?: Key): Promise { return (await Promise.all( result.map(async (row) => { const key = _key ?? "rows"; const { [key]: rows, ...r } = row; return { ...r, rows: await this.transformResultRows(rows), }; }), )) as any; } protected validateFieldSpecType(type: string): type is FieldSpec["type"] { if (!FieldSpecTypes.includes(type as any)) { throw new Error( `Invalid field type "${type}". Allowed types are: ${FieldSpecTypes.join(", ")}`, ); } return true; } abstract getFieldSchema(spec: FieldSpec, strict?: boolean): SchemaResponse; toDriver(value: unknown, field: Field): unknown { return value; } fromDriver(value: any, field: Field): unknown { return value; } async close(): Promise { // no-op by default } } export function customIntrospector>( dialect: T, introspector: Constructor, options: BaseIntrospectorConfig = {}, ) { return { create(...args: ConstructorParameters) { return new (class extends dialect { override createIntrospector(db: Kysely): DatabaseIntrospector { return new introspector(db, options); } })(...args); }, }; }