CRUD Endpoints
#
SchematicsWe recommend using the schematics package to quickly scaffold your CRUD modules:
Install
nestjs-prisma-crud-schematics
globally:npm i nestjs-prisma-crud-schematics --save-dev
Scaffold the CRUD module and endpoints (replace post with your model's name):
nest g -c nestjs-prisma-crud-schematics crud-resource post
The above will scaffold the entire CRUD module for you, most notably:
post.controller.ts
where you can add, remove or extend your controllers' functionality.post.service.ts
where you can configure your crud endpoints.
#
CRUD ControllerThe CRUD controller is just a regular NestJS controller with a few characteristics:
- All routes use the generated
<ModelName>Service
for performing the CRUD operations. - All routes retrieve
@Query('crudQuery') crudQuery: string
and pass it along to the service.
tip
The schematic generates this file for you.
import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common';import { PostService } from './post.service';import { CreatePostDto } from './dto/create-post.dto';import { UpdatePostDto } from './dto/update-post.dto';
@Controller('post')export class PostController { constructor(private readonly postService: PostService) {}
@Post() async create(@Body() createPostDto: CreatePostDto, @Query('crudQuery') crudQuery: string) { const created = await this.postService.create(createPostDto, { crudQuery }); return created; }
@Get() async findMany(@Query('crudQuery') crudQuery: string) { const matches = await this.postService.findMany({ crudQuery }); return matches; }
@Get(':id') async findOne(@Param('id') id: string, @Query('crudQuery') crudQuery: string) { const match = await this.postService.findOne(id, { crudQuery }); return match; }
@Patch(':id') async update( @Param('id') id: string, @Body() updatePostDto: UpdatePostDto, @Query('crudQuery') crudQuery: string, ) { const updated = await this.postService.update(id, updatePostDto, { crudQuery }); return updated; }
@Delete(':id') async remove(@Param('id') id: string, @Query('crudQuery') crudQuery: string) { return this.postService.remove(id, { crudQuery }); }}
#
CRUD Servicetip
The schematic generates this file for you.
import { Injectable } from '@nestjs/common';import { PrismaCrudService } from 'nestjs-prisma-crud';
@Injectable()export class PostService extends PrismaCrudService { constructor() { super({ model: 'post', allowedJoins: [], }); }}
The configuration of your crud endpoints is defined in the super()
call:
export interface CrudServiceOpts { model: string; prismaClient?: PrismaClient; allowedJoins: string[]; defaultJoins?: string[]; forbiddenPaths?: Array<string | RegExp>; idPropertyName?: string; paginationConfig?: PaginationConfig;}
Below you can find a description of each option.
#
opts.modelType: string
Mandatory: Yes
Description:
The prismaClient.model
on which you wish to perform the CRUD operations.
Example: 'post'
#
opts.prismaClientType: PrismaClient | PrismaService
Mandatory: No
Description:
Set this value if for some reason want to use a different PrismaService
from the globally provided one.
For most use cases it can be left blank.
Example: prismaService
#
opts.allowedJoinsType: Array<string>
Mandatory: No
Description:
The relations which clients can ask to include in responses (see client side usage).
Supports dot notation.
Example: ['comments.author.posts']
Default: []
#
opts.defaultJoinsType: Array<string>
Mandatory: No
Description:
The default relations to be included in responses.
Note: Paths must be shallower or same depth as provided in allowedJoins
Example: ['comments.author']
or []
;
Default: opts.allowedJoins
#
opts.forbiddenPathsType: Array<string | RegExp>
Mandatory: No
Description:
The paths you wish to omit in the returned objects.
Important: These values still get fetched from the database, and are excluded just before the function returns!!
Example:
[ 'some.nested.exact.string.path',
// RegExp: delete anything containing the word password /.*password.*/,
// RegExp: \d+ targets all comments in an array, deleting their .somethingSecret /comments\.\d+\.somethingSecret/,];
Default: []
#
opts.idPropertyNameType: string
Mandatory: No
Description:
The name of the model's primary key.
Example: uuid
Default: id
#
opts.paginationConfigType: PaginationConfig
export type PaginationConfig = { defaultPageSize?: number; maxPageSize?: number; defaultOrderBy?: { [key: string]: 'asc' | 'desc' }[];};
Mandatory: No
Description:
defaultPageSize
: when clients do not specify a pageSize, this option will be used.
maxPageSize
: client's pageSize option will be capped at this value.
defaultOrderBy
: when clients do not specify a sorting field, this option will be used by default.
Default:
const PAGINATION_DEFAULTS: PaginationConfig = { defaultPageSize: 25, maxPageSize: 100, defaultOrderBy: [{ [this.idPropertyName]: 'asc' }],};
#
Extending Controller Functionality with Transaction Supportinfo
Transaction support relies on prisma's Interactive Transactions which are currently a preview feature.
In order to use this example, you must add the following to your prisma schema:
generator client { provider = "prisma-client-js" previewFeatures = ["interactiveTransactions"]}
There are times when we want to extend a CRUD controller's functionality and perform additional database operations. In those cases we usually want all database operations to happen atomically (ie. if one database operation fails, cancel all other operations and leave the database unchanged).
#
ExampleSuppose you have a SalesController
where, aside from the CRUD sale
operations, you also wish to increment and decrement the balance of the users involved.
The example below achieves atomicity by following the following steps:
- Start a prisma interactive transaction.
- Pass
prismaTransaction
into thePrismaCrudService
methods. - Use the
prismaTransaction
instead ofprismaClient
for performing your database operations.
interface CreateSaleDTO { itemId: string; sellerId: string; buyerId: string; dollarAmt: number;}
@Controller('sales')export class SalesController { constructor( private readonly salesService: SalesService, private readonly prismaService: PrismaService, ) {}
@Post() async createSale(@Body() createSaleDto: CreateSaleDTO, @Query('crudQuery') crudQuery: string) { let createdSale; // 0. Start the interactive transaction await this.prismaService.$transaction(async (prismaTransaction) => { // 1. create the sale record createdSale = await this.salesService.create(createSaleDto, { crudQuery, prismaTransaction, // pass prisma transaction into PrismaCrudService });
// 2. increment seller's ballance // NOTE: using `prismaTransaction` instead of `this.prismaService` await prismaTransaction.user.update({ data: { balance: { increment: createSaleDto.dollarAmt, }, }, where: { id: createSaleDto.sellerId, }, });
// 3. decrement buyer's ballance // NOTE: using `prismaTransaction` instead of `this.prismaService` await prismaTransaction.user.update({ data: { balance: { decrement: createSaleDto.dollarAmt, }, }, where: { id: createSaleDto.buyerId, }, }); });
return createdSale; }}