diff --git a/package-lock.json b/package-lock.json index 758614bb..60c8e473 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "pascal-case": "^3.1.2", "rapiq": "^0.8.0", "reflect-metadata": "^0.1.13", + "smob": "^1.0.0", "yargs": "^17.7.1" }, "bin": { @@ -5139,11 +5140,6 @@ "smob": "^1.0.0" } }, - "node_modules/ebec/node_modules/smob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.0.0.tgz", - "integrity": "sha512-fnePEPpgGjAdBDk0nV7L9jcStbbcUsKS5TC+RYambCSU9Dm1k2rqDivdg5LBRVWF/NXe0Rq8yfnKKQI08kSXIg==" - }, "node_modules/electron-to-chromium": { "version": "1.3.878", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.878.tgz", @@ -12666,6 +12662,11 @@ "smob": "^0.1.0" } }, + "node_modules/rapiq/node_modules/smob": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-0.1.0.tgz", + "integrity": "sha512-u6ezVF7hN3AxA1onkbMsl46XZr1HYrtMksmckSVpkLI8bYJ5I34kHMvDZk8qNCfA0y54VnhWqCoU6DQMMStzpw==" + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -13493,9 +13494,9 @@ } }, "node_modules/smob": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-0.1.0.tgz", - "integrity": "sha512-u6ezVF7hN3AxA1onkbMsl46XZr1HYrtMksmckSVpkLI8bYJ5I34kHMvDZk8qNCfA0y54VnhWqCoU6DQMMStzpw==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.0.0.tgz", + "integrity": "sha512-fnePEPpgGjAdBDk0nV7L9jcStbbcUsKS5TC+RYambCSU9Dm1k2rqDivdg5LBRVWF/NXe0Rq8yfnKKQI08kSXIg==" }, "node_modules/source-map": { "version": "0.6.1", @@ -18880,13 +18881,6 @@ "integrity": "sha512-tPS/jSTrzChdC/EkxGcHRHvLZYnEbEepqvnOr8PnwsHlOpoKsQbQv8VZf6NmdR1AXyj+6suiq+1AoOXrKtKkTw==", "requires": { "smob": "^1.0.0" - }, - "dependencies": { - "smob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.0.0.tgz", - "integrity": "sha512-fnePEPpgGjAdBDk0nV7L9jcStbbcUsKS5TC+RYambCSU9Dm1k2rqDivdg5LBRVWF/NXe0Rq8yfnKKQI08kSXIg==" - } } }, "electron-to-chromium": { @@ -24388,6 +24382,13 @@ "integrity": "sha512-qAWUHJP++IDKE9ul9mwXoix45a6Hne3wDiqTd7BhoRNVOQv2gQvAlCOWCQCNTbQA/Ia9jW5ugcOmbd4eRGklSQ==", "requires": { "smob": "^0.1.0" + }, + "dependencies": { + "smob": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-0.1.0.tgz", + "integrity": "sha512-u6ezVF7hN3AxA1onkbMsl46XZr1HYrtMksmckSVpkLI8bYJ5I34kHMvDZk8qNCfA0y54VnhWqCoU6DQMMStzpw==" + } } }, "rc": { @@ -25006,9 +25007,9 @@ "dev": true }, "smob": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-0.1.0.tgz", - "integrity": "sha512-u6ezVF7hN3AxA1onkbMsl46XZr1HYrtMksmckSVpkLI8bYJ5I34kHMvDZk8qNCfA0y54VnhWqCoU6DQMMStzpw==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.0.0.tgz", + "integrity": "sha512-fnePEPpgGjAdBDk0nV7L9jcStbbcUsKS5TC+RYambCSU9Dm1k2rqDivdg5LBRVWF/NXe0Rq8yfnKKQI08kSXIg==" }, "source-map": { "version": "0.6.1", diff --git a/package.json b/package.json index cb86052a..9eec84d7 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "pascal-case": "^3.1.2", "rapiq": "^0.8.0", "reflect-metadata": "^0.1.13", + "smob": "^1.0.0", "yargs": "^17.7.1" }, "peerDependencies": { diff --git a/src/data-source/options/module.ts b/src/data-source/options/module.ts index 4939437a..41a03ff0 100644 --- a/src/data-source/options/module.ts +++ b/src/data-source/options/module.ts @@ -2,7 +2,11 @@ import type { DataSourceOptions } from 'typeorm'; import { ConnectionOptionsReader } from 'typeorm'; import type { DataSourceOptionsBuildContext } from './type'; import { setDefaultSeederOptions } from '../../seeder'; -import { adjustFilePathsForDataSourceOptions } from './utils'; +import { + adjustFilePathsForDataSourceOptions, + mergeDataSourceOptionsWithEnv, + readDataSourceOptionsFromEnv, +} from './utils'; import { findDataSource } from '../find'; export async function extendDataSourceOptions( @@ -57,10 +61,23 @@ export async function buildDataSourceOptions( }); if (dataSource) { - return extendDataSourceOptions( + const options = await extendDataSourceOptions( dataSource.options, tsconfigDirectory, ); + + if (context.experimental) { + return mergeDataSourceOptionsWithEnv(options); + } + + return options; + } + + if (context.experimental) { + const options = readDataSourceOptionsFromEnv(); + if (options) { + return options; + } } return buildLegacyDataSourceOptions(context); diff --git a/src/data-source/options/type.ts b/src/data-source/options/type.ts index d5cb4a4f..f133ab6e 100644 --- a/src/data-source/options/type.ts +++ b/src/data-source/options/type.ts @@ -27,5 +27,11 @@ export type DataSourceOptionsBuildContext = { * Directory path to the tsconfig.json file * Default: process.cwd() */ - tsconfigDirectory?: string + tsconfigDirectory?: string, + + /** + * Use experimental features, + * like merging env and file data-source options. + */ + experimental?: boolean }; diff --git a/src/data-source/options/utils/env.ts b/src/data-source/options/utils/env.ts new file mode 100644 index 00000000..6659e190 --- /dev/null +++ b/src/data-source/options/utils/env.ts @@ -0,0 +1,129 @@ +import { merge } from 'smob'; +import type { DataSourceOptions } from 'typeorm'; +import type { BaseDataSourceOptions } from 'typeorm/data-source/BaseDataSourceOptions'; +import type { BetterSqlite3ConnectionOptions } from 'typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions'; +import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; +import type { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions'; +import type { SqlServerConnectionOptions } from 'typeorm/driver/sqlserver/SqlServerConnectionOptions'; +import type { DatabaseType } from 'typeorm/driver/types/DatabaseType'; +import type { LoggerOptions } from 'typeorm/logger/LoggerOptions'; +import { useEnv } from '../../../env'; + +export function hasEnvDataSourceOptions() : boolean { + return !!useEnv('type'); +} + +export function readDataSourceOptionsFromEnv() : DataSourceOptions | undefined { + if (!hasEnvDataSourceOptions()) { + return undefined; + } + + // todo: include seeder options + const base : BaseDataSourceOptions = { + type: useEnv('type') as DatabaseType, + entities: useEnv('entities'), + subscribers: useEnv('subscribers'), + migrations: useEnv('migrations'), + migrationsTableName: useEnv('migrationsTableName'), + // migrationsTransactionMode: useEnv('migra') + metadataTableName: useEnv('metadataTableName'), + logging: useEnv('logging') as LoggerOptions, + logger: useEnv('logger') as BaseDataSourceOptions['logger'], + maxQueryExecutionTime: useEnv('maxQueryExecutionTime'), + synchronize: useEnv('synchronize'), + migrationsRun: useEnv('migrationsRun'), + dropSchema: useEnv('schemaDrop'), + entityPrefix: useEnv('entityPrefix'), + extra: useEnv('extra'), + cache: useEnv('cache'), + }; + + const credentialOptions = { + url: useEnv('url'), + host: useEnv('host'), + port: useEnv('port'), + username: useEnv('username'), + password: useEnv('password'), + database: useEnv('database'), + }; + + if (base.type === 'mysql' || base.type === 'mariadb') { + return { + ...base, + ...credentialOptions, + type: base.type, + }; + } + + if (base.type === 'postgres') { + return { + ...base, + ...credentialOptions, + type: base.type, + schema: useEnv('schema'), + uuidExtension: useEnv('uuidExtension') as PostgresConnectionOptions['uuidExtension'], + }; + } + + if (base.type === 'cockroachdb') { + return { + ...base, + ...credentialOptions, + type: base.type, + schema: useEnv('schema'), + timeTravelQueries: true, + }; + } + + if (base.type === 'sqlite') { + return { + ...base, + type: base.type, + database: useEnv('database'), + } as SqliteConnectionOptions; + } + + if (base.type === 'better-sqlite3') { + return { + ...base, + type: base.type, + database: useEnv('database'), + } as BetterSqlite3ConnectionOptions; + } + + if (base.type === 'mssql') { + return { + ...base, + ...credentialOptions, + type: base.type, + schema: useEnv('schema'), + } as SqlServerConnectionOptions; + } + + if (base.type === 'oracle') { + return { + ...base, + ...credentialOptions, + type: base.type, + sid: useEnv('sid'), + }; + } + + return { + ...base, + ...credentialOptions, + } as DataSourceOptions; +} + +export function mergeDataSourceOptionsWithEnv(options: DataSourceOptions) { + const env = readDataSourceOptionsFromEnv(); + if (!env) { + return options; + } + + if (env.type !== options.type) { + return options; + } + + return merge({}, env, options); +} diff --git a/src/data-source/options/utils.ts b/src/data-source/options/utils/file-path.ts similarity index 91% rename from src/data-source/options/utils.ts rename to src/data-source/options/utils/file-path.ts index 6182b40f..d8435c98 100644 --- a/src/data-source/options/utils.ts +++ b/src/data-source/options/utils/file-path.ts @@ -1,12 +1,12 @@ import type { DataSourceOptions } from 'typeorm'; -import type { SeederOptions } from '../../seeder'; +import type { SeederOptions } from '../../../seeder'; import { CodeTransformation, hasOwnProperty, isCodeTransformation, transformFilePath, -} from '../../utils'; -import { readTsConfig } from '../../utils/tsconfig'; +} from '../../../utils'; +import { readTsConfig } from '../../../utils/tsconfig'; const keys = [ 'entities', diff --git a/src/data-source/options/utils/index.ts b/src/data-source/options/utils/index.ts new file mode 100644 index 00000000..cd51ec12 --- /dev/null +++ b/src/data-source/options/utils/index.ts @@ -0,0 +1,2 @@ +export * from './env'; +export * from './file-path'; diff --git a/src/env/constants.ts b/src/env/constants.ts index 8537ac94..9ad1d4dc 100644 --- a/src/env/constants.ts +++ b/src/env/constants.ts @@ -22,8 +22,8 @@ export enum EnvironmentVariableName { FACTORIES_ALT = 'TYPEORM_SEEDING_FACTORIES', // Database - CONNECTION = 'DB_CONNECTION', - CONNECTION_ALT = 'TYPEORM_CONNECTION', + TYPE = 'DB_TYPE', + TYPE_ALT = 'TYPEORM_CONNECTION', URL = 'DB_URL', URL_ALT = 'TYPEORM_URL', diff --git a/src/env/module.ts b/src/env/module.ts index e29661c5..894ede5f 100644 --- a/src/env/module.ts +++ b/src/env/module.ts @@ -1,3 +1,4 @@ +import type { DatabaseType } from 'typeorm/driver/types/DatabaseType'; import { EnvironmentName, EnvironmentVariableName } from './constants'; import type { Environment } from './type'; import { @@ -115,7 +116,7 @@ export function useEnv(key?: string) : any { EnvironmentVariableName.ENTITY_PREFIX, EnvironmentVariableName.ENTITY_PREFIX_ALT, ]), - maxQueryExecutionTime: readFromProcessEnv([ + maxQueryExecutionTime: readIntFromProcessEnv([ EnvironmentVariableName.MAX_QUERY_EXECUTION_TIME, EnvironmentVariableName.MAX_QUERY_EXECUTION_TIME_ALT, ]), @@ -139,8 +140,8 @@ export function useEnv(key?: string) : any { } let type : string | undefined; - if (hasProcessEnv([EnvironmentVariableName.CONNECTION, EnvironmentVariableName.CONNECTION_ALT])) { - type = readFromProcessEnv([EnvironmentVariableName.CONNECTION, EnvironmentVariableName.CONNECTION_ALT]); + if (hasProcessEnv([EnvironmentVariableName.TYPE, EnvironmentVariableName.TYPE_ALT])) { + type = readFromProcessEnv([EnvironmentVariableName.TYPE, EnvironmentVariableName.TYPE_ALT]); } else if (hasProcessEnv([EnvironmentVariableName.URL, EnvironmentVariableName.URL_ALT])) { const temp = readFromProcessEnv([EnvironmentVariableName.URL, EnvironmentVariableName.URL_ALT]); if (temp) { @@ -152,7 +153,7 @@ export function useEnv(key?: string) : any { } } if (type) { - output.type = type; + output.type = type as DatabaseType; // todo: maybe validation here } instance = output; @@ -163,3 +164,9 @@ export function useEnv(key?: string) : any { return instance; } + +export function resetEnv() { + if (typeof instance !== 'undefined') { + instance = undefined; + } +} diff --git a/src/env/type.ts b/src/env/type.ts index 4fa8b165..600d4134 100644 --- a/src/env/type.ts +++ b/src/env/type.ts @@ -1,3 +1,4 @@ +import type { DatabaseType } from 'typeorm/driver/types/DatabaseType'; import type { DataSourceCacheOption } from '../data-source'; import type { EnvironmentName } from './constants'; @@ -9,7 +10,7 @@ export interface Environment { factories: string[], // DataSource - type?: string, + type?: DatabaseType, url?: string, host?: string, port?: number, @@ -33,7 +34,7 @@ export interface Environment { subscribers: string[], logging: string[] | boolean | string, logger?: string, - maxQueryExecutionTime?: string, + maxQueryExecutionTime?: number, debug?: string, cache?: DataSourceCacheOption, uuidExtension?: string diff --git a/test/unit/data-source-options/env.spec.ts b/test/unit/data-source-options/env.spec.ts new file mode 100644 index 00000000..8baf170e --- /dev/null +++ b/test/unit/data-source-options/env.spec.ts @@ -0,0 +1,75 @@ +import {DataSourceOptions} from "typeorm"; +import { + buildDataSourceOptions, + hasEnvDataSourceOptions, + mergeDataSourceOptionsWithEnv, + readDataSourceOptionsFromEnv +} from "../../../src"; +import {EnvironmentVariableName, resetEnv, } from "../../../src/env"; +import {User} from "../../data/entity/user"; + +describe('src/data-source/options/env', function () { + it('should read env data-source options', () => { + process.env[ + EnvironmentVariableName.URL + ] = 'mysql://admin:start123@localhost:3306'; + + expect(hasEnvDataSourceOptions()).toEqual(true); + + const env = readDataSourceOptionsFromEnv(); + expect(env).toBeDefined(); + if(env) { + expect(env.type).toEqual('mysql'); + if(env.type === 'mysql') { + expect(env.url).toEqual('mysql://admin:start123@localhost:3306'); + } else { + expect(true).toEqual(false); + } + } + + delete process.env[EnvironmentVariableName.URL]; + + resetEnv(); + }); + + it('should merge data-source options', () => { + let options : DataSourceOptions = { + type: 'better-sqlite3', + entities: [User], + database: ':memory:', + extra: { + charset: "UTF8_GENERAL_CI" + } + }; + + process.env = { + ...process.env, + [EnvironmentVariableName.TYPE]: 'better-sqlite3', + [EnvironmentVariableName.DATABASE]: 'db.sqlite', + }; + + options = mergeDataSourceOptionsWithEnv(options); + + expect(options.type).toEqual('better-sqlite3'); + expect(options.database).toEqual('db.sqlite'); + + resetEnv(); + }); + + it('should build data-source options with experimental option', async () => { + process.env = { + ...process.env, + [EnvironmentVariableName.TYPE]: 'better-sqlite3', + [EnvironmentVariableName.DATABASE]: 'db.sqlite', + }; + + const options = await buildDataSourceOptions({ + directory: 'test/data/typeorm', + experimental: true + }); + + expect(options).toBeDefined(); + expect(options.type).toEqual('better-sqlite3'); + expect(options.database).toEqual('db.sqlite'); + }) +}); diff --git a/test/unit/data-source-options/module.spec.ts b/test/unit/data-source-options/module.spec.ts new file mode 100644 index 00000000..1d830bef --- /dev/null +++ b/test/unit/data-source-options/module.spec.ts @@ -0,0 +1,34 @@ +import { + buildDataSourceOptions, + hasDataSource, + hasDataSourceOptions, + setDataSourceOptions, + unsetDataSource, + useDataSource +} from "../../../src"; +import {dataSource} from "../../data/typeorm/data-source"; + +describe('src/data-source/options', function () { + it('should set and use data-source options', async () => { + setDataSourceOptions(dataSource.options); + + expect(hasDataSourceOptions()).toBeTruthy(); + expect(hasDataSource()).toBeFalsy(); + + const instance = await useDataSource(); + expect(instance.options).toEqual(dataSource.options); + + unsetDataSource(); + }); + + it('should build data-source options', async () => { + const options = await buildDataSourceOptions({ + directory: 'test/data/typeorm' + }); + + expect(options).toBeDefined(); + expect(options.type).toEqual('better-sqlite3'); + expect(options.database).toBeDefined(); + expect(options.extra).toBeDefined(); + }) +}); diff --git a/test/unit/data-source/singleton.ts b/test/unit/data-source/singleton.ts index 3479af5e..a8c88569 100644 --- a/test/unit/data-source/singleton.ts +++ b/test/unit/data-source/singleton.ts @@ -1,9 +1,7 @@ import {dataSource} from "../../data/typeorm/data-source"; import { hasDataSource, - hasDataSourceOptions, setDataSource, - setDataSourceOptions, unsetDataSource, useDataSource } from "../../../src"; @@ -38,16 +36,4 @@ describe('src/data-source/singleton.ts', () => { unsetDataSource('foo'); expect(hasDataSource('foo')).toBeFalsy(); }) - - it('should set and use data-source options', async () => { - setDataSourceOptions(dataSource.options); - - expect(hasDataSourceOptions()).toBeTruthy(); - expect(hasDataSource()).toBeFalsy(); - - const instance = await useDataSource(); - expect(instance.options).toEqual(dataSource.options); - - unsetDataSource(); - }) })