Skip to content

Commit

Permalink
Merge branch 'master' into #59
Browse files Browse the repository at this point in the history
  • Loading branch information
regevbr committed Apr 27, 2020
2 parents 39a6c1b + abbe040 commit 62e935f
Show file tree
Hide file tree
Showing 23 changed files with 288 additions and 101 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -72,3 +72,4 @@ typings/
.stryker-tmp
/reports
!@types/**
!/tools
1 change: 1 addition & 0 deletions .npmignore
Expand Up @@ -99,3 +99,4 @@ tools/
/stryker.conf.js
/.stryker-tmp/
/reports/
tools/
2 changes: 1 addition & 1 deletion jest.config.js
Expand Up @@ -6,7 +6,7 @@ const config = {
},
},
testEnvironment: 'node',
collectCoverageFrom: ['<rootDir>/src/**/*.ts', '!<rootDir>/src/**/*.d.ts'],
collectCoverageFrom: ['<rootDir>/src/**/*.ts', '!<rootDir>/src/**/*.d.ts', '!<rootDir>/src/db/migrations/**'],
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
coverageDirectory: 'coverage',
coverageReporters: ['text-summary', 'lcov'],
Expand Down
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -43,7 +43,7 @@
"envcheck": "node -e \"if(!/yarn\\.js$/.test(process.env.npm_execpath))throw new Error('Use yarn')\"",
"preinstall": "yarn run envcheck",
"postinstall": "yarn run compile",
"versionupdate": "yarn run ncu && rm yarn.lock && yarn install",
"versionupdate": "yarn run ncu --error-level 2 || (yarn run ncu && rm yarn.lock && yarn install)",
"clean:dist": "rm -rf dist && rm -f .buildcache",
"prebuild": "yarn run lint && yarn run clean:dist",
"build": "yarn run tsc -p tsconfig.build.json",
Expand Down Expand Up @@ -72,7 +72,8 @@
"format:fix": "yarn run prettier --write \"./**/*.{ts,js,json,md}\"",
"format:check": "yarn run prettier --list-different \"./**/*.{ts,js,json,md}\"",
"check:spelling": "cspell --config=.cspell.json \"**/*.{md,ts,js}\"",
"generate-contributors": "yarn run ts-node --transpile-only ./tools/generate-contributors.ts && yarn run all-contributors generate",
"generate-contributors": "yarn run ts-node --transpile-only ./tools/generateContributors.ts && yarn run all-contributors generate",
"generate-migrations": "yarn run ts-node node_modules/.bin/typeorm migration:generate --config tools/getOrmConnection.ts --name autoGeneratedMigration",
"check-clean-workspace-after-install": "git diff --quiet --exit-code",
"docs:changelog": "yarn run changelog --allow-unknown",
"mutation-test": "yarn run stryker run"
Expand Down
49 changes: 49 additions & 0 deletions src/db/impl/connectionOptionsProvider.ts
@@ -0,0 +1,49 @@
import { injectable, multiInject } from 'inversify';
import * as path from 'path';
import { memoize } from '../../utils/memoize/memoize';
import { IConnectionSettings } from '../interfaces/IConnectionSettings';
import { IEntity, IEntityConstructor } from '../interfaces/IEntity';
import { ConnectionOptions } from 'typeorm/connection/ConnectionOptions';
import { IConnectionOptionsProvider } from '../interfaces/IConnectionOptionsProvider';

const DB_FILE = `cache.db`;
const MIGRATIONS_DIR = path.join(__dirname, `..`, `migrations`);
const MIGRATIONS_DIR_RELATIVE = MIGRATIONS_DIR.slice(process.cwd().length + 1);
const MIGRATIONS_FILES = path.join(MIGRATIONS_DIR, `**`, `!(*.d).[jt]s`);

@injectable()
export class ConnectionOptionsProvider extends IConnectionOptionsProvider {
constructor(
private readonly settings: IConnectionSettings,
@multiInject(IEntity) private readonly entities: IEntityConstructor[]
) {
super();
}

@memoize()
public getConnectionOptions(): ConnectionOptions {
const connectionOptions: ConnectionOptions = {
name: this.settings.migrationGenerationConfig ? `default` : this.settings.databaseFilePath,
type: `sqlite`,
database: path.join(this.settings.databaseFilePath, DB_FILE),
synchronize: false,
migrations: [MIGRATIONS_FILES],
logging: false,
dropSchema: false,
entities: this.entities,
cli: {
migrationsDir: MIGRATIONS_DIR_RELATIVE,
},
};
Object.defineProperty(connectionOptions, `migrationsRun`, {
get: (): boolean => {
return true;
},
set: (): void => {
// Do nothing as we want migration generation to build incremental queries
},
enumerable: true,
});
return connectionOptions;
}
}
31 changes: 13 additions & 18 deletions src/db/impl/connectionProvider.ts
@@ -1,25 +1,19 @@
import { inject, injectable, multiInject } from 'inversify';
import { inject, injectable } from 'inversify';
import { Connection } from 'typeorm/connection/Connection';
import * as path from 'path';
import { memoize } from '../../utils/memoize/memoize';
import { IConnectionProvider } from '../interfaces/IConnectionProvider';
import { IConnectionSettings } from '../interfaces/IConnectionSettings';
import { TypeOrm, TYPES } from '../../container/nodeModulesContainer';
import { IEntity, IEntityConstructor } from '../interfaces/IEntity';
import { ILoggerFactory } from '../../utils/logger';
import { ILogger } from '../../utils/logger/interfaces/ILogger';
import { ConnectionOptions } from 'typeorm/connection/ConnectionOptions';

const DB_FILE = `cache.db`;
import { IConnectionOptionsProvider } from '../interfaces/IConnectionOptionsProvider';

@injectable()
export class ConnectionProvider extends IConnectionProvider {
private readonly logger: ILogger;

constructor(
private readonly settings: IConnectionSettings,
@inject(TYPES.TypeOrm) private readonly typeorm: TypeOrm,
@multiInject(IEntity) private readonly entities: IEntityConstructor[],
private readonly connectionOptionsProvider: IConnectionOptionsProvider,
loggerFactory: ILoggerFactory
) {
super();
Expand All @@ -28,18 +22,19 @@ export class ConnectionProvider extends IConnectionProvider {

@memoize()
public async getConnection(): Promise<Connection> {
const connectionOptions: ConnectionOptions = {
name: this.settings.databaseFilePath,
type: `sqlite`,
database: path.join(this.settings.databaseFilePath, DB_FILE),
synchronize: true,
logging: false,
dropSchema: this.settings.dropSchema,
entities: this.entities,
};
const connectionOptions = this.connectionOptionsProvider.getConnectionOptions();
this.logger.debug(`Creating connection of type ${connectionOptions.type} to db ${connectionOptions.database}`);
const connection = await this.typeorm.createConnection(connectionOptions);
this.logger.debug(`Connection created successfully`);
const isOutOfSync = await this.isOutOfSync(connection);
if (isOutOfSync) {
throw new Error(`DB schema is out of sync`);
}
return connection;
}

private async isOutOfSync(connection: Connection): Promise<boolean> {
const sqlInMemory = await connection.driver.createSchemaBuilder().log();
return sqlInMemory.upQueries.length > 0;
}
}
4 changes: 4 additions & 0 deletions src/db/index.ts
Expand Up @@ -10,6 +10,8 @@ import { IEntity } from './interfaces/IEntity';
import { Dependency } from './entities/dependency';
import { DependencyVersion } from './entities/dependencyVersion';
import { namedOrMultiConstraint } from '../container/utils';
import { IConnectionOptionsProvider } from './interfaces/IConnectionOptionsProvider';
import { ConnectionOptionsProvider } from './impl/connectionOptionsProvider';

export const EntitiesTags = {
dependencyVersion: DependencyVersion.TAG,
Expand All @@ -23,6 +25,7 @@ export const dbModulesBinder = (bind: Bind): void => {
bind<IDependencyVersionRepositoryProvider>(IDependencyVersionRepositoryProvider)
.to(DependencyVersionRepositoryProvider)
.inSingletonScope();
bind<IConnectionOptionsProvider>(IConnectionOptionsProvider).to(ConnectionOptionsProvider).inSingletonScope();
bind<IConnectionProvider>(IConnectionProvider).to(ConnectionProvider).inSingletonScope();
bind<IEntity>(IEntity).toConstantValue(Dependency).when(namedOrMultiConstraint(EntitiesTags.dependency, IEntity));
bind<IEntity>(IEntity)
Expand All @@ -34,5 +37,6 @@ export * from './interfaces/IConnectionProvider';
export * from './interfaces/IDependencyRepositoryProvider';
export * from './interfaces/IDependencyVersionRepositoryProvider';
export * from './interfaces/IConnectionSettings';
export * from './interfaces/IConnectionOptionsProvider';
export * from './entities/dependency';
export * from './entities/dependencyVersion';
5 changes: 5 additions & 0 deletions src/db/interfaces/IConnectionOptionsProvider.ts
@@ -0,0 +1,5 @@
import { ConnectionOptions } from 'typeorm';

export abstract class IConnectionOptionsProvider {
public abstract getConnectionOptions(): ConnectionOptions;
}
2 changes: 1 addition & 1 deletion src/db/interfaces/IConnectionSettings.ts
@@ -1,4 +1,4 @@
export abstract class IConnectionSettings {
public abstract readonly databaseFilePath: string;
public abstract readonly dropSchema: boolean;
public abstract readonly migrationGenerationConfig?: boolean;
}
22 changes: 22 additions & 0 deletions src/db/migrations/1587733162051-autoGeneratedMigration.ts
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

// eslint-disable-next-line @typescript-eslint/class-name-casing
export class autoGeneratedMigration1587733162051 implements MigrationInterface {
name = `autoGeneratedMigration1587733162051`;

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "dependency" ("name" text NOT NULL, "version" text NOT NULL, "targetNode" text NOT NULL, "match" boolean, "reason" text, PRIMARY KEY ("name", "version", "targetNode"))`,
undefined
);
await queryRunner.query(
`CREATE TABLE "dependency_version" ("name" text NOT NULL, "version" text NOT NULL, "repoUrl" text NOT NULL, "releaseDate" text NOT NULL, "commitSha" text, PRIMARY KEY ("name", "version"))`,
undefined
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "dependency_version"`, undefined);
await queryRunner.query(`DROP TABLE "dependency"`, undefined);
}
}
56 changes: 27 additions & 29 deletions test/common/testers/repositoryProviderTester.ts
Expand Up @@ -11,37 +11,35 @@ export const testRepositoryProvider = <E extends IEntity, P extends IRepositoryP
Entity: new () => E,
RepositoryProvider: new (connectionProvider: IConnectionProvider) => P
): void => {
describe(`${Entity} repository provider`, () => {
const repositoryMock = mock<Repository<E>>();
const connectionMock = mock<Connection>();
connectionMock.getRepository.mockReturnValue(repositoryMock);
const connectionProviderMock = mock<IConnectionProvider>();
connectionProviderMock.getConnection.mockResolvedValue(connectionMock);
let dependencyRepositoryProvider: IRepositoryProvider<E>;
const repositoryMock = mock<Repository<E>>();
const connectionMock = mock<Connection>();
connectionMock.getRepository.mockReturnValue(repositoryMock);
const connectionProviderMock = mock<IConnectionProvider>();
connectionProviderMock.getConnection.mockResolvedValue(connectionMock);
let dependencyRepositoryProvider: IRepositoryProvider<E>;

beforeEach(() => {
mockClear(connectionProviderMock);
mockClear(connectionMock);
mockClear(repositoryMock);
dependencyRepositoryProvider = new RepositoryProvider(connectionProviderMock);
});
beforeEach(() => {
mockClear(connectionProviderMock);
mockClear(connectionMock);
mockClear(repositoryMock);
dependencyRepositoryProvider = new RepositoryProvider(connectionProviderMock);
});

it(`should cache repo`, async () => {
const repo = await dependencyRepositoryProvider.getRepository();
const repo2 = await dependencyRepositoryProvider.getRepository();
expect(repo).toBe(repo2);
expect(repo).toBe(repositoryMock);
expect(connectionMock.getRepository).toBeCalledTimes(1);
expect(connectionProviderMock.getConnection).toBeCalledTimes(1);
});
it(`should cache repo`, async () => {
const repo = await dependencyRepositoryProvider.getRepository();
const repo2 = await dependencyRepositoryProvider.getRepository();
expect(repo).toBe(repo2);
expect(repo).toBe(repositoryMock);
expect(connectionMock.getRepository).toBeCalledTimes(1);
expect(connectionProviderMock.getConnection).toBeCalledTimes(1);
});

it(`should use connection properly`, async () => {
const repo = await dependencyRepositoryProvider.getRepository();
expect(repo).toBe(repositoryMock);
expect(connectionProviderMock.getConnection).toBeCalledTimes(1);
expect(connectionMock.getRepository).toBeCalledTimes(1);
expect(connectionProviderMock.getConnection).toHaveBeenCalledWith();
expect(connectionMock.getRepository).toHaveBeenCalledWith(Entity);
});
it(`should use connection properly`, async () => {
const repo = await dependencyRepositoryProvider.getRepository();
expect(repo).toBe(repositoryMock);
expect(connectionProviderMock.getConnection).toBeCalledTimes(1);
expect(connectionMock.getRepository).toBeCalledTimes(1);
expect(connectionProviderMock.getConnection).toHaveBeenCalledWith();
expect(connectionMock.getRepository).toHaveBeenCalledWith(Entity);
});
};
81 changes: 81 additions & 0 deletions test/src/db/impl/connectionOptionsProvider.spec.ts
@@ -0,0 +1,81 @@
import * as path from 'path';
import { Dependency, IConnectionSettings } from '../../../../src/db';
import * as tmp from 'tmp';
import { normalize, join } from 'path';
import { ConnectionOptionsProvider } from '../../../../src/db/impl/connectionOptionsProvider';

describe(`connection options provider`, () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = tmp.dirSync().name;
});

it(`should not be able to change the migrationsRun property`, async () => {
const settings: IConnectionSettings = {
databaseFilePath: tmpDir,
};
const connectionOptionsProvider = new ConnectionOptionsProvider(settings, [Dependency]);
const options = connectionOptionsProvider.getConnectionOptions();
expect(options.migrationsRun).toBe(true);
Object.assign(options, {
migrationsRun: false,
});
expect(options.migrationsRun).toBe(true);
});

it(`should generate options properly`, async () => {
const settings: IConnectionSettings = {
databaseFilePath: tmpDir,
};
const connectionOptionsProvider = new ConnectionOptionsProvider(settings, [Dependency]);
const options = connectionOptionsProvider.getConnectionOptions();
expect(options).toEqual({
name: tmpDir,
type: `sqlite`,
database: path.join(tmpDir, `cache.db`),
synchronize: false,
migrationsRun: true,
migrations: [normalize(join(__dirname, `../../../../src/db/migrations/**/!(*.d).[jt]s`))],
cli: {
migrationsDir: normalize(`src/db/migrations`),
},
logging: false,
dropSchema: false,
entities: [Dependency],
});
});

it(`should generate options properly for migration`, async () => {
const settings: IConnectionSettings = {
databaseFilePath: tmpDir,
migrationGenerationConfig: true,
};
const connectionOptionsProvider = new ConnectionOptionsProvider(settings, [Dependency]);
const options = connectionOptionsProvider.getConnectionOptions();
expect(options).toEqual({
name: `default`,
type: `sqlite`,
database: path.join(tmpDir, `cache.db`),
synchronize: false,
migrationsRun: true,
migrations: [normalize(join(__dirname, `../../../../src/db/migrations/**/!(*.d).[jt]s`))],
cli: {
migrationsDir: normalize(`src/db/migrations`),
},
logging: false,
dropSchema: false,
entities: [Dependency],
});
});

it(`should cache connection options`, async () => {
const settings: IConnectionSettings = {
databaseFilePath: tmpDir,
};
const connectionOptionsProvider = new ConnectionOptionsProvider(settings, []);
const options = connectionOptionsProvider.getConnectionOptions();
const options2 = connectionOptionsProvider.getConnectionOptions();
expect(options).toBe(options2);
});
});

0 comments on commit 62e935f

Please sign in to comment.