Subscription.2

김종민·2022년 9월 21일
0

Nuber-Server

목록 보기
32/34

들어가기
subscription을
실제 nuber-eats에 적용시켜 본다
1. subscription 즉, 구독하는 부분과
2. trigger하는 부분으로 구성된다.
3. 설정은 app.tsx의 graphql부분에서 설정.
4. test는 Altire에서 하며, 설정에 graphql-ws와
ConnectionParams에 반드시 'x-jwt'를 담아 주어야 한다.
5. 개인적으로 server에서 가장 어려운 부분이므로
주의주의주의해서 봐 둔다.


1. app.module.ts

import { Context } from 'apollo-server-core';
///반드시 Context는 'apollo-server-core'에서 import 해야된다.
///'graph-ws'에서 import하다가 몇일을 날려먹음.ㅠㅠ

import { GraphQLModule } from '@nestjs/graphql';


GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: true,
      subscriptions: {
        'graphql-ws': {
          onConnect: (context: Context<any>) => {
            const { connectionParams, extra } = context;
            extra.token = connectionParams['x-jwt'];
            console.log('extraToken', extra.token);
          },
        },
      },
      context: ({ req, extra }) => {
        return { token: req ? req.headers['x-jwt'] : extra.token };
      },
    }),
    ///위의 설정을 참고하고, req요청일때는 req.headers['x-jwt']에 담아서 token전달
    ///ws 요청일때는 extra.token에 token을 담아서 context에 전달

test는 Altire에서 해야하며, 반드시 아래와 같이 설정.
trigger는 playground에서 해 줘도 된다.


반드시 "x-jwt":"asd" 쌍따음표로 감싸주어야 한다. ConnectionParams 설정시.
sw통신에서는 connectionParams에 token을 담아서 context에 보내줌..

2. orders/orders.resolver.ts

가. 3개의 subscription구성

  1. customer가 주문을 했을때, Owner가 Order를 받는 subscription.
    (Owner가 받음)
  2. Owner가 Order상태를 Cooked(조리끝)으로 status를 바꾸었을떄,
    요리가 다 되었다는 status를 받는 subscription
    (Delivery가 받음)
  3. Order와 관련된 customer, owner, delivery가 order의 status를 받는
    subscription
  4. filter에 대한 설명,
    -filter는 오직 true/false 두개만 return함.
    -filter는 함수로 구성되며 3개의 prop(payload, variables, context)
    -문법은 filter(payload, variables, context)=>{}
    -반드시 console로 위 3개를 찍어볼것.
///client가 order떄렸을떄, Owner가 oreder받는 subscription
///return은 type는 Order임
  @Subscription(() => Order, {
    filter: ({ pendingOrders: { ownerId } }, _, { user }) => {
      console.log(ownerId, user.id);
      return ownerId === user.id;
    },
    resolve: ({ pendingOrders: { order } }) => order,
  })
  ///1. payload에서 pendingOrders안의 ownerId를 뽑아낸다.
  ///pendingOrders는 trigger하는 orders.service.ts의 
  ///createOrder에 만들어 놓음.
  ///2. variables는 사용안하니, _, 로 패스함.
  ///3. context에서 user를 뽑아냄.
  ///4. ownerId와 user.id가 같을때만, subscription될수 있게 함.
  ///5. resolve는 subscription할 data를 말함.
  ///6. order.service.ts의 createOrder에서 만든, pendingOrders의
  ///order를 받을 data로 날려줌.
  
  @Role(['Owner'])
  pendingOrders() {
    return this.pubSub.asyncIterator(NEW_PENDING_ORDER);
  }
  ///Owner만 받을 수 있게 guard해 놓음.
  ///NEW_PENDING_ORDER는 key로써, service에서 trigger하는 부분과 연결됨.
  ///그리고 trigger key들은 common/commom.constants.ts에 모와 놓음..
  ///밑에 참고할것.
  
	///Delivery가 Owner가 Order status의 상태를 pending에서
    ///cooked로 바꾼것을 subscription으로 바꾸는것.
  @Subscription(() => Order)
  @Role(['Delivery'])
  cookedOrders() {
    return this.pubSub.asyncIterator(NEW_COOKED_ORDER);
  }

	///owner, customer, delivery등 order와 관련된 User가
    ///User와 연관된 Order의 status를 실시간으로 update되는
    ///subscription.
  @Subscription(() => Order, {
    filter: (
      { orderUpdates: order }: { orderUpdates: Order },
      ///payload /// :{}부분은 type을 섫명해 놓은것임.
      { input }: { input: OrderUpdatesInput },
      ///variables(dto에 만들어 놓음) /// :{}부분은 type을 섫명해 놓은것임.
      { user }: { user: User },
      ///context ///:{}부분은 type을 섫명해 놓은것임.
    ) => {
      if (
        order.driverId !== user.id && 
        order.customnerId !== user.id &&
        order.restaurant.ownerId !== user.id
      ) {
        return false;
      }
      return order.id === input.id;  
      ///두개가 같을때 subscription이 true가 되게 설정
      
    },
  })
  @Role(['Any'])
  orderUpdates(@Args('input') orderUpdatesInput: OrderUpdatesInput) {
    return this.pubSub.asyncIterator(NEW_ORDER_UPDATE);
  }

***햇갈릴때는 반드시 console을 찍어볼것!!!

3. common/common.constants.ts

trigger key들을 모와놓았음..

  export const PUB_SUB = 'PUB_SUB';
export const NEW_PENDING_ORDER = 'NEW_PENDING_ORDER';
export const NEW_COOKED_ORDER = 'NEW_COOKED_ORDER';
export const NEW_ORDER_UPDATE = 'NEW_ORDER_UPDATE';

4. order.service.ts

trigger하는 부분은 createOrder와 editOrder
2개 이기 때문에 2개만 가져옴
trigger하는 부분만 집중해서 볼것!!!

  async createOrder(
    customer: User,
    { restaurantId, items }: CreateOrderInput,
  ): Promise<CreateOrderOutput> {
    try {
      const restaurant = await this.restaurants.findOne({
        where: {
          id: restaurantId,
        },
      });
      if (!restaurant) {
        return {
          ok: false,
          error: 'Restaurant not found',
        };
      }
      let orderFinalPrice = 0;
      const orderItems: OrderItem[] = [];
      for (const item of items) {
        const dish = await this.dishes.findOne({
          where: {
            id: item.dishId,
          },
        });
        console.log(`Dish Price:${dish.price}`);
        console.log(dish);
        if (!dish) {
          return {
            ok: false,
            error: 'Dish not found',
          };
        }
        let dishFinalPrice = dish.price;
        for (const itemOption of item.options) {
          // console.log('opitons', item.options);
          const dishOption = dish.options.find(
            (dishOption) => dishOption.name === itemOption.name,
          );
          console.log(itemOption.name, dishOption.name);
          if (dishOption) {
            const dishOptionChoice = dishOption.choices.find(
              (optionChoice) => optionChoice.name === itemOption.choice,
            );
            console.log(dishOptionChoice.extra);
            if (dishOptionChoice) {
              if (dishOptionChoice.extra) {
                dishFinalPrice = dishFinalPrice + dishOptionChoice.extra;
              }
            }
            if (dishOption) {
              if (dishOption.extra) {
                dishFinalPrice = dishFinalPrice + dishOption.extra;
              }
            }
          }
        }
        console.log('dishFinalPrice', dishFinalPrice);

        orderFinalPrice = orderFinalPrice + dishFinalPrice;
        const orederItem = await this.orderItems.save(
          this.orderItems.create({
            dish,
            options: item.options,
          }),
        );
        orderItems.push(orederItem);
        console.log('OrderFinalPrice', orderFinalPrice);
      }

      const order = await this.orders.save(
        this.orders.create({
          customer,
          restaurant,
          total: orderFinalPrice,
          items: orderItems,
        }),
      );
      console.log(order);
      
      ***********************************************
      await this.pubSub.publish(NEW_PENDING_ORDER, {
        pendingOrders: { order, ownerId: restaurant.ownerId },
      });
      ///trigger하는 부분.
      ///order가 create되면, Owner에게 subscription으로
      ///payload에
      ///pendingOrders에 order와 ownerId를 담아서
      ///trigger해서 위의 data를 날려줌.
      ///문법 확실히 봐둘것!!
      ***********************************************
      
      return {
        ok: true,
      };
    } catch {
      return {
        ok: false,
        error: 'Could not create Order.',
      };
    }
  }

   async editOrder(
    user: User,
    { id: orderId, status }: EditOrderInput,
  ): Promise<EditOrderOutput> {
    try {
      const order = await this.orders.findOne({
        where: {
          id: orderId,
        },
      });
      if (!order) {
        return {
          ok: false,
          error: 'Order not found',
        };
      }
      if (!this.canSeeOrder(user, order)) {
        return {
          ok: false,
          error: 'Can not see this',
        };
      }
      let canEdit = true;
      if (user.role === UserRole.Client) {
        canEdit = false;
      }
      if (user.role === UserRole.Owner) {
        if (status !== OrderStatus.Cooking && status !== OrderStatus.Cooked) {
          canEdit = false;
        }
      }
      if (user.role === UserRole.Delivery) {
        if (
          status !== OrderStatus.PickedUp &&
          status !== OrderStatus.Deliverd
        ) {
          canEdit = false;
        }
      }
      if (!canEdit) {
        return {
          ok: false,
          error: 'You can not do that',
        };
      }

      await this.orders.save({
        id: orderId,
        status,
      });
      const newOrder = { ...order, status };
      
      *********************************************
      if (user.role === UserRole.Owner) {
        if (status === OrderStatus.Cooked) {
          await this.pubSub.publish(NEW_COOKED_ORDER, {
            cookedOrders: newOrder,
          });
        }
      }
      ///Owner가 order status를 cooked로 바꾸었으면,
      ///Delivery에게 newOrder를 날려줌.
      *************************************************
      
      **********************************************
        await this.pubSub.publish(NEW_ORDER_UPDATE, {
        orderUpdates: newOrder,
      });
      ///order의 status가 바뀔때마다, order와 관련된
      ///owner, customer, delivery에게
      ///update된 status상태를 trigger해줌.
      ///누가 subscription하는 가에 대해서는
      ///resolver에서 설정해  놓음.
      ********************************************
      return {
        ok: true,
      };
    } catch {
      return {
        ok: false,
        error: 'Could not edit order',
      };
    }
  }

5. orders/dtos/order-updates.dto.ts

별거 없음..

 import { InputType, PickType } from '@nestjs/graphql';
import { Order } from '../entities/order.entity';

@InputType()
export class OrderUpdatesInput extends PickType(Order, ['id']) {}

NOTICE !!!
subscription부분은 매우 어려우므로 마르고 닳도록 볼것!!!
filter에서 payload, variables, context 를 받고,
boolean return한다는 것도 확실히 알아 둘것!!!!

profile
코딩하는초딩쌤

0개의 댓글