Skip to content

Instantly share code, notes, and snippets.

@renoirb
Created December 3, 2020 14:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save renoirb/45ce98d5865a863bccc56948d83d1a05 to your computer and use it in GitHub Desktop.
Save renoirb/45ce98d5865a863bccc56948d83d1a05 to your computer and use it in GitHub Desktop.
TypeORM Only MySQL database handler
/**
* 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