Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { EventHandlersModule } from './event-handlers/event-handlers.module';
rejectUnauthorized: false,
},
},
// subscribers: [ImageQuerySubscriber], // TODO - move to tests.module
}),
UserModule,
FundingModule,
Expand Down
60 changes: 32 additions & 28 deletions src/features/comment/comment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { GiftogetherExceptions } from 'src/filters/giftogether-exception';
import { ValidCheck } from 'src/util/valid-check';
import { ImageInstanceManager } from '../image/image-instance-manager';
import { ImageType } from 'src/enums/image-type.enum';
import { SelectQueryBuilder } from 'typeorm/browser';

function convertToGetCommentDto(comment: Comment): GetCommentDto {
const { comId, content, regAt, isMod, authorId, author } = comment;
Expand Down Expand Up @@ -51,15 +52,16 @@ export class CommentService {
if (!funding) {
throw this.g2gException.FundingNotExists;
}
const author = await this.userRepository.findOne({
where: { userId: user.userId! },
});
const authorQb = this.userRepository
.createQueryBuilder('author')
.where('author.userId = :userId', { userId: user.userId! });

this.imageInstanceManager.mapImage(authorQb);

const author = await authorQb.getOne();
if (!author) {
throw this.g2gException.UserNotFound;
}
author.image = await this.imageInstanceManager
.getImages(author)
.then((images) => images[0]);

const newComment = new Comment({
funding,
Expand Down Expand Up @@ -94,20 +96,12 @@ export class CommentService {
{ isDel: false },
)
.leftJoinAndSelect('comment.author', 'author')
.leftJoinAndMapOne(
'author.image', // map to property 'image' of 'author'
'image', // property name of 'author'
'authorImage', // alias of 'image' table
`
(author.defaultImgId IS NOT NULL AND authorImage.imgId = author.defaultImgId)
OR
(author.defaultImgId IS NULL AND authorImage.subId = author.userId AND authorImage.imgType = :imgType)
`,
{ imgType: ImageType.User },
)
.where('funding.fundUuid = :fundUuid', { fundUuid })
.orderBy('comment.regAt', 'DESC');

this.imageInstanceManager.mapImage(fundingQb, 'author');
// this.imageInstanceManager.mapImage(fundingQb, 'funding'); // 이런 식으로 매핑하고 싶은 엔티티 alias를 붙여주면 됩니다.

const funding = await fundingQb.getOne();
if (!funding) {
throw this.g2gException.FundingNotExists;
Expand All @@ -124,10 +118,17 @@ export class CommentService {
): Promise<GetCommentDto> {
const { content } = updateCommentDto;

const comment = await this.commentRepository.findOne({
relations: { funding: true, author: true },
where: { comId, funding: { fundUuid } },
});
const commentQb = this.commentRepository
.createQueryBuilder('comment')
.leftJoinAndSelect('comment.funding', 'funding')
.leftJoinAndSelect('comment.author', 'author')
.where('comment.comId = :comId AND funding.fundUuid = :fundUuid', {
comId,
fundUuid,
});
this.imageInstanceManager.mapImage(commentQb, 'author');

const comment = await commentQb.getOne();
if (!comment) {
throw this.g2gException.CommentNotFound;
}
Expand All @@ -149,10 +150,16 @@ export class CommentService {
* soft delete
*/
async remove(user: Partial<User>, fundUuid: string, comId: number) {
const comment = await this.commentRepository.findOne({
relations: { funding: true, author: true },
where: { comId, funding: { fundUuid }, isDel: false },
});
const commentQb = this.commentRepository
.createQueryBuilder('comment')
.leftJoinAndSelect('comment.funding', 'funding')
.leftJoinAndSelect('comment.author', 'author')
.where('comment.comId = :comId AND funding.fundUuid = :fundUuid', {
comId,
fundUuid,
});

const comment = await commentQb.getOne();
if (!comment) {
throw this.g2gException.CommentNotFound;
}
Expand All @@ -162,9 +169,6 @@ export class CommentService {
comment.isDel = true;
this.commentRepository.save(comment);

comment.author.image = await this.imageInstanceManager
.getImages(comment.author)
.then((images) => images[0]);
return convertToGetCommentDto(comment);
}
}
110 changes: 107 additions & 3 deletions src/features/image/image-instance-manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { IImageId } from 'src/interfaces/image-id.interface';
import { ImageService } from './image.service';
import { Image } from 'src/entities/image.entity';
import { SelectQueryBuilder } from 'typeorm';
import { T } from 'node_modules/@faker-js/faker/dist/airline-BnpeTvY9.cjs';
import { ImageType } from 'src/enums/image-type.enum';
import { User } from 'src/entities/user.entity';
import { Funding } from 'src/entities/funding.entity';
import { Gift } from 'src/entities/gift.entity';

@Injectable()
export class ImageInstanceManager {
Expand All @@ -22,13 +28,108 @@ export class ImageInstanceManager {
);
}

/**
* The function inspects the alias’s metadata to determine the entity type.
* It then uses that to build a join condition that first checks if the entity’s
* defaultImgId is set (in which case it matches image.imgId to defaultImgId). If not,
* it falls back to matching image.subId to the entity’s id field (userId, fundId, or giftId)
* and image.imgType to the proper ImageType.
*
* @param qb The TypeORM query builder for one of the entities.
* @param alias Optional alias for the entity in the query.
* @returns The query builder with the left join mapped to the `image` property.
* @see https://orkhan.gitbook.io/typeorm/docs/select-query-builder#joining-and-mapping-functionality
* @example
const fundingQb = this.fundingRepository
.createQueryBuilder('funding')
.leftJoinAndSelect(
'funding.comments',
'comment',
'comment.isDel = :isDel',
{ isDel: false },
)
.leftJoinAndSelect('comment.author', 'author')
.where('funding.fundUuid = :fundUuid', { fundUuid })
.orderBy('comment.regAt', 'DESC');

// NOTE - 이런 식으로 매핑하고 싶은 엔티티 alias를 붙여주면 됩니다.
this.imageInstanceManager.mapImage(fundingQb, 'author');
this.imageInstanceManager.mapImage(fundingQb, 'funding');

const funding = await fundingQb.getOne();
console.log(funding.image.imgUrl);
console.log(funding.author.image.imgUrl);

const authorQb = this.userRepository
.createQueryBuilder('author')
.where('author.userId = :userId', { userId: user.userId! });

// NOTE - 또는 아래와 같이 alias를 생략하면 query builder의 main alias가 자동으로 사용됩니다.
this.imageInstanceManager.mapImage(authorQb);
*/
mapImage(
qb: SelectQueryBuilder<any>,
alias?: string,
): SelectQueryBuilder<any> {
// Use provided alias or fall back to the query builder's own alias.
const entityAlias = alias || qb.alias;

// Determine the correct id field and image type by inspecting the query builder's metadata.
let idField: string;
let imgType: ImageType;
const joinedAlias = qb.expressionMap.findAliasByName(entityAlias);
const target = joinedAlias.metadata.target;

if (target === User) {
idField = 'userId';
imgType = ImageType.User;
} else if (target === Funding) {
idField = 'fundId';
imgType = ImageType.Funding;
} else if (target === Gift) {
idField = 'giftId';
imgType = ImageType.Gift;
} else {
throw new Error(`mapImage does not support entity type: ${target}`);
}

// Build the left join condition:
// If defaultImgId is set then join on image.imgId;
// otherwise, join on image.subId matching the entity id and image.imgType.
return qb.leftJoinAndMapOne(
`${entityAlias}.image`, // the property to map the result to (e.g. user.image)
'image', // the Image entity
`${entityAlias}Image`, // alias for the joined image table
`
(${entityAlias}.defaultImgId IS NOT NULL AND ${entityAlias}Image.imgId = ${entityAlias}.defaultImgId)
OR
(${entityAlias}.defaultImgId IS NULL AND ${entityAlias}Image.subId = ${entityAlias}.${idField} AND ${entityAlias}Image.imgType = :imgType)
`,
{ imgType },
);
}

mapImages<T extends IImageId>(
queryBuilder: SelectQueryBuilder<T>,
mapToProperty: string,
property: string,
imgType: ImageType,
): SelectQueryBuilder<T> {
return queryBuilder.leftJoinAndMapOne(mapToProperty, property, 'i', ``, {
imgType,
});
}

/**
* 이미지 Update 수행중 새 이미지 URL로 교체하는 유스케이스에 대응합니다.
* 기본이미지로 교체하고 싶다면 `resetToDefault`를 사용하세요.
* TODO - Implement
* @param newImageUrls not default image URL
*/
async overwrite<T extends IImageId>(entity: T, newImageUrls: string[]): Promise<string[]> {
async overwrite<T extends IImageId>(
entity: T,
newImageUrls: string[],
): Promise<string[]> {
throw new Error('Not Implemented');
}

Expand All @@ -39,7 +140,10 @@ export class ImageInstanceManager {
* TODO - Implement
* @param defaultImgId 여기에선 이 id가 기본 이미지인지 여부를 검사하지 않습니다.
*/
async resetToDefault<T extends IImageId>(entity: T, defaultImgId: number): Promise<string> {
async resetToDefault<T extends IImageId>(
entity: T,
defaultImgId: number,
): Promise<string> {
throw new Error('Not Implemented');
}
}
36 changes: 36 additions & 0 deletions src/features/image/image-query.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { DataSource } from 'typeorm';
import { Test } from '@nestjs/testing';

describe('TypeORM Query Counting', () => {
let dataSource: DataSource;
let queryCount = 0;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [],
}).compile();

dataSource = moduleRef.get<DataSource>(DataSource);

// Listen to all queries executed
dataSource.driver.connection.on('query', () => {
queryCount++;
});
});

beforeEach(() => {
queryCount = 0; // Reset the count before each test
});

it('should execute expected number of queries', async () => {
// Perform the database operation
await dataSource.getRepository(SomeEntity).find();

console.log(`Executed SQL Queries: ${queryCount}`);
expect(queryCount).toBeGreaterThan(0); // Replace with expected count
});

afterAll(async () => {
await dataSource.destroy();
});
});
23 changes: 23 additions & 0 deletions src/tests/query-subscriber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Image } from 'src/entities/image.entity';
import { EntitySubscriberInterface, EventSubscriber } from 'typeorm';

/**
* 모든 Image 쿼리의 실행횟수를 검사합니다.
*/
@EventSubscriber()
export class ImageQuerySubscriber implements EntitySubscriberInterface<Image> {
private _queryCount = 0;

public get queryCount() {
return this._queryCount;
}

listenTo() {
return Image;
}

beforeQuery() {
console.log(`Executed SQL Queries: ${this.queryCount}`);
this._queryCount++;
}
}