Skip to content

Commit

Permalink
feat(backend): introduce User module with basic entity and signup
Browse files Browse the repository at this point in the history
  • Loading branch information
Skona27 committed Jul 27, 2021
1 parent 65b4e8a commit d128707
Show file tree
Hide file tree
Showing 12 changed files with 302 additions and 0 deletions.
17 changes: 17 additions & 0 deletions apps/backend/src/app/app.module.ts
@@ -1,10 +1,14 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AwsSdkModule } from 'nest-aws-sdk';
import { CognitoIdentityServiceProvider } from 'aws-sdk';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AppConfigService } from '../config/config.service';
import { AppConfigModule } from '../config/config.module';
import { UserModule } from '../user/user.module';
import { LoggerModule } from '../logger/logger.module';

@Module({
Expand All @@ -17,8 +21,21 @@ import { LoggerModule } from '../logger/logger.module';
return configService.databaseConfig;
},
}),
AwsSdkModule.forRootAsync({
defaultServiceOptions: {
useFactory: (configService: AppConfigService) => {
return {
region: configService.awsConfig.region,
};
},
inject: [AppConfigService],
imports: [AppConfigModule],
},
services: [CognitoIdentityServiceProvider],
}),
AppConfigModule,
LoggerModule,
UserModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
16 changes: 16 additions & 0 deletions apps/backend/src/common/model/base-editable.entity.ts
@@ -0,0 +1,16 @@
import {
CreateDateColumn,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';

export abstract class BaseEditableEntity {
@PrimaryGeneratedColumn('uuid')
id: string;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
9 changes: 9 additions & 0 deletions apps/backend/src/common/model/base-readonly.enity.ts
@@ -0,0 +1,9 @@
import { CreateDateColumn, PrimaryGeneratedColumn } from 'typeorm';

export abstract class BaseReadonlyEntity {
@PrimaryGeneratedColumn('uuid')
id: string;

@CreateDateColumn()
createdAt: Date;
}
15 changes: 15 additions & 0 deletions apps/backend/src/database/migrations/1627407734939-user.ts
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class user1627407734939 implements MigrationInterface {
name = 'user1627407734939';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "auth_id" character varying(40) NOT NULL, "email" character varying(255) NOT NULL, "first_name" character varying(80), "last_name" character varying(80), CONSTRAINT "UQ_56d00ec31dc3eed1c3f6bff4f58" UNIQUE ("auth_id"), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "user"`);
}
}
19 changes: 19 additions & 0 deletions apps/backend/src/user/dto/create-user.dto.ts
@@ -0,0 +1,19 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class CreateUserDto {
@IsString()
@IsNotEmpty()
firstName: string;

@IsString()
@IsNotEmpty()
lastName: string;

@IsEmail()
@IsNotEmpty()
email: string;

@IsString()
@IsNotEmpty()
password: string;
}
5 changes: 5 additions & 0 deletions apps/backend/src/user/error/create-user.error.ts
@@ -0,0 +1,5 @@
export class CreateUserError extends Error {
constructor(error: Error) {
super(`Failed to create a new user. ${error}`);
}
}
29 changes: 29 additions & 0 deletions apps/backend/src/user/user.controller.spec.ts
@@ -0,0 +1,29 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';

describe('UserController', () => {
let controller: UserController;

const userServiceMock = {
createUser: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
{
provide: UserService,
useValue: userServiceMock,
},
],
}).compile();

controller = module.get<UserController>(UserController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
59 changes: 59 additions & 0 deletions apps/backend/src/user/user.controller.ts
@@ -0,0 +1,59 @@
import {
Controller,
Post,
HttpStatus,
HttpException,
Body,
} from '@nestjs/common';
import { ApiResponse, ApiTags } from '@nestjs/swagger';

import { CognitoCreateUserError } from '../cognito/error/cognito-create-user.error';
import { CreateUserDto } from './dto/create-user.dto';
import { User } from './user.entity';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@Post()
@ApiTags('user')
@ApiResponse({
status: HttpStatus.CREATED,
description: 'User has been successfully createds',
})
@ApiResponse({
status: HttpStatus.SERVICE_UNAVAILABLE,
description: 'Third party service error',
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: 'Internal server error',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Bad request',
})
public async createUser(@Body() createUserDto: CreateUserDto): Promise<User> {
const { firstName, lastName, email, password } = createUserDto;
try {
return await this.userService.createUser(
firstName,
lastName,
email,
password,
);
} catch (error) {
if (error instanceof CognitoCreateUserError) {
throw new HttpException(
'Service temporary unavailable',
HttpStatus.SERVICE_UNAVAILABLE,
);
}
throw new HttpException(
'Internal server error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
29 changes: 29 additions & 0 deletions apps/backend/src/user/user.entity.ts
@@ -0,0 +1,29 @@
import { Column, Entity } from 'typeorm';
import { BaseEditableEntity } from '../common/model/base-editable.entity';

@Entity()
export class User extends BaseEditableEntity {
@Column({
name: 'auth_id',
type: 'varchar',
length: 40,
nullable: false,
unique: true,
})
public authId: string;

@Column({
name: 'email',
type: 'varchar',
length: 255,
nullable: false,
unique: true,
})
public email: string;

@Column({ name: 'first_name', type: 'varchar', length: 80, nullable: true })
public firstName: string;

@Column({ name: 'last_name', type: 'varchar', length: 80, nullable: true })
public lastName: string;
}
15 changes: 15 additions & 0 deletions apps/backend/src/user/user.module.ts
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { UserService } from './user.service';
import { UserController } from './user.controller';
import { CognitoModule } from '../cognito/cognito.module';
import { User } from './user.entity';
import { LoggerModule } from '../logger/logger.module';

@Module({
providers: [UserService],
controllers: [UserController],
imports: [TypeOrmModule.forFeature([User]), CognitoModule, LoggerModule],
})
export class UserModule {}
46 changes: 46 additions & 0 deletions apps/backend/src/user/user.service.spec.ts
@@ -0,0 +1,46 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';

import { User } from './user.entity';
import { UserService } from './user.service';
import { LoggerService } from '../logger/logger.service';
import { LoggerServiceMock } from '../logger/logger.service.mock';
import { CognitoService } from '../cognito/cognito.service';

describe('UserService', () => {
let service: UserService;

const userRepositoryMock = {
save: jest.fn((user: User) => Promise.resolve(user)),
};

const cognitoServiceMock = {
createUser: jest.fn((user) => Promise.resolve(user)),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: LoggerService,
useValue: LoggerServiceMock,
},
{
provide: getRepositoryToken(User),
useValue: userRepositoryMock,
},
{
provide: CognitoService,
useValue: cognitoServiceMock,
},
],
}).compile();

service = module.get<UserService>(UserService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
43 changes: 43 additions & 0 deletions apps/backend/src/user/user.service.ts
@@ -0,0 +1,43 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

import { User } from './user.entity';
import { LoggerService } from '../logger/logger.service';
import { CognitoService } from '../cognito/cognito.service';
import { CreateUserError } from './error/create-user.error';

@Injectable()
export class UserService {
constructor(
private readonly loggerService: LoggerService,
private readonly cognitoService: CognitoService,
@InjectRepository(User) private readonly usersRepository: Repository<User>,
) {
this.loggerService.setContext(UserService.name);
}

public async createUser(
firstName: string,
lastName: string,
email: string,
password: string,
): Promise<User> {
const user = new User();

user.firstName = firstName;
user.lastName = lastName;
user.email = email;

const authId = await this.cognitoService.createUser(user.email, password);

user.authId = authId;

try {
return await this.usersRepository.save(user);
} catch (error) {
this.loggerService.log(`Failed to create a new user. ${error}`);
throw new CreateUserError(error);
}
}
}

0 comments on commit d128707

Please sign in to comment.