들어가기
subscription을
실제 nuber-eats에 적용시켜 본다
1. subscription 즉, 구독하는 부분과
2. trigger하는 부분으로 구성된다.
3. 설정은 app.tsx의 graphql부분에서 설정.
4. test는 Altire에서 하며, 설정에 graphql-ws와
ConnectionParams에 반드시 'x-jwt'를 담아 주어야 한다.
5. 개인적으로 server에서 가장 어려운 부분이므로
주의주의주의해서 봐 둔다.
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에 보내줌..
가. 3개의 subscription구성
///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을 찍어볼것!!!
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';
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',
};
}
}
별거 없음..
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한다는 것도 확실히 알아 둘것!!!!