Skip to content

Commit

Permalink
feat: health indicator for Prisma ORM (#2250)
Browse files Browse the repository at this point in the history
  • Loading branch information
ThallesP authored and BrunnerLivio committed Jun 16, 2023
1 parent 6ecdc1f commit 6960af6
Show file tree
Hide file tree
Showing 30 changed files with 7,479 additions and 8 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
tests/**
lib/**/*.spec.ts
lib/**/*.spec.ts
e2e/prisma/generated/**
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ npm-debug.log
/test
/coverage
/.nyc_output
e2e/prisma/generated

# dist
dist
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
e2e/prisma/generated/
133 changes: 133 additions & 0 deletions e2e/health-checks/prisma-orm.health.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { INestApplication } from '@nestjs/common';
import { PrismaClient as MongoPrismaClient } from '../prisma/generated/mongodb';
import { PrismaClient as MySQLPrismaClient } from '../prisma/generated/mysql';
import { bootstrapTestingModule, DynamicHealthEndpointFn } from '../helper';
import * as request from 'supertest';

jest.setTimeout(30000);

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

describe('mongodb', () => {
beforeEach(
() =>
(setHealthEndpoint = bootstrapTestingModule()
.withPrisma()
.andMongo().setHealthEndpoint),
);

describe('#pingCheck', () => {
it('should check if the prisma is available', async () => {
app = await setHealthEndpoint(({ healthCheck, prisma }) =>
healthCheck.check([
async () =>
prisma.pingCheck('prismamongo', new MongoPrismaClient(), {
timeout: 30 * 1000,
}),
]),
).start();

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

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

return request(app.getHttpServer())
.get('/health')
.expect(503)
.expect({
status: 'error',
info: {},
error: {
prismamongo: {
status: 'down',
message: 'timeout of 1ms exceeded',
},
},
details: {
prismamongo: {
status: 'down',
message: 'timeout of 1ms exceeded',
},
},
});
});
});
});

describe('mysql', () => {
beforeEach(
() =>
(setHealthEndpoint = bootstrapTestingModule()
.withPrisma()
.andMySql().setHealthEndpoint),
);

describe('#pingCheck', () => {
it('should check if the prisma is available', async () => {
app = await setHealthEndpoint(({ healthCheck, prisma }) =>
healthCheck.check([
async () =>
prisma.pingCheck('prisma', new MySQLPrismaClient(), {
timeout: 30 * 1000,
}),
]),
).start();

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

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

return request(app.getHttpServer())
.get('/health')
.expect(503)
.expect({
status: 'error',
error: {
prisma: { status: 'down', message: 'timeout of 1ms exceeded' },
},
info: {},
details: {
prisma: { status: 'down', message: 'timeout of 1ms exceeded' },
},
});
});
});
});

afterEach(async () => await app.close());
});
16 changes: 16 additions & 0 deletions e2e/helper/bootstrap-testing-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
MemoryHealthIndicator,
MicroserviceHealthIndicator,
MongooseHealthIndicator,
PrismaORMHealthIndicator,
SequelizeHealthIndicator,
TerminusModule,
TypeOrmHealthIndicator,
Expand All @@ -37,6 +38,7 @@ type TestingHealthFunc = (props: {
sequelize: SequelizeHealthIndicator;
typeorm: TypeOrmHealthIndicator;
mikroOrm: MikroOrmHealthIndicator;
prisma: PrismaORMHealthIndicator;
}) => Promise<HealthCheckResult>;

function createHealthController(func: TestingHealthFunc) {
Expand All @@ -52,6 +54,7 @@ function createHealthController(func: TestingHealthFunc) {
private readonly sequelize: SequelizeHealthIndicator,
private readonly typeorm: TypeOrmHealthIndicator,
private readonly mikroOrm: MikroOrmHealthIndicator,
private readonly prisma: PrismaORMHealthIndicator,
) {}
@Get('health')
health() {
Expand All @@ -65,6 +68,7 @@ function createHealthController(func: TestingHealthFunc) {
sequelize: this.sequelize,
typeorm: this.typeorm,
mikroOrm: this.mikroOrm,
prisma: this.prisma,
});
}
}
Expand Down Expand Up @@ -178,6 +182,17 @@ export function bootstrapTestingModule() {
};
}

function withPrisma() {
return {
andMySql: () => {
return { setHealthEndpoint };
},
andMongo: () => {
return { setHealthEndpoint };
},
};
}

function withHttp() {
imports.push(HttpModule);
return { setHealthEndpoint };
Expand All @@ -188,6 +203,7 @@ export function bootstrapTestingModule() {
withTypeOrm,
withSequelize,
withHttp,
withPrisma,
withMikroOrm,
setHealthEndpoint,
};
Expand Down
11 changes: 11 additions & 0 deletions e2e/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
declare module '*prisma/generated/mongodb' {
class PrismaClient {
$runCommandRaw(command: unknown): any;
}
}

declare module '*prisma/generated/mysql' {
class PrismaClient {
$queryRawUnsafe(query: string): any;
}
}
1 change: 1 addition & 0 deletions e2e/jest-e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"setupFiles": ["<rootDir>/jest.setup.ts"],
"testRegex": "/e2e/.*\\.(e2e-test|e2e-spec).(ts|tsx|js)$",
"collectCoverageFrom": [
"src/**/*.{js,jsx,tsx,ts}",
Expand Down
4 changes: 4 additions & 0 deletions e2e/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { execSync } from 'child_process';

execSync(`npx [email protected] generate --schema e2e/prisma/schema-mysql.prisma`);
execSync(`npx [email protected] generate --schema e2e/prisma/schema-mongodb.prisma`);
16 changes: 16 additions & 0 deletions e2e/prisma/schema-mongodb.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
output = "./generated/mongodb"
}

datasource db {
provider = "mongodb"
url = "mongodb://0.0.0.0:27017/test"
}

model Test {
id String @id @map("_id") @db.ObjectId
}
16 changes: 16 additions & 0 deletions e2e/prisma/schema-mysql.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
output = "./generated/mysql"
}

datasource db {
provider = "mysql"
url = "mysql://root:[email protected]/test"
}

model Test {
id Int @id @default(autoincrement())
}
90 changes: 90 additions & 0 deletions lib/health-indicator/database/prisma-orm.health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
promiseTimeout,
TimeoutError as PromiseTimeoutError,
} from '../../utils';
import { HealthIndicator } from '../health-indicator';
import { TimeoutError } from '../../errors';
import { HealthCheckError } from '../../health-check';
import { NotImplementedException } from '@nestjs/common';

type PingCommandSignature = { [Key in string]?: number };

type PrismaClientDocument = {
$runCommandRaw: (command: PingCommandSignature) => any;
};

type PrismaClientSQL = {
$queryRawUnsafe: (query: string) => any;
};

type ThePrismaClient = PrismaClientDocument | PrismaClientSQL;

export interface PrismaClientPingCheckSettings {
/**
* The amount of time the check should require in ms
*/
timeout?: number;
}

export class PrismaORMHealthIndicator extends HealthIndicator {
constructor() {
super();
}

private async pingDb(
timeout: number,
prismaClientSQLOrMongo: ThePrismaClient,
) {
// The prisma client generates two different typescript types for different databases
// but inside they've the same methods
// But they will fail when using a document method on sql database, that's why we do the try catch down below
const prismaClient = prismaClientSQLOrMongo as PrismaClientSQL &
PrismaClientDocument;

try {
await promiseTimeout(timeout, prismaClient.$runCommandRaw({ ping: 1 }));
} catch (error) {
if (
error instanceof Error &&
error.toString().includes('Use the mongodb provider')
) {
await promiseTimeout(timeout, prismaClient.$queryRawUnsafe('SELECT 1'));
return;
}

throw error;
}
}

public async pingCheck(
key: string,
prismaClient: ThePrismaClient,
options: PrismaClientPingCheckSettings = {},
): Promise<any> {
let isHealthy = false;
const timeout = options.timeout || 1000;

try {
await this.pingDb(timeout, prismaClient);
isHealthy = true;
} catch (error) {
if (error instanceof PromiseTimeoutError) {
throw new TimeoutError(
timeout,
this.getStatus(key, isHealthy, {
message: `timeout of ${timeout}ms exceeded`,
}),
);
}
}

if (isHealthy) {
return this.getStatus(key, isHealthy);
} else {
throw new HealthCheckError(
`${key} is not available`,
this.getStatus(key, isHealthy),
);
}
}
}
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 @@ -10,6 +10,7 @@ import {
MicroserviceHealthIndicator,
HealthIndicator,
GRPCHealthIndicator,
PrismaORMHealthIndicator,
} from '.';
import { MikroOrmHealthIndicator } from './database/mikro-orm.health';

Expand All @@ -26,4 +27,5 @@ export const HEALTH_INDICATORS: Type<HealthIndicator>[] = [
MicroserviceHealthIndicator,
GRPCHealthIndicator,
MikroOrmHealthIndicator,
PrismaORMHealthIndicator,
];
1 change: 1 addition & 0 deletions lib/health-indicator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './database/mongoose.health';
export * from './database/typeorm.health';
export * from './database/mikro-orm.health';
export * from './database/sequelize.health';
export * from './database/prisma-orm.health';
export * from './microservice/microservice.health';
export * from './microservice/grpc.health';
export * from './disk';
Expand Down
Loading

0 comments on commit 6960af6

Please sign in to comment.