├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .gitignore ├── .prettierrc ├── .yarnrc.yml ├── README.md ├── docker-compose.yml ├── mysql-schema.sql ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package.json ├── renovate.json ├── src ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── entities │ ├── Author.ts │ ├── BaseEntity.ts │ ├── Book.ts │ ├── BookTag.ts │ ├── Publisher.ts │ └── index.ts ├── main.hmr.ts ├── main.ts ├── mikro-orm.config.ts └── modules │ ├── author │ ├── author.controller.spec.ts │ ├── author.controller.ts │ └── author.module.ts │ ├── book │ ├── book.controller.spec.ts │ ├── book.controller.ts │ └── book.module.ts │ └── orm │ └── orm.module.ts ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [B4nan] 2 | custom: 'https://paypal.me/mikroorm' 3 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [ master, renovate/** ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: Tests 12 | if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | node-version: [ 20, 22 ] 18 | steps: 19 | - name: Checkout Source code 20 | uses: actions/checkout@v4 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Enable corepack 28 | run: | 29 | corepack enable 30 | corepack prepare yarn@stable --activate 31 | 32 | - name: Init docker 33 | run: docker compose up -d 34 | 35 | - name: Install 36 | run: yarn 37 | 38 | - name: Test 39 | run: yarn test 40 | 41 | - name: Teardown docker 42 | run: docker compose down 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | temp 3 | node_modules 4 | .idea 5 | .yarn/* 6 | !.yarn/patches 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nest + MySQL + TypeScript example integration 2 | 3 | 1. Install dependencies via `yarn` or `npm install` 4 | 2. Run `docker-compose up -d` to start mysql 5 | 3. Create database schema via `npx mikro-orm schema:create -r`. This will also create the 6 | database if it does not exist. 7 | 4. Run via `yarn start` or `yarn start:dev` (nodemon) 8 | 5. Example API is running on http://localhost:3000 9 | 10 | Available routes: 11 | 12 | ``` 13 | GET /author finds all authors 14 | GET /author/:id finds author by id 15 | POST /author creates new author 16 | PUT /author/:id updates author by id 17 | ``` 18 | 19 | ``` 20 | GET /book finds all books 21 | GET /book/:id finds book by id 22 | POST /book creates new book 23 | PUT /book/:id updates book by id 24 | ``` 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | 3 | services: 4 | mysql: 5 | container_name: mysql 6 | image: mysql:9.3 7 | restart: unless-stopped 8 | ports: 9 | - "3307:3306" 10 | environment: 11 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 12 | volumes: 13 | - mysql:/var/lib/mysql 14 | 15 | volumes: 16 | mysql: 17 | -------------------------------------------------------------------------------- /mysql-schema.sql: -------------------------------------------------------------------------------- 1 | # ************************************************************ 2 | # Sequel Pro SQL dump 3 | # Version 4541 4 | # 5 | # http://www.sequelpro.com/ 6 | # https://github.com/sequelpro/sequelpro 7 | # 8 | # Host: 127.0.0.1 (MySQL 5.7.24) 9 | # Database: mikro_orm_test 10 | # Generation Time: 2019-01-13 12:48:05 +0000 11 | # ************************************************************ 12 | 13 | 14 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 15 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 16 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 17 | /*!40101 SET NAMES utf8 */; 18 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 19 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 20 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 21 | 22 | 23 | # Dump of table author 24 | # ------------------------------------------------------------ 25 | 26 | DROP TABLE IF EXISTS `author`; 27 | 28 | CREATE TABLE `author` ( 29 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 30 | `created_at` datetime(3) DEFAULT NULL, 31 | `updated_at` datetime(3) DEFAULT NULL, 32 | `terms_accepted` tinyint(1) DEFAULT NULL, 33 | `name` varchar(255) DEFAULT NULL, 34 | `email` varchar(255) DEFAULT NULL, 35 | `born` datetime DEFAULT NULL, 36 | `favourite_book_id` int(11) DEFAULT NULL, 37 | PRIMARY KEY (`id`) 38 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 39 | 40 | 41 | 42 | # Dump of table book_to_book_tag 43 | # ------------------------------------------------------------ 44 | 45 | DROP TABLE IF EXISTS `book_to_book_tag`; 46 | 47 | CREATE TABLE `book_to_book_tag` ( 48 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 49 | `book_id` int(11) DEFAULT NULL, 50 | `book_tag_id` int(11) DEFAULT NULL, 51 | PRIMARY KEY (`id`) 52 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 53 | 54 | 55 | 56 | # Dump of table book_tag 57 | # ------------------------------------------------------------ 58 | 59 | DROP TABLE IF EXISTS `book_tag`; 60 | 61 | CREATE TABLE `book_tag` ( 62 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 63 | `name` varchar(50) DEFAULT NULL, 64 | PRIMARY KEY (`id`) 65 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 66 | 67 | 68 | 69 | # Dump of table book 70 | # ------------------------------------------------------------ 71 | 72 | DROP TABLE IF EXISTS `book`; 73 | 74 | CREATE TABLE `book` ( 75 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 76 | `created_at` datetime(3) DEFAULT NULL, 77 | `updated_at` datetime(3) DEFAULT NULL, 78 | `title` varchar(255) DEFAULT NULL, 79 | `foo` varchar(255) DEFAULT NULL, 80 | `author_id` int(11) DEFAULT NULL, 81 | `publisher_id` int(11) DEFAULT NULL, 82 | PRIMARY KEY (`id`) 83 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 84 | 85 | 86 | 87 | # Dump of table publisher 88 | # ------------------------------------------------------------ 89 | 90 | DROP TABLE IF EXISTS `publisher`; 91 | 92 | CREATE TABLE `publisher` ( 93 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 94 | `name` varchar(255) DEFAULT NULL, 95 | `type` varchar(255) DEFAULT NULL, 96 | PRIMARY KEY (`id`) 97 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 98 | 99 | 100 | 101 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 102 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 103 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 104 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 105 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 106 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 107 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register src/main.ts" 6 | } -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mikro-orm-nest-ts-example", 3 | "version": "0.0.1", 4 | "description": "Example integration of MikroORM into Nest (in typescript)", 5 | "author": "Martin Adamek", 6 | "license": "MIT", 7 | "scripts": { 8 | "format": "prettier --write \"src/**/*.ts\"", 9 | "start": "ts-node -r tsconfig-paths/register src/main.ts", 10 | "start:dev": "nodemon", 11 | "start:debug": "nodemon --config nodemon-debug.json", 12 | "prestart:prod": "rimraf dist && tsc", 13 | "start:prod": "node dist/main.js", 14 | "start:hmr": "node dist/server", 15 | "lint": "tslint -p tsconfig.json -c tslint.json", 16 | "test": "jest --runInBand", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:e2e": "jest --config ./test/jest-e2e.json", 20 | "webpack": "webpack --config webpack.config.js" 21 | }, 22 | "dependencies": { 23 | "@mikro-orm/core": "^6.4.4", 24 | "@mikro-orm/mysql": "^6.4.4", 25 | "@mikro-orm/nestjs": "^6.1.0", 26 | "@mikro-orm/reflection": "^6.4.4", 27 | "@mikro-orm/sql-highlighter": "^1.0.1", 28 | "@nestjs/common": "^11.0.6", 29 | "@nestjs/core": "^11.0.6", 30 | "@nestjs/platform-express": "^11.0.6", 31 | "reflect-metadata": "^0.2.0", 32 | "rxjs": "^7.8.1", 33 | "typescript": "^5.3.3" 34 | }, 35 | "devDependencies": { 36 | "@mikro-orm/cli": "^6.4.4", 37 | "@nestjs/testing": "^11.0.6", 38 | "@types/express": "^5.0.0", 39 | "@types/jest": "^29.5.14", 40 | "@types/node": "^22.12.0", 41 | "@types/supertest": "^6.0.2", 42 | "jest": "^29.7.0", 43 | "nodemon": "^3.0.3", 44 | "prettier": "^3.2.5", 45 | "rimraf": "^6.0.0", 46 | "supertest": "^7.0.0", 47 | "ts-jest": "^29.1.2", 48 | "ts-loader": "^9.5.1", 49 | "ts-node": "^10.9.2", 50 | "tsconfig-paths": "^4.2.0", 51 | "tslint": "6.1.3", 52 | "webpack": "^5.90.1", 53 | "webpack-cli": "^6.0.0", 54 | "webpack-node-externals": "^3.0.0" 55 | }, 56 | "jest": { 57 | "moduleFileExtensions": [ 58 | "js", 59 | "json", 60 | "ts" 61 | ], 62 | "rootDir": "src", 63 | "testRegex": ".spec.ts$", 64 | "transform": { 65 | "^.+\\.(t|j)s$": "ts-jest" 66 | }, 67 | "coverageDirectory": "../coverage", 68 | "testEnvironment": "node" 69 | }, 70 | "mikro-orm": { 71 | "useTsNode": true, 72 | "configPaths": [ 73 | "./src/mikro-orm.config.ts", 74 | "./dist/mikro-orm.config.js" 75 | ] 76 | }, 77 | "packageManager": "yarn@4.9.2" 78 | } 79 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges", 5 | ":semanticCommitTypeAll(chore)" 6 | ], 7 | "semanticCommits": "enabled", 8 | "separateMajorMinor": false, 9 | "dependencyDashboard": false, 10 | "packageRules": [ 11 | { 12 | "matchUpdateTypes": [ 13 | "patch", 14 | "minor" 15 | ], 16 | "groupName": "patch/minor dependencies", 17 | "groupSlug": "all-non-major", 18 | "automerge": true, 19 | "automergeType": "branch" 20 | } 21 | ], 22 | "schedule": [ 23 | "every weekday" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | root(): string { 10 | return this.appService.root(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { OrmModule } from './modules/orm/orm.module'; 5 | import { AuthorModule } from './modules/author/author.module'; 6 | import { BookModule } from './modules/book/book.module'; 7 | 8 | @Module({ 9 | imports: [OrmModule, AuthorModule, BookModule], 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }) 13 | export class AppModule {} 14 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | root(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/entities/Author.ts: -------------------------------------------------------------------------------- 1 | import { Cascade, Collection, Entity, OneToMany, Property, ManyToOne, Opt } from '@mikro-orm/core'; 2 | 3 | import { Book } from '.'; 4 | import { BaseEntity } from './BaseEntity'; 5 | 6 | @Entity() 7 | export class Author extends BaseEntity { 8 | 9 | @Property() 10 | name: string; 11 | 12 | @Property() 13 | email: string; 14 | 15 | @Property({ nullable: true }) 16 | age?: number; 17 | 18 | @Property() 19 | termsAccepted: boolean & Opt = false; 20 | 21 | @Property({ nullable: true }) 22 | born?: Date; 23 | 24 | @OneToMany(() => Book, b => b.author, { cascade: [Cascade.ALL] }) 25 | books = new Collection(this); 26 | 27 | @ManyToOne(() => Book, { nullable: true }) 28 | favouriteBook?: Book; 29 | 30 | constructor(name: string, email: string) { 31 | super(); 32 | this.name = name; 33 | this.email = email; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/entities/BaseEntity.ts: -------------------------------------------------------------------------------- 1 | import { Opt, PrimaryKey, Property } from '@mikro-orm/core'; 2 | 3 | export abstract class BaseEntity { 4 | 5 | @PrimaryKey() 6 | id!: number; 7 | 8 | @Property() 9 | createdAt: Date & Opt = new Date(); 10 | 11 | @Property({ onUpdate: () => new Date() }) 12 | updatedAt: Date & Opt = new Date(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/entities/Book.ts: -------------------------------------------------------------------------------- 1 | import { Cascade, Collection, Entity, ManyToMany, ManyToOne, Property } from '@mikro-orm/core'; 2 | import { Author } from './Author'; 3 | import { BookTag } from './BookTag'; 4 | import { Publisher } from './Publisher'; 5 | import { BaseEntity } from './BaseEntity'; 6 | 7 | @Entity() 8 | export class Book extends BaseEntity { 9 | 10 | @Property() 11 | title: string; 12 | 13 | @ManyToOne(() => Author) 14 | author: Author; 15 | 16 | @ManyToOne(() => Publisher, { cascade: [Cascade.PERSIST, Cascade.REMOVE], nullable: true }) 17 | publisher?: Publisher; 18 | 19 | @ManyToMany(() => BookTag) 20 | tags = new Collection(this); 21 | 22 | @Property({ nullable: true }) 23 | metaObject?: object; 24 | 25 | @Property({ nullable: true }) 26 | metaArray?: any[]; 27 | 28 | @Property({ nullable: true }) 29 | metaArrayOfStrings?: string[]; 30 | 31 | constructor(title: string, author: Author) { 32 | super(); 33 | this.title = title; 34 | this.author = author; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/entities/BookTag.ts: -------------------------------------------------------------------------------- 1 | import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core'; 2 | import { Book } from './Book'; 3 | 4 | @Entity() 5 | export class BookTag { 6 | 7 | @PrimaryKey() 8 | id!: number; 9 | 10 | @Property() 11 | name: string; 12 | 13 | @ManyToMany(() => Book, b => b.tags) 14 | books = new Collection(this); 15 | 16 | constructor(name: string) { 17 | this.name = name; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/entities/Publisher.ts: -------------------------------------------------------------------------------- 1 | import { Collection, Entity, Enum, OneToMany, Opt, PrimaryKey, Property } from '@mikro-orm/core'; 2 | import { Book } from './Book'; 3 | 4 | @Entity() 5 | export class Publisher { 6 | 7 | @PrimaryKey() 8 | id!: number; 9 | 10 | @Property() 11 | name: string; 12 | 13 | @OneToMany(() => Book, b => b.publisher) 14 | books = new Collection(this); 15 | 16 | @Enum(() => PublisherType) 17 | type: PublisherType & Opt; 18 | 19 | constructor(name: string, type = PublisherType.LOCAL) { 20 | this.name = name; 21 | this.type = type; 22 | } 23 | 24 | } 25 | 26 | export enum PublisherType { 27 | LOCAL = 'local', 28 | GLOBAL = 'global', 29 | } 30 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Author'; 2 | export * from './Book'; 3 | export * from './BookTag'; 4 | export * from './Publisher'; 5 | export * from './BaseEntity'; 6 | -------------------------------------------------------------------------------- /src/main.hmr.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | declare const module: any; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | await app.listen(3000); 9 | 10 | if (module.hot) { 11 | module.hot.accept(); 12 | module.hot.dispose(() => app.close()); 13 | } 14 | } 15 | bootstrap(); 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { MikroORM } from '@mikro-orm/core'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | await app.get(MikroORM).getSchemaGenerator().ensureDatabase(); 8 | await app.get(MikroORM).getSchemaGenerator().updateSchema(); 9 | await app.listen(3000); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /src/mikro-orm.config.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { SqlHighlighter } from '@mikro-orm/sql-highlighter'; 3 | import { defineConfig } from '@mikro-orm/mysql'; 4 | import { Author, BaseEntity, Book, BookTag, Publisher } from './entities'; 5 | 6 | const logger = new Logger('MikroORM'); 7 | 8 | export default defineConfig({ 9 | entities: [Author, Book, BookTag, Publisher, BaseEntity], 10 | dbName: 'mikro-orm-nest-ts', 11 | port: 3307, 12 | highlighter: new SqlHighlighter(), 13 | debug: true, 14 | logger: logger.log.bind(logger), 15 | }); 16 | -------------------------------------------------------------------------------- /src/modules/author/author.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM } from '@mikro-orm/core'; 2 | import { Test } from '@nestjs/testing'; 3 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 4 | import config from '../../mikro-orm.config'; 5 | import { Author } from '../../entities'; 6 | import { AuthorController } from './author.controller'; 7 | 8 | describe('author controller', () => { 9 | 10 | let authorController: AuthorController; 11 | let orm: MikroORM; 12 | 13 | beforeEach(async () => { 14 | const module = await Test.createTestingModule({ 15 | imports: [ 16 | MikroOrmModule.forRoot({ 17 | ...config, 18 | dbName: 'nest-mikro-test-db', 19 | allowGlobalContext: true, 20 | }), 21 | MikroOrmModule.forFeature({ entities: [Author] }), 22 | ], 23 | controllers: [AuthorController], 24 | }).compile(); 25 | 26 | authorController = module.get(AuthorController); 27 | orm = module.get(MikroORM); 28 | await orm.getSchemaGenerator().refreshDatabase(); 29 | }); 30 | 31 | afterAll(async () => await orm.close(true)); 32 | 33 | it(`CRUD`, async () => { 34 | const res1 = await authorController.create({ name: 'a1', email: 'e1', books: [{ title: 'b1' }, { title: 'b2' }] }); 35 | expect(res1.id).toBeDefined(); 36 | expect(res1.name).toBe('a1'); 37 | expect(res1.email).toBe('e1'); 38 | expect(res1.termsAccepted).toBe(false); 39 | expect(res1.books).toHaveLength(2); 40 | 41 | const id = res1.id; 42 | 43 | const res2 = await authorController.find(); 44 | expect(res2).toHaveLength(1); 45 | expect(res2[0].id).toBeDefined(); 46 | expect(res2[0].name).toBe('a1'); 47 | expect(res2[0].email).toBe('e1'); 48 | expect(res2[0].termsAccepted).toBe(false); 49 | expect(res2[0].books).toHaveLength(2); 50 | 51 | const res3 = await authorController.update(id, { name: 'a2' }); 52 | expect(res3.id).toBeDefined(); 53 | expect(res3.name).toBe('a2'); 54 | expect(res3.email).toBe('e1'); 55 | expect(res3.termsAccepted).toBe(false); 56 | }); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /src/modules/author/author.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, HttpException, HttpStatus, Param, ParseIntPipe, Post, Put } from '@nestjs/common'; 2 | import { EntityRepository, QueryOrder, wrap, EntityManager } from '@mikro-orm/mysql'; 3 | import { InjectRepository } from '@mikro-orm/nestjs'; 4 | import { Author } from '../../entities'; 5 | 6 | @Controller('author') 7 | export class AuthorController { 8 | 9 | constructor( 10 | @InjectRepository(Author) private readonly authorRepository: EntityRepository, 11 | private readonly em: EntityManager, 12 | ) { } 13 | 14 | @Get() 15 | async find() { 16 | return await this.authorRepository.findAll({ 17 | populate: ['books'], 18 | orderBy: { name: QueryOrder.DESC }, 19 | limit: 20, 20 | }); 21 | } 22 | 23 | @Get(':id') 24 | async findOne(@Param('id', ParseIntPipe) id: number) { 25 | return await this.authorRepository.findOneOrFail(id, { 26 | populate: ['books'], 27 | }); 28 | } 29 | 30 | @Post() 31 | async create(@Body() body: any) { 32 | if (!body.name || !body.email) { 33 | throw new HttpException('One of `name, email` is missing', HttpStatus.BAD_REQUEST); 34 | } 35 | 36 | const author = this.authorRepository.create(body); 37 | await this.em.flush(); 38 | 39 | return author; 40 | } 41 | 42 | @Put(':id') 43 | async update(@Param('id', ParseIntPipe) id: number, @Body() body: any) { 44 | const author = await this.authorRepository.findOneOrFail(id); 45 | wrap(author).assign(body); 46 | await this.em.flush(); 47 | 48 | return author; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/author/author.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthorController } from './author.controller'; 3 | import { OrmModule } from '../orm/orm.module'; 4 | 5 | @Module({ 6 | imports: [OrmModule], 7 | controllers: [AuthorController], 8 | providers: [], 9 | }) 10 | export class AuthorModule {} 11 | -------------------------------------------------------------------------------- /src/modules/book/book.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { MikroORM } from '@mikro-orm/core'; 2 | import { Test } from '@nestjs/testing'; 3 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 4 | import config from '../../mikro-orm.config'; 5 | import { Book } from '../../entities'; 6 | import { BookController } from './book.controller'; 7 | 8 | describe('author controller', () => { 9 | 10 | let bookController: BookController; 11 | let orm: MikroORM; 12 | 13 | beforeEach(async () => { 14 | const module = await Test.createTestingModule({ 15 | imports: [ 16 | MikroOrmModule.forRoot({ 17 | ...config, 18 | dbName: 'nest-mikro-test-db', 19 | allowGlobalContext: true, 20 | }), 21 | MikroOrmModule.forFeature({ entities: [Book] }), 22 | ], 23 | controllers: [BookController], 24 | }).compile(); 25 | 26 | bookController = module.get(BookController); 27 | orm = module.get(MikroORM); 28 | await orm.getSchemaGenerator().refreshDatabase(); 29 | }); 30 | 31 | afterAll(async () => await orm.close(true)); 32 | 33 | it(`CRUD`, async () => { 34 | const res1 = await bookController.create({ title: 'b1', author: { name: 'a1', email: 'e1' } }); 35 | expect(res1.id).toBeDefined(); 36 | expect(res1.title).toBe('b1'); 37 | expect(res1.author.name).toBe('a1'); 38 | expect(res1.author.email).toBe('e1'); 39 | expect(res1.author.termsAccepted).toBe(false); 40 | expect(res1.author.books).toHaveLength(1); 41 | 42 | const id = res1.id; 43 | 44 | const res2 = await bookController.find(); 45 | expect(res2[0].id).toBeDefined(); 46 | expect(res2[0].title).toBe('b1'); 47 | expect(res2[0].author.name).toBe('a1'); 48 | expect(res2[0].author.email).toBe('e1'); 49 | expect(res2[0].author.termsAccepted).toBe(false); 50 | 51 | const res3 = await bookController.update(id, { title: 'b2' }); 52 | expect(res3.id).toBeDefined(); 53 | expect(res3.title).toBe('b2'); 54 | expect(res3.author).toBeDefined(); 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /src/modules/book/book.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, HttpException, HttpStatus, Param, ParseIntPipe, Post, Put } from '@nestjs/common'; 2 | import { EntityRepository, QueryOrder, wrap } from '@mikro-orm/core'; 3 | import { InjectRepository } from '@mikro-orm/nestjs'; 4 | import { Book } from '../../entities'; 5 | import { EntityManager } from '@mikro-orm/mysql'; 6 | 7 | @Controller('book') 8 | export class BookController { 9 | 10 | constructor( 11 | @InjectRepository(Book) private readonly bookRepository: EntityRepository, 12 | private readonly em: EntityManager, 13 | ) { } 14 | 15 | @Get() 16 | async find() { 17 | return await this.bookRepository.findAll({ 18 | populate: ['author'], 19 | orderBy: { title: QueryOrder.DESC }, 20 | limit: 20, 21 | }); 22 | } 23 | 24 | @Get(':id') 25 | async findOne(@Param('id', ParseIntPipe) id: number) { 26 | return await this.bookRepository.findOneOrFail(id, { 27 | populate: ['author'], 28 | }); 29 | } 30 | 31 | @Post() 32 | async create(@Body() body: any) { 33 | if (!body.title || !body.author) { 34 | throw new HttpException('One of `title, author` is missing', HttpStatus.BAD_REQUEST); 35 | } 36 | 37 | const book = this.bookRepository.create(body); 38 | await this.em.flush(); 39 | 40 | return book; 41 | } 42 | 43 | @Put(':id') 44 | async update(@Param('id', ParseIntPipe) id: number, @Body() body: any) { 45 | const book = await this.bookRepository.findOne(id); 46 | 47 | if (!book) { 48 | throw new HttpException('Book not found', HttpStatus.NOT_FOUND); 49 | } 50 | 51 | wrap(book).assign(body); 52 | await this.em.flush(); 53 | 54 | return book; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/modules/book/book.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BookController } from './book.controller'; 3 | import { OrmModule } from '../orm/orm.module'; 4 | 5 | @Module({ 6 | imports: [OrmModule], 7 | controllers: [BookController], 8 | providers: [], 9 | }) 10 | export class BookModule {} 11 | -------------------------------------------------------------------------------- /src/modules/orm/orm.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MikroOrmModule } from '@mikro-orm/nestjs'; 3 | 4 | import { Author, Book, BookTag, Publisher } from '../../entities'; 5 | 6 | @Module({ 7 | imports: [ 8 | MikroOrmModule.forRoot(), 9 | MikroOrmModule.forFeature({ 10 | entities: [Author, Book, BookTag, Publisher], 11 | }), 12 | ], 13 | exports: [MikroOrmModule], 14 | }) 15 | export class OrmModule { } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "strict": true, 9 | "allowSyntheticDefaultImports": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "target": "es6", 13 | "sourceMap": true, 14 | "outDir": "./dist", 15 | "baseUrl": "./src" 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/*.spec.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "eofline": false, 11 | "quotemark": [ 12 | true, 13 | "single" 14 | ], 15 | "indent": false, 16 | "member-access": [ 17 | false 18 | ], 19 | "ordered-imports": [ 20 | false 21 | ], 22 | "max-line-length": [ 23 | true, 24 | 150 25 | ], 26 | "member-ordering": [ 27 | false 28 | ], 29 | "curly": false, 30 | "interface-name": [ 31 | false 32 | ], 33 | "array-type": [ 34 | false 35 | ], 36 | "no-empty-interface": false, 37 | "no-empty": false, 38 | "arrow-parens": false, 39 | "object-literal-sort-keys": false, 40 | "no-unused-expression": false, 41 | "max-classes-per-file": [ 42 | false 43 | ], 44 | "variable-name": [ 45 | false 46 | ], 47 | "one-line": [ 48 | false 49 | ], 50 | "one-variable-per-declaration": [ 51 | false 52 | ] 53 | }, 54 | "rulesDirectory": [] 55 | } 56 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: ['webpack/hot/poll?1000', './src/main.hmr.ts'], 7 | watch: true, 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | allowlist: ['webpack/hot/poll?1000'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: "development", 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | ], 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: 'server.js', 33 | }, 34 | }; 35 | --------------------------------------------------------------------------------