|  | 
| 1 | 1 | import Debug from 'debug' | 
| 2 |  | -import { DataSource } from 'typeorm' | 
| 3 |  | -import { BaseDataSourceOptions } from 'typeorm/data-source/BaseDataSourceOptions' | 
| 4 |  | -import { DataSourceOptions } from 'typeorm/data-source/DataSourceOptions' | 
|  | 2 | +import {DataSource} from 'typeorm' | 
|  | 3 | +import {BaseDataSourceOptions} from 'typeorm/data-source/BaseDataSourceOptions' | 
| 5 | 4 | 
 | 
| 6 |  | -const debug = Debug(`demo:databaseService`) | 
|  | 5 | +import {DataSourceOptions} from 'typeorm/data-source/DataSourceOptions' | 
|  | 6 | +import {DatabaseType} from "typeorm/driver/types/DatabaseType"; | 
|  | 7 | + | 
|  | 8 | + | 
|  | 9 | +const debug = Debug(`sphereon:ssi-sdk:database`) | 
| 7 | 10 | 
 | 
| 8 | 11 | export class DataSources { | 
| 9 |  | -  private dataSources = new Map<string, DataSource>() | 
| 10 |  | -  private configs | 
|  | 12 | +    get defaultDbType(): SupportedDatabaseType { | 
|  | 13 | +        return this._defaultDbType; | 
|  | 14 | +    } | 
|  | 15 | + | 
|  | 16 | +    set defaultDbType(value: SupportedDatabaseType) { | 
|  | 17 | +        this._defaultDbType = value; | 
|  | 18 | +    } | 
|  | 19 | +    private dataSources = new Map<string, DataSource>() | 
|  | 20 | +    private configs: Map<string, DataSourceOptions> | 
|  | 21 | +    private _defaultDbType: SupportedDatabaseType = 'postgres' | 
| 11 | 22 | 
 | 
| 12 |  | -  private static singleton: DataSources | 
|  | 23 | +    private static singleton: DataSources | 
| 13 | 24 | 
 | 
| 14 |  | -  public static singleInstance() { | 
| 15 |  | -    if (!DataSources.singleton) { | 
| 16 |  | -      DataSources.singleton = new DataSources() | 
|  | 25 | +    public static singleInstance() { | 
|  | 26 | +        if (!DataSources.singleton) { | 
|  | 27 | +            DataSources.singleton = new DataSources() | 
|  | 28 | +        } | 
|  | 29 | +        return DataSources.singleton | 
| 17 | 30 |     } | 
| 18 |  | -    return DataSources.singleton | 
| 19 |  | -  } | 
| 20 | 31 | 
 | 
| 21 |  | -  public static newInstance(configs?: Map<string, DataSourceOptions>) { | 
| 22 |  | -    return new DataSources(configs) | 
| 23 |  | -  } | 
|  | 32 | +    public static newInstance(configs?: Map<string, DataSourceOptions>) { | 
|  | 33 | +        return new DataSources(configs) | 
|  | 34 | +    } | 
| 24 | 35 | 
 | 
| 25 |  | -  private constructor(configs?: Map<string, DataSourceOptions>) { | 
| 26 |  | -    this.configs = configs ?? new Map<string, BaseDataSourceOptions>() | 
| 27 |  | -  } | 
|  | 36 | +    private constructor(configs?: Map<string, DataSourceOptions>) { | 
|  | 37 | +        (configs ?? new Map<string, DataSourceOptions>()).forEach((config, name) => this.addConfig(name, config)) | 
| 28 | 38 | 
 | 
| 29 |  | -  addConfig(dbName: string, config: DataSourceOptions): this { | 
| 30 |  | -    this.configs.set(dbName, config) | 
| 31 |  | -    return this | 
| 32 |  | -  } | 
|  | 39 | +    } | 
| 33 | 40 | 
 | 
| 34 |  | -  deleteConfig(dbName: string): this { | 
| 35 |  | -    this.configs.delete(dbName) | 
| 36 |  | -    return this | 
| 37 |  | -  } | 
|  | 41 | +    addConfig(dbName: string, config: DataSourceOptions): this { | 
|  | 42 | +        this.configs.set(dbName, config) | 
|  | 43 | +        // yes we are aware last one wins | 
|  | 44 | +        this._defaultDbType = config.type | 
|  | 45 | +        return this | 
|  | 46 | +    } | 
| 38 | 47 | 
 | 
| 39 |  | -  getConfig(dbName: string): BaseDataSourceOptions { | 
| 40 |  | -    const config = this.configs.get(dbName) | 
| 41 |  | -    if (!config) { | 
| 42 |  | -      throw Error(`No DB config found for ${dbName}`) | 
|  | 48 | +    deleteConfig(dbName: string): this { | 
|  | 49 | +        this.configs.delete(dbName) | 
|  | 50 | +        return this | 
|  | 51 | +    } | 
|  | 52 | +    has(dbName: string) { | 
|  | 53 | +        return this.configs.has(dbName) && this.dataSources.has(dbName) | 
| 43 | 54 |     } | 
| 44 |  | -    return config | 
| 45 |  | -  } | 
| 46 | 55 | 
 | 
| 47 |  | -  public getDbNames(): string[] { | 
| 48 |  | -    return [...this.configs.keys()] | 
| 49 |  | -  } | 
|  | 56 | +    delete(dbName: string): this { | 
|  | 57 | +        this.deleteConfig(dbName) | 
|  | 58 | +        this.dataSources.delete(dbName) | 
|  | 59 | +        return this | 
|  | 60 | +    } | 
| 50 | 61 | 
 | 
| 51 |  | -  async getDbConnection(dbName: string): Promise<DataSource> { | 
| 52 |  | -    const config = this.getConfig(dbName) | 
| 53 |  | -    /*if (config.synchronize) { | 
| 54 |  | -            return Promise.reject( | 
| 55 |  | -                `WARNING: Automatic migrations need to be disabled in this app! Adjust the database configuration and set synchronize to false` | 
|  | 62 | +    getConfig(dbName: string): BaseDataSourceOptions { | 
|  | 63 | +        const config = this.configs.get(dbName) | 
|  | 64 | +        if (!config) { | 
|  | 65 | +            throw Error(`No DB config found for ${dbName}`) | 
|  | 66 | +        } | 
|  | 67 | +        return config | 
|  | 68 | +    } | 
|  | 69 | + | 
|  | 70 | +    public getDbNames(): string[] { | 
|  | 71 | +        return [...this.configs.keys()] | 
|  | 72 | +    } | 
|  | 73 | + | 
|  | 74 | +    async getDbConnection(dbName: string): Promise<DataSource> { | 
|  | 75 | +        const config = this.getConfig(dbName) | 
|  | 76 | +        if (!this._defaultDbType) { | 
|  | 77 | +            this._defaultDbType = config.type | 
|  | 78 | +        } | 
|  | 79 | +        /*if (config.synchronize) { | 
|  | 80 | +                return Promise.reject( | 
|  | 81 | +                    `WARNING: Automatic migrations need to be disabled in this app! Adjust the database configuration and set synchronize to false` | 
|  | 82 | +                ) | 
|  | 83 | +            }*/ | 
|  | 84 | + | 
|  | 85 | +        let dataSource = this.dataSources.get(dbName) | 
|  | 86 | +        if (dataSource) { | 
|  | 87 | +            return dataSource | 
|  | 88 | +        } | 
|  | 89 | + | 
|  | 90 | + | 
|  | 91 | +        dataSource = await new DataSource({...(config as DataSourceOptions), name: dbName}).initialize() | 
|  | 92 | +        this.dataSources.set(dbName, dataSource) | 
|  | 93 | +        if (config.synchronize) { | 
|  | 94 | +            debug(`WARNING: Automatic migrations need to be disabled in this app! Adjust the database configuration and set synchronize to false`) | 
|  | 95 | +        } else if (config.migrationsRun) { | 
|  | 96 | +            debug( | 
|  | 97 | +                `Migrations are currently managed from config. Please set migrationsRun and synchronize to false to get consistent behaviour. We run migrations from code explicitly`, | 
| 56 | 98 |             ) | 
| 57 |  | -        }*/ | 
|  | 99 | +        } else { | 
|  | 100 | +            debug(`Running ${dataSource.migrations.length} migration(s) from code if needed...`) | 
|  | 101 | +            await dataSource.runMigrations() | 
|  | 102 | +            debug(`${dataSource.migrations.length} migration(s) from code were inspected and applied`) | 
|  | 103 | +        } | 
|  | 104 | +        return dataSource | 
|  | 105 | +    } | 
|  | 106 | +} | 
| 58 | 107 | 
 | 
| 59 |  | -    let dataSource = this.dataSources.get(dbName) | 
| 60 |  | -    if (dataSource) { | 
| 61 |  | -      return dataSource | 
|  | 108 | +export type SupportedDatabaseType = Pick<DatabaseType, 'postgres' & 'sqlite'> | 
|  | 109 | +export type DateTimeType = 'timestamp' | 'datetime' | 
|  | 110 | + | 
|  | 111 | +export type DateType = 'date' | 
|  | 112 | + | 
|  | 113 | + | 
|  | 114 | +export const getDbType = (opts?: { defaultType: SupportedDatabaseType }): SupportedDatabaseType => { | 
|  | 115 | +    const type = (typeof process === 'object' ? process?.env?.DB_TYPE : undefined) ?? DataSources.singleInstance().defaultDbType ?? opts?.defaultType | 
|  | 116 | +    if (!type) { | 
|  | 117 | +        throw Error(`Could not determine DB type. Please set the DB_TYPE global variable or env var to one of 'postgres' or 'sqlite'`) | 
|  | 118 | +    } | 
|  | 119 | +    return type as SupportedDatabaseType | 
|  | 120 | +} | 
|  | 121 | + | 
|  | 122 | + | 
|  | 123 | +const typeOrmDateTime = (opts?: { defaultType: SupportedDatabaseType }): DateTimeType => { | 
|  | 124 | +    switch (getDbType(opts)) { | 
|  | 125 | +        case "postgres": | 
|  | 126 | +            return 'timestamp' | 
|  | 127 | +        case "sqlite": | 
|  | 128 | +            return 'datetime' | 
|  | 129 | +        default: | 
|  | 130 | +            throw Error(`DB type ${getDbType(opts)} not supported`) | 
|  | 131 | +    } | 
|  | 132 | +} | 
|  | 133 | + | 
|  | 134 | +const typeormDate = (opts?: { defaultType: SupportedDatabaseType }): DateType => { | 
|  | 135 | +    // The same for both DBs | 
|  | 136 | +    return 'date' | 
|  | 137 | +} | 
|  | 138 | +export const TYPEORM_DATE_TIME_TYPE = typeOrmDateTime() | 
|  | 139 | + | 
|  | 140 | + | 
|  | 141 | +export const TYPEORM_DATE_TYPE = typeormDate() | 
|  | 142 | + | 
|  | 143 | + | 
|  | 144 | +/** | 
|  | 145 | + * Gets the database connection. | 
|  | 146 | + * | 
|  | 147 | + * Also makes sure that migrations are run (versioning for DB schema's), so we can properly update over time | 
|  | 148 | + * | 
|  | 149 | + * @param connectionName The database name | 
|  | 150 | + * @param opts | 
|  | 151 | + */ | 
|  | 152 | +export const getDbConnection = async (connectionName: string, opts?: { | 
|  | 153 | +    config: BaseDataSourceOptions | any | 
|  | 154 | +}): Promise<DataSource> => { | 
|  | 155 | +    return DataSources.singleInstance().addConfig(connectionName, opts?.config).getDbConnection(connectionName) | 
|  | 156 | +} | 
|  | 157 | + | 
|  | 158 | +export const dropDatabase = async (dbName: string): Promise<void> => { | 
|  | 159 | +    if (!DataSources.singleInstance().has(dbName)) { | 
|  | 160 | +        return Promise.reject(Error(`No database present with name: ${dbName}`)); | 
| 62 | 161 |     } | 
| 63 | 162 | 
 | 
| 64 |  | -    dataSource = await new DataSource({ ...(config as DataSourceOptions), name: dbName }).initialize() | 
| 65 |  | -    this.dataSources.set(dbName, dataSource) | 
| 66 |  | -    if (config.synchronize) { | 
| 67 |  | -      debug(`WARNING: Automatic migrations need to be disabled in this app! Adjust the database configuration and set synchronize to false`) | 
| 68 |  | -    } else if (config.migrationsRun) { | 
| 69 |  | -      debug( | 
| 70 |  | -        `Migrations are currently managed from config. Please set migrationsRun and synchronize to false to get consistent behaviour. We run migrations from code explicitly`, | 
| 71 |  | -      ) | 
|  | 163 | +    const connection: DataSource = await getDbConnection(dbName); | 
|  | 164 | +    await connection.dropDatabase(); | 
|  | 165 | +    DataSources.singleInstance().delete(dbName); | 
|  | 166 | +}; | 
|  | 167 | + | 
|  | 168 | +/** | 
|  | 169 | + * Runs a migration down (drops DB schema) | 
|  | 170 | + * @param dataSource | 
|  | 171 | + */ | 
|  | 172 | +export const revertMigration = async (dataSource: DataSource): Promise<void> => { | 
|  | 173 | +    if (dataSource.isInitialized) { | 
|  | 174 | +        await dataSource.undoLastMigration() | 
| 72 | 175 |     } else { | 
| 73 |  | -      debug(`Running ${dataSource.migrations.length} migration(s) from code if needed...`) | 
| 74 |  | -      await dataSource.runMigrations() | 
| 75 |  | -      debug(`${dataSource.migrations.length} migration(s) from code were inspected and applied`) | 
|  | 176 | +        console.error('DataSource is not initialized') | 
| 76 | 177 |     } | 
| 77 |  | -    return dataSource | 
| 78 |  | -  } | 
| 79 | 178 | } | 
|  | 179 | +export const resetDatabase = async (dbName: string): Promise<void> => { | 
|  | 180 | +    await dropDatabase(dbName); | 
|  | 181 | +    await getDbConnection(dbName); | 
|  | 182 | +}; | 
0 commit comments