Skip to content

Commit

Permalink
refactor(s3, redis): Dynamic module
Browse files Browse the repository at this point in the history
  • Loading branch information
son-daehyeon committed Aug 19, 2024
1 parent d7c071d commit 1298bfe
Show file tree
Hide file tree
Showing 11 changed files with 112 additions and 99 deletions.
36 changes: 29 additions & 7 deletions src/common/database/redis/redis.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import { Module } from '@nestjs/common';

import { DynamicModule, Module } from '@nestjs/common';
import { RedisService } from './service';

@Module({
providers: [RedisService],
exports: [RedisService],
})
export class RedisModule {}
export interface RedisModuleOptions {
group: string;
}

@Module({})
export class RedisModule {
static forRoot(options: RedisModuleOptions): DynamicModule {
return {
module: RedisModule,
providers: [
{
provide: 'REDIS_MODULE_OPTIONS_GROUP',
useValue: options.group,
},
{
provide: `REDIS_SERVICE_${options.group.toUpperCase()}`,
useClass: RedisService,
},
],
exports: [
{
provide: `REDIS_SERVICE_${options.group.toUpperCase()}`,
useClass: RedisService,
},
],
};
}
}
35 changes: 29 additions & 6 deletions src/common/s3/s3.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import { Module } from '@nestjs/common';
import { DynamicModule, Module } from '@nestjs/common';

import { S3Service } from './service';

@Module({
providers: [S3Service],
exports: [S3Service],
})
export class S3Module {}
export interface S3ModuleOptions {
directory: string;
}

@Module({})
export class S3Module {
static forRoot(options: S3ModuleOptions): DynamicModule {
return {
module: S3Module,
providers: [
{
provide: 'S3_MODULE_OPTIONS_DIRECTORY',
useValue: options.directory,
},
{
provide: `S3_SERVICE_${options.directory.toUpperCase()}`,
useClass: S3Service,
},
],
exports: [
{
provide: `S3_SERVICE_${options.directory.toUpperCase()}`,
useClass: S3Service,
},
],
};
}
}
15 changes: 3 additions & 12 deletions src/common/s3/service/s3.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';

Expand All @@ -17,12 +17,12 @@ import { extname } from 'path';
export class S3Service {
private readonly s3Client: S3Client;

public directory?: string;

constructor(
private readonly configService: ConfigService,

private readonly eventEmitter: EventEmitter2,

@Inject('S3_MODULE_OPTIONS_DIRECTORY') private readonly directory: string,
) {
this.s3Client = new S3Client({
region: configService.getOrThrow<string>('s3.region'),
Expand Down Expand Up @@ -91,13 +91,4 @@ export class S3Service {

return url.split(`.com/`)[1].substring(this.directory.length + 1);
}

sub(directory: string): S3Service {
if (this.directory) throw new Error('Directory already set');
const s3Service = new S3Service(this.configService, this.eventEmitter);
s3Service.directory = directory.endsWith('/')
? directory.substring(0, directory.length - 1)
: directory;
return s3Service;
}
}
30 changes: 9 additions & 21 deletions src/domain/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,18 @@ import { AuthService } from '@wink/auth/service';

import { MemberModule } from '@wink/member/member.module';

import { RedisModule, RedisService } from '@wink/redis';
import { RedisModule } from '@wink/redis';
import { MailModule } from '@wink/mail';

@Module({
imports: [MemberModule, MailModule, RedisModule],
controllers: [AuthController],
providers: [
AuthService,

{
provide: `${RedisService.name}-refresh`,
useFactory: (repository: RedisService) => repository.sub('auth:refresh'),
inject: [RedisService],
},
{
provide: `${RedisService.name}-code`,
useFactory: (repository: RedisService) => repository.sub('auth:code'),
inject: [RedisService],
},
{
provide: `${RedisService.name}-token`,
useFactory: (repository: RedisService) => repository.sub('auth:token'),
inject: [RedisService],
},
imports: [
MemberModule,
MailModule,
RedisModule.forRoot({ group: 'verify_code' }),
RedisModule.forRoot({ group: 'verify_token' }),
RedisModule.forRoot({ group: 'refresh_token' }),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}
26 changes: 13 additions & 13 deletions src/domain/auth/service/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ export class AuthService {
private readonly jwtService: JwtService,

private readonly memberRepository: MemberRepository,
@Inject(`${RedisService.name}-refresh`) private readonly redisRefreshRepository: RedisService,
@Inject(`${RedisService.name}-code`) private readonly redisCodeRepository: RedisService,
@Inject(`${RedisService.name}-token`) private readonly redisTokenRepository: RedisService,
@Inject('REDIS_SERVICE_VERIFY_CODE') private readonly verifyCodeService: RedisService,
@Inject('REDIS_SERVICE_VERIFY_TOKEN') private readonly verifyTokenService: RedisService,
@Inject('REDIS_SERVICE_REFRESH_TOKEN') private readonly refreshTokenService: RedisService,

private readonly eventEmitter: EventEmitter2,
private readonly mailService: MailService,
Expand Down Expand Up @@ -85,7 +85,7 @@ export class AuthService {

const refreshToken = await this.jwtService.signAsync({}, { expiresIn: this.refreshExpiresIn });

await this.redisRefreshRepository.ttl(
await this.refreshTokenService.ttl(
refreshToken,
member._id,
ms(this.refreshExpiresIn as StringValue),
Expand All @@ -97,11 +97,11 @@ export class AuthService {
}

async refresh({ refreshToken }: RefreshRequestDto): Promise<RefreshResponseDto> {
const memberId = await this.redisRefreshRepository.get(refreshToken);
const memberId = await this.refreshTokenService.get(refreshToken);
if (!memberId) {
throw new InvalidRefreshTokenException();
}
await this.redisRefreshRepository.delete(refreshToken);
await this.refreshTokenService.delete(refreshToken);

const member = await this.memberRepository.findById(memberId);
if (!member) {
Expand All @@ -118,7 +118,7 @@ export class AuthService {
{ expiresIn: this.refreshExpiresIn },
);

await this.redisRefreshRepository.ttl(
await this.refreshTokenService.ttl(
newRefreshToken,
memberId,
ms(this.refreshExpiresIn as StringValue),
Expand All @@ -130,7 +130,7 @@ export class AuthService {
}

async register({ name, studentId, password, verifyToken }: RegisterRequestDto): Promise<void> {
const email = await this.redisTokenRepository.get(verifyToken);
const email = await this.verifyTokenService.get(verifyToken);

if (!email) {
throw new InvalidVerifyTokenException();
Expand All @@ -149,7 +149,7 @@ export class AuthService {

const member = await this.memberRepository.save({ name, studentId, email, password: hash });

await this.redisTokenRepository.delete(verifyToken);
await this.verifyTokenService.delete(verifyToken);

this.mailService.sendTemplate(email, new RegisterCompleteTemplate(name)).then((_) => _);

Expand All @@ -165,24 +165,24 @@ export class AuthService {
.toString()
.padStart(6, '0');

await this.redisCodeRepository.ttl(email, code, 60 * 10);
await this.verifyCodeService.ttl(email, code, 60 * 10);

this.mailService.sendTemplate(email, new VerifyCodeTemplate(email, code)).then((_) => _);

this.eventEmitter.emit(SendCodeEvent.EVENT_NAME, new SendCodeEvent(email, code));
}

async verifyCode({ email, code }: VerifyCodeRequestDto): Promise<VerifyCodeResponseDto> {
const storedCode = await this.redisCodeRepository.get(email);
const storedCode = await this.verifyCodeService.get(email);

if (storedCode !== code) {
throw new InvalidVerifyCodeException();
}

await this.redisCodeRepository.delete(email);
await this.verifyCodeService.delete(email);

const verifyToken = uuid();
await this.redisTokenRepository.ttl(verifyToken, email, 60 * 60);
await this.verifyTokenService.ttl(verifyToken, email, 60 * 60);

this.eventEmitter.emit(VerifyCodeEvent.EVENT_NAME, new VerifyCodeEvent(email, verifyToken));

Expand Down
22 changes: 7 additions & 15 deletions src/domain/member/member.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,19 @@ import { MemberRepository } from '@wink/member/repository';
import { PurgeUnusedAvatarJob } from '@wink/member/util/scheduler';

import { MongoModelFactory } from '@wink/mongo';
import { S3Module, S3Service } from '@wink/s3';
import { S3Module } from '@wink/s3';
import { MailModule } from '@wink/mail';

const modelFactory = MongoModelFactory.generate<Member>(Member.name, MemberSchema);

@Module({
imports: [MongooseModule.forFeatureAsync([modelFactory]), S3Module, MailModule],
controllers: [MemberController, MemberAdminController],
providers: [
MemberService,
MemberAdminService,
MemberRepository,

PurgeUnusedAvatarJob,

{
provide: `${S3Service}-avatar`,
useFactory: (s3Service: S3Service) => s3Service.sub('avatar'),
inject: [S3Service],
},
imports: [
MongooseModule.forFeatureAsync([modelFactory]),
S3Module.forRoot({ directory: 'avatar' }),
MailModule,
],
controllers: [MemberController, MemberAdminController],
providers: [MemberService, MemberAdminService, MemberRepository, PurgeUnusedAvatarJob],
exports: [MemberRepository],
})
export class MemberModule {}
12 changes: 6 additions & 6 deletions src/domain/member/service/member.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import * as bcrypt from 'bcrypt';
export class MemberService {
constructor(
private readonly memberRepository: MemberRepository,
@Inject(`${S3Service}-avatar`) private readonly s3AvatarService: S3Service,
@Inject('S3_SERVICE_AVATAR') private readonly avatarService: S3Service,

private readonly eventEmitter: EventEmitter2,
) {}
Expand Down Expand Up @@ -84,13 +84,13 @@ export class MemberService {
): Promise<UpdateMyAvatarResponseDto> {
const { _id: id, avatar: original } = member;

const avatar = await this.s3AvatarService.upload(file);
const avatar = await this.avatarService.upload(file);
await this.memberRepository.updateAvatar(id, avatar);

if (original) {
const key = this.s3AvatarService.extractKeyFromUrl(original);
const key = this.avatarService.extractKeyFromUrl(original);

await this.s3AvatarService.delete(key);
await this.avatarService.delete(key);
}

this.eventEmitter.emit(UpdateMyAvatarEvent.EVENT_NAME, new UpdateMyAvatarEvent(member, avatar));
Expand All @@ -102,9 +102,9 @@ export class MemberService {
const { _id: id, avatar } = member;

if (avatar) {
const key = this.s3AvatarService.extractKeyFromUrl(avatar);
const key = this.avatarService.extractKeyFromUrl(avatar);

await this.s3AvatarService.delete(key);
await this.avatarService.delete(key);
await this.memberRepository.updateAvatar(id, null);

this.eventEmitter.emit(DeleteMyAvatarEvent.EVENT_NAME, new DeleteMyAvatarEvent(member));
Expand Down
9 changes: 5 additions & 4 deletions src/domain/member/util/scheduler/purge-unused-avatar.job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { PurgeUnusedAvatarEvent } from '@wink/event';
export class PurgeUnusedAvatarJob {
constructor(
private readonly memberRepository: MemberRepository,
@Inject('S3_SERVICE_AVATAR') private readonly avatarService: S3Service,

private readonly eventEmitter: EventEmitter2,
@Inject(`${S3Service}-avatar`) private readonly s3AvatarService: S3Service,
) {}

@Timeout(0)
Expand All @@ -29,13 +30,13 @@ export class PurgeUnusedAvatarJob {
const usedAvatars = (await this.memberRepository.findAll())
.map((member) => member.avatar)
.filter((avatar) => avatar !== null)
.map((avatar) => this.s3AvatarService.extractKeyFromUrl(avatar));
.map((avatar) => this.avatarService.extractKeyFromUrl(avatar));

const savedAvatars = await this.s3AvatarService.getKeys();
const savedAvatars = await this.avatarService.getKeys();

const unusedAvatars = savedAvatars.filter((a) => !usedAvatars.includes(a));

unusedAvatars.forEach((key) => this.s3AvatarService.delete(key).then((_) => _));
unusedAvatars.forEach((key) => this.avatarService.delete(key).then((_) => _));

this.eventEmitter.emit(
PurgeUnusedAvatarEvent.EVENT_NAME,
Expand Down
8 changes: 4 additions & 4 deletions test/member/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Readable } from 'stream';

describe('MemberService', () => {
let memberService: MemberService;
let s3AvatarService: S3Service;
let avatarService: S3Service;

let memoryMemberRepository: Member[];

Expand All @@ -25,7 +25,7 @@ describe('MemberService', () => {
({ memoryMemberRepository } = mock);

memberService = module.get<MemberService>(MemberService);
s3AvatarService = module.get<S3Service>(`${S3Service}-avatar`);
avatarService = module.get<S3Service>('S3_SERVICE_AVATAR');
});

afterEach(() => {
Expand Down Expand Up @@ -264,7 +264,7 @@ describe('MemberService', () => {
// Then
await expect(result).resolves.toStrictEqual({ avatar: fileUrl });
expect(memoryMemberRepository[0].avatar).toBe(fileUrl);
expect(s3AvatarService.delete).not.toHaveBeenCalled();
expect(avatarService.delete).not.toHaveBeenCalled();
});

it('Change avatar (not first time)', async () => {
Expand Down Expand Up @@ -298,7 +298,7 @@ describe('MemberService', () => {
// Then
await expect(result).resolves.toStrictEqual({ avatar: fileUrl });
expect(memoryMemberRepository[0].avatar).toBe(fileUrl);
expect(s3AvatarService.delete).toHaveBeenCalledWith(previousFileUrl);
expect(avatarService.delete).toHaveBeenCalledWith(previousFileUrl);
});
});
});
Loading

0 comments on commit 1298bfe

Please sign in to comment.