Skip to content

Commit

Permalink
feat(health-indicator): add mikro-orm health indicator
Browse files Browse the repository at this point in the history
Resolves #1877
  • Loading branch information
jbw authored and BrunnerLivio committed Jun 27, 2022
1 parent bb14d7a commit e67d939
Show file tree
Hide file tree
Showing 8 changed files with 403 additions and 14 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@


# [8.1.0-beta.1](https://github.com/nestjs/terminus/compare/8.1.0-beta.0...8.1.0-beta.1) (2022-06-26)

# [8.1.0-beta.0](https://github.com/nestjs/terminus/compare/8.0.8...8.1.0-beta.0) (2022-06-26)


### Features

* **health-indicator:** add mikro-orm health indicator ([aecbe4b](https://github.com/nestjs/terminus/commit/aecbe4b66881b74542279b1ed7a9864db1e0831e))

## [8.0.8](https://github.com/nestjs/terminus/compare/8.0.7...8.0.8) (2022-06-19)

## [8.0.6](https://github.com/nestjs/terminus/compare/8.0.5...8.0.6) (2022-03-16)
Expand Down
53 changes: 53 additions & 0 deletions e2e/health-checks/mikro-orm.health.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { bootstrapTestingModule, DynamicHealthEndpointFn } from '../helper';

describe('MikroOrmHealthIndicator', () => {
let app: INestApplication;
let setHealthEndpoint: DynamicHealthEndpointFn;

beforeEach(
() =>
(setHealthEndpoint =
bootstrapTestingModule().withMikroOrm().setHealthEndpoint),
);

describe('#pingCheck', () => {
it('should check if the mikroOrm is available', async () => {
app = await setHealthEndpoint(({ healthCheck, mikroOrm }) =>
healthCheck.check([async () => mikroOrm.pingCheck('mikroOrm')]),
).start();
const details = { mikroOrm: { status: 'up' } };
return request(app.getHttpServer()).get('/health').expect(200).expect({
status: 'ok',
info: details,
error: {},
details,
});
});

it('should throw an error if runs into timeout error', async () => {
app = await setHealthEndpoint(({ healthCheck, mikroOrm }) =>
healthCheck.check([
async () => mikroOrm.pingCheck('mikroOrm', { timeout: 1 }),
]),
).start();

const details = {
mikroOrm: {
status: 'down',
message: 'timeout of 1ms exceeded',
},
};

return request(app.getHttpServer()).get('/health').expect(503).expect({
status: 'error',
info: {},
error: details,
details,
});
});
});

afterEach(async () => await app.close());
});
23 changes: 23 additions & 0 deletions e2e/helper/bootstrap-testing-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
import { SequelizeModule } from '@nestjs/sequelize';
import { MongooseModule } from '@nestjs/mongoose';
import { HttpModule } from '@nestjs/axios';
import { MikroOrmHealthIndicator } from '../../lib/health-indicator/database/mikro-orm.health';
import { MikroOrmModule } from '@mikro-orm/nestjs';

type TestingHealthFunc = (props: {
healthCheck: HealthCheckService;
Expand All @@ -34,6 +36,7 @@ type TestingHealthFunc = (props: {
mongoose: MongooseHealthIndicator;
sequelize: SequelizeHealthIndicator;
typeorm: TypeOrmHealthIndicator;
mikroOrm: MikroOrmHealthIndicator;
}) => Promise<HealthCheckResult>;

function createHealthController(func: TestingHealthFunc) {
Expand All @@ -48,6 +51,7 @@ function createHealthController(func: TestingHealthFunc) {
private readonly mongoose: MongooseHealthIndicator,
private readonly sequelize: SequelizeHealthIndicator,
private readonly typeorm: TypeOrmHealthIndicator,
private readonly mikroOrm: MikroOrmHealthIndicator,
) {}
// @ts-ignore
@Get('health')
Expand All @@ -61,6 +65,7 @@ function createHealthController(func: TestingHealthFunc) {
mongoose: this.mongoose,
sequelize: this.sequelize,
typeorm: this.typeorm,
mikroOrm: this.mikroOrm,
});
}
}
Expand Down Expand Up @@ -140,6 +145,23 @@ export function bootstrapTestingModule() {
return { setHealthEndpoint };
}

function withMikroOrm() {
imports.push(
MikroOrmModule.forRoot({
type: 'mysql',
host: '0.0.0.0',
port: 3306,
user: 'root',
password: 'root',
dbName: 'test',
discovery: { warnWhenNoEntities: false },
strict: true,
}),
);

return { setHealthEndpoint };
}

function withHttp() {
imports.push(HttpModule);
return { setHealthEndpoint };
Expand All @@ -150,6 +172,7 @@ export function bootstrapTestingModule() {
withTypeOrm,
withSequelize,
withHttp,
withMikroOrm,
setHealthEndpoint,
};
}
141 changes: 141 additions & 0 deletions lib/health-indicator/database/mikro-orm.health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Injectable, NotImplementedException, Scope } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { HealthCheckError } from '../../health-check/health-check.error';

import * as MikroOrmNestJS from '@mikro-orm/core';
import { TimeoutError } from '../../errors';
import {
TimeoutError as PromiseTimeoutError,
promiseTimeout,
checkPackages,
} from '../../utils';
import { HealthIndicator, HealthIndicatorResult } from '..';

export interface MikroOrmPingCheckSettings {
/**
* The connection which the ping check should get executed
*/
connection?: any;
/**
* The amount of time the check should require in ms
*/
timeout?: number;
}

/**
* The MikroOrmHealthIndicator contains health indicators
* which are used for health checks related to MikroOrm
*
* @publicApi
* @module TerminusModule
*/
@Injectable({ scope: Scope.TRANSIENT })
export class MikroOrmHealthIndicator extends HealthIndicator {
/**
* Initializes the MikroOrmHealthIndicator
*
* @param {ModuleRef} moduleRef The NestJS module reference
*/
constructor(private moduleRef: ModuleRef) {
super();
this.checkDependantPackages();
}

/**
* Checks if responds in (default) 1000ms and
* returns a result object corresponding to the result
* @param key The key which will be used for the result object
* @param options The options for the ping
*
* @example
* MikroOrmHealthIndicator.pingCheck('database', { timeout: 1500 });
*/
public async pingCheck(
key: string,
options: MikroOrmPingCheckSettings = {},
): Promise<HealthIndicatorResult> {
this.checkDependantPackages();

const connection = options.connection || this.getContextConnection();
const timeout = options.timeout || 1000;

if (!connection) {
return this.getStatus(key, false);
}

try {
await this.pingDb(connection, timeout);
} catch (error) {
// Check if the error is a timeout error
if (error instanceof PromiseTimeoutError) {
throw new TimeoutError(
timeout,
this.getStatus(key, false, {
message: `timeout of ${timeout}ms exceeded`,
}),
);
}

// Check if the error is a connection not found error
if (error instanceof Error) {
throw new HealthCheckError(
error.message,
this.getStatus(key, false, {
message: error.message,
}),
);
}
}

return this.getStatus(key, true);
}

private checkDependantPackages() {
checkPackages(
['@mikro-orm/nestjs', '@mikro-orm/core'],
this.constructor.name,
);
}

/**
* Returns the connection of the current DI context
*/
private getContextConnection(): MikroOrmNestJS.Connection | null {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { MikroORM } = require('@mikro-orm/core') as typeof MikroOrmNestJS;
const mikro = this.moduleRef.get(MikroORM, { strict: false });

const connection: MikroOrmNestJS.Connection = mikro.em.getConnection();

if (!connection) {
return null;
}
return connection;
}

/**
* Pings a mikro-orm connection
*
* @param connection The connection which the ping should get executed
* @param timeout The timeout how long the ping should maximum take
*
*/
private async pingDb(connection: MikroOrmNestJS.Connection, timeout: number) {
let check: Promise<any>;
const type = connection.getPlatform().getConfig().get('type');

switch (type) {
case 'postgresql':
case 'mysql':
case 'mariadb':
case 'sqlite':
check = connection.execute('SELECT 1');
break;
default:
throw new NotImplementedException(
`${type} ping check is not implemented yet`,
);
}
return await promiseTimeout(timeout, check);
}
}
2 changes: 2 additions & 0 deletions lib/health-indicator/health-indicators.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
HealthIndicator,
GRPCHealthIndicator,
} from '.';
import { MikroOrmHealthIndicator } from './database/mikro-orm.health';

/**
* All the health indicators terminus provides as array
Expand All @@ -24,4 +25,5 @@ export const HEALTH_INDICATORS: Type<HealthIndicator>[] = [
MemoryHealthIndicator,
MicroserviceHealthIndicator,
GRPCHealthIndicator,
MikroOrmHealthIndicator,
];
1 change: 1 addition & 0 deletions lib/health-indicator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './health-indicator';
export * from './http/http.health';
export * from './database/mongoose.health';
export * from './database/typeorm.health';
export * from './database/mikro-orm.health';
export * from './database/sequelize.health';
export * from './microservice/microservice.health';
export * from './microservice/grpc.health';
Expand Down
Loading

0 comments on commit e67d939

Please sign in to comment.