Commit 5152dcd0 authored by Daffa 's avatar Daffa

initial commit for boiler plate

parent 7d14eb9e
Pipeline #2158 failed
# Database # # Database
DATABASE_DIALECT="mysql" DATABASE_DIALECT="postgres"
DATABASE_HOST=localhost DATABASE_HOST="localhost"
DATABASE_PORT=3306 DATABASE_PORT=5432
DATABASE_USERNAME=root DATABASE_USERNAME=
DATABASE_PASSWORD= DATABASE_PASSWORD=
DATABASE_NAME=nestjs_development DATABASE_NAME=
DATABASE_TIMEZONE="+07:00"
DATABASE_BENCHMARK=true
# only for managed DB
DB_SSL=false DB_SSL=false
# App # App
......
# NestJS Starter # NestJS Boilerplate
## Requirement A production-ready NestJS boilerplate with authentication, PostgreSQL, Sequelize ORM, and Redis integration.
1. Node.js 16.15.0
2. Git ## Features
3. PostgreSQL
- **Authentication**: JWT-based authentication with refresh tokens
- **Database**: PostgreSQL with Sequelize ORM
- **Caching**: Redis integration
- **Validation**: Class-validator for request validation
- **API Documentation**: Swagger/OpenAPI documentation
- **Email**: Mailgun integration for sending emails
- **File Upload**: Built-in file upload support
- **Security**: Helmet, CORS, rate limiting
- **Code Quality**: ESLint, Prettier
## Requirements
- Node.js 16.17.0+
- npm 8.15.0+
- PostgreSQL 12+
- Redis (optional, for caching)
- Git
## Installation ## Installation
1. Clone this project ### 1. Clone the repository
```bash
git clone <your-repo-url>
cd nest-boiler-plate
npm install
```
### 2. Environment Setup
Copy the example environment file and update the configuration:
```bash
cp .env.example .env
```
Update the following variables in `.env`:
- **Database**: `DATABASE_DIALECT`, `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`, `DATABASE_NAME`
- **Redis**: `REDIS_HOST`, `REDIS_PORT`
- **JWT**: Keys are already generated, but you can replace them
- **Mail**: `MAIL_API_KEY`, `MAIL_DOMAIN` (if using Mailgun)
### 3. Database Setup
Create the database:
```bash
npm run db:create
```
Run migrations:
```bash ```bash
$ git clone git@git.seni.cloud:nodejs/nestjs-starter.git npm run db:migrate
$ cd nestjs-starter
$ npm install
``` ```
2. Create Environment (Update Database dan Email Environment) (Optional) Seed the database:
```bash
npm run db:seed
```
## Development
Start the development server with hot-reload:
```bash
npm run start:dev
```
The API will be available at `http://localhost:3000`
## Production
Build the application:
```bash
npm run build
```
Start the production server:
```bash
npm run start:prod
```
## Database Commands
### Migrations
Create a new migration:
```bash ```bash
$ cd nestjs-starter npx sequelize migration:generate --name create-your-table-name
$ cp .env.example .env
``` ```
3. Create Database Run all pending migrations:
```bash ```bash
$ npm run db:create npm run db:migrate
``` ```
4. Migrate Database Check migration status:
```bash ```bash
$ npm run db:migrate npm run db:migrate:status
$ npm run db:seed
``` ```
## How To Start Develop Undo the last migration:
```bash
npm run db:migrate:undo
```
1. Run Application Reset database (undo all migrations and re-run them):
```bash ```bash
$ npm run start:dev npm run db:reset
``` ```
## How To Start Production ### Seeds
Create a new seed file:
```bash
npx sequelize seed:generate --name seed-name
```
1. Run Application Run all seeds:
```bash ```bash
$ npm run start:prod npm run db:seed
``` ```
## API Documentation ## API Documentation
Open the API documentation from browser: [http://localhost:3000/api-documentation](http://localhost:3000/api-documentation)
Once the application is running, access the Swagger documentation at:
```
http://localhost:3000/api-documentation
```
## Available Scripts
- `npm run start:dev` - Start development server with hot-reload
- `npm run start:prod` - Start production server
- `npm run build` - Build the application
- `npm run lint` - Run ESLint
- `npm run format` - Format code with Prettier
- `npm run test` - Run unit tests
- `npm run test:watch` - Run tests in watch mode
- `npm run test:cov` - Run tests with coverage
- `npm run db:create` - Create database
- `npm run db:migrate` - Run migrations
- `npm run db:migrate:status` - Check migration status
- `npm run db:migrate:undo` - Undo last migration
- `npm run db:reset` - Reset database (undo all and re-run)
- `npm run db:seed` - Run seeders
## Project Structure
```
src/
├── common/ # Shared utilities, constants, decorators
├── database/ # Database models, migrations, seeders
│ ├── migrations/ # Sequelize migrations
│ ├── models/ # Sequelize models
│ └── seeders/ # Database seeders
├── interceptors/ # Global interceptors
├── middleware/ # Custom middleware
├── modules/ # Feature modules
│ ├── auth/ # Authentication module
│ └── users/ # Users module
├── share/ # Shared services and guards
├── app.module.ts # Root module
└── main.ts # Application entry point
```
## Creating New Migrations
### Example: Create a products table
1. Generate migration file:
```bash
npx sequelize migration:generate --name create-products-table
```
2. Edit the generated file in `src/database/migrations/`:
```javascript
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('products', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: Sequelize.STRING,
allowNull: false
},
description: {
type: Sequelize.TEXT,
allowNull: true
},
price: {
type: Sequelize.DECIMAL(10, 2),
allowNull: false
},
createdAt: {
type: Sequelize.DATE,
field: 'created_at',
allowNull: false,
defaultValue: Sequelize.fn('NOW')
},
updatedAt: {
type: Sequelize.DATE,
field: 'updated_at',
allowNull: false,
defaultValue: Sequelize.fn('NOW')
}
});
await queryInterface.addIndex('products', ['name']);
},
async down(queryInterface) {
await queryInterface.dropTable('products');
}
};
```
3. Run the migration:
```bash
npm run db:migrate
```
## Contributors
- Daffa Praramadhana - [daffa.praramadhana@mdd.co.id](mailto:daffa.praramadhana@mdd.co.id)
## License
Copyright © 2025 Multidaya Dinamika. All rights reserved.
This diff is collapsed.
'use strict';
module.exports = {
async up (queryInterface, Sequelize) {
await queryInterface.createTable('roles', {
id: {
type: Sequelize.BIGINT,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: Sequelize.STRING(50),
allowNull: false
},
displayName: {
type: Sequelize.STRING,
field: 'display_name',
allowNull: true,
},
active: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
createdAt: {
type: Sequelize.DATE,
field: 'created_at',
allowNull: false,
defaultValue:Sequelize.fn('NOW'),
},
updatedAt: {
type: Sequelize.DATE,
field: 'updated_at',
allowNull: false,
defaultValue:Sequelize.fn('NOW'),
},
deletedAt: {
type: Sequelize.DATE,
field: 'deleted_at',
allowNull: true,
defaultValue: null
}
});
},
async down (queryInterface) {
await queryInterface.dropTable('roles');
}
};
...@@ -29,11 +29,6 @@ module.exports = { ...@@ -29,11 +29,6 @@ module.exports = {
allowNull: false, allowNull: false,
defaultValue: false, defaultValue: false,
}, },
roleId: {
type: Sequelize.BIGINT,
field: 'role_id',
references: { model: 'roles', key: 'id' }
},
emailVerificationSentAt: { emailVerificationSentAt: {
type: Sequelize.DATE, type: Sequelize.DATE,
field: 'email_verification_sent_at', field: 'email_verification_sent_at',
...@@ -63,9 +58,8 @@ module.exports = { ...@@ -63,9 +58,8 @@ module.exports = {
}); });
await queryInterface.addIndex('users', ['email']); await queryInterface.addIndex('users', ['email']);
await queryInterface.addIndex('users', ['phone_number']); await queryInterface.addIndex('users', ['phone_number']);
await queryInterface.addIndex('users', ['active']); await queryInterface.addIndex('users', ['active']);
await queryInterface.addIndex('users', ['role_id']);
}, },
async down (queryInterface) { async down (queryInterface) {
......
...@@ -2,32 +2,17 @@ import { ...@@ -2,32 +2,17 @@ import {
AllowNull, AllowNull,
BeforeCreate, BeforeCreate,
BeforeUpdate, BeforeUpdate,
BelongsTo,
Column, Column,
DefaultScope, DefaultScope,
DeletedAt, DeletedAt,
ForeignKey,
HasMany,
HasOne,
Scopes,
Table, Table,
Unique, Unique,
} from "sequelize-typescript"; } from "sequelize-typescript";
import { generateHash } from "src/common/hash"; import { generateHash } from "src/common/hash";
import { TimestampModel } from "src/database/models/timestamp.model"; import { TimestampModel } from "src/database/models/timestamp.model";
import { Role } from "./role.model";
@DefaultScope(() => ({ @DefaultScope(() => ({
attributes: { exclude: ["password","deletedAt"] }, attributes: { exclude: ["password","deletedAt"] },
include: [Role],
}))
@Scopes(() => ({
role: () => ({
include: [
{model: Role.unscoped(), attributes: ['id', 'name', 'displayName', 'active']}
],
})
})) }))
@Table({ timestamps: true, tableName: "users" }) @Table({ timestamps: true, tableName: "users" })
...@@ -54,10 +39,6 @@ export class User extends TimestampModel { ...@@ -54,10 +39,6 @@ export class User extends TimestampModel {
@Column({ defaultValue: false }) @Column({ defaultValue: false })
active: boolean; active: boolean;
@ForeignKey(() => Role)
@Column({ field: "role_id" })
roleId: number;
@Column({ field: "email_verification_sent_at" }) @Column({ field: "email_verification_sent_at" })
emailVerificationSentAt: Date; emailVerificationSentAt: Date;
...@@ -68,9 +49,6 @@ export class User extends TimestampModel { ...@@ -68,9 +49,6 @@ export class User extends TimestampModel {
@Column({ field: "deleted_at" }) @Column({ field: "deleted_at" })
deletedAt?: Date; deletedAt?: Date;
@BelongsTo(() => Role)
role: Role;
@BeforeCreate @BeforeCreate
static hashPassword(instance: User) { static hashPassword(instance: User) {
if (instance.password) { if (instance.password) {
......
...@@ -48,7 +48,6 @@ export class AuthController { ...@@ -48,7 +48,6 @@ export class AuthController {
const token = await this.authService.createAccessToken({ const token = await this.authService.createAccessToken({
user: user.id, user: user.id,
role: user.roleId,
}); });
const data_user = await this.userService.findOne(user.id); const data_user = await this.userService.findOne(user.id);
......
...@@ -77,7 +77,6 @@ export class AuthService { ...@@ -77,7 +77,6 @@ export class AuthService {
} }
async createAccessToken(data: { async createAccessToken(data: {
role: number;
user: number; user: number;
}): Promise<TokenDto> { }): Promise<TokenDto> {
...@@ -89,7 +88,6 @@ export class AuthService { ...@@ -89,7 +88,6 @@ export class AuthService {
accessToken: await this.jwtService.signAsync({ accessToken: await this.jwtService.signAsync({
type: this.appConfigService.jwt().accessTokenType, type: this.appConfigService.jwt().accessTokenType,
user: data.user, user: data.user,
role: data.role,
}, { }, {
expiresIn: expiresIn, expiresIn: expiresIn,
}), }),
......
import { ApiProperty } from "@nestjs/swagger";
import { IsDefined } from "class-validator";
export class assignPermissionDto {
@ApiProperty({
example: [1, 2],
required: true,
})
@IsDefined()
permissionIds: any;
}
import {
IsDefined,
IsString,
IsBoolean,
IsEnum,
IsOptional,
} from "class-validator";
import { Transform } from "class-transformer";
import { ApiProperty } from "@nestjs/swagger";
import { toBoolean } from "src/common/cast";
import { ROLE_EXCEPT_SUPER_ENUM } from "./../../../common/constant";
export class CreateRoleDto {
@ApiProperty({ example: "Admin", description: "The role name" })
@IsDefined()
@IsString()
@IsEnum(ROLE_EXCEPT_SUPER_ENUM, {
message: "Nama role harus bernilai Admin atau Patient"
})
name: ROLE_EXCEPT_SUPER_ENUM;
@ApiProperty({ example: "Admin", description: "The role display name" })
@IsDefined()
@IsString()
displayName: string;
@ApiProperty({
example: true,
description: "Set true if you want to activate this role",
required: false,
})
@Transform(({ value }) => toBoolean(value))
@IsOptional()
@IsBoolean()
active?: boolean;
}
import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsOptional } from "class-validator";
import { Op } from "sequelize";
import { QueryDto } from "src/common/dto/query.dto";
export class PermissionQueryDto extends QueryDto {
@ApiProperty({ required: false })
@IsOptional()
@Transform(({ value }) => ({ [Op.iLike]: `%${value}%` }))
name: string;
}
import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer";
import { IsOptional } from "class-validator";
import { Op } from "sequelize";
import { toBoolean } from "src/common/cast";
import { QueryDto } from "src/common/dto/query.dto";
export class RoleQueryDto extends QueryDto {
@ApiProperty({ required: false })
@IsOptional()
@Transform(({ value }) => ({ [Op.iLike]: `%${value}%` }))
name: string;
@ApiProperty({ required: false })
@Transform(({ value }) => toBoolean(value))
@IsOptional()
active?: boolean;
}
import {
IsDefined,
IsInt,
IsBoolean,
} from "class-validator";
import { Transform, Type } from "class-transformer";
import { ApiProperty } from "@nestjs/swagger";
import { toBoolean } from "src/common/cast";
export class UpdateRolePermissionDto {
@ApiProperty({ example: "1", description: "id of permissions you want add" })
@IsDefined()
@IsInt()
permission_id: number;
@ApiProperty({
example: true,
description: "Set true if you want to activate this role",
required: false,
})
@Transform(({ value }) => toBoolean(value))
@IsDefined()
@IsBoolean()
active?: boolean;
}
import {
IsString,
IsBoolean,
IsEnum,
IsOptional,
} from "class-validator";
import { Transform, Type } from "class-transformer";
import { ApiProperty } from "@nestjs/swagger";
import { toBoolean } from "src/common/cast";
import { ROLE_ENUM } from "./../../../common/constant";
export class UpdateRoleDto {
@ApiProperty({ example: "admin", description: "The role name" })
@IsOptional()
@IsString()
@IsEnum(ROLE_ENUM)
name: ROLE_ENUM;
@ApiProperty({ example: "admin", description: "The role display name" })
@IsOptional()
@IsString()
displayName: string;
@ApiProperty({
example: true,
description: "Set true if you want to activate this role",
required: false,
})
@Transform(({ value }) => toBoolean(value))
@IsOptional()
@IsBoolean()
active?: boolean;
}
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Put,
Query,
UseGuards
} from "@nestjs/common";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { JwtAuthGuard } from "../auth/guards/jwt-auth.guard";
import { IMeta } from "src/common/interfaces/IMeta";
import { CreateRoleDto } from "./dto/create-role.dto";
import { RoleQueryDto } from "./dto/role.query.dto";
import { Roles } from "../../share/decorators/roles.decorator";
import { RolesGuard } from "../../share/guards/roles.guard";
import { RolesService } from "./roles.service";
import { Role } from "src/database/models/role.model";
import { UpdateRoleDto } from "./dto/update-role.dto";
import { UpdateRolePermissionDto } from "./dto/update-role-permission.dto";
import { assignPermissionDto } from "./dto/assign-permission.dto";
@ApiTags("roles")
@ApiBearerAuth()
@Controller("roles")
export class RolesController {
constructor(
private readonly rolesService: RolesService
) {}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get()
@Roles("role.view")
@HttpCode(HttpStatus.OK)
async findAll(
@Query() query: RoleQueryDto
): Promise<{ meta: IMeta; data: Role[] }> {
const data = await this.rolesService.findAll(query);
const count = await this.rolesService.count(query);
const { page, limit, orderBy, order } = query;
return {
meta: {
totalData: count,
totalPage: Math.ceil(count / query.limit),
page,
limit,
orderBy,
order,
},
data,
};
}
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin')
@Get(":id")
@Roles("role.view")
@HttpCode(HttpStatus.OK)
find(@Param("id") id: number): Promise<Role> {
return this.rolesService.findOne(+id);
}
}
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { InjectModel } from "@nestjs/sequelize";
import { Op } from "sequelize";
import * as _ from "lodash";
import { CreateRoleDto } from "./dto/create-role.dto";
import { RoleQueryDto } from "./dto/role.query.dto";
import { UsersService } from "./users.service";
import { Role } from "src/database/models/role.model";
import { Permission } from "src/database/models/permission.model";
import { UpdateRoleDto } from "./dto/update-role.dto";
import { UpdateRolePermissionDto } from "./dto/update-role-permission.dto";
import { assignPermissionDto } from "./dto/assign-permission.dto";
import { ROLE_ENUM } from "./../../common/constant";
import { error_message } from "src/common/error_message";
@Injectable()
export class RolesService {
constructor(
@InjectModel(Role)
private readonly roleModel: typeof Role,
@InjectModel(Permission)
private readonly permissionModel: typeof Permission,
private readonly usersService: UsersService
) { }
async create(payload: CreateRoleDto): Promise<Role> {
if (payload.displayName) {
await this.validateRole(payload)
}
return this.roleModel.create({
name: payload.name,
displayName: payload.displayName,
active: payload.active,
});
}
async count(query?: RoleQueryDto): Promise<number> {
return this.roleModel.unscoped().count({ where: this.createWhere(query) });
}
findAll(query?: RoleQueryDto): Promise<Role[]> {
const { page, limit, order, orderBy } = query;
return this.roleModel.findAll({
where: this.createWhere(query),
limit,
offset: (page - 1) * limit,
order: [[orderBy, order]],
});
}
async findOne(id: number): Promise<Role> {
const data = await this.roleModel.findOne({
where: {
id,
},
});
if (!data) throw new NotFoundException(error_message.not_found);
return data;
}
private async validateRole(payload) {
const whereRole = [];
if (payload.role) {
whereRole[0] = {
id: {[Op.ne]: payload.role.id}
}
whereRole[1] = {
displayName: {[Op.eq]: payload.displayName}
}
} else {
whereRole[0] = {
displayName: {[Op.eq]: payload.displayName}
}
}
const existRoleByName = await this.roleModel.count({
where: {
[Op.and]: whereRole,
}
});
if (existRoleByName) {
throw new BadRequestException('Nama display role tidak boleh sama.');
}
}
async update(id: number, payload: UpdateRoleDto): Promise<Role> {
const role = await this.findOne(id);
if (!role) throw new NotFoundException("Role not found");
if (payload.displayName != role.displayName) {
Object.assign(payload, {role: role})
await this.validateRole(payload)
}
await role.update(payload);
return role;
}
async assignPermission(id: number, payload: assignPermissionDto): Promise<Role> {
const permissionData = await this.permissionModel.findAll({
where: {
id: payload.permissionIds,
superadmin: false
}
}).then((permissions) => {
const result = [];
permissions.forEach(async permission => {
result.push({
permissionId: permission.id,
roleId: id
})
});
return result;
});
return await this.findOne(id);
}
async findSuperAdmin(): Promise<Role> {
const superAdmin = await this.roleModel.findOne({where: {name: ROLE_ENUM.SUPERADMIN}})
return superAdmin;
}
async delete(id: number): Promise<void> {
// user
const users = await this.usersService.findNoScope({
where: {
roleId: id,
},
});
if (users && users.length) {
throw new NotFoundException("Role telah digunakan");
}
const role = await this.findOne(id);
if (!role) throw new NotFoundException("Role not found");
await role.destroy();
}
private createWhere(conditions) {
const { page, limit, order, orderBy, ...rest } = conditions; // eslint-disable-line
const where = _.isEmpty(rest) ? {} : { [Op.and]: { ...rest } };
return where;
}
}
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { SequelizeModule } from "@nestjs/sequelize"; import { SequelizeModule } from "@nestjs/sequelize";
import { Permission } from "src/database/models/permission.model";
import { Role } from "src/database/models/role.model";
import { User } from "../../database/models/user.model"; import { User } from "../../database/models/user.model";
import { RolesController } from "./roles.controller";
import { RolesService } from "./roles.service";
import { UsersController } from "./users.controller"; import { UsersController } from "./users.controller";
import { UsersService } from "./users.service"; import { UsersService } from "./users.service";
import { IsUserEmailUniqueRule } from "./validators/is_user_email_unique"; import { IsUserEmailUniqueRule } from "./validators/is_user_email_unique";
...@@ -14,21 +10,17 @@ import { IsUserPhoneUniqueRule } from "./validators/is_user_phone_unique"; ...@@ -14,21 +10,17 @@ import { IsUserPhoneUniqueRule } from "./validators/is_user_phone_unique";
imports: [ imports: [
SequelizeModule.forFeature([ SequelizeModule.forFeature([
User, User,
Role,
Permission,
]) ])
], ],
providers: [ providers: [
UsersService, UsersService,
RolesService,
// Validator // Validator
IsUserEmailUniqueRule, IsUserEmailUniqueRule,
IsUserPhoneUniqueRule IsUserPhoneUniqueRule
], ],
controllers: [ controllers: [
UsersController, UsersController,
RolesController,
], ],
exports: [UsersService], exports: [UsersService],
}) })
......
import { Injectable } from "@nestjs/common";
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
import { Role } from "src/database/models/role.model";
@ValidatorConstraint({ name: 'IsExistRole', async: true })
@Injectable()
export class IsExistRoleRule implements ValidatorConstraintInterface {
async validate(roleId: number): Promise<boolean> {
return await Role.findByPk(roleId).then(role => {
return role
}) !== null
}
defaultMessage() {
return `Role tidak ditemukan`;
}
}
export function IsExistRole(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsExistRoleRule,
})
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment