mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
public commit
This commit is contained in:
97
app/src/data/connection/Connection.ts
Normal file
97
app/src/data/connection/Connection.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
type AliasableExpression,
|
||||
type DatabaseIntrospector,
|
||||
type Expression,
|
||||
type Kysely,
|
||||
type KyselyPlugin,
|
||||
type RawBuilder,
|
||||
type SelectQueryBuilder,
|
||||
type SelectQueryNode,
|
||||
type Simplify,
|
||||
sql
|
||||
} from "kysely";
|
||||
|
||||
export type QB = SelectQueryBuilder<any, any, any>;
|
||||
|
||||
export type IndexMetadata = {
|
||||
name: string;
|
||||
table: string;
|
||||
isUnique: boolean;
|
||||
columns: { name: string; order: number }[];
|
||||
};
|
||||
|
||||
export interface ConnectionIntrospector extends DatabaseIntrospector {
|
||||
getIndices(tbl_name?: string): Promise<IndexMetadata[]>;
|
||||
}
|
||||
|
||||
export interface SelectQueryBuilderExpression<O> extends AliasableExpression<O> {
|
||||
get isSelectQueryBuilder(): true;
|
||||
toOperationNode(): SelectQueryNode;
|
||||
}
|
||||
|
||||
export type DbFunctions = {
|
||||
jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O> | null>;
|
||||
jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O>[]>;
|
||||
jsonBuildObject<O extends Record<string, Expression<unknown>>>(
|
||||
obj: O
|
||||
): RawBuilder<
|
||||
Simplify<{
|
||||
[K in keyof O]: O[K] extends Expression<infer V> ? V : never;
|
||||
}>
|
||||
>;
|
||||
};
|
||||
|
||||
export abstract class Connection {
|
||||
kysely: Kysely<any>;
|
||||
|
||||
constructor(
|
||||
kysely: Kysely<any>,
|
||||
public fn: Partial<DbFunctions> = {},
|
||||
protected plugins: KyselyPlugin[] = []
|
||||
) {
|
||||
this.kysely = kysely;
|
||||
}
|
||||
|
||||
getIntrospector(): ConnectionIntrospector {
|
||||
return this.kysely.introspection as ConnectionIntrospector;
|
||||
}
|
||||
|
||||
supportsBatching(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsIndices(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async ping(): Promise<boolean> {
|
||||
const res = await sql`SELECT 1`.execute(this.kysely);
|
||||
return res.rows.length > 0;
|
||||
}
|
||||
|
||||
protected async batch<Queries extends QB[]>(
|
||||
queries: [...Queries]
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
throw new Error("Batching not supported");
|
||||
}
|
||||
|
||||
async batchQuery<Queries extends QB[]>(
|
||||
queries: [...Queries]
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
// bypass if no client support
|
||||
if (!this.supportsBatching()) {
|
||||
const data: any = [];
|
||||
for (const q of queries) {
|
||||
const result = await q.execute();
|
||||
data.push(result);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
return await this.batch(queries);
|
||||
}
|
||||
}
|
||||
100
app/src/data/connection/LibsqlConnection.ts
Normal file
100
app/src/data/connection/LibsqlConnection.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { type Client, type InStatement, createClient } from "@libsql/client/web";
|
||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin, sql } from "kysely";
|
||||
import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin";
|
||||
import { KyselyPluginRunner } from "../plugins/KyselyPluginRunner";
|
||||
import type { QB } from "./Connection";
|
||||
import { SqliteConnection } from "./SqliteConnection";
|
||||
import { SqliteIntrospector } from "./SqliteIntrospector";
|
||||
|
||||
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
|
||||
export type LibSqlCredentials = {
|
||||
url: string;
|
||||
authToken?: string;
|
||||
protocol?: (typeof LIBSQL_PROTOCOLS)[number];
|
||||
};
|
||||
|
||||
class CustomLibsqlDialect extends LibsqlDialect {
|
||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db, {
|
||||
excludeTables: ["libsql_wasm_func_table"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class LibsqlConnection extends SqliteConnection {
|
||||
private client: Client;
|
||||
|
||||
constructor(client: Client);
|
||||
constructor(credentials: LibSqlCredentials);
|
||||
constructor(clientOrCredentials: Client | LibSqlCredentials) {
|
||||
const plugins = [new FilterNumericKeysPlugin(), new ParseJSONResultsPlugin()];
|
||||
let client: Client;
|
||||
if ("url" in clientOrCredentials) {
|
||||
let { url, authToken, protocol } = clientOrCredentials;
|
||||
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
|
||||
console.log("changing protocol to", protocol);
|
||||
const [, rest] = url.split("://");
|
||||
url = `${protocol}://${rest}`;
|
||||
}
|
||||
|
||||
//console.log("using", url, { protocol });
|
||||
|
||||
client = createClient({ url, authToken });
|
||||
} else {
|
||||
//console.log("-- client provided");
|
||||
client = clientOrCredentials;
|
||||
}
|
||||
|
||||
const kysely = new Kysely({
|
||||
// @ts-expect-error libsql has type issues
|
||||
dialect: new CustomLibsqlDialect({ client }),
|
||||
plugins
|
||||
//log: ["query"],
|
||||
});
|
||||
|
||||
super(kysely, {}, plugins);
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
override supportsBatching(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override supportsIndices(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getClient(): Client {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
protected override async batch<Queries extends QB[]>(
|
||||
queries: [...Queries]
|
||||
): Promise<{
|
||||
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
|
||||
}> {
|
||||
const stms: InStatement[] = queries.map((q) => {
|
||||
const compiled = q.compile();
|
||||
//console.log("compiled", compiled.sql, compiled.parameters);
|
||||
return {
|
||||
sql: compiled.sql,
|
||||
args: compiled.parameters as any[]
|
||||
};
|
||||
});
|
||||
|
||||
const res = await this.client.batch(stms);
|
||||
|
||||
// let it run through plugins
|
||||
const kyselyPlugins = new KyselyPluginRunner(this.plugins);
|
||||
|
||||
const data: any = [];
|
||||
for (const r of res) {
|
||||
const rows = await kyselyPlugins.transformResultRows(r.rows);
|
||||
data.push(rows);
|
||||
}
|
||||
//console.log("data", data);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
22
app/src/data/connection/SqliteConnection.ts
Normal file
22
app/src/data/connection/SqliteConnection.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Kysely, KyselyPlugin } from "kysely";
|
||||
import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import { Connection, type DbFunctions } from "./Connection";
|
||||
|
||||
export class SqliteConnection extends Connection {
|
||||
constructor(kysely: Kysely<any>, fn: Partial<DbFunctions> = {}, plugins: KyselyPlugin[] = []) {
|
||||
super(
|
||||
kysely,
|
||||
{
|
||||
...fn,
|
||||
jsonArrayFrom,
|
||||
jsonObjectFrom,
|
||||
jsonBuildObject
|
||||
},
|
||||
plugins
|
||||
);
|
||||
}
|
||||
|
||||
override supportsIndices(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
164
app/src/data/connection/SqliteIntrospector.ts
Normal file
164
app/src/data/connection/SqliteIntrospector.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type {
|
||||
DatabaseIntrospector,
|
||||
DatabaseMetadata,
|
||||
DatabaseMetadataOptions,
|
||||
ExpressionBuilder,
|
||||
Kysely,
|
||||
SchemaMetadata,
|
||||
TableMetadata,
|
||||
} from "kysely";
|
||||
import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely";
|
||||
import type { ConnectionIntrospector, IndexMetadata } from "./Connection";
|
||||
|
||||
export type SqliteIntrospectorConfig = {
|
||||
excludeTables?: string[];
|
||||
};
|
||||
|
||||
export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntrospector {
|
||||
readonly #db: Kysely<any>;
|
||||
readonly _excludeTables: string[] = [];
|
||||
|
||||
constructor(db: Kysely<any>, config: SqliteIntrospectorConfig = {}) {
|
||||
this.#db = db;
|
||||
this._excludeTables = config.excludeTables ?? [];
|
||||
}
|
||||
|
||||
async getSchemas(): Promise<SchemaMetadata[]> {
|
||||
// Sqlite doesn't support schemas.
|
||||
return [];
|
||||
}
|
||||
|
||||
async getIndices(tbl_name?: string): Promise<IndexMetadata[]> {
|
||||
const indices = await this.#db
|
||||
.selectFrom("sqlite_master")
|
||||
.where("type", "=", "index")
|
||||
.$if(!!tbl_name, (eb) => eb.where("tbl_name", "=", tbl_name))
|
||||
.select("name")
|
||||
.$castTo<{ name: string }>()
|
||||
.execute();
|
||||
|
||||
return Promise.all(indices.map(({ name }) => this.#getIndexMetadata(name)));
|
||||
}
|
||||
|
||||
async #getIndexMetadata(index: string): Promise<IndexMetadata> {
|
||||
const db = this.#db;
|
||||
|
||||
// Get the SQL that was used to create the index.
|
||||
const indexDefinition = await db
|
||||
.selectFrom("sqlite_master")
|
||||
.where("name", "=", index)
|
||||
.select(["sql", "tbl_name", "type"])
|
||||
.$castTo<{ sql: string | undefined; tbl_name: string; type: string }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
//console.log("--indexDefinition--", indexDefinition, index);
|
||||
|
||||
// check unique by looking for the word "unique" in the sql
|
||||
const isUnique = indexDefinition.sql?.match(/unique/i) != null;
|
||||
|
||||
const columns = await db
|
||||
.selectFrom(
|
||||
sql<{
|
||||
seqno: number;
|
||||
cid: number;
|
||||
name: string;
|
||||
}>`pragma_index_info(${index})`.as("index_info"),
|
||||
)
|
||||
.select(["seqno", "cid", "name"])
|
||||
.orderBy("cid")
|
||||
.execute();
|
||||
|
||||
return {
|
||||
name: index,
|
||||
table: indexDefinition.tbl_name,
|
||||
isUnique: isUnique,
|
||||
columns: columns.map((col) => ({
|
||||
name: col.name,
|
||||
order: col.seqno,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private excludeTables(tables: string[] = []) {
|
||||
return (eb: ExpressionBuilder<any, any>) => {
|
||||
const and = tables.map((t) => eb("name", "!=", t));
|
||||
return eb.and(and);
|
||||
};
|
||||
}
|
||||
|
||||
async getTables(
|
||||
options: DatabaseMetadataOptions = { withInternalKyselyTables: false },
|
||||
): Promise<TableMetadata[]> {
|
||||
let query = this.#db
|
||||
.selectFrom("sqlite_master")
|
||||
.where("type", "in", ["table", "view"])
|
||||
.where("name", "not like", "sqlite_%")
|
||||
.select("name")
|
||||
.orderBy("name")
|
||||
.$castTo<{ name: string }>();
|
||||
|
||||
if (!options.withInternalKyselyTables) {
|
||||
query = query.where(
|
||||
this.excludeTables([DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK_TABLE]),
|
||||
);
|
||||
}
|
||||
if (this._excludeTables.length > 0) {
|
||||
query = query.where(this.excludeTables(this._excludeTables));
|
||||
}
|
||||
|
||||
const tables = await query.execute();
|
||||
return Promise.all(tables.map(({ name }) => this.#getTableMetadata(name)));
|
||||
}
|
||||
|
||||
async getMetadata(options?: DatabaseMetadataOptions): Promise<DatabaseMetadata> {
|
||||
return {
|
||||
tables: await this.getTables(options),
|
||||
};
|
||||
}
|
||||
|
||||
async #getTableMetadata(table: string): Promise<TableMetadata> {
|
||||
const db = this.#db;
|
||||
|
||||
// Get the SQL that was used to create the table.
|
||||
const tableDefinition = await db
|
||||
.selectFrom("sqlite_master")
|
||||
.where("name", "=", table)
|
||||
.select(["sql", "type"])
|
||||
.$castTo<{ sql: string | undefined; type: string }>()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
// Try to find the name of the column that has `autoincrement` 🤦
|
||||
const autoIncrementCol = tableDefinition.sql
|
||||
?.split(/[\(\),]/)
|
||||
?.find((it) => it.toLowerCase().includes("autoincrement"))
|
||||
?.trimStart()
|
||||
?.split(/\s+/)?.[0]
|
||||
?.replace(/["`]/g, "");
|
||||
|
||||
const columns = await db
|
||||
.selectFrom(
|
||||
sql<{
|
||||
name: string;
|
||||
type: string;
|
||||
notnull: 0 | 1;
|
||||
dflt_value: any;
|
||||
}>`pragma_table_info(${table})`.as("table_info"),
|
||||
)
|
||||
.select(["name", "type", "notnull", "dflt_value"])
|
||||
.orderBy("cid")
|
||||
.execute();
|
||||
|
||||
return {
|
||||
name: table,
|
||||
isView: tableDefinition.type === "view",
|
||||
columns: columns.map((col) => ({
|
||||
name: col.name,
|
||||
dataType: col.type,
|
||||
isNullable: !col.notnull,
|
||||
isAutoIncrementing: col.name === autoIncrementCol,
|
||||
hasDefaultValue: col.dflt_value != null,
|
||||
comment: undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
31
app/src/data/connection/SqliteLocalConnection.ts
Normal file
31
app/src/data/connection/SqliteLocalConnection.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { DatabaseIntrospector, SqliteDatabase } from "kysely";
|
||||
import { Kysely, SqliteDialect } from "kysely";
|
||||
import { DeserializeJsonValuesPlugin } from "../plugins/DeserializeJsonValuesPlugin";
|
||||
import { SqliteConnection } from "./SqliteConnection";
|
||||
import { SqliteIntrospector } from "./SqliteIntrospector";
|
||||
|
||||
class CustomSqliteDialect extends SqliteDialect {
|
||||
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db, {
|
||||
excludeTables: ["test_table"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SqliteLocalConnection extends SqliteConnection {
|
||||
constructor(private database: SqliteDatabase) {
|
||||
const plugins = [new DeserializeJsonValuesPlugin()];
|
||||
const kysely = new Kysely({
|
||||
dialect: new CustomSqliteDialect({ database }),
|
||||
plugins
|
||||
//log: ["query"],
|
||||
});
|
||||
|
||||
super(kysely);
|
||||
this.plugins = plugins;
|
||||
}
|
||||
|
||||
override supportsIndices(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user