NestJs Chapter 9

yeopยท2022๋…„ 7์›” 25์ผ

Nest JS ์ •๋ฆฌ

๋ชฉ๋ก ๋ณด๊ธฐ
9/10

๐Ÿ“‘ Order Subscriptions

๐Ÿ”ท Graphql-subscriptions

GraphQL subscriptions์€ GraphQL์—์„œ subscriptions์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด pubsub ์‹œ์Šคํ…œ๊ณผ GraphQL์„ ์—ฐ๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” Npm ํŒจํ‚ค์ง€์ด๋‹ค.
๋ชจ๋“  GraphQL ํด๋ผ์ด์–ธํŠธ ๋ฐ ์„œ๋ฒ„์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

npm i graphql-subscriptions
https://www.npmjs.com/package/graphql-subscriptions

๐Ÿ”ธWeb Socket(ws) Error

๋‹ค์Œ๊ณผ ๊ฐ™์ด PubSub ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  Subscription decorator๋ฅผ ์‚ฌ์šฉํ•ด pubsub.asyncIterator๋ฅผ return ํ•  ๊ฒฝ์šฐ "error": "Could not connect to websocket endpoint ws://localhost:3000/graphql. Please check if the endpoint url is correct." ์™€ ๊ฐ™์€ ์—๋Ÿฌ๋ฅผ returnํ•œ๋‹ค.

์šฐ๋ฆฌ ์„œ๋ฒ„๋Š” http ํ”„๋กœํ† ์ฝœ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”๋ฐ ws ๋˜ํ•œ ํ”„๋กœํ† ์ฝœ์ด๋ฉด realtime์„ ์ฒ˜๋ฆฌํ•˜๋Š” WebSocket์„ ๋งํ•œ๋‹ค. mutation๊ณผ query๋Š” http๋ฅผ ํ•„์š”๋กœ ํ•˜๊ณ  subscription์€ webSocket์„ ํ•„์š”๋กœ ํ•˜๊ธฐ๋•Œ๋ฌธ์— ๋‘ ๊ณณ์—์„œ ๋ชจ๋‘ ์„œ๋ฒ„๊ฐ€ ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ์–ด์•ผํ•œ๋‹ค.

const pubsub = new PubSub();

@Subscription((returns) => String)
  hotPotatos() {
    return pubsub.asyncIterator('hotPotatos');
  }

- ํ•ด๊ฒฐ๋ฐฉ๋ฒ• (Subscriptions ํ™œ์„ฑํ™”)

subscriptions์„ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด app module์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •ํ•ด์•ผํ•œ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์„œ๋ฒ„์—์„œ ๋‘๊ฐ€์ง€ ํ”„๋กœํ† ์ฝœ์„ ๋ชจ๋‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ๋œ๋‹ค.

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  subscriptions: {
    'graphql-ws': true
  },
})

๐Ÿ”ท Using both 'http' and 'ws'

  • httpํ”„๋กœํ† ์ฝœ์—๋Š” request๊ฐ€ ์žˆ๊ณ  wsํ”„๋กœํ† ์ฝœ์—๋Š” ์ด์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•˜๋Š” connection์ด ์žˆ๋‹ค.

๐Ÿ”น Flow

๋จผ์ € Guard์™€ AuthUser ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์—์„œ user๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๊ณผ์ •์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž

๋กœ๊ทธ์ธํ•ด์„œ ๋ฐ›์€ ํ† ํฐ์„ http header์— ๋„ฃ์–ด์„œ request๋ฅผ ๋ณด๋‚ด๋ฉด jwtmiddleware์—์„œ ํ† ํฐ์„ ๋ฐ›์•„ decodingํ•œ ํ›„ user๋ฅผ req์— ๋„ฃ์–ด์„œ ๋ฆฌํ„ดํ•œ๋‹ค.
๊ทธ๋Ÿผ graphql module์—์„œ context์— req๋ฅผ ๋„ฃ์–ด guard์—์„œ ๋ฐ›์€ ํ›„ context๋ฅผ ํ†ตํ•ด role์„ ์•Œ์•„๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ”น Problem & Fix

ํ•˜์ง€๋งŒ middleware๋ฅผ ์‚ฌ์šฉํ•ด์„œ๋Š” request๋งŒ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ์–ด httpํ”„๋กœํ† ์ฝœ๋งŒ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋ฉฐ connection์„ ๋ฐ›์•„์™€์•ผํ•˜๋Š” wsํ”„๋กœํ† ์ฝœ์€ ์‚ฌ์šฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค. ๊ทธ๋ž˜์„œ app module์—์„œ middleware๋ฅผ ์ œ๊ฑฐํ•ด์ค€ ํ›„ graphql module์— ์•„๋ž˜์˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

subscriptions ๋‚ด๋ถ€์˜ ์ฝ”๋“œ๋Š” connection์— ํ† ํฐ์„ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ์ฝ”๋“œ์ด๊ณ  ์•„๋ž˜์˜ context ๋‚ด๋ถ€์˜ ์ฝ”๋“œ๋Š” req์— ํ† ํฐ์„ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ์ฝ”๋“œ์ด๋‹ค.

GraphQLModule.forRoot({
      driver: ApolloDriver,
      subscriptions: {
        'subscriptions-transport-ws': {
          onConnect: (connectionParams) => {
            console.log('connectionParams', connectionParams);
            const authToken = connectionParams['x-jwt'];
            if (!authToken) {
              throw new Error('Token is not valid');
            }
            const token = authToken;
            return { token };
          },
        },
      },
      context: ({ req }) => ({ token: req.headers['x-jwt'] }),
    }),

์ดํ›„ Guard์—์„œ ํ† ํฐ์„ ๋ฐ›์•„ middleware์—์„œ ํ•˜๋˜ decoding์„ Guard์—์„œ ์•„๋ž˜์˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด ์ง„ํ–‰ํ•œ๋‹ค.

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly reflector: Reflector,
    private readonly jwtService: JwtService,
    private readonly userService: UserService,
  ) {}
  async canActivate(context: ExecutionContext) {
    const roles = this.reflector.get<AllowedRoles>(
      'roles',
      context.getHandler(),
    );
    if (!roles) {
      return true;
    }
    const gqlContext = GqlExecutionContext.create(context).getContext();
    const token = gqlContext.token;
    if (token) {
      const decoded = this.jwtService.verify(token.toString());
      if (typeof decoded === 'object' && decoded.hasOwnProperty('id')) {
        const { user } = await this.userService.findById(decoded['id']);
        if (user) {
          gqlContext['user'] = user;
          if (roles.includes('Any')) {
            return true;
          }
          return roles.includes(user.role);
        }
      }
    }
    return false;
  }
}

๐Ÿ”ท PubSub

  • PubSub ๋ชจ๋ธ์€ ํŠน์ •ํ•œ ์ฃผ์ œ(Topic)์— ๋Œ€ํ•˜์—ฌ ๊ตฌ๋…(Subscribe)ํ•œ ๋ชจ๋‘์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐœ์ƒ(Publish)ํ•˜๋Š” ํ†ต์‹  ๋ฐฉ๋ฒ•์ด๋‹ค.

PubSub์€ App ์ „์ฒด์—์„œ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๋งŒ ์ƒ์„ฑ๋˜์–ด์•ผ ํ•œ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ์ธ์Šคํ„ด์Šค๋ฅผ common Module์— ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ ์–ธํ•œ ํ›„ common Module์„ @Global() ์ฒ˜๋ฆฌ ํ•ด์ค€๋‹ค.

const pubsub = new PubSub();

@Global()
@Module({
  providers: [
    {
      provide: PUB_SUB,
      useValue: pubsub,
    },
  ],
  exports: [PUB_SUB],
})
export class CommonModule {}

Resolver์—์„œ๋Š” PubSub์„ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•  ๋•Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด @Inject๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค

@Resolver(of => Order)
export class OrderResolver {
  constructor(
    @Inject(PUB_SUB) private readonly pubSub: PubSub,
  ) {}

๐Ÿ”ท Subscriptions Option

๐Ÿ”น Filtering Subscriptions

Subscriptions์„ ์‚ฌ์šฉํ•  ๋•Œ ํŠน์ • ์ด๋ฒคํŠธ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๋ ค๋ฉด ํ•„ํ„ฐ ์†์„ฑ์„ ํ•„ํ„ฐ ํ•จ์ˆ˜๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

//EX
@Subscription(returns => Comment, {
filter: (payload, variables, context) =>
payload.commentAdded.title === variables.title,
})
commentAdded(@Args('title') title: string) {
return pubSub.asyncIterator('commentAdded');
}
  • payload: pubsub.publish()๋ฅผ ํ†ตํ•ด ์ „๋‹ฌํ•œ ๊ฐ์ฒด
  • variables: subscription์— ์ „๋‹ฌํ•œ ๊ฐ์ฒด(์ธ์ž)
  • context: gqlContext๊ฐ์ฒด
  @Mutation((returns) => Boolean)
  async potatoReady(@Args('potatoId') potatoId: number) {
    await this.pubSub.publish('hotPotatos', {
      readyPotato: potatoId,
    });
    return true;
  }

  @Subscription((returns) => String, {
    filter: ({ readyPotato }, { potatoId }) => {
      return readyPotato === potatoId;
    },
  })
  @Role(['Any'])
  readyPotato(@AuthUser() user: User, @Args('potatoId') potatoId: number) {
    return this.pubSub.asyncIterator('hotPotatos');
  }

potatoId๊ฐ€ ๊ฐ™์„ ๊ฒฝ์šฐ์—๋งŒ ๊ฒฐ๊ณผ๋ฅผ Loadํ•œ๋‹ค.

https://docs.nestjs.com/graphql/subscriptions#filtering-subscriptions

๐Ÿ”น Resolve

resolverํ•จ์ˆ˜๊ฐ€ ๋ฆฌํ„ดํ•˜๋Š” ๊ฐ’์€ pubsub.asyncIterator()๋ฅผ ํ†ตํ•ด ๋ฐ›๋Š” ๊ฐ’์ด ๋œ๋‹ค. publishํ•œ event payload๋ฅผ ๋ณ€ํ˜•ํ•˜๋ ค๋ฉด resolve ์†์„ฑ์„ ํ•จ์ˆ˜๋กœ ์„ค์ •ํ•œ๋‹ค. ํ•จ์ˆ˜๋Š” ์ด๋ฒคํŠธ payload๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ  ์ ์ ˆํ•œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํžŒ๋‹ค.

@Subscription((returns) => String, {
    filter: ({ readyPotato }, { potatoId }) => {
      return readyPotato === potatoId;
    },
    resolve: ({ readyPotato }) =>
      `Your potato with the id ${readyPotato} is ready!`,
  })

๐Ÿ”ท Pending Orders

//orders.service.ts / createOrder part
await this.pubSub.publish(NEW_PENDING_ORDER, {
        pendingOrders: { order, ownerId: restaurant.ownerId },
      });
//orders.resolver.ts
@Subscription((returns) => Order, {
    filter: ({ pendingOrders: { ownerId } }, _, { user }) => {
      return ownerId === user.id;
    },
    resolve: ({ pendingOrders: { order } }) => order,
  })
  @Role(['Owner'])
  pendingOrders() {
    return this.pubSub.asyncIterator(NEW_PENDING_ORDER);
  }

service์˜ payload์— Order์ด ์•„๋‹Œ {pendingOrders: { order, ownerId: restaurant.ownerId }} ์ด์™€ ๊ฐ™์€ Object๋ฅผ ๋„ฃ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— Resolve Option์„ ํ†ตํ•ด Order๋ฅผ ๋ฐ˜ํ™˜ํ•ด์ค˜์•ผํ•œ๋‹ค.

๐Ÿ”ท Eager relations

Eager relation์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋กœ๋“œํ•  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ relation ํ•„๋“œ๋“ค์„ ๋กœ๋“œํ•œ๋‹ค. (eager: true๋ฅผ ์ถ”๊ฐ€)

@ManyToMany(type => Category, category => category.questions, {
eager: true
})
@JoinTable()
categories: Category[];

https://orkhan.gitbook.io/typeorm/docs/eager-and-lazy-relations#eager-relations

๐Ÿ”ท Lazy relations

Lazy relation์€ ํ•ด๋‹น ํ•„๋“œ์— ์ ‘๊ทผํ•˜๋ฉด ๋กœ๋“œ๋œ๋‹ค. Lazy relation์€ ํƒ€์ž…์œผ๋กœ Promise๋ฅผ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค. Promise์— ๊ฐ’์„ ์ €์žฅํ•˜๊ณ  ๋กœ๋“œํ•  ๋•Œ๋„ Promise๋ฅผ ๋ฐ˜ํ™˜ํžŒ๋‹ค.

@ManyToMany(type => Question, question => question.categories)
questions: Promise< Question[]>;

@ManyToMany(type => Category, category => category.questions)
@JoinTable()
categories: Promise< Category[]>;

const question = await connection.getRepository(Question).findOne(1);
const categories = await question.categories;

https://orkhan.gitbook.io/typeorm/docs/eager-and-lazy-relations#lazy-relations

0๊ฐœ์˜ ๋Œ“๊ธ€