Created
December 3, 2020 14:36
-
-
Save renoirb/45ce98d5865a863bccc56948d83d1a05 to your computer and use it in GitHub Desktop.
TypeORM Only MySQL database handler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* EXTRA: | |
* Étant donné que et que la question demande une "solution idiomatique" et | |
* que je crains que je n'ai aucune expérience avec les langages donnés et | |
* que je me sens limité à exprimer avec la bonne syntaxe ma réponse, | |
* j'ai décidé d'ajouter cet extra. | |
* | |
* Je me permet donc ici de fournir une version sommaire d'un gestionnaire de | |
* connection qui permet de supporter plus d'un vendeur. | |
* | |
* J'ai copié et tronqué le code provenant de TypeORM | |
*/ | |
// ---------------------------------------------------------------------------------------- | |
// Un système d'abstraction de base de donnée peux supporter plus d'une vendeur | |
// Limiter les vendeurs possibles | |
export type DatabaseType = 'mysql' | 'mariadb' | |
// Définir statiquement les membres de DatabaseType | |
export const SUPPORTED_DATABASE_TYPE: ReadonlyArray<DatabaseType> = [ | |
'mysql', | |
'mariadb', | |
] as const | |
/** | |
* Mode de fonctionnement de la TableColumn. | |
* C'est de cette façon qu'on peux ajouter des colones auto-remplies | |
* pour "slowly changing dimension" (i.e. date d'invalidité d'une row au lieu de DELETE) | |
*/ | |
export type ColumnMode = 'regular' | 'createDate' | 'updateDate' | 'deleteDate' | |
export type WithPrecisionColumnType = | |
| 'numeric' // postgres, mssql, sqlite, mysql | |
| 'time' // mysql, postgres, mssql, cockroachdb | |
| 'timestamp' // mysql, postgres, mssql, oracle, cockroachdb | |
export type WithLengthColumnType = | |
| 'varchar' // mysql, postgres, mssql, sqlite, cockroachdb | |
| 'char' // mysql, postgres, mssql, oracle, cockroachdb, sap | |
export type WithWidthColumnType = | |
| 'tinyint' // mysql | |
| 'smallint' // mysql | |
| 'mediumint' // mysql | |
| 'int' // mysql | |
| 'bigint' // mysql | |
export type SimpleColumnType = | |
// numeric types | |
| 'float' // mysql, mssql, oracle, sqlite, sap | |
// boolean types | |
| 'boolean' // postgres, sqlite, mysql, cockroachdb | |
| 'blob' // mysql, oracle, sqlite, cockroachdb, sap | |
| 'text' // mysql, postgres, mssql, sqlite, cockroachdb, sap | |
// date types | |
| 'date' // mysql, postgres, mssql, oracle, sqlite | |
| 'year' // mysql | |
/** | |
* Any column type column can be. | |
*/ | |
export type ColumnType = | |
| WithPrecisionColumnType | |
| WithLengthColumnType | |
| WithWidthColumnType | |
| SimpleColumnType | |
| BooleanConstructor | |
| DateConstructor | |
| NumberConstructor | |
| StringConstructor | |
export interface BaseConnectionOptions { | |
readonly type: DatabaseType | |
/** | |
* Connection name. If connection name is not given then it will be called "default". | |
* Different connections must have different names. | |
*/ | |
readonly name?: string | |
} | |
export interface ColumnMetadataOptions { | |
connection: Connection | |
referencedColumn?: ColumnMetadata | |
args: ColumnMetadataArgs | |
} | |
export interface TableColumnOptions { | |
name: string | |
type: string | |
isNullable?: boolean | |
isPrimary?: boolean | |
} | |
export interface ColumnCommonOptions { | |
name?: string | |
nullable?: boolean | |
primary?: boolean | |
unique?: boolean | |
/** | |
* Specifies a value transformer that is to be used to (un)marshal | |
* this column when reading or writing to the database. | |
*/ | |
transformer?: ValueTransformer | ValueTransformer[] | |
} | |
export interface ColumnOptions extends ColumnCommonOptions { | |
type?: ColumnType | |
/** | |
* Specifies a value transformer that is to be used to (un)marshal | |
* this column when reading or writing to the database. | |
*/ | |
transformer?: ValueTransformer | ValueTransformer[] | |
} | |
export interface ColumnMetadataArgs { | |
/** | |
* Class's property name to which column is applied. | |
*/ | |
readonly propertyName: string | |
readonly mode: ColumnMode | |
/** | |
* Extra column options. | |
*/ | |
readonly options: ColumnOptions | |
} | |
export interface ColumnMetadataConstructorOptions { | |
connection: Connection | |
referencedColumn?: ColumnMetadata | |
} | |
export interface Driver { | |
options: BaseConnectionOptions | |
database?: string | |
/** | |
* Performs connection to the database. | |
* Depend on driver type it may create a connection pool. | |
*/ | |
connect(): Promise<void> | |
/** | |
* Closes connection with database and releases all resources. | |
*/ | |
disconnect(): Promise<void> | |
/** | |
* Escapes a table name, column name or an alias. | |
*/ | |
escape(name: string): string | |
/** | |
* Build full table name with database name, schema name and table name. | |
* E.g. "myDB"."mySchema"."myTable" | |
*/ | |
buildTableName(tableName: string, schema?: string, database?: string): string | |
} | |
export interface ValueTransformer<FROM = any, TO = any> { | |
/** | |
* Used to marshal data when writing to the database. | |
*/ | |
to(value: FROM): TO | |
/** | |
* Used to unmarshal data when reading from the database. | |
*/ | |
from(value: TO): FROM | |
} | |
// ---------------------------------------------------------------------------------------- | |
export class MissingDriverError extends Error { | |
name = 'MissingDriverError' | |
constructor(driverType: string) { | |
super() | |
Object.setPrototypeOf(this, MissingDriverError.prototype) | |
const list = SUPPORTED_DATABASE_TYPE.map((n) => `"${n}"`).join(', ') | |
this.message = `Wrong driver: "${driverType}" given. Supported drivers are: ${list}.` | |
} | |
} | |
export class DriverPackageNotInstalledError extends Error { | |
name = 'DriverPackageNotInstalledError' | |
constructor(driverName: string, packageName: string) { | |
super() | |
Object.setPrototypeOf(this, DriverPackageNotInstalledError.prototype) | |
this.message = `${driverName} package has not been found installed. Try to install it: npm install ${packageName} --save` | |
} | |
} | |
export class ConnectionIsNotSetError extends Error { | |
name = 'ConnectionIsNotSetError' | |
constructor(dbType: string) { | |
super() | |
Object.setPrototypeOf(this, ConnectionIsNotSetError.prototype) | |
this.message = `Connection with ${dbType} database is not established. Check connection configuration.` | |
} | |
} | |
// ---------------------------------------------------------------------------------------- | |
export interface MysqlConnectionCredentialsOptions { | |
readonly url?: string | |
readonly host?: string | |
readonly port?: number | |
readonly username?: string | |
readonly password?: string | |
readonly database?: string | |
} | |
/** | |
* MySQL specific connection credential options. | |
* | |
* @see https://github.com/mysqljs/mysql#connection-options | |
*/ | |
export interface MysqlConnectionOptions | |
extends BaseConnectionOptions, | |
MysqlConnectionCredentialsOptions { | |
readonly type: 'mysql' | 'mariadb' | |
} | |
// ---------------------------------------------------------------------------------------- | |
export class TableColumn { | |
name: string | |
type: string | |
isNullable: boolean = false | |
isPrimary: boolean = false | |
constructor(options?: TableColumnOptions) { | |
if (options) { | |
this.name = options.name | |
this.type = options.type || '' | |
this.isNullable = options.isNullable || false | |
this.isPrimary = options.isPrimary || false | |
} | |
} | |
} | |
export class ColumnMetadata { | |
type: ColumnType | |
propertyName: string | |
isPrimary: boolean = false | |
isNullable: boolean = false | |
transformer?: ValueTransformer | ValueTransformer[] | |
referencedColumn?: ColumnMetadata | |
// ... | |
// Toute les options qui permettent de transformer et reverse-transformer (i.e. serialization) | |
// les données d'objet à leur type natifs pour chaque vendeur | |
// ... | |
constructor(options: ColumnMetadataOptions) { | |
this.referencedColumn = options.referencedColumn | |
if (options.args.propertyName) { | |
this.propertyName = options.args.propertyName | |
} | |
if (options.args.options.type) { | |
this.type = options.args.options.type | |
} | |
if (options.args.options.primary) { | |
this.isPrimary = options.args.options.primary | |
} | |
if (options.args.options.nullable !== undefined) { | |
this.isNullable = options.args.options.nullable | |
} | |
if (options.args.options.transformer) { | |
this.transformer = options.args.options.transformer | |
} | |
} | |
} | |
export class Connection { | |
readonly name: string | |
readonly options: BaseConnectionOptions | |
readonly isConnected: boolean | |
readonly driver: Driver | |
constructor(options: BaseConnectionOptions) { | |
this.name = options.name || 'default' | |
this.options = options | |
this.driver = new DriverFactory().create(this) | |
this.isConnected = false | |
} | |
} | |
export abstract class AbstractMysqlDriver implements Driver { | |
connection: Connection | |
pool: any | |
poolCluster: any | |
options: MysqlConnectionOptions | |
/** | |
* Gestionnaire de connexion MySQL abstrait. | |
* | |
* @see https://github.com/mysqljs/mysql | |
*/ | |
protected mysql: any | |
// ... | |
// Mapping des colones du ORM pour chaque type | |
// pour permettre l'ORM d'utiliser un vocabulaire | |
// connu et restreint, mais supporter plusieurs vendeurs. | |
// ... | |
constructor(connection: Connection) { | |
this.connection = connection | |
this.options = { | |
// Fournir ici les options spécifiques par défaut | |
// qui sont sensibles au DBAL driver | |
// e.g. collation, charset, etc. | |
...connection.options, | |
} as MysqlConnectionOptions | |
this._loadDependencies() | |
} | |
// BEGIN: La question de l'examen | |
abstract connect(): Promise<void> | |
abstract disconnect(): Promise<void> | |
// END: La question | |
escape(columnName: string): string { | |
// Column name escaping is different for each Drivers | |
return '`' + columnName + '`' | |
} | |
buildTableName(tableName: string, _?: string, database?: string): string { | |
return database ? `${database}.${tableName}` : tableName | |
} | |
protected _loadDependencies(): void { | |
try { | |
this.mysql = import('mysql') // try to load first supported package | |
} catch (e) { | |
try { | |
this.mysql = import('mysql2') // try to load second supported package | |
} catch (e) { | |
throw new DriverPackageNotInstalledError('Mysql', 'mysql') | |
} | |
} | |
} | |
protected createConnectionOptions( | |
options: MysqlConnectionOptions, | |
credentials: MysqlConnectionCredentialsOptions, | |
): MysqlConnectionOptions { | |
credentials = Object.assign( | |
{}, | |
credentials, | |
// DriverUtils.buildDriverOptions(credentials), | |
) | |
// build connection options for the driver | |
const out: MysqlConnectionOptions = Object.assign( | |
{}, | |
{ | |
type: options.type, | |
host: credentials.host, | |
user: credentials.username, | |
password: credentials.password, | |
database: credentials.database, | |
port: credentials.port, | |
}, | |
) | |
return out | |
} | |
} | |
export class DriverFactory { | |
create(connection: Connection): Driver { | |
const { type } = connection.options | |
switch (type) { | |
case 'mysql': | |
return new ConnexionBd(connection) | |
case 'mariadb': | |
return new ConnexionBd(connection) | |
default: | |
throw new MissingDriverError(type) | |
} | |
} | |
} | |
export class ConnexionBd extends AbstractMysqlDriver implements Driver { | |
async connect(): Promise<void> { | |
this.pool = await this.createPool( | |
this.createConnectionOptions(this.options, this.options), | |
) | |
} | |
disconnect(): Promise<void> { | |
if (!this.poolCluster && !this.pool) | |
return Promise.reject(new ConnectionIsNotSetError('mysql')) | |
if (this.poolCluster) { | |
return new Promise<void>((resolve, reject) => { | |
this.poolCluster.end((err: any) => (err ? reject(err) : resolve())) | |
this.poolCluster = undefined | |
}) | |
} | |
if (this.pool) { | |
return new Promise<void>((resolve, reject) => { | |
this.pool.end((err: any) => { | |
if (err) return reject(err) | |
this.pool = undefined | |
resolve() | |
}) | |
}) | |
} | |
} | |
protected createPool(connectionOptions: any): Promise<any> { | |
const pool = this.mysql.createPool(connectionOptions) | |
return new Promise<void>((resolve, reject) => { | |
pool.getConnection((err: any, connection: any) => { | |
if (err) return pool.end(() => reject(err)) | |
connection.release() | |
resolve(pool) | |
}) | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment