parent
687e54a3e4
commit
9252605b6d
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run format
|
||||
npm run format:check
|
||||
npm run lint
|
||||
npm run test
|
||||
|
|
|
@ -9,12 +9,13 @@
|
|||
"install:husky": "./node_modules/.bin/husky install",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest --passWithNoTests",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DatabaseModule } from './database/database.module';
|
||||
import * as Joi from 'joi';
|
||||
import { UsersModule } from './users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -15,6 +16,7 @@ import * as Joi from 'joi';
|
|||
}),
|
||||
}),
|
||||
DatabaseModule,
|
||||
UsersModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
class User {
|
||||
@PrimaryColumn({
|
||||
unique: true,
|
||||
type: 'bigint',
|
||||
})
|
||||
public discordId: string;
|
||||
|
||||
@Column({
|
||||
unique: true,
|
||||
})
|
||||
public username: string;
|
||||
}
|
||||
|
||||
export default User;
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { UsersService } from './users.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import User from './users.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
|
@ -0,0 +1,81 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { mockedUsersRepository, userModel } from '../utils/mocks/users.service';
|
||||
import User from './users.entity';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
describe('UsersService', () => {
|
||||
let service: UsersService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
UsersService,
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useValue: mockedUsersRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UsersService>(UsersService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('when getting a user by discordId', () => {
|
||||
beforeEach(() => {
|
||||
mockedUsersRepository.findOneBy.mockImplementationOnce(
|
||||
({ discordId }) => {
|
||||
if (discordId === userModel.discordId) {
|
||||
return userModel;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
});
|
||||
describe('and the user is matched', () => {
|
||||
it('should return the user', async () => {
|
||||
const fetchedUser = await service.getByDiscordId(userModel.discordId);
|
||||
expect(fetchedUser).toEqual(userModel);
|
||||
});
|
||||
});
|
||||
describe('and the user is not matched', () => {
|
||||
it('should throw an error', async () => {
|
||||
await expect(service.getByDiscordId('unknown')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getting a user by username', () => {
|
||||
beforeEach(() => {
|
||||
mockedUsersRepository.findOneBy.mockImplementationOnce(({ username }) => {
|
||||
if (username === userModel.username) {
|
||||
return userModel;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
describe('and the user is matched', () => {
|
||||
it('should return the user', async () => {
|
||||
const fetchedUser = await service.getByUserName(userModel.username);
|
||||
expect(fetchedUser).toEqual(userModel);
|
||||
});
|
||||
});
|
||||
describe('and the user is not matched', () => {
|
||||
it('should throw an error', async () => {
|
||||
await expect(service.getByUserName('unknown')).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when creating a user', () => {
|
||||
it('should return the user', async () => {
|
||||
mockedUsersRepository.create.mockResolvedValueOnce(userModel);
|
||||
const fetchedUser = await service.create(userModel);
|
||||
expect(fetchedUser).toEqual(userModel);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import User from './users.entity';
|
||||
import { NotFoundError } from '../utils/errors/notFound.error';
|
||||
import {
|
||||
AlreadyExistsError,
|
||||
extractDetailsQueryError,
|
||||
} from '../utils/errors/alreadyExists.error';
|
||||
import PostgresErrorCode from '../utils/errors/postgresErrorCode.enum';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private usersRepository: Repository<User>,
|
||||
) {}
|
||||
private static readonly logger: Logger = new Logger('UsersService');
|
||||
|
||||
async getByDiscordId(discordId: string) {
|
||||
const user = await this.usersRepository.findOneBy({ discordId });
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
throw new NotFoundError('User', 'discordId', discordId);
|
||||
}
|
||||
|
||||
async getByUserName(username: string) {
|
||||
const user = await this.usersRepository.findOneBy({ username });
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
throw new NotFoundError('User', 'username', username);
|
||||
}
|
||||
|
||||
async create(user: User) {
|
||||
try {
|
||||
const newUser = this.usersRepository.create(user);
|
||||
await this.usersRepository.save(newUser);
|
||||
UsersService.logger.log(`New user ${user.username} created`);
|
||||
return newUser;
|
||||
} catch (error) {
|
||||
if (error.code === PostgresErrorCode.UniqueViolation) {
|
||||
const details = extractDetailsQueryError(error.detail);
|
||||
throw new AlreadyExistsError('User', details.property, details.value);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
interface AlreadyExistsErrorDetails {
|
||||
property: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function extractDetailsQueryError(
|
||||
detail: string,
|
||||
): AlreadyExistsErrorDetails {
|
||||
const matches: string[] = detail
|
||||
.match(/\(.*?\)/g)
|
||||
.map((x) => x.replace(/[()]/g, ''));
|
||||
return {
|
||||
property: matches[0],
|
||||
value: matches[1],
|
||||
};
|
||||
}
|
||||
|
||||
export class AlreadyExistsError extends Error {
|
||||
constructor(entity: string, property: string, value: string) {
|
||||
super(`${entity} with ${property} ${value} already exists`);
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, AlreadyExistsError);
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.constructor.name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export class NotFoundError extends Error {
|
||||
constructor(entity: string, property: string, value: string) {
|
||||
super(`${entity} with ${property} ${value} not found`);
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, NotFoundError);
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.constructor.name;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
enum PostgresErrorCode {
|
||||
UniqueViolation = '23505',
|
||||
}
|
||||
|
||||
export default PostgresErrorCode;
|
|
@ -0,0 +1,12 @@
|
|||
import User from '../../users/users.entity';
|
||||
|
||||
export const userModel: User = {
|
||||
discordId: 'test',
|
||||
username: 'test',
|
||||
};
|
||||
|
||||
export const mockedUsersRepository = {
|
||||
findOneBy: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: () => undefined,
|
||||
};
|
Loading…
Reference in New Issue