quaerateum
Version:
Simple typescript ORM for node.js based on data-mapper, unit-of-work and identity-map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JS.
145 lines (110 loc) • 5.2 kB
text/typescript
import { FilterQuery, IDatabaseDriver } from './IDatabaseDriver';
import { EntityData, EntityMetadata, EntityProperty, IEntity, IEntityType, IPrimaryKey } from '../decorators';
import { MetadataStorage } from '../metadata';
import { Connection, QueryResult } from '../connections';
import { Configuration, Utils } from '../utils';
import { QueryOrder } from '../query';
import { Platform } from '../platforms';
export abstract class DatabaseDriver<C extends Connection> implements IDatabaseDriver<C> {
protected readonly connection: C;
protected readonly platform: Platform;
protected readonly metadata = MetadataStorage.getMetadata();
protected readonly logger = this.config.getLogger();
protected transactionLevel = 0;
protected transactionRolledBack = false;
constructor(protected readonly config: Configuration) { }
abstract async find<T extends IEntity>(entityName: string, where: FilterQuery<T>, populate?: string[], orderBy?: Record<string, QueryOrder>, limit?: number, offset?: number): Promise<T[]>;
abstract async findOne<T extends IEntity>(entityName: string, where: FilterQuery<T> | string, populate: string[], orderBy?: Record<string, QueryOrder>): Promise<T | null>;
abstract async nativeInsert<T extends IEntityType<T>>(entityName: string, data: EntityData<T>): Promise<QueryResult>;
abstract async nativeUpdate<T extends IEntity>(entityName: string, where: FilterQuery<IEntity> | IPrimaryKey, data: EntityData<T>): Promise<QueryResult>;
abstract async nativeDelete<T extends IEntity>(entityName: string, where: FilterQuery<IEntity> | IPrimaryKey): Promise<QueryResult>;
abstract async count<T extends IEntity>(entityName: string, where: FilterQuery<T>): Promise<number>;
async aggregate(entityName: string, pipeline: any[]): Promise<any[]> {
throw new Error(`Aggregations are not supported by ${this.constructor.name} driver`);
}
async loadFromPivotTable<T extends IEntity>(prop: EntityProperty, owners: IPrimaryKey[]): Promise<Record<string, T[]>> {
if (!this.platform.usesPivotTable()) {
throw new Error(`${this.constructor.name} does not use pivot tables`);
}
const fk1 = prop.joinColumn;
const fk2 = prop.inverseJoinColumn;
const pivotTable = prop.owner ? prop.pivotTable : this.metadata[prop.type].properties[prop.mappedBy].pivotTable;
const orderBy = { [`${pivotTable}.${this.metadata[pivotTable].primaryKey}`]: QueryOrder.ASC };
const items = owners.length ? await this.find(prop.type, { [fk1]: { $in: owners } }, [pivotTable], orderBy) : [];
const map: Record<string, T[]> = {};
owners.forEach(owner => map['' + owner] = []);
items.forEach((item: any) => {
map['' + item[fk1]].push(item);
delete item[fk1];
delete item[fk2];
});
return map;
}
mapResult<T extends IEntityType<T>>(result: T, meta: EntityMetadata): T {
if (!result || !meta) {
return result || null;
}
const ret = Object.assign({}, result);
Object.values(meta.properties).forEach(prop => {
if (prop.fieldName && prop.fieldName in ret) {
Utils.renameKey(ret, prop.fieldName, prop.name);
}
if (prop.type === 'boolean') {
ret[prop.name as keyof T] = !!ret[prop.name as keyof T] as T[keyof T];
}
});
return ret;
}
getConnection(): C {
return this.connection as C;
}
async beginTransaction(): Promise<void> {
this.transactionLevel++;
await this.runTransaction('beginTransaction');
}
async commit(): Promise<void> {
if (this.transactionRolledBack) {
throw new Error('Transaction commit failed because the transaction has been marked for rollback only');
}
await this.runTransaction('commit');
this.transactionLevel = Math.max(this.transactionLevel - 1, 0);
}
async rollback(): Promise<void> {
await this.runTransaction('rollback');
if (this.transactionLevel === 1) {
this.transactionRolledBack = false;
} else if (!this.platform.supportsSavePoints()) {
this.transactionRolledBack = true;
}
this.transactionLevel = Math.max(this.transactionLevel - 1, 0);
}
async transactional(cb: () => Promise<any>): Promise<any> {
try {
await this.beginTransaction();
const ret = await cb();
await this.commit();
return ret;
} catch (e) {
await this.rollback();
throw e;
}
}
isInTransaction(): boolean {
return this.transactionLevel > 0;
}
getPlatform(): Platform {
return this.platform;
}
protected getPrimaryKeyField(entityName: string): string {
return this.metadata[entityName] ? this.metadata[entityName].primaryKey : this.config.getNamingStrategy().referenceColumnName();
}
private async runTransaction(method: 'beginTransaction' | 'commit' | 'rollback'): Promise<void> {
if (this.transactionLevel === 1 || this.platform.supportsSavePoints()) {
const useSavepoint = this.transactionLevel !== 1 && this.platform.supportsSavePoints();
await this.connection[method](useSavepoint ? this.getSavePointName() : undefined);
}
}
private getSavePointName(): string {
return `${this.constructor.name}_${this.transactionLevel}`;
}
}