Skip to content

Commit

Permalink
feat(prisma): ✨ add @bit-ocean/prisma
Browse files Browse the repository at this point in the history
  • Loading branch information
recallwei committed Jul 23, 2024
1 parent 3f93dba commit 76869f8
Show file tree
Hide file tree
Showing 21 changed files with 838 additions and 0 deletions.
Empty file removed packages/prisma/.gitkeep
Empty file.
16 changes: 16 additions & 0 deletions packages/prisma/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# @bit-ocean/prisma

![npm](https://img.shields.io/npm/v/@bit-ocean/prisma?logo=prisma&label=prisma)
[![Made with Prisma](http://made-with.prisma.io/dark.svg)](https://prisma.io)

> Universal Prisma module.
## Installation

```bash
pnpm add @bit-ocean/prisma
```

## License

[MIT](/LICENSE) License © 2024 Bit Ocean
66 changes: 66 additions & 0 deletions packages/prisma/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "@bit-ocean/prisma",
"version": "0.0.0",
"description": "Universal Prisma module.",
"author": "Bruce Song <[email protected]> (https://github.com/recallwei/)",
"homepage": "https://github.com/bit-ocean-studio/infra#readme",
"bugs": "https://github.com/bit-ocean-studio/infra/issues",
"repository": {
"type": "git",
"url": "https://github.com/bit-ocean-studio/infra.git",
"directory": "packages/prisma"
},
"keywords": [
"bit-ocean",
"bit-ocean-infra",
"prisma",
"prisma-module",
"nest-prisma",
"nest-prisma-module"
],
"files": [
"dist"
],
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"prebuild": "pnpm prisma generate",
"type:check": "tsc --pretty --noEmit",
"cspell:check": "cspell --no-progress --show-suggestions --show-context --cache **",
"eslint:check": "eslint . --color --cache",
"eslint:fix": "eslint . --color --cache --fix",
"prettier:check": "prettier --check --cache --ignore-unknown --ignore-path=../../.prettierignore .",
"prettier:fix": "prettier --write --cache --ignore-unknown --ignore-path=../../.prettierignore ."
},
"peerDependencies": {
"@nestjs/common": "^9.0.0 || ^10.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0",
"@prisma/client": "^5.0.0",
"prisma": "^5.0.0",
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
"@nestjs/common": "^10.3.10",
"@nestjs/core": "^10.3.10",
"@prisma/client": "^5.17.0",
"prisma": "^5.17.0",
"reflect-metadata": "^0.2.2"
},
"prisma": {
"schema": "prisma/schema.prisma"
},
"publishConfig": {
"access": "public"
},
"license": "MIT"
}
13 changes: 13 additions & 0 deletions packages/prisma/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
}

model User {
id Int @id @default(autoincrement())
name String?
}
7 changes: 7 additions & 0 deletions packages/prisma/src/basic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './prisma.constants'
export * from './prisma.interfaces'
export * from './prisma.module'
export * from './prisma.service'
export * from './prisma-client-exception.filter'
export * from './prisma-logging.middleware'
export * from './soft-delete.extension'
106 changes: 106 additions & 0 deletions packages/prisma/src/basic/prisma-client-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ArgumentsHost, Catch, HttpException, HttpServer, HttpStatus } from '@nestjs/common'
import { APP_FILTER, BaseExceptionFilter, HttpAdapterHost } from '@nestjs/core'
import { Prisma } from '@prisma/client'

export declare type GqlContextType = 'graphql'

export type ErrorCodesStatusMapping = {
[key: string]:
| number
| {
statusCode?: number
errorMessage?: string
}
}

@Catch(Prisma?.PrismaClientKnownRequestError)
export class PrismaClientExceptionFilter extends BaseExceptionFilter {
/**
* Default error codes mapping.
* Error codes definition for Prisma Client (Query Engine).
* @see https://www.prisma.io/docs/reference/api-reference/error-reference#prisma-client-query-engine
*/
private readonly defaultMapping = new Map([
['P2000', HttpStatus.BAD_REQUEST],
['P2002', HttpStatus.CONFLICT],
['P2025', HttpStatus.NOT_FOUND]
])

private readonly userDefinedMapping?: ErrorCodesStatusMapping

/**
* @param applicationRef
* @param errorCodesStatusMapping
*/
constructor(applicationRef?: HttpServer, errorCodesStatusMapping?: ErrorCodesStatusMapping) {
super(applicationRef)
this.userDefinedMapping = errorCodesStatusMapping
}

catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
return this.catchClientKnownRequestError(exception, host)
}

private catchClientKnownRequestError(
exception: Prisma.PrismaClientKnownRequestError,
host: ArgumentsHost
) {
const statusCode = this.userDefinedStatusCode(exception) ?? this.defaultStatusCode(exception)

const message =
this.userDefinedExceptionMessage(exception) ?? this.defaultExceptionMessage(exception)

if (host.getType() === 'http') {
if (statusCode === undefined) {
return super.catch(exception, host)
}

return super.catch(new HttpException({ statusCode, message }, statusCode), host)
}
if (host.getType<GqlContextType>() === 'graphql') {
if (statusCode === undefined) {
return exception
}

return new HttpException({ statusCode, message }, statusCode)
}
return super.catch(exception, host)
}

private userDefinedStatusCode(
exception: Prisma.PrismaClientKnownRequestError
): number | undefined {
const userDefinedValue = this.userDefinedMapping?.[exception.code]
return typeof userDefinedValue === 'number' ? userDefinedValue : userDefinedValue?.statusCode
}

private defaultStatusCode(exception: Prisma.PrismaClientKnownRequestError): number | undefined {
return this.defaultMapping.get(exception.code)
}

private userDefinedExceptionMessage(
exception: Prisma.PrismaClientKnownRequestError
): string | undefined {
const userDefinedValue = this.userDefinedMapping?.[exception.code]
return typeof userDefinedValue === 'number' ? undefined : userDefinedValue?.errorMessage
}

private defaultExceptionMessage(exception: Prisma.PrismaClientKnownRequestError): string {
const shortMessage = exception.message.substring(exception.message.indexOf('→'))
return `[${exception.code}]: ${shortMessage
.substring(shortMessage.indexOf('\n'))
.replace(/\n/g, '')
.trim()}`
}
}

export function providePrismaClientExceptionFilter(
errorCodesStatusMapping?: ErrorCodesStatusMapping
) {
return {
provide: APP_FILTER,
useFactory: ({ httpAdapter }: HttpAdapterHost) =>
new PrismaClientExceptionFilter(httpAdapter, errorCodesStatusMapping),
inject: [HttpAdapterHost]
}
}
68 changes: 68 additions & 0 deletions packages/prisma/src/basic/prisma-logging.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Logger } from '@nestjs/common'
import type { Prisma } from '@prisma/client'

export interface LoggingMiddlewareOptions {
logger: Console | Logger
logLevel: 'log' | 'debug' | 'warn' | 'error'
/**
* Create a custom log message.
*/
logMessage?: (query: QueryInfo) => string
}

export interface QueryInfo {
/**
* The queried prisma model.
*/
model: string
/**
* The performed action on the model e.g. `create`, `findUnique`.
*/
action: string
/**
* Time `Date.now()` before the query execution.
*
*/
before: number
/**
* Time `Date.now()` after the query execution.
*/
after: number
/**
* Execution time of the query in milliseconds.
*/
executionTime: number
}

export function loggingMiddleware(
{ logger, logMessage, logLevel }: LoggingMiddlewareOptions = {
logger: console,
logLevel: 'debug'
}
): Prisma.Middleware {
return async (params, next) => {
const before = Date.now()

const result = await next(params)

const after = Date.now()

const executionTime = after - before

if (logMessage) {
logger[logLevel](
logMessage({
model: params.model!,
action: params.action,
before,
after,
executionTime
})
)
} else {
logger[logLevel](`Prisma Query ${params.model}.${params.action} took ${executionTime}ms`)
}

return result
}
}
1 change: 1 addition & 0 deletions packages/prisma/src/basic/prisma.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PRISMA_SERVICE_OPTIONS = 'PRISMA_SERVICE_OPTIONS'
50 changes: 50 additions & 0 deletions packages/prisma/src/basic/prisma.interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ModuleMetadata, Type } from '@nestjs/common'
import type { Prisma } from '@prisma/client'

export interface PrismaModuleOptions {
/**
* If `true`, registers `PrismaModule` as a global module.
* @See https://docs.nestjs.com/modules#global-modules
*/
isGlobal?: boolean
/**
* Options for `PrismaService`.
*/
prismaServiceOptions?: PrismaServiceOptions
}

export interface PrismaServiceOptions {
/**
* Pass options directly to the `PrismaClient`.
* @see https://www.prisma.io/docs/reference/api-reference/prisma-client-reference/#prismaclient
*/
prismaOptions?: Prisma.PrismaClientOptions
/**
* If `true`, the `PrismaClient` explicitly creates a connection pool and your first query will respond instantly.
* For most use cases the lazy connect behavior of `PrismaClient` will do. The first query of `PrismaClient` creates the connection pool.
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-prismaclient/connection-management
*/
explicitConnect?: boolean
/**
* Apply Prisma middlewares to perform actions before or after db queries.
* @deprecated
* @see https://www.prisma.io/docs/orm/prisma-client/client-extensions/middleware
*/
middlewares?: Prisma.Middleware[]
}

export interface PrismaOptionsFactory {
createPrismaOptions: () => PrismaServiceOptions | Promise<PrismaServiceOptions>
}

export interface PrismaModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
/**
* If `true`, registers `PrismaModule` as a global module.
* @See https://docs.nestjs.com/modules#global-modules
*/
isGlobal?: boolean
useExisting?: Type<PrismaOptionsFactory>
useClass?: Type<PrismaOptionsFactory>
useFactory?: (...args: any[]) => PrismaServiceOptions | Promise<PrismaServiceOptions>
inject?: any[]
}
Loading

0 comments on commit 76869f8

Please sign in to comment.