[ PART 6 ] GraphQL, Typescript 및 React로 Twitter 복제본 만들기( 트윗 추가 )
39021 단어 showdevwebdevcareerjavascript
데이터베이스 다이어그램 링크: Twitter DbDiagram
먼저 마이그레이션을 생성합니다.
knex migrate:make add_tweets_table -x ts
import * as Knex from 'knex'
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('tweets', (t) => {
t.increments('id')
t.text('body').notNullable()
t.integer('user_id').unsigned().notNullable()
t.integer('parent_id').unsigned()
t.enum('visibility', ['public', 'followers']).defaultTo('public')
t.enum('type', ['tweet', 'retweet', 'comment']).defaultTo('tweet')
t.timestamps(false, true)
t.foreign('user_id').references('id').inTable('users').onDelete('CASCADE')
t.foreign('parent_id')
.references('id')
.inTable('tweets')
.onDelete('CASCADE')
})
}
export async function down(knex: Knex): Promise<void> {
return knex.raw('DROP TABLE tweets CASCADE')
}
knex migrate:latest
더 쉽게 작업할 수 있도록 데이터베이스에 트윗도 추가하겠습니다. 이를 위해 faker 라이브러리를 추가합니다.
yarn add -D faker
yarn add -D @types/faker
knex seed:make seed -x ts
*src/db/seeds/seed.ts
import * as Knex from 'knex'
import faker from 'faker'
import argon2 from 'argon2'
import User from '../../entities/User'
export async function seed(knex: Knex): Promise<void> {
await knex('users').del()
await knex('tweets').del()
for (let user of await createUsers()) {
const [insertedUser] = await knex('users').insert(user).returning('*')
const tweetsToInsert = await createTweets(insertedUser)
await knex('tweets').insert(tweetsToInsert)
}
}
const createUsers = async () => {
let users = []
const hash = await argon2.hash('password')
for (let i = 0; i < 10; i++) {
users.push({
username: faker.internet.userName(),
display_name: faker.name.firstName(),
email: faker.internet.email(),
avatar: faker.internet.avatar(),
password: hash,
})
}
return users
}
const createTweets = async (user: User) => {
let tweets = []
for (let i = 0; i < 20; i++) {
tweets.push({
body: faker.lorem.sentence(),
type: 'tweet',
user_id: user.id,
visibility: faker.random.arrayElement(['public', 'followers']),
})
}
return tweets
}
knex seed:run
우리 데이터베이스에는 이제 우리가 즐길 수 있는 데이터가 있습니다 ;)
먼저 Tweet 엔터티를 생성해 보겠습니다.
src/entities/Tweet.ts
import { Field, ID, ObjectType } from 'type-graphql'
import User from './User'
@ObjectType()
class Tweet {
@Field((type) => ID)
id: number
@Field()
body: string
@Field()
visibility: string
@Field()
type: string
@Field()
user: User
user_id: number
@Field()
created_at: Date
@Field()
updated_at: Date
}
export default Tweet
트윗 작성자를 검색할 수 있는 사용자 속성이 있습니다. 노출하지 않는 user_id 속성도 있습니다. 각 트윗으로 반드시 사용자를 검색해야 한다는 점을 감안할 때 user_id를 노출할 시점이 보이지 않습니다. 그러면 최악의 경우 마음이 바뀌거나 생각이 나지 않으면 바뀌기 쉽습니다 ;).
이제 TweetResolver에서 작업해 보겠습니다.
src/resolvers/TweetResolver.ts
import { Ctx, Query, Resolver } from 'type-graphql'
import Tweet from '../entities/Tweet'
import { MyContext } from '../types/types'
@Resolver()
class TweetResolver {
@Query(() => [Tweet])
async feed(@Ctx() ctx: MyContext) {
const { db } = ctx
const tweets = await db('tweets').limit(50)
return tweets
}
}
export default TweetResolver
테스트를 위해 데이터베이스의 모든 트윗을 검색하기만 하면 됩니다. 우리는 나중에 로직(우리가 팔로우하는 사람들의 트윗만 검색, 페이지 매김 등 ...)에 대해 볼 것입니다.
리졸버에 리졸버를 추가하는 것을 잊지 마십시오.
src/server.ts
export const schema = async () => {
return await buildSchema({
resolvers: [AuthResolver, TweetResolver],
authChecker: authChecker,
})
}
여기서 첫 번째 "문제"가 발생하기 시작합니다 ;). 연결된 사용자를 가져오지 않고 쿼리를 실행하면 아무런 문제 없이 작동합니다.
내 콘솔에는 다음 SQL 쿼리가 있습니다.
SQL (8.414 ms) select * from "tweets"
이제 연결된 사용자를 검색하여 시도해 보겠습니다.
예상대로 내 SQL 쿼리는 사용자 속성을 반환하지 않으므로 오류가 발생합니다. 이를 해결하기 위해 몇 가지 선택이 있습니다. 예를 들어 메서드 피드에서 직접 사용자를 검색할 수 있습니다. 쿼리 빌더인 Knex를 사용하면 약간의 코드를 작성해야 하지만 ORM을 사용하면 훨씬 쉽게 할 수 있습니다. 예를 들어 Laravel( PHP )을 사용하면 다음과 같은 결과를 얻을 수 있습니다. $tweets = Tweet::with('author')->get(); 나는 아직 Node.js 세계에서 어떤 ORM도 사용하지 않았지만 의심할 여지 없이 같은 것이 있습니다 ;).
하지만 지금은 Knex.js를 사용합니다.
@Query(() => [Tweet])
async feed(@Ctx() ctx: MyContext) {
const { db } = ctx
// Fetch the tweets
const tweets = await db('tweets').limit(50)
// Get the userIds from the tweets and remove duplicates.
// Array.from is used for the whereIn below ;)
const userIds = Array.from(new Set(tweets.map((t) => t.user_id)))
// Fetch the users needed
const users = await db('users').whereIn('id', userIds)
// Remap the tweets array to add the user property
return tweets.map((t) => {
return {
...t,
user: users.find((u) => u.id === t.user_id),
}
})
}
예상대로 작동합니다 ;).
그러나 우리가 바로 보게 될 또 다른 진행 방법이 있습니다 ;)
@FieldResolver를 사용하여 사용자를 복구하는 방법을 알려줍니다.
src/resolvers/TweetResolver.ts
import { Ctx, FieldResolver, Query, Resolver, Root } from 'type-graphql'
import Tweet from '../entities/Tweet'
import User from '../entities/User'
import { MyContext } from '../types/types'
@Resolver((of) => Tweet)
class TweetResolver {
@Query(() => [Tweet])
async feed(@Ctx() ctx: MyContext) {
const { db } = ctx
const tweets = await db('tweets').limit(50)
return tweets
}
@FieldResolver(() => User)
async user(@Root() tweet: Tweet, @Ctx() ctx: MyContext) {
const { db } = ctx
const [user] = await db('users').where('id', tweet.user_id)
return user
}
}
export default TweetResolver
하지만 내 요청을 다시 시작하면 작동하지만 내 로그를 보면 작은 문제가 표시됩니다. ;)
실제로 사용자를 검색하기 위해 각 트윗에 대한 쿼리를 만들 것입니다. 별로 ;). 이 문제를 극복하기 위해 우리는 dataloader 라이브러리를 사용할 것입니다.
yarn add dataloader
GraphQL도 배우면서 데이터 로더와 관련된 파일을 어떻게 구성해야 할지 잘 모르겠습니다. 제안할 제안이나 리포지토리가 있으면 자유롭게 공유하세요 ;).
src/dataloaders/dataloaders.ts
import DataLoader from 'dataloader'
import db from '../db/connection'
import User from '../entities/User'
export const dataloaders = {
userDataloader: new DataLoader<number, any, unknown>(async (ids) => {
const users = await db('users').whereIn('id', ids)
return ids.map((id) => users.find((u) => u.id === id))
}),
}
Dataloader는 매개변수에서 키를 수신합니다. 여기에서 whereIn을 사용하여 사용자를 검색합니다.
남은 것은 ID를 매핑하여 해당 사용자를 검색하는 것입니다.
그런 다음 액세스할 수 있도록 컨텍스트에 데이터 로더를 추가합니다.
src/server.ts
import { dataloaders } from './dataloaders/dataloaders'
export const defaultContext = ({ req, res }: any) => {
return {
req,
res,
db,
dataloaders,
}
}
남은 일은 @FieldResolver 사용자를 업데이트하는 것입니다.
src/resolvers/TweetResolver.ts
@FieldResolver(() => User)
async user(@Root() tweet: Tweet, @Ctx() ctx: MyContext) {
const {
db,
dataloaders: { userDataloader },
} = ctx
return await userDataloader.load(tweet.user_id)
}
쿼리를 실행하면 모든 것이 다시 작동하고 내 콘솔에서 작성된 SQL 쿼리를 확인하면 다음과 같이 됩니다.
우리는 훨씬 더 합리적인 수의 요청으로 끝납니다 ;). 반면에 dataloader는 요청을 캐시하므로 예를 들어 트윗을 추가할 때 캐시를 지우는 것을 잊지 말아야 합니다. 하지만 이에 대해서는 나중에 다시 다루겠습니다.
보고 싶다면 테스트도 추가했습니다 ;).
src/tests/tweets.test.ts
import db from '../db/connection'
import { FEED } from './queries/tweets.queries'
import { testClient } from './setup'
describe('Tweets', () => {
beforeEach(async () => {
await db.migrate.rollback()
await db.migrate.latest()
await db.seed.run()
})
afterEach(async () => {
await db.migrate.rollback()
})
test('it should fetch the tweets with user', async () => {
const { query } = await testClient()
const res = await query({
query: FEED,
})
expect(res.data.feed[0]).toMatchSnapshot()
})
})
글쎄, 나는이 기사에 충분하다고 생각합니다 ;). 트윗을 삽입하는 방법을 볼 수 있는 다음 에피소드에서 뵙겠습니다 ;).
안녕히 계세요! 🍹
당신은 2-3 가지를 배우고 나에게 커피를 사주고 싶어;)?
https://www.buymeacoffee.com/ipscoding
Reference
이 문제에 관하여([ PART 6 ] GraphQL, Typescript 및 React로 Twitter 복제본 만들기( 트윗 추가 )), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/ipscodingchallenge/part-6-creating-a-twitter-clone-with-graphql-typescript-and-react-adding-tweet-233n텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)