-
-
Notifications
You must be signed in to change notification settings - Fork 99
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: health indicator for Prisma ORM (#2250)
- Loading branch information
Showing
30 changed files
with
7,479 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
tests/** | ||
lib/**/*.spec.ts | ||
lib/**/*.spec.ts | ||
e2e/prisma/generated/** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ npm-debug.log | |
/test | ||
/coverage | ||
/.nyc_output | ||
e2e/prisma/generated | ||
|
||
# dist | ||
dist | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
e2e/prisma/generated/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.