[ PART 10 ] GraphQL, Typescript 및 React를 사용하여 Twitter 복제본 만들기( 댓글 및 리트윗 )
44639 단어 showdevgraphqlcareerjavascript
참고로 저는 이 챌린지를 하고 있습니다 ;): Tweeter challenge
Db diagram
트윗 테이블에 parent_id 필드와 "tweet | retweet | comment"유형 필드가 있으므로 댓글과 리트윗을 추가할 가능성이 이미 있습니다. 그러나, 나는 (트위터를 사용하면서 :D) 우리가 같은 트윗을 여러 번 리트윗할 가능성이 없어야 한다는 것을 알아차렸습니다 :D. 따라서 addTweet 메소드에서 확인하는 것이 좋습니다.
우선 데이터베이스 스키마에서 수행한 작업과 일치하도록 두 개의 enum 클래스를 추가했습니다.
src/entities/Tweet.ts
export enum TweetTypeEnum {
TWEET = 'tweet',
RETWEET = 'retweet',
COMMENT = 'comment',
}
export enum TweetVisibilityEnum {
PUBLIC = 'public',
FOLLOWERS = 'followers',
}
그런 다음 트윗을 추가할 때 유효성 검사 규칙을 완료합니다. @ValidateIf() 유효성 검사기와 약간의 어려움을 겪은 후 Typegraphql에서 skipMissingProperties 옵션이 false로 설정되어 있다는 사실을 알게 되었습니다. 지금은 유효성 검사 규칙이 작동하도록 true로 변경하겠습니다.
src/server.ts
export const schema = async () => {
return await buildSchema({
resolvers: [AuthResolver, TweetResolver, LikeResolver],
authChecker: authChecker,
validate: {
skipMissingProperties: false, // set false instead of true
},
})
}
예를 들어, addTweetPayload에 항상 존재하는 parent_id와 유형을 갖도록 강제함으로써 다르게 할 수 있습니다. 하지만 지금은 이렇게 합시다. 나중에 문제가 있으면 변경할 수 있습니다. 어쨌든 필요한 경우 리팩터링하는 데 도움이 되도록 몇 가지 테스트를 작성할 것입니다 ;).
이제 AddTweetPayload를 살펴보겠습니다.
src/dto/AddTweetPayload.ts
import { IsIn, IsNotEmpty, MinLength, ValidateIf } from 'class-validator'
import { Field, InputType, Int } from 'type-graphql'
import { TweetTypeEnum } from '../entities/Tweet'
@InputType()
class AddTweetPayload {
@Field()
@IsNotEmpty()
@MinLength(2)
body: string
@Field(() => Int, { nullable: true })
@ValidateIf((o) => o.type !== undefined)
@IsNotEmpty()
parent_id?: number
@Field(() => String, { nullable: true })
@ValidateIf((o) => o.parent_id !== undefined)
@IsIn([TweetTypeEnum.COMMENT, TweetTypeEnum.RETWEET])
type?: TweetTypeEnum
@Field(() => String, { nullable: true })
visibility?: string
}
export default AddTweetPayload
유형이 전송되면 리트윗 또는 댓글임을 의미하는 parent_id가 있어야 합니다. 같은 방식으로 페이로드에 parent_id가 있는 경우 유형은 "댓글"또는 "리트윗"이어야 합니다. 그리고 우리가 이미 리트윗한 트윗을 리트윗하지 않도록 리졸버에서 직접 확인하겠습니다. id가 parent_id인 트윗이 존재하는지 확인할 수도 있습니다.
src/resolvers/TweetResolver.ts
@Mutation(() => Tweet)
@Authorized()
async addTweet(
@Arg('payload') payload: AddTweetPayload,
@Ctx() ctx: MyContext
) {
const { db, userId } = ctx
// Maybe I should add a mutation to handle the retweet?
// For the comment, we can comment as much as we want so I could
// still add the comment here.
// Feel free to share your opinion ;)
if (payload.type === TweetTypeEnum.RETWEET && payload.parent_id) {
const [alreadyRetweeted] = await db('tweets').where({
parent_id: payload.parent_id,
type: TweetTypeEnum.RETWEET,
user_id: userId,
})
if (alreadyRetweeted) {
throw new ApolloError('You already retweeted that tweet')
}
}
try {
const [tweet] = await db('tweets')
.insert({
...payload,
user_id: userId,
})
.returning('*')
return tweet
} catch (e) {
throw new ApolloError(e.message)
}
}
내가 아무것도 깨지 않았는지 확인하기 위해 몇 가지 테스트를 작성해 봅시다 ;). class-validator 라이브러리를 처음 사용하기 때문에 잘못될 수 있는 다양한 시나리오를 확인하기 위해 더 많은 테스트를 작성하기로 결정했습니다 ;).
src/tests/tweets.test.ts
it('should insert a comment', async () => {
const user = await createUser()
const tweet = await createTweet(user)
const { mutate } = await testClient({
req: {
headers: {
authorization: 'Bearer ' + generateToken(user),
},
},
})
const res = await mutate({
mutation: ADD_TWEET,
variables: {
payload: {
body: 'Bouh',
type: 'comment',
parent_id: tweet.id,
},
},
})
const tweets = await db('tweets')
expect(tweets.length).toEqual(2)
expect(res.data.addTweet.body).toEqual('Bouh')
expect(res.data.addTweet.type).toEqual('comment')
expect(res.data.addTweet.parent_id).toEqual(tweet.id)
expect(res.errors).toBeUndefined()
})
it('should insert a retweet', async () => {
const user = await createUser()
const tweet = await createTweet(user)
const { mutate } = await testClient({
req: {
headers: {
authorization: 'Bearer ' + generateToken(user),
},
},
})
const res = await mutate({
mutation: ADD_TWEET,
variables: {
payload: {
body: 'Bouh',
type: 'retweet',
parent_id: tweet.id,
},
},
})
const tweets = await db('tweets')
expect(tweets.length).toEqual(2)
expect(res.data.addTweet.body).toEqual('Bouh')
expect(res.data.addTweet.type).toEqual('retweet')
expect(res.data.addTweet.parent_id).toEqual(tweet.id)
expect(res.errors).toBeUndefined()
})
it('should not insert a comment if the type is provided but the parent_id is not provided', async () => {
const user = await createUser()
const tweet = await createTweet(user)
const { mutate } = await testClient({
req: {
headers: {
authorization: 'Bearer ' + generateToken(user),
},
},
})
const res = await mutate({
mutation: ADD_TWEET,
variables: {
payload: {
body: 'Bouh',
type: 'comment',
},
},
})
const tweets = await db('tweets')
expect(tweets.length).toEqual(1)
expect(res.errors).not.toBeUndefined()
const {
extensions: {
exception: { validationErrors },
},
}: any = res.errors![0]
expect((validationErrors[0] as ValidationError).constraints).toEqual({
isNotEmpty: 'parent_id should not be empty',
})
})
it('should not insert a comment if the parent_id is provided but the type is not provided', async () => {
const user = await createUser()
const tweet = await createTweet(user)
const { mutate } = await testClient({
req: {
headers: {
authorization: 'Bearer ' + generateToken(user),
},
},
})
const res = await mutate({
mutation: ADD_TWEET,
variables: {
payload: {
body: 'Bouh',
parent_id: tweet.id,
},
},
})
const tweets = await db('tweets')
expect(tweets.length).toEqual(1)
expect(res.errors).not.toBeUndefined()
const {
extensions: {
exception: { validationErrors },
},
}: any = res.errors![0]
expect((validationErrors[0] as ValidationError).constraints).toEqual({
isIn: 'type must be one of the following values: comment,retweet',
})
})
it('should not insert a retweet if the type is provided but not the parent_id', async () => {
const user = await createUser()
const tweet = await createTweet(user)
const { mutate } = await testClient({
req: {
headers: {
authorization: 'Bearer ' + generateToken(user),
},
},
})
const res = await mutate({
mutation: ADD_TWEET,
variables: {
payload: {
body: 'Bouh',
type: 'retweet',
},
},
})
const tweets = await db('tweets')
expect(tweets.length).toEqual(1)
expect(res.errors).not.toBeUndefined()
const {
extensions: {
exception: { validationErrors },
},
}: any = res.errors![0]
expect((validationErrors[0] as ValidationError).constraints).toEqual({
isNotEmpty: 'parent_id should not be empty',
})
})
it('should not insert a retweet if the parent_id is provided but not the type', async () => {
const user = await createUser()
const tweet = await createTweet(user)
const { mutate } = await testClient({
req: {
headers: {
authorization: 'Bearer ' + generateToken(user),
},
},
})
const res = await mutate({
mutation: ADD_TWEET,
variables: {
payload: {
body: 'Bouh',
parent_id: tweet.id,
},
},
})
const tweets = await db('tweets')
expect(tweets.length).toEqual(1)
expect(res.errors).not.toBeUndefined()
const {
extensions: {
exception: { validationErrors },
},
}: any = res.errors![0]
expect((validationErrors[0] as ValidationError).constraints).toEqual({
isIn: 'type must be one of the following values: comment,retweet',
})
})
it('should not insert a retweet if the user already retweeted the tweet', async () => {
const user = await createUser()
const tweet = await createTweet(user)
const retweet = await createTweet(
user,
'test',
'retweet',
'public',
tweet.id
)
const { mutate } = await testClient({
req: {
headers: {
authorization: 'Bearer ' + generateToken(user),
},
},
})
const res = await mutate({
mutation: ADD_TWEET,
variables: {
payload: {
body: 'Bouh',
type: 'retweet',
parent_id: tweet.id,
},
},
})
expect(res.errors).not.toBeUndefined()
expect(res.errors![0].message).toEqual('You already retweeted that tweet')
})
모든 것이 녹색입니다;). 다음 부분으로 넘어가겠습니다. Github Workflows에 대해 조금 이야기해야 합니다.
다음 편에서 만나요 ;).
잘 지내세요 ;).
Reference
이 문제에 관하여([ PART 10 ] GraphQL, Typescript 및 React를 사용하여 Twitter 복제본 만들기( 댓글 및 리트윗 )), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/ipscodingchallenge/part-10-creating-a-twitter-clone-with-graphql-typescript-and-react-comment-retweet-kjj텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)