├── src ├── modules │ ├── tenancy │ │ ├── tenancy.symbols.ts │ │ ├── tenancy.middleware.ts │ │ ├── tenancy.utils.ts │ │ └── tenancy.module.ts │ ├── tenanted │ │ └── cats │ │ │ ├── dto │ │ │ └── create-cat.dto.ts │ │ │ ├── cat.entity.ts │ │ │ ├── cats.module.ts │ │ │ ├── cats.controller.ts │ │ │ └── cats.service.ts │ └── public │ │ └── tenants │ │ ├── dto │ │ └── create-tenant.dto.ts │ │ ├── tenant.entity.ts │ │ ├── tenants.module.ts │ │ ├── tenants.controller.ts │ │ └── tenants.service.ts ├── custom_typings │ └── express │ │ └── index.d.ts ├── tenants-orm.config.ts ├── abstract.entity.ts ├── orm.config.ts ├── migrations │ ├── public │ │ └── 1638963391898-AddTenants.ts │ └── tenanted │ │ └── 1638963474130-AddCats.ts ├── app.module.ts ├── main.ts └── snake-naming.strategy.ts ├── tsconfig.build.json ├── README.md ├── .gitignore ├── jest.json ├── tsconfig.json ├── .eslintrc.js └── package.json /src/modules/tenancy/tenancy.symbols.ts: -------------------------------------------------------------------------------- 1 | export const CONNECTION = Symbol('CONNECTION'); 2 | -------------------------------------------------------------------------------- /src/modules/tenanted/cats/dto/create-cat.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateCatDto { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/public/tenants/dto/create-tenant.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateTenantDto { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/custom_typings/express/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | interface Request { 3 | tenantId?: string; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schema-based multitenancy with NestJS, TypeORM and PostgreSQL 2 | 3 | See [this blog article](https://thomasvds.com/schema-based-multitenancy-with-nest-js-type-orm-and-postgres-sql/) for a complete walkthrough of the codebase. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.awcache 7 | /.vscode 8 | 9 | # misc 10 | npm-debug.log 11 | 12 | # example 13 | /quick-start 14 | 15 | # tests 16 | /test 17 | /coverage 18 | /.nyc_output 19 | 20 | # dist 21 | /dist -------------------------------------------------------------------------------- /src/modules/tenanted/cats/cat.entity.ts: -------------------------------------------------------------------------------- 1 | import { AbstractEntity } from '../../../abstract.entity'; 2 | import { Column, Entity } from 'typeorm'; 3 | 4 | @Entity({ name: 'cats'}) 5 | export class Cat extends AbstractEntity { 6 | @Column() 7 | name: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/public/tenants/tenant.entity.ts: -------------------------------------------------------------------------------- 1 | import { AbstractEntity } from '../../../abstract.entity'; 2 | import { Column, Entity } from 'typeorm'; 3 | 4 | @Entity({ name: 'tenants'}) 5 | export class Tenant extends AbstractEntity { 6 | @Column() 7 | name: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/tenants-orm.config.ts: -------------------------------------------------------------------------------- 1 | import * as ormconfig from './orm.config'; 2 | 3 | import { join } from 'path'; 4 | 5 | module.exports = { 6 | ...ormconfig, 7 | entities: [join(__dirname, './modules/tenanted/**/*.entity{.ts,.js}')], 8 | migrations: [join(__dirname, './migrations/tenanted/*{.ts,.js}')], 9 | }; 10 | -------------------------------------------------------------------------------- /src/modules/tenancy/tenancy.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | const TENANT_HEADER = 'x-tenant-id' 4 | 5 | export function tenancyMiddleware(req: Request, _res: Response, next: NextFunction): void { 6 | const header = req.headers[TENANT_HEADER] as string; 7 | req.tenantId = header?.toString() || null; 8 | next(); 9 | } 10 | -------------------------------------------------------------------------------- /jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "ts", 4 | "tsx", 5 | "js", 6 | "json" 7 | ], 8 | "transform": { 9 | "^.+\\.tsx?$": "babel-jest" 10 | }, 11 | "testRegex": "/src/.*\\.(test|spec).(ts|tsx|js)$", 12 | "collectCoverageFrom" : ["src/**/*.{js,jsx,tsx,ts}", "!**/node_modules/**", "!**/vendor/**"], 13 | "coverageReporters": ["json", "lcov"] 14 | } -------------------------------------------------------------------------------- /src/modules/tenanted/cats/cats.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Cat } from './cat.entity'; 4 | import { CatsController } from './cats.controller'; 5 | import { CatsService } from './cats.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Cat])], 9 | providers: [CatsService], 10 | controllers: [CatsController], 11 | }) 12 | export class CatsModule {} 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true 15 | }, 16 | "include": ["src/**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/public/tenants/tenants.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { Tenant } from './tenant.entity'; 4 | import { TenantsController } from './tenants.controller'; 5 | import { TenantsService } from './tenants.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([Tenant])], 9 | providers: [TenantsService], 10 | controllers: [TenantsController], 11 | }) 12 | export class TenantsModule {} 13 | -------------------------------------------------------------------------------- /src/abstract.entity.ts: -------------------------------------------------------------------------------- 1 | import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | export abstract class AbstractEntity { 4 | @PrimaryGeneratedColumn('uuid') 5 | id: string; 6 | 7 | @CreateDateColumn({ 8 | type: 'timestamp without time zone', 9 | name: 'created_at', 10 | }) 11 | createdAt: Date; 12 | 13 | @UpdateDateColumn({ 14 | type: 'timestamp without time zone', 15 | name: 'updated_at', 16 | }) 17 | updatedAt: Date; 18 | } 19 | -------------------------------------------------------------------------------- /src/orm.config.ts: -------------------------------------------------------------------------------- 1 | import { SnakeNamingStrategy } from './snake-naming.strategy'; 2 | 3 | import { join } from 'path'; 4 | 5 | module.exports = { 6 | type: 'postgres', 7 | host: 'localhost', 8 | port: 5432, 9 | username: 'thomasvanderstraeten', 10 | password: 'root', 11 | database: 'nestjs-multi-tenant', 12 | namingStrategy: new SnakeNamingStrategy(), 13 | logging: true, 14 | autoLoadEntities: true, 15 | entities: [join(__dirname, './modules/public/**/*.entity{.ts,.js}')], 16 | migrations: [join(__dirname, './migrations/public/*{.ts,.js}')], 17 | }; -------------------------------------------------------------------------------- /src/modules/tenanted/cats/cats.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import { CreateCatDto } from './dto/create-cat.dto'; 3 | import { Cat } from './cat.entity'; 4 | import { CatsService } from './cats.service'; 5 | 6 | @Controller('cats') 7 | export class CatsController { 8 | constructor(private readonly catsService: CatsService) {} 9 | 10 | @Post() 11 | create(@Body() createCatDto: CreateCatDto): Promise { 12 | return this.catsService.create(createCatDto); 13 | } 14 | 15 | @Get() 16 | findAll(): Promise { 17 | return this.catsService.findAll(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/modules/public/tenants/tenants.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | import { CreateTenantDto } from './dto/create-tenant.dto'; 3 | import { Tenant } from './tenant.entity'; 4 | import { TenantsService } from './tenants.service'; 5 | 6 | @Controller('tenants') 7 | export class TenantsController { 8 | constructor(private readonly tenantsService: TenantsService) {} 9 | 10 | @Post() 11 | create(@Body() createTenantDto: CreateTenantDto): Promise { 12 | return this.tenantsService.create(createTenantDto); 13 | } 14 | 15 | @Get() 16 | findAll(): Promise { 17 | return this.tenantsService.findAll(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/migrations/public/1638963391898-AddTenants.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class AddTenants1638963391898 implements MigrationInterface { 4 | // eslint-disable-next-line @typescript-eslint/typedef 5 | name = 'AddTenants1638963391898'; 6 | 7 | public async up(queryRunner: QueryRunner): Promise { 8 | await queryRunner.query( 9 | 'CREATE TABLE "public"."tenants" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, CONSTRAINT "PK_a920abe6f6dd7764ee0f8108f57" PRIMARY KEY ("id"))', 10 | ); 11 | } 12 | 13 | public async down(queryRunner: QueryRunner): Promise { 14 | await queryRunner.query('DROP TABLE "public"."tenants"'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/tenanted/cats/cats.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { Connection, Repository } from 'typeorm'; 3 | import { CreateCatDto } from './dto/create-cat.dto'; 4 | import { Cat } from './cat.entity'; 5 | import { CONNECTION } from '../../tenancy/tenancy.symbols'; 6 | 7 | @Injectable() 8 | export class CatsService { 9 | private readonly catsRepository: Repository; 10 | 11 | constructor( 12 | @Inject(CONNECTION) connection: Connection, 13 | ) { 14 | this.catsRepository = connection.getRepository(Cat); 15 | } 16 | 17 | create(createCatDto: CreateCatDto): Promise { 18 | const cat = new Cat(); 19 | cat.name = createCatDto.name; 20 | 21 | return this.catsRepository.save(cat); 22 | } 23 | 24 | async findAll(): Promise { 25 | return this.catsRepository.find(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { TenantsModule } from './modules/public/tenants/tenants.module'; 4 | import { TenancyModule } from './modules/tenancy/tenancy.module'; 5 | import { CatsModule } from './modules/tenanted/cats/cats.module'; 6 | 7 | import * as ormconfig from './orm.config'; 8 | 9 | @Module({ 10 | imports: [ 11 | // TypeOrmModule.forRoot({ 12 | // type: 'postgres', 13 | // host: 'localhost', 14 | // port: 5432, 15 | // username: 'thomasvanderstraeten', 16 | // password: 'root', 17 | // database: 'nestjs-multi-tenant', 18 | // autoLoadEntities: true, 19 | // }), 20 | TypeOrmModule.forRoot(ormconfig), 21 | TenantsModule, 22 | TenancyModule, 23 | CatsModule 24 | ], 25 | }) 26 | export class AppModule {} 27 | -------------------------------------------------------------------------------- /src/modules/tenancy/tenancy.utils.ts: -------------------------------------------------------------------------------- 1 | import { Connection, createConnection, getConnectionManager } from 'typeorm'; 2 | import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; 3 | 4 | import * as tenantsOrmconfig from '../../tenants-orm.config'; 5 | 6 | export function getTenantConnection(tenantId: string): Promise { 7 | const connectionName = `tenant_${tenantId}`; 8 | const connectionManager = getConnectionManager(); 9 | 10 | if (connectionManager.has(connectionName)) { 11 | const connection = connectionManager.get(connectionName); 12 | return Promise.resolve(connection.isConnected ? connection : connection.connect()); 13 | } 14 | 15 | return createConnection({ 16 | ...(tenantsOrmconfig as PostgresConnectionOptions), 17 | name: connectionName, 18 | schema: connectionName, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { getConnection, getManager } from 'typeorm'; 4 | import { getTenantConnection } from './modules/tenancy/tenancy.utils'; 5 | import { tenancyMiddleware } from './modules/tenancy/tenancy.middleware'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | 10 | app.use(tenancyMiddleware); 11 | 12 | await getConnection().runMigrations() 13 | 14 | const schemas = await getManager().query('select schema_name as name from information_schema.schemata;'); 15 | 16 | for (let i = 0; i < schemas.length; i += 1) { 17 | const { name: schema } = schemas[i]; 18 | 19 | if (schema.startsWith('tenant_')) { 20 | const tenantId = schema.replace('tenant_', ''); 21 | const connection = await getTenantConnection(tenantId); 22 | await connection.runMigrations() 23 | await connection.close(); 24 | } 25 | } 26 | 27 | await app.listen(3000); 28 | } 29 | bootstrap(); 30 | -------------------------------------------------------------------------------- /src/modules/tenancy/tenancy.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module, Scope } from '@nestjs/common'; 2 | import { REQUEST } from '@nestjs/core'; 3 | import { Request as ExpressRequest } from 'express'; 4 | import { getTenantConnection } from './tenancy.utils'; 5 | 6 | import { CONNECTION } from './tenancy.symbols'; 7 | 8 | /** 9 | * Note that because of Scope Hierarchy, all injectors of this 10 | * provider will be request-scoped by default. Hence there is 11 | * no need for example to specify that a consuming tenant-level 12 | * service is itself request-scoped. 13 | * https://docs.nestjs.com/fundamentals/injection-scopes#scope-hierarchy 14 | */ 15 | const connectionFactory = { 16 | provide: CONNECTION, 17 | scope: Scope.REQUEST, 18 | useFactory: (request: ExpressRequest) => { 19 | const { tenantId } = request; 20 | 21 | if (tenantId) { 22 | return getTenantConnection(tenantId); 23 | } 24 | 25 | return null; 26 | }, 27 | inject: [REQUEST], 28 | }; 29 | 30 | @Global() 31 | @Module({ 32 | providers: [connectionFactory], 33 | exports: [CONNECTION], 34 | }) 35 | export class TenancyModule {} 36 | -------------------------------------------------------------------------------- /src/migrations/tenanted/1638963474130-AddCats.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface } from 'typeorm'; 2 | import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; 3 | import { PostgresQueryRunner } from 'typeorm/driver/postgres/PostgresQueryRunner'; 4 | 5 | export class AddCats1638963474130 implements MigrationInterface { 6 | // eslint-disable-next-line @typescript-eslint/typedef 7 | name = 'AddCats1638963474130'; 8 | 9 | public async up(queryRunner: PostgresQueryRunner): Promise { 10 | const { schema } = queryRunner.connection.options as PostgresConnectionOptions; 11 | 12 | await queryRunner.query( 13 | `CREATE TABLE "${schema}"."cats" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, CONSTRAINT "PK_${schema}_4873483882def9ba79ad5ccddbf" PRIMARY KEY ("id"))`, 14 | ); 15 | } 16 | 17 | public async down(queryRunner: PostgresQueryRunner): Promise { 18 | const { schema } = queryRunner.connection.options as PostgresConnectionOptions; 19 | 20 | await queryRunner.query(`DROP TABLE "${schema}"."cats"`); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/public/tenants/tenants.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { getTenantConnection } from '../../tenancy/tenancy.utils'; 4 | import { getManager, Repository } from 'typeorm'; 5 | import { CreateTenantDto } from './dto/create-tenant.dto'; 6 | import { Tenant } from './tenant.entity'; 7 | 8 | @Injectable() 9 | export class TenantsService { 10 | constructor( 11 | @InjectRepository(Tenant) 12 | private readonly tenantsRepository: Repository, 13 | ) {} 14 | 15 | async create(createTenantDto: CreateTenantDto): Promise { 16 | let tenant = new Tenant(); 17 | tenant.name = createTenantDto.name; 18 | 19 | tenant = await this.tenantsRepository.save(tenant); 20 | 21 | const schemaName = `tenant_${tenant.id}`; 22 | await getManager().query(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`); 23 | 24 | const connection = await getTenantConnection(`${tenant.id}`); 25 | await connection.runMigrations() 26 | await connection.close(); 27 | 28 | return tenant 29 | } 30 | 31 | async findAll(): Promise { 32 | return this.tenantsRepository.find(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/snake-naming.strategy.ts: -------------------------------------------------------------------------------- 1 | import { DefaultNamingStrategy, NamingStrategyInterface } from 'typeorm'; 2 | import { snakeCase } from 'typeorm/util/StringUtils'; 3 | 4 | export class SnakeNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface { 5 | tableName(className: string, customName: string): string { 6 | return customName || snakeCase(className); 7 | } 8 | 9 | columnName(propertyName: string, customName: string, embeddedPrefixes: string[]): string { 10 | return snakeCase(embeddedPrefixes.join('_')) + (customName || snakeCase(propertyName)); 11 | } 12 | 13 | relationName(propertyName: string): string { 14 | return snakeCase(propertyName); 15 | } 16 | 17 | joinColumnName(relationName: string, referencedColumnName: string): string { 18 | return snakeCase(`${relationName}_${referencedColumnName}`); 19 | } 20 | 21 | joinTableName(firstTableName: string, secondTableName: string, firstPropertyName: string): string { 22 | return snakeCase(`${firstTableName}_${firstPropertyName.replace(/\./gi, '_')}_${secondTableName}`); 23 | } 24 | 25 | joinTableColumnName(tableName: string, propertyName: string, columnName?: string): string { 26 | return snakeCase(`${tableName}_${columnName || propertyName}`); 27 | } 28 | 29 | classTableInheritanceParentColumnName(parentTableName: string, parentTableIdPropertyName: string): string { 30 | return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-typescript-starter", 3 | "version": "1.0.0", 4 | "description": "Nest TypeScript starter repository", 5 | "license": "MIT", 6 | "scripts": { 7 | "prebuild": "rimraf dist", 8 | "build": "nest build", 9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 10 | "start": "nest start", 11 | "start:dev": "nest start --watch", 12 | "start:debug": "nest start --debug --watch", 13 | "start:prod": "node dist/main", 14 | "lint": "eslint '{src,apps,libs,test}/**/*.ts' --fix", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:cov": "jest --coverage", 18 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 19 | "test:e2e": "echo 'No e2e tests implemented yet.'" 20 | }, 21 | "dependencies": { 22 | "@nestjs/common": "8.2.3", 23 | "@nestjs/core": "8.2.3", 24 | "@nestjs/platform-express": "8.2.3", 25 | "@nestjs/typeorm": "8.0.2", 26 | "pg": "^8.7.1", 27 | "reflect-metadata": "0.1.13", 28 | "rimraf": "3.0.2", 29 | "rxjs": "7.4.0", 30 | "typeorm": "0.2.41" 31 | }, 32 | "devDependencies": { 33 | "@nestjs/cli": "8.1.5", 34 | "@nestjs/schematics": "8.0.5", 35 | "@nestjs/testing": "8.2.3", 36 | "@types/express": "4.17.13", 37 | "@types/jest": "27.0.3", 38 | "@types/node": "16.11.11", 39 | "@types/supertest": "2.0.11", 40 | "@types/ws": "7.4.7", 41 | "@typescript-eslint/eslint-plugin": "4.33.0", 42 | "@typescript-eslint/parser": "4.33.0", 43 | "eslint": "7.32.0", 44 | "eslint-config-prettier": "8.3.0", 45 | "eslint-plugin-import": "2.25.3", 46 | "jest": "27.3.1", 47 | "prettier": "2.5.1", 48 | "supertest": "6.1.6", 49 | "ts-jest": "27.0.7", 50 | "ts-loader": "9.2.6", 51 | "ts-node": "10.4.0", 52 | "tsconfig-paths": "3.11.0", 53 | "typescript": "4.3.5" 54 | }, 55 | "jest": { 56 | "moduleFileExtensions": [ 57 | "js", 58 | "json", 59 | "ts" 60 | ], 61 | "rootDir": "src", 62 | "testRegex": ".spec.ts$", 63 | "transform": { 64 | "^.+\\.(t|j)s$": "ts-jest" 65 | }, 66 | "collectCoverageFrom": [ 67 | "**/*.(t|j)s" 68 | ], 69 | "coverageDirectory": "../coverage", 70 | "testEnvironment": "node" 71 | } 72 | } 73 | --------------------------------------------------------------------------------