Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat ✨ follow api 추가 #412

Merged
merged 26 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
31f1455
feat: :sparkles: follow api 형식 제안
niamu01 Dec 13, 2023
ab0710f
feat: :sparkles: followList 타입 생성 및 리스트 함수 추가
niamu01 Dec 13, 2023
23a8eb7
feat: :sparkles: FollowerList 추가
niamu01 Dec 14, 2023
3ea4d7e
refactor: :recycle: 리스트에 자신이 있는 경우 isFollowing을 null로 반환
niamu01 Dec 14, 2023
bad161f
feat: :sparkles: follow/unfollow에 대해 성공했을 시 subscription 추가
niamu01 Dec 16, 2023
2ac0c50
refactor: :recycle: 팔로우 리스트 최신순으로 반환하도록 변경
niamu01 Dec 24, 2023
6e85655
refactor: :recycle: 팔로우 리스트에 limit 추가
niamu01 Dec 24, 2023
0de57a1
feat: :sparkles: follow list pagination 구현
niamu01 Dec 29, 2023
4403cee
feat: :sparkles: 자신이 팔로우한 여부 확인 쿼리 추가
niamu01 Dec 29, 2023
877225e
refactor: :recycle: list 쿼리에도 sort의 defaultvalue 추가
niamu01 Dec 29, 2023
b034841
refactor: :recycle: aggregate -> find~~ 로 변경, 필요한 데이터만 받도록 select 사용
niamu01 Dec 30, 2023
3442797
refactor: :recycle: followingStatus -> isFollowing 으로 변경 및 중복되는 부분 삭제
niamu01 Dec 30, 2023
8661352
fix: :bug: pubSub.publish 함수에 await 추가
niamu01 Jan 5, 2024
1d3d5c3
refactor: :recycle: 한개만 찾는 요소에 대하여 find -> findOne로 변경
niamu01 Jan 5, 2024
7f5667c
refactor: :recycle: evalLog의 fieldExtractor의 커서 검사 사항 추가
niamu01 Jan 10, 2024
f268b91
refactor: :recycle: pagination cursor base -> index base
niamu01 Jan 10, 2024
df90c93
refactor: :recycle: unionType -> throw Exception
niamu01 Jan 10, 2024
1652b9b
refactor: :recycle: targetLogin -> targetId 로 변경
niamu01 Jan 10, 2024
80ba12d
feat: :sparkles: 테스트 용 temp_데이터베이스 사용
niamu01 Jan 20, 2024
9f14267
feat: :sparkles: follower, following 구분해 캐시 형태 결정
niamu01 Jan 20, 2024
ee2e835
refactor: :recycle: db와 cache에 저장되는 타입이 다름을 고려 후 로직 변경
niamu01 Jan 25, 2024
264b7f1
feat: :bug: isFollowing 도 cache에서 확인하도록 변경
niamu01 Feb 14, 2024
295e1ff
feat: :bug: cache된 리스트를 반환할 때도 sort한 후 반환하도록 추가
niamu01 Feb 15, 2024
bf1abd5
refactor: :recycle: follow시 push하기 때문에 set을 다시 호출하던 부분 삭제, 못 찾는 경우와 삭…
niamu01 Feb 20, 2024
1a64608
refactor: :recycle: 타입, 변수명 변경
niamu01 Feb 20, 2024
06aa897
refactor: :recycle: 사용하는 db 변경 temp_follows -> follows
niamu01 Feb 20, 2024
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 app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"graphql": "^16.8.1",
"graphql-query-complexity": "^0.12.0",
"graphql-scalars": "^1.22.2",
"graphql-subscriptions": "^2.0.0",
"lru-cache": "^10.0.0",
"mongoose": "^7.3.3",
"reflect-metadata": "^0.1.13",
Expand Down
12 changes: 12 additions & 0 deletions app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions app/src/api/cursusUser/cursusUser.cache.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CacheUtilService,
type UserFullProfileMap,
} from 'src/cache/cache.util.service';
import { UserPreview } from 'src/common/models/common.user.model';
import { DateTemplate } from 'src/dateRange/dtos/dateRange.dto';
import { CursusUserService } from './cursusUser.service';

Expand All @@ -32,6 +33,22 @@ export class CursusUserCacheService {
private readonly cacheUtilRankingService: CacheUtilRankingService,
) {}

async getUserPreview(userId: number): Promise<UserPreview | undefined> {
const userFullProfile = await this.getUserFullProfile(userId);

const cursusUserProfile = userFullProfile?.cursusUser.user;

if (!cursusUserProfile) {
return undefined;
}

return {
id: cursusUserProfile.id,
login: cursusUserProfile.login,
imgUrl: cursusUserProfile.image.link,
};
}

async getUserFullProfile(
userId: number,
): Promise<UserFullProfileCache | undefined> {
Expand Down
14 changes: 14 additions & 0 deletions app/src/api/cursusUser/cursusUser.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,20 @@ export class CursusUserService {
return this.cursusUserModel.aggregate<ReturnType>();
}

async getuserIdByLogin(login: string): Promise<number | null> {
const cursusUser: { user: Pick<cursus_user['user'], 'id'> } | null =
await this.findOneAndLean({
filter: { 'user.login': login },
select: { 'user.id': 1 },
});

if (!cursusUser) {
return null;
}

return cursusUser.user.id;
}

async findAllAndLean(
queryArgs?: QueryArgs<cursus_user>,
): Promise<cursus_user[]> {
Expand Down
10 changes: 10 additions & 0 deletions app/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { JWT_CONFIG } from './config/jwt';
import { RUNTIME_CONFIG } from './config/runtime';
import { MongooseRootModule } from './database/mongoose/database.mongoose.module';
import { DateWrapper } from './dateWrapper/dateWrapper';
import { FollowModule } from './follow/follow.module';
import { HealthCheckModule } from './healthcheck/healthcheck.module';
import { LambdaModule } from './lambda/lambda.module';
import { LoginModule } from './login/login.module';
Expand Down Expand Up @@ -74,6 +75,14 @@ import { TeamInfoModule } from './page/teamInfo/teamInfo.module';
numberScalarMode: 'integer',
},
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
subscriptions: {
'graphql-ws': {
path: '/graphql',
},
'subscriptions-transport-ws': {
path: '/graphql',
},
},
};
},
}),
Expand All @@ -97,6 +106,7 @@ export class AppRootModule {}
EvalLogModule,
SettingModule,
CalculatorModule,
FollowModule,
LambdaModule,
HealthCheckModule,
CacheDecoratorOnReturnModule,
Expand Down
18 changes: 18 additions & 0 deletions app/src/follow/db/follow.database.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';

export type UserDocument = HydratedDocument<follow>;

@Schema({ collection: 'follows' })
export class follow {
@Prop({ required: true })
userId: number;

@Prop({ required: true })
followId: number;

@Prop({ required: true })
followAt: Date;
}

export const FollowSchema = SchemaFactory.createForClass(follow);
20 changes: 20 additions & 0 deletions app/src/follow/dto/follow.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ArgsType, Field, registerEnumType } from '@nestjs/graphql';
import { PaginationIndexArgs } from 'src/pagination/index/dtos/pagination.index.dto.args';

export enum FollowSortOrder {
FOLLOW_AT_ASC,
FOLLOW_AT_DESC,
}

registerEnumType(FollowSortOrder, { name: 'FollowSortOrder' });

@ArgsType()
export class FollowPaginatedArgs extends PaginationIndexArgs {
@Field()
targetId: number;

@Field((_type) => FollowSortOrder, {
defaultValue: FollowSortOrder.FOLLOW_AT_DESC,
})
sortOrder: FollowSortOrder;
}
40 changes: 40 additions & 0 deletions app/src/follow/follow.cache.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable } from '@nestjs/common';
import { CacheUtilService } from 'src/cache/cache.util.service';
import { Follow } from './model/follow.model';

export const FOLLOW_LISTS = 'followLists';

@Injectable()
export class FollowCacheService {
constructor(
@Inject(CACHE_MANAGER)
private readonly cacheUtilService: CacheUtilService,
) {}

async set({
id,
type,
list,
}: {
id: number;
type: 'follower' | 'following';
list: Follow[];
}): Promise<void> {
const key = `${id}:${type}:${FOLLOW_LISTS}`;

await this.cacheUtilService.set(key, list, 0);
}

async get(userId: number, type: 'follower' | 'following'): Promise<Follow[]> {
const key = `${userId}:${type}:${FOLLOW_LISTS}`;

const cachedData = await this.cacheUtilService.get<Follow[]>(key);

if (!cachedData) {
return []; //todo: 흠,,,
}

return cachedData;
}
}
22 changes: 22 additions & 0 deletions app/src/follow/follow.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CursusUserModule } from 'src/api/cursusUser/cursusUser.module';
import { CacheUtilModule } from 'src/cache/cache.util.module';
import { PaginationIndexModule } from 'src/pagination/index/pagination.index.module';
import { FollowSchema, follow } from './db/follow.database.schema';
import { FollowCacheService } from './follow.cache.service';
import { FollowResolver } from './follow.resolver';
import { FollowService } from './follow.service';

@Module({
imports: [
MongooseModule.forFeature([{ name: follow.name, schema: FollowSchema }]),
CursusUserModule,
PaginationIndexModule,
CacheUtilModule,
],
providers: [FollowResolver, FollowService, FollowCacheService],
exports: [FollowService, FollowCacheService],
})
// eslint-disable-next-line
export class FollowModule {}
89 changes: 89 additions & 0 deletions app/src/follow/follow.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { NotFoundException, UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { MyUserId } from 'src/auth/myContext';
import { StatAuthGuard } from 'src/auth/statAuthGuard';
import { HttpExceptionFilter } from 'src/http-exception.filter';
import { FollowPaginatedArgs } from './dto/follow.dto';
import { FollowService } from './follow.service';
import { FollowSuccess, MyFollowPaginated } from './model/follow.model';

const pubSub = new PubSub();

@UseFilters(HttpExceptionFilter)
@Resolver()
export class FollowResolver {
constructor(private readonly followService: FollowService) {}

@Subscription((_returns) => FollowSuccess, { name: 'followUpdated' })
followUpdated() {
return pubSub.asyncIterator('followUpdated');
}

@UseGuards(StatAuthGuard)
@Mutation((_returns) => FollowSuccess)
async followUser(
@MyUserId() userId: number,
@Args('targetId') targetId: number,
): Promise<FollowSuccess> {
try {
const followResult = await this.followService.followUser(
userId,
targetId,
);

await pubSub.publish('followUpdated', { followUpdated: followResult });

return followResult;
} catch (e) {
throw new NotFoundException();
}
}

@UseGuards(StatAuthGuard)
@Mutation((_returns) => FollowSuccess)
async unfollowUser(
@MyUserId() userId: number,
@Args('targetId') targetId: number,
): Promise<FollowSuccess> {
try {
const followResult = await this.followService.unfollowUser(
userId,
targetId,
);

await pubSub.publish('followUpdated', { followUpdated: followResult });

return followResult;
} catch (e) {
throw new NotFoundException();
}
}

@UseGuards(StatAuthGuard)
@Query((_returns) => Boolean)
async getIsFollowing(
@MyUserId() userId: number,
@Args('targetId') targetId: number,
): Promise<boolean> {
return await this.followService.isFollowing(userId, targetId);
}

@UseGuards(StatAuthGuard)
@Query((_returns) => MyFollowPaginated)
async getFollowerPaginated(
@MyUserId() userId: number,
@Args() args: FollowPaginatedArgs,
): Promise<MyFollowPaginated> {
return await this.followService.followerPaginated(userId, args);
}

@UseGuards(StatAuthGuard)
@Query((_returns) => MyFollowPaginated)
async getFollowingPaginated(
@MyUserId() userId: number,
@Args() args: FollowPaginatedArgs,
): Promise<MyFollowPaginated> {
return await this.followService.followingPaginated(userId, args);
}
}
Loading