CRUD Endpoints
Schematics#
We recommend using the schematics package to quickly scaffold your CRUD modules:
Install
nestjs-prisma-crud-schematicsglobally:npm i nestjs-prisma-crud-schematics --save-devScaffold 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.tswhere you can add, remove or extend your controllers' functionality.post.service.tswhere you can configure your crud endpoints.
CRUD Controller#
The CRUD controller is just a regular NestJS controller with a few characteristics:
- All routes use the generated
<ModelName>Servicefor performing the CRUD operations. - All routes retrieve
@Query('crudQuery') crudQuery: stringand 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 Service#
tip
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.model#
Type: string
Mandatory: Yes
Description:
The prismaClient.model on which you wish to perform the CRUD operations.
Example: 'post'
opts.prismaClient#
Type: 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.allowedJoins#
Type: 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.defaultJoins#
Type: 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.forbiddenPaths#
Type: 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.idPropertyName#
Type: string
Mandatory: No
Description:
The name of the model's primary key.
Example: uuid
Default: id
opts.paginationConfig#
Type: 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 Support#
info
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).
Example#
Suppose 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
prismaTransactioninto thePrismaCrudServicemethods. - Use the
prismaTransactioninstead ofprismaClientfor 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; }}