267 lines
7.4 KiB
TypeScript
Raw Normal View History

2023-07-19 16:35:50 +02:00
import type { Knex } from 'knex';
import path from 'node:path';
2023-07-19 16:35:50 +02:00
import { Dialect, getDialect } from './dialects';
2023-09-20 15:52:30 +02:00
import { createSchemaProvider, SchemaProvider } from './schema';
2023-09-12 16:26:42 +02:00
import { createMetadata, Metadata } from './metadata';
2023-09-20 15:52:30 +02:00
import { createEntityManager, EntityManager } from './entity-manager';
import { createMigrationsProvider, MigrationProvider, type Migration } from './migrations';
2023-09-20 15:52:30 +02:00
import { createLifecyclesProvider, LifecycleProvider } from './lifecycles';
2023-07-19 16:35:50 +02:00
import { createConnection } from './connection';
import * as errors from './errors';
2023-09-21 16:49:53 +02:00
import { Callback, transactionCtx, TransactionObject } from './transaction-context';
2023-07-19 16:35:50 +02:00
import { validateDatabase } from './validations';
import type { Model } from './types';
import type { Identifiers } from './utils/identifiers';
import { createRepairManager, type RepairManager } from './repairs';
2023-07-19 16:35:50 +02:00
export { isKnexQuery } from './utils/knex';
2023-07-19 16:35:50 +02:00
interface Settings {
forceMigration?: boolean;
runMigrations?: boolean;
2024-03-13 15:40:30 +01:00
migrations: {
dir: string;
};
2023-07-19 16:35:50 +02:00
[key: string]: unknown;
}
export type Logger = Record<
'info' | 'warn' | 'error' | 'debug',
(message: string | Record<string, unknown>) => void
>;
2023-07-19 16:35:50 +02:00
export interface DatabaseConfig {
connection: Knex.Config;
2023-09-12 21:47:42 +02:00
settings: Settings;
logger?: Logger;
2023-07-19 16:35:50 +02:00
}
2021-05-10 15:36:09 +02:00
2024-03-13 15:40:30 +01:00
const afterCreate =
(db: Database) =>
(
nativeConnection: unknown,
done: (error: Error | null, nativeConnection: unknown) => Promise<void>
) => {
// run initialize for it since commands such as postgres SET and sqlite PRAGMA are per-connection
db.dialect.initialize(nativeConnection).then(() => {
return done(null, nativeConnection);
});
};
2021-05-10 15:36:09 +02:00
class Database {
2023-07-19 16:35:50 +02:00
connection: Knex;
dialect: Dialect;
config: DatabaseConfig;
2023-09-12 16:26:42 +02:00
metadata: Metadata;
2023-07-19 16:35:50 +02:00
2023-09-20 15:52:30 +02:00
schema: SchemaProvider;
2023-07-19 16:35:50 +02:00
2023-09-20 15:52:30 +02:00
migrations: MigrationProvider;
2023-07-19 16:35:50 +02:00
2023-09-20 15:52:30 +02:00
lifecycles: LifecycleProvider;
2023-07-19 16:35:50 +02:00
2023-09-20 15:52:30 +02:00
entityManager: EntityManager;
2023-07-19 16:35:50 +02:00
repair: RepairManager;
logger: Logger;
2023-07-19 16:35:50 +02:00
constructor(config: DatabaseConfig) {
this.config = {
2023-09-12 21:47:42 +02:00
...config,
settings: {
2021-11-15 09:41:00 +01:00
forceMigration: true,
2022-12-13 19:30:55 +00:00
runMigrations: true,
2023-09-12 21:47:42 +02:00
...(config.settings ?? {}),
},
};
this.logger = config.logger ?? console;
2021-06-28 12:34:29 +02:00
this.dialect = getDialect(this);
let knexConfig: Knex.Config = this.config.connection;
// for object connections, we can configure the dialect synchronously
if (typeof this.config.connection.connection !== 'function') {
this.dialect.configure();
}
// for connection functions, we wrap it so that we can modify it with dialect configure before it reaches knex
else {
this.logger.warn(
'Knex connection functions are currently experimental. Attempting to access the connection object before database initialization will result in errors.'
);
knexConfig = {
...this.config.connection,
connection: async () => {
// @ts-expect-error confirmed it was a function above
const conn = await this.config.connection.connection();
this.dialect.configure(conn);
return conn;
},
};
}
this.metadata = createMetadata([]);
this.connection = createConnection(knexConfig, {
2024-03-13 15:40:30 +01:00
pool: { afterCreate: afterCreate(this) },
});
2021-05-18 10:16:03 +02:00
this.schema = createSchemaProvider(this);
2021-05-10 15:36:09 +02:00
2021-09-20 19:15:50 +02:00
this.migrations = createMigrationsProvider(this);
this.lifecycles = createLifecyclesProvider(this);
2021-06-17 16:17:15 +02:00
this.entityManager = createEntityManager(this);
this.repair = createRepairManager(this);
2021-05-10 15:36:09 +02:00
}
2021-05-17 16:34:19 +02:00
2024-03-13 15:40:30 +01:00
async init({ models }: { models: Model[] }) {
if (typeof this.config.connection.connection === 'function') {
/*
* User code needs to be able to access `connection.connection` directly as if
* it were always an object. For a connection function, that doesn't happen
* until the pool is created, so we need to do that here
*
* TODO: In the next major version, we need to replace all internal code that
* directly references `connection.connection` prior to init, and make a breaking
* change that it cannot be relied on to exist before init so that we can call
* this feature stable.
*/
this.logger.debug('Forcing Knex to make real connection to db');
// sqlite does not support connection pooling so acquireConnection doesn't work
if (this.config.connection.client === 'sqlite') {
await this.connection.raw('SELECT 1');
} else {
await this.connection.client.acquireConnection();
}
}
2024-03-13 15:40:30 +01:00
this.metadata.loadModels(models);
await validateDatabase(this);
return this;
}
2023-07-19 16:35:50 +02:00
query(uid: string) {
2021-06-22 17:13:11 +02:00
if (!this.metadata.has(uid)) {
throw new Error(`Model ${uid} not found`);
}
2021-06-17 16:17:15 +02:00
return this.entityManager.getRepository(uid);
}
2023-04-12 18:42:43 +02:00
inTransaction() {
return !!transactionCtx.get();
}
2023-09-21 16:49:53 +02:00
transaction(): Promise<TransactionObject>;
transaction<TCallback extends Callback>(c: TCallback): Promise<ReturnType<TCallback>>;
async transaction<TCallback extends Callback>(
cb?: TCallback
): Promise<ReturnType<TCallback> | TransactionObject> {
const notNestedTransaction = !transactionCtx.get();
2023-09-12 13:26:59 +02:00
const trx = notNestedTransaction
? await this.connection.transaction()
: (transactionCtx.get() as Knex.Transaction);
async function commit() {
if (notNestedTransaction) {
await transactionCtx.commit(trx);
}
}
async function rollback() {
if (notNestedTransaction) {
await transactionCtx.rollback(trx);
}
}
2022-09-12 15:24:53 +02:00
if (!cb) {
return { commit, rollback, get: () => trx };
2022-09-12 15:24:53 +02:00
}
return transactionCtx.run(trx, async () => {
2022-09-12 15:24:53 +02:00
try {
const callbackParams = {
trx,
commit,
rollback,
onCommit: transactionCtx.onCommit,
onRollback: transactionCtx.onRollback,
};
const res = await cb(callbackParams);
await commit();
2022-09-12 15:24:53 +02:00
return res;
} catch (error) {
await rollback();
2022-09-12 15:24:53 +02:00
throw error;
}
});
}
2023-09-12 21:47:42 +02:00
getSchemaName(): string | undefined {
2023-07-19 16:35:50 +02:00
return this.connection.client.connectionSettings.schema;
}
getConnection(): Knex;
2023-09-12 21:47:42 +02:00
getConnection(tableName?: string): Knex.QueryBuilder;
2023-07-19 16:35:50 +02:00
getConnection(tableName?: string): Knex | Knex.QueryBuilder {
const schema = this.getSchemaName();
const connection = tableName ? this.connection(tableName) : this.connection;
return schema ? connection.withSchema(schema) : connection;
}
// Returns basic info about the database connection
getInfo() {
const connectionSettings = this.connection?.client?.connectionSettings || {};
const client = this.dialect?.client || '';
let displayName = '';
let schema;
// For SQLite, get the relative filename
if (client === 'sqlite') {
const absolutePath = connectionSettings?.filename;
if (absolutePath) {
displayName = path.relative(process.cwd(), absolutePath);
}
}
// For other dialects, get the database name
else {
displayName = connectionSettings?.database;
schema = connectionSettings?.schema;
}
return {
displayName,
schema,
client,
};
}
getSchemaConnection(trx = this.connection) {
2023-07-19 16:35:50 +02:00
const schema = this.getSchemaName();
return schema ? trx.schema.withSchema(schema) : trx.schema;
}
2023-07-19 16:35:50 +02:00
queryBuilder(uid: string) {
return this.entityManager.createQueryBuilder(uid);
}
2021-06-17 16:17:15 +02:00
async destroy() {
await this.lifecycles.clear();
2021-06-17 16:17:15 +02:00
await this.connection.destroy();
2021-05-17 16:34:19 +02:00
}
2021-05-10 15:36:09 +02:00
}
2019-09-20 12:44:24 +02:00
export { Database, errors };
export type { Model, Identifiers, Migration };