329 lines
8.2 KiB
TypeScript
329 lines
8.2 KiB
TypeScript
import { Kysely, MysqlDialect, PostgresDialect, SqliteDialect } from 'kysely';
|
|
import { createPool as createMysqlPool } from 'mysql2';
|
|
import { Pool as PostgresPool } from 'pg';
|
|
|
|
import type { IdentityDatabaseSchema } from '../types/database';
|
|
import { IdentityDBConfigurationError } from '../core/errors';
|
|
|
|
export interface SqliteConnectionConfig {
|
|
client: 'sqlite';
|
|
filename: string;
|
|
readonly?: boolean;
|
|
}
|
|
|
|
export interface PostgresConnectionConfig {
|
|
client: 'postgres';
|
|
connectionString?: string;
|
|
host?: string;
|
|
port?: number;
|
|
database?: string;
|
|
user?: string;
|
|
password?: string;
|
|
ssl?: boolean;
|
|
}
|
|
|
|
export interface MysqlConnectionConfig {
|
|
client: 'mysql' | 'mariadb';
|
|
uri?: string;
|
|
host?: string;
|
|
port?: number;
|
|
database?: string;
|
|
user?: string;
|
|
password?: string;
|
|
}
|
|
|
|
export type IdentityDBConnectionConfig =
|
|
| SqliteConnectionConfig
|
|
| PostgresConnectionConfig
|
|
| MysqlConnectionConfig;
|
|
|
|
export interface DatabaseConnection {
|
|
client: IdentityDBConnectionConfig['client'];
|
|
db: Kysely<IdentityDatabaseSchema>;
|
|
destroy: () => Promise<void>;
|
|
}
|
|
|
|
interface BunSqliteStatement {
|
|
columnNames: ReadonlyArray<string>;
|
|
all(parameters?: ReadonlyArray<unknown>): unknown[];
|
|
run(parameters?: ReadonlyArray<unknown>): {
|
|
changes: number | bigint;
|
|
lastInsertRowid: number | bigint;
|
|
};
|
|
iterate(parameters?: ReadonlyArray<unknown>): IterableIterator<unknown>;
|
|
}
|
|
|
|
interface BunSqliteDatabase {
|
|
close(): void;
|
|
exec(sql: string): void;
|
|
prepare(sql: string): BunSqliteStatement;
|
|
}
|
|
|
|
interface BunSqliteModule {
|
|
Database: {
|
|
open(filename: string, flags?: number): BunSqliteDatabase;
|
|
};
|
|
constants: {
|
|
SQLITE_OPEN_CREATE: number;
|
|
SQLITE_OPEN_MEMORY: number;
|
|
SQLITE_OPEN_READONLY: number;
|
|
SQLITE_OPEN_READWRITE: number;
|
|
};
|
|
}
|
|
|
|
interface NodeSqliteStatement {
|
|
all(...parameters: ReadonlyArray<unknown>): unknown[];
|
|
columns(): ReadonlyArray<unknown>;
|
|
iterate(...parameters: ReadonlyArray<unknown>): IterableIterator<unknown>;
|
|
run(...parameters: ReadonlyArray<unknown>): {
|
|
changes: number | bigint;
|
|
lastInsertRowid: number | bigint;
|
|
};
|
|
}
|
|
|
|
interface NodeSqliteDatabase {
|
|
close(): void;
|
|
exec(sql: string): void;
|
|
prepare(sql: string): NodeSqliteStatement;
|
|
}
|
|
|
|
interface NodeSqliteModule {
|
|
DatabaseSync: new (
|
|
filename: string,
|
|
options?: {
|
|
readOnly?: boolean;
|
|
},
|
|
) => NodeSqliteDatabase;
|
|
}
|
|
|
|
interface KyselyCompatibleSqliteStatement {
|
|
readonly reader: boolean;
|
|
all(parameters: ReadonlyArray<unknown>): unknown[];
|
|
run(parameters: ReadonlyArray<unknown>): {
|
|
changes: number | bigint;
|
|
lastInsertRowid: number | bigint;
|
|
};
|
|
iterate(parameters: ReadonlyArray<unknown>): IterableIterator<unknown>;
|
|
}
|
|
|
|
interface KyselyCompatibleSqliteDatabase {
|
|
close(): void;
|
|
prepare(sql: string): KyselyCompatibleSqliteStatement;
|
|
}
|
|
|
|
const BUN_SQLITE_MODULE = 'bun:sqlite';
|
|
const NODE_SQLITE_MODULE = 'node:sqlite';
|
|
|
|
function createUnsupportedSqliteRuntimeError(): IdentityDBConfigurationError {
|
|
return new IdentityDBConfigurationError(
|
|
'SQLite connections now require a runtime with a built-in SQLite driver. Use Bun for bun:sqlite support, or Node 22+ for node:sqlite support.',
|
|
);
|
|
}
|
|
|
|
function isBunRuntime(): boolean {
|
|
return typeof globalThis === 'object' && 'Bun' in globalThis;
|
|
}
|
|
|
|
async function createBunSqliteDatabase(
|
|
config: SqliteConnectionConfig,
|
|
bunSqliteModule?: BunSqliteModule,
|
|
): Promise<KyselyCompatibleSqliteDatabase> {
|
|
const bunSqlite = bunSqliteModule
|
|
?? ((await import(BUN_SQLITE_MODULE).catch(() => {
|
|
throw createUnsupportedSqliteRuntimeError();
|
|
})) as BunSqliteModule);
|
|
|
|
const flags = config.readonly
|
|
? bunSqlite.constants.SQLITE_OPEN_READONLY
|
|
: bunSqlite.constants.SQLITE_OPEN_READWRITE
|
|
| bunSqlite.constants.SQLITE_OPEN_CREATE
|
|
| (config.filename === ':memory:' ? bunSqlite.constants.SQLITE_OPEN_MEMORY : 0);
|
|
|
|
const database = bunSqlite.Database.open(config.filename, flags);
|
|
database.exec('PRAGMA foreign_keys = ON');
|
|
|
|
return {
|
|
close() {
|
|
database.close();
|
|
},
|
|
prepare(sql: string): KyselyCompatibleSqliteStatement {
|
|
const statement = database.prepare(sql);
|
|
|
|
return {
|
|
reader: statement.columnNames.length > 0,
|
|
all(parameters) {
|
|
return statement.all(Array.from(parameters));
|
|
},
|
|
run(parameters) {
|
|
return statement.run(Array.from(parameters));
|
|
},
|
|
iterate(parameters) {
|
|
return statement.iterate(Array.from(parameters));
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
function createNodeSqliteDatabase(
|
|
config: SqliteConnectionConfig,
|
|
nodeSqlite: NodeSqliteModule,
|
|
): KyselyCompatibleSqliteDatabase {
|
|
const database = new nodeSqlite.DatabaseSync(config.filename, {
|
|
readOnly: config.readonly ?? false,
|
|
});
|
|
|
|
database.exec('PRAGMA foreign_keys = ON');
|
|
|
|
return {
|
|
close() {
|
|
database.close();
|
|
},
|
|
prepare(sql: string): KyselyCompatibleSqliteStatement {
|
|
const statement = database.prepare(sql);
|
|
|
|
return {
|
|
reader: statement.columns().length > 0,
|
|
all(parameters) {
|
|
return statement.all(...parameters);
|
|
},
|
|
run(parameters) {
|
|
return statement.run(...parameters);
|
|
},
|
|
iterate(parameters) {
|
|
return statement.iterate(...parameters);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
async function createSqliteDatabase(
|
|
config: SqliteConnectionConfig,
|
|
): Promise<KyselyCompatibleSqliteDatabase> {
|
|
if (isBunRuntime()) {
|
|
return createBunSqliteDatabase(config);
|
|
}
|
|
|
|
const nodeSqlite = await import(NODE_SQLITE_MODULE).catch(() => null);
|
|
|
|
if (nodeSqlite) {
|
|
return createNodeSqliteDatabase(config, nodeSqlite as NodeSqliteModule);
|
|
}
|
|
|
|
throw createUnsupportedSqliteRuntimeError();
|
|
}
|
|
|
|
export async function createDatabase(
|
|
config: IdentityDBConnectionConfig,
|
|
): Promise<DatabaseConnection> {
|
|
switch (config.client) {
|
|
case 'sqlite': {
|
|
const sqlite = await createSqliteDatabase(config);
|
|
|
|
const db = new Kysely<IdentityDatabaseSchema>({
|
|
dialect: new SqliteDialect({
|
|
database: sqlite,
|
|
}),
|
|
});
|
|
|
|
return {
|
|
client: config.client,
|
|
db,
|
|
destroy: async () => {
|
|
await db.destroy();
|
|
},
|
|
};
|
|
}
|
|
|
|
case 'postgres': {
|
|
const pool = new PostgresPool({
|
|
connectionString: config.connectionString,
|
|
host: config.host,
|
|
port: config.port,
|
|
database: config.database,
|
|
user: config.user,
|
|
password: config.password,
|
|
ssl: config.ssl ? { rejectUnauthorized: false } : undefined,
|
|
});
|
|
|
|
const db = new Kysely<IdentityDatabaseSchema>({
|
|
dialect: new PostgresDialect({ pool }),
|
|
});
|
|
|
|
return {
|
|
client: config.client,
|
|
db,
|
|
destroy: async () => {
|
|
await db.destroy();
|
|
await pool.end();
|
|
},
|
|
};
|
|
}
|
|
|
|
case 'mysql':
|
|
case 'mariadb': {
|
|
const mysqlOptions: {
|
|
host?: string;
|
|
port?: number;
|
|
database?: string;
|
|
user?: string;
|
|
password?: string;
|
|
} = {};
|
|
|
|
if (config.host !== undefined) {
|
|
mysqlOptions.host = config.host;
|
|
}
|
|
|
|
if (config.port !== undefined) {
|
|
mysqlOptions.port = config.port;
|
|
}
|
|
|
|
if (config.database !== undefined) {
|
|
mysqlOptions.database = config.database;
|
|
}
|
|
|
|
if (config.user !== undefined) {
|
|
mysqlOptions.user = config.user;
|
|
}
|
|
|
|
if (config.password !== undefined) {
|
|
mysqlOptions.password = config.password;
|
|
}
|
|
|
|
const pool = config.uri
|
|
? createMysqlPool(config.uri)
|
|
: createMysqlPool(mysqlOptions);
|
|
|
|
const db = new Kysely<IdentityDatabaseSchema>({
|
|
dialect: new MysqlDialect({ pool }),
|
|
});
|
|
|
|
return {
|
|
client: config.client,
|
|
db,
|
|
destroy: async () => {
|
|
await db.destroy();
|
|
await new Promise<void>((resolve, reject) => {
|
|
pool.end((error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
default: {
|
|
const neverClient: never = config;
|
|
throw new IdentityDBConfigurationError(
|
|
`Unsupported database client: ${JSON.stringify(neverClient)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|