feat: init project and add user module

This commit is contained in:
2026-02-17 12:29:31 +03:30
commit b9779f7cb6
29 changed files with 11774 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
import { Module } from '@nestjs/common';
import { UsersModule } from './modules/users/users.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import appConfig from './config/app.config';
import environmentValidation from './config/environment.validation';
import { TypeOrmModule } from '@nestjs/typeorm';
import databaseConfig from './config/database.config';
import { HeaderResolver, I18nJsonLoader, I18nModule } from 'nestjs-i18n';
import path from 'path';
const ENV = process.env.NODE_ENV;
@Module({
imports: [
/**
* Local Modules
*/
UsersModule,
/**
* Third-party Modules
*/
// I18n
I18nModule.forRoot({
fallbackLanguage: 'fa',
loader: I18nJsonLoader,
loaderOptions: {
path: path.join(__dirname, '/i18n/'),
watch: true,
},
resolvers: [{ use: HeaderResolver, options: ['lang'] }],
}),
// Config
ConfigModule.forRoot({
isGlobal: true,
envFilePath: !ENV ? '.env' : `.env.${ENV}`,
load: [appConfig, databaseConfig],
validationSchema: environmentValidation,
}),
// Typeorm
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get('database.host'),
port: +configService.get('database.port'),
username: configService.get('database.user'),
password: configService.get('database.password'),
database: configService.get('database.name'),
autoLoadEntities: configService.get<boolean>('database.autoLoad'),
synchronize: configService.get<boolean>('database.synchronize'),
}),
}),
],
})
export class AppModule {}
+7
View File
@@ -0,0 +1,7 @@
import * as StringUtils from './string-utils';
const Utils = {
StringUtils,
};
export default Utils;
+9
View File
@@ -0,0 +1,9 @@
export const normalizePhone = (phone: string): string => {
phone = phone.replace(/\D/g, '');
if (phone.startsWith('9')) {
return '0' + phone;
}
return phone;
};
+5
View File
@@ -0,0 +1,5 @@
import { registerAs } from '@nestjs/config';
export default registerAs('app', () => ({
environment: process.env.NODE_ENV || 'production',
}));
+11
View File
@@ -0,0 +1,11 @@
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT || '5432'),
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASS,
name: process.env.DATABASE_NAME,
autoLoad: process.env.DATABASE_AUTOLOAD === 'true' ? true : false,
synchronize: process.env.DATABASE_SYNC === 'true' ? true : false,
}));
+14
View File
@@ -0,0 +1,14 @@
import Joi from 'joi';
export default Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'test', 'production', 'staging')
.default('development'),
DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.number().port().default(5432),
DATABASE_USER: Joi.string().required(),
DATABASE_PASS: Joi.string().required(),
DATABASE_NAME: Joi.string().required(),
DATABASE_AUTOLOAD: Joi.boolean().default('true'),
DATABASE_SYNC: Joi.boolean().default('true'),
});
+11
View File
@@ -0,0 +1,11 @@
{
"fields": {
"firstName": "Firstname",
"lastName": "Lastname",
"phone": "Phone",
"password": "Password"
},
"errors": {
"passwordPattern": "Password should contain atleast 8 characters, including letters, numbers and symbols."
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"requiredField": "{field} is required.",
"minLengthField": "{field} should contain atleast {min} characters.",
"maxLengthField": "{field} should contain maximum {max} characters.",
"wrongField": "{field} is not correct."
}
+11
View File
@@ -0,0 +1,11 @@
{
"fields": {
"firstName": "نام",
"lastName": "نام خانوادگی",
"phone": "شماره موبایل",
"password": "رمز عبور"
},
"errors": {
"passwordPattern": "رمز عبور باید حداقل شامل 8 کاراکتر شامل حروف، اعداد و نشانه ها باشد."
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"requiredField": "{field} اجباری است.",
"minLengthField": "{field} باید حداقل شامل {min} کاراکتر باشد.",
"maxLengthField": "{field} باید حداکثر شامل {max} کاراکتر باشد.",
"wrongField": "{field} معتبر نیست."
}
+36
View File
@@ -0,0 +1,36 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { I18nValidationPipe, I18nValidationExceptionFilter } from 'nestjs-i18n';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new I18nValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
app.useGlobalFilters(new I18nValidationExceptionFilter());
const config = new DocumentBuilder()
.setTitle('Borna API')
.setDescription('Borna API Documentation')
.setVersion('1.0')
.setLicense('MIT', 'https://opensource.org/license/mit')
.addServer('http://localhost:3000')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/v1', app, document);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
+114
View File
@@ -0,0 +1,114 @@
import { Transform } from 'class-transformer';
import {
IsNotEmpty,
IsString,
Matches,
MaxLength,
MinLength,
} from 'class-validator';
import Utils from '@/common/utils';
import { i18nValidationMessage as t } from 'nestjs-i18n';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDTO {
@ApiProperty({
type: 'string',
description: `User's Firstname`,
example: 'Radmehr',
required: true,
})
@IsString()
@IsNotEmpty({
message: t('validation.requiredField', {
field: '$t(users.fields.firstName)',
}),
})
@MinLength(3, {
message: t('validation.minLengthField', {
field: '$t(users.fields.firstName)',
min: 3,
}),
})
@MaxLength(96, {
message: t('validation.maxLengthField', {
field: '$t(users.fields.firstName)',
max: 96,
}),
})
firstName: string;
@ApiProperty({
type: 'string',
description: `User's Lastname`,
example: 'Tarnas',
required: true,
})
@IsString()
@IsNotEmpty({
message: t('validation.requiredField', {
field: '$t(users.fields.lastName)',
}),
})
@MinLength(3, {
message: t('validation.minLengthField', {
field: '$t(users.fields.lastName)',
min: 3,
}),
})
@MaxLength(96, {
message: t('validation.maxLengthField', {
field: '$t(users.fields.lastName)',
min: 3,
}),
})
lastName: string;
@ApiProperty({
type: 'string',
description: `User's Phone Number`,
example: '09121111111',
required: true,
})
@IsString()
@IsNotEmpty({
message: t('validation.requiredField', {
field: '$t(users.fields.phone)',
}),
})
@Matches(/^(?:09|9)[0-9]{9}$/, {
message: t('validation.wrongField', {
field: '$t(users.fields.phone)',
}),
})
@Transform(({ value }) => Utils.StringUtils.normalizePhone(value as string))
phone: string;
@ApiProperty({
type: 'string',
description: 'Password for User Account',
example: 'Password1@',
required: true,
})
@IsString()
@IsNotEmpty({
message: t('validation.requiredField', {
field: '$t(users.fields.password)',
}),
})
@MinLength(8, {
message: t('validation.minLengthField', {
field: '$t(users.fields.password)',
min: 8,
}),
})
@MaxLength(96, {
message: t('validation.maxLengthField', {
field: '$t(users.fields.password)',
min: 96,
}),
})
@Matches(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[^A-Za-z\d]).{8,}$/, {
message: t('users.errors.passwordPattern'),
})
password: string;
}
@@ -0,0 +1,3 @@
GET http://localhost:3000/users
GET http://localhost:3000/users/2
@@ -0,0 +1,10 @@
POST http://localhost:3000/users
Content-Type: application/json
lang: fa
{
"firstName": "Ra",
"lastName": "Tnas",
"phone": "9333026363",
"password": "ra"
}
@@ -0,0 +1,31 @@
import { Injectable, RequestTimeoutException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from '../user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { CreateUserDTO } from '../dto/create-user.dto';
@Injectable()
export class UsersService {
constructor(
/**
* Injecting User Repository
*/
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
public async createOne(createUserDto: CreateUserDTO) {
const newUser = this.userRepository.create(createUserDto);
try {
await this.userRepository.save(newUser);
} catch (err) {
throw new RequestTimeoutException(
'Unable to process your request at the moment. Please try again later.',
{ cause: err, description: 'Error connecting to the database' },
);
}
return newUser;
}
}
+48
View File
@@ -0,0 +1,48 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: 'varchar',
length: 96,
nullable: false,
})
firstName: string;
@Column({
type: 'varchar',
length: 96,
nullable: false,
})
lastName: string;
@Column({
type: 'varchar',
length: 11,
nullable: false,
unique: true,
})
phone: string;
@Column({
type: 'varchar',
length: 96,
nullable: false,
})
password: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
+35
View File
@@ -0,0 +1,35 @@
import {
Body,
Controller,
Get,
Param,
ParseIntPipe,
Post,
} from '@nestjs/common';
import { CreateUserDTO } from './dto/create-user.dto';
import { UsersService } from './providers/users.service';
@Controller('users')
export class UsersController {
constructor(
/**
* Injecting Users Service
*/
private readonly usersService: UsersService,
) {}
@Get()
public getAllUsers() {
return ['user1', 'user2'];
}
@Get(':id')
public getUserById(@Param('id', ParseIntPipe) id: number) {
return `user ${id}`;
}
@Post()
public async createUser(@Body() createUserDto: CreateUserDTO) {
return await this.usersService.createOne(createUserDto);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './providers/users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}