Skip to content

Commit 876d6da

Browse files
authored
Merge pull request #2 from DouglasNeuroInformatics/prisma-module
2 parents a4b370e + 6bd4df9 commit 876d6da

11 files changed

+259
-0
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"scripts": {
3535
"build": "tsc",
3636
"build:docs": "typedoc",
37+
"db:generate": "prisma generate",
3738
"format": "prettier --write src",
3839
"lint": "tsc --noEmit && eslint --fix src",
3940
"prepare": "husky",
@@ -45,6 +46,7 @@
4546
"@nestjs/core": "^10.2.3",
4647
"@nestjs/platform-express": "^10.2.5",
4748
"@nestjs/testing": "^10.2.5",
49+
"@prisma/client": "^5.21.1",
4850
"express": "^4.18.2",
4951
"mongodb": "^6.3.0",
5052
"reflect-metadata": "^0.1.13",
@@ -76,6 +78,7 @@
7678
"eslint": "^8.52.0",
7779
"husky": "^9.0.11",
7880
"prettier": "^3.2.5",
81+
"prisma": "^5.19.1",
7982
"typedoc": "^0.26.4",
8083
"typescript": "~5.5.3",
8184
"unplugin-swc": "^1.4.4",

pnpm-lock.yaml

Lines changed: 73 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prisma/schema.prisma

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
generator client {
2+
provider = "prisma-client-js"
3+
binaryTargets = ["native"]
4+
}
5+
6+
datasource db {
7+
provider = "mongodb"
8+
url = env("_")
9+
}
10+
11+
model User {
12+
id String @id @default(auto()) @map("_id") @db.ObjectId
13+
email String @unique
14+
name String?
15+
}

src/modules/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,10 @@ export * from './crypto/crypto.config.js';
22
export * from './crypto/crypto.module.js';
33
export * from './crypto/crypto.service.js';
44
export * from './logging/logging.module.js';
5+
export * from './prisma/prisma.config.js';
6+
export * from './prisma/prisma.decorators.js';
7+
export * from './prisma/prisma.factory.js';
8+
export * from './prisma/prisma.module.js';
9+
export * from './prisma/prisma.service.js';
10+
export * from './prisma/prisma.types.js';
11+
export * from './prisma/prisma.utils.js';

src/modules/prisma/prisma.config.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ConfigurableModuleBuilder } from '@nestjs/common';
2+
import type { Prisma } from '@prisma/client';
3+
4+
type PrismaModuleOptions = Prisma.PrismaClientOptions;
5+
6+
const { ASYNC_OPTIONS_TYPE, ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
7+
new ConfigurableModuleBuilder<PrismaModuleOptions>({
8+
moduleName: 'Prisma'
9+
})
10+
.setClassMethodName('forRoot')
11+
.setExtras({}, (definition) => ({
12+
...definition,
13+
global: true
14+
}))
15+
.build();
16+
17+
export {
18+
ASYNC_OPTIONS_TYPE as PRISMA_ASYNC_OPTIONS_TYPE,
19+
ConfigurableModuleClass as ConfigurablePrismaModule,
20+
MODULE_OPTIONS_TOKEN as PRISMA_MODULE_OPTIONS_TOKEN,
21+
OPTIONS_TYPE as PRISMA_OPTIONS_TYPE
22+
};
23+
export type { PrismaModuleOptions };
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Inject } from '@nestjs/common';
2+
import type { Prisma } from '@prisma/client';
3+
4+
import { getModelToken } from './prisma.utils.js';
5+
6+
export const InjectModel = <T extends Prisma.ModelName>(modelName: T): ParameterDecorator & PropertyDecorator => {
7+
return Inject(getModelToken(modelName));
8+
};

src/modules/prisma/prisma.factory.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { Prisma, PrismaClient } from '@prisma/client';
3+
4+
export const PRISMA_CLIENT_TOKEN = 'PRISMA_CLIENT';
5+
6+
@Injectable()
7+
export class PrismaFactory {
8+
static createClient(options: Prisma.PrismaClientOptions) {
9+
return new PrismaClient(options).$extends({
10+
model: {
11+
$allModels: {
12+
async exists<T>(this: T, where: Prisma.Args<T, 'findFirst'>['where']): Promise<boolean> {
13+
const context = Prisma.getExtensionContext(this);
14+
const result = await (context as PrismaClient[Lowercase<Prisma.ModelName>]).findFirst({ where });
15+
return result !== null;
16+
}
17+
}
18+
}
19+
});
20+
}
21+
}

src/modules/prisma/prisma.module.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { type DynamicModule, Module } from '@nestjs/common';
2+
import type { Prisma } from '@prisma/client';
3+
4+
import {
5+
ConfigurablePrismaModule,
6+
PRISMA_ASYNC_OPTIONS_TYPE,
7+
PRISMA_MODULE_OPTIONS_TOKEN,
8+
PRISMA_OPTIONS_TYPE,
9+
type PrismaModuleOptions
10+
} from './prisma.config.js';
11+
import { PRISMA_CLIENT_TOKEN, PrismaFactory } from './prisma.factory.js';
12+
import { PrismaService } from './prisma.service.js';
13+
import { getModelToken } from './prisma.utils.js';
14+
15+
import type { ExtendedPrismaClient } from './prisma.types.js';
16+
17+
@Module({})
18+
export class PrismaModule extends ConfigurablePrismaModule {
19+
static forFeature<T extends Prisma.ModelName>(modelName: T): DynamicModule {
20+
const modelToken = getModelToken(modelName);
21+
return {
22+
exports: [modelToken],
23+
module: PrismaModule,
24+
providers: [
25+
{
26+
inject: [PRISMA_CLIENT_TOKEN],
27+
provide: modelToken,
28+
useFactory: (client: ExtendedPrismaClient) => {
29+
return client[modelName.toLowerCase() as Lowercase<T>];
30+
}
31+
}
32+
]
33+
};
34+
}
35+
36+
static forRoot(options: typeof PRISMA_OPTIONS_TYPE) {
37+
return this.createModule(super.forRoot(options));
38+
}
39+
40+
static forRootAsync(options: typeof PRISMA_ASYNC_OPTIONS_TYPE) {
41+
return this.createModule(super.forRootAsync(options));
42+
}
43+
44+
private static createModule({ exports = [], providers = [], ...base }: DynamicModule): DynamicModule {
45+
return {
46+
...base,
47+
exports: [...exports, PRISMA_CLIENT_TOKEN, PrismaService],
48+
providers: [
49+
...providers,
50+
PrismaService,
51+
{
52+
inject: [PRISMA_MODULE_OPTIONS_TOKEN],
53+
provide: PRISMA_CLIENT_TOKEN,
54+
useFactory: (options: PrismaModuleOptions) => {
55+
return PrismaFactory.createClient(options);
56+
}
57+
}
58+
]
59+
};
60+
}
61+
}

src/modules/prisma/prisma.service.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
2+
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
3+
4+
import { PRISMA_CLIENT_TOKEN } from './prisma.factory.js';
5+
6+
import type { ExtendedPrismaClient } from './prisma.types.js';
7+
8+
@Injectable()
9+
export class PrismaService implements OnModuleInit, OnApplicationShutdown {
10+
constructor(@Inject(PRISMA_CLIENT_TOKEN) public readonly client: ExtendedPrismaClient) {}
11+
12+
async dropDatabase() {
13+
const result = await this.client.$runCommandRaw({ dropDatabase: 1 });
14+
if (result.ok !== 1) {
15+
throw new InternalServerErrorException('Failed to drop database: raw mongodb command returned unexpected value', {
16+
cause: result
17+
});
18+
}
19+
}
20+
21+
async getDbStats() {
22+
return (await this.client.$runCommandRaw({ dbStats: 1 })) as {
23+
collections: number;
24+
db: string;
25+
objects: number;
26+
};
27+
}
28+
29+
async onApplicationShutdown() {
30+
await this.client.$disconnect();
31+
}
32+
33+
async onModuleInit() {
34+
await this.client.$connect();
35+
}
36+
}

src/modules/prisma/prisma.types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Prisma } from '@prisma/client';
2+
3+
import type { PrismaFactory } from './prisma.factory.js';
4+
5+
export type ExtendedPrismaClient = ReturnType<(typeof PrismaFactory)['createClient']>;
6+
7+
export type Model<T extends Prisma.ModelName> = ExtendedPrismaClient[`${Uncapitalize<T>}`];

src/modules/prisma/prisma.utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { Prisma } from '@prisma/client';
2+
3+
export function getModelToken<T extends Prisma.ModelName>(modelName: T) {
4+
return modelName + 'PrismaModel';
5+
}

0 commit comments

Comments
 (0)