I am using Apollo Server and GraphQL + Prisma. I am running into some trouble when trying to create an ecomm feature of order creation.
Since order creation involves a payment, order and orderItems, creating the mutation using dataSources + resolvers has proven challenging.
I am close to a working solution but for some reason my return values are null for my order
fields and my orderItems
fields.
Graphql Schema + SDL:
type Mutation {
"Mutation for when a user places an order"
placeOrder(input: PlaceOrderInput!): PlaceOrderMutationResponse!
}
input PlaceOrderInput {
user_id: ID!
orderItems: [OrderItemInput!]!
payment: PaymentInput!
}
"""
This type represents the composite response containing order details,
including associated payment and order items.
"""
type CompositeOrder {
"Order details"
order: Order
"Payment details associated with the order"
payment: Payment
"List of order items associated with the order"
orderItems: [OrderItem!]
}
"""
Mutation response for the placeOrder mutation
"""
type PlaceOrderMutationResponse {
"Response code"
code: Int!
"Indicates if the operation was successful"
success: Boolean!
"Message associated with the response"
message: String!
"Composite order details"
order: CompositeOrder
}
This type represents an order in the system"
type Order {
"Unique identifier for the order"
id: ID!
"User who placed the order"
user: User!
"Current status of the order (e.g., "pending", "shipped")"
status: OrderStatus!
"Total price of the order"
total_price: Float!
"List of order items associated with this order (required, cannot be empty)"
orderItems: [OrderItem!]!
"Payment associated with the order"
payment: Payment
}
"This type represents an item within an order"
type OrderItem {
"Unique identifier for the order item"
id: ID!
"Quantity of the product included in this order item"
quantity: Int!
"Price of the product at the time the order was placed"
price: Float!
"Order this order item belongs to"
order: Order!
"Product associated with this order item"
product: Product!
}
"This type represents a payment for an order"
type Payment {
"Unique identifier for the payment"
id: ID!
"User who owns this payment transaction"
user: User
"Mock payment method used (e.g., credit card)"
payment_method: String
"Optional month of expiration for the payment method"
exp_month: Int
"Optional year of expiration for the payment method"
exp_year: Int
"Masked card number for the payment method (e.g., "XXXX XXXX XXXX XXXX")"
card_number: String
"Mock payment status (e.g., "PENDING", "PROCESSED")"
payment_status: String!
}
orderAPI.ts file where the bulk of the creation logic is handled:
export class OrderAPI {
// Dependency injection of product api for flexibility
constructor(public readonly productAPI: ProductAPIInterface) {}
async placeOrder(input: PlaceOrderInput, user: User): Promise<PlaceOrderMutationResponse> {
try {
const PAYMENT_STATUS = {
PENDING: 'PENDING',
PROCESSED: 'PROCESSED',
};
// Step 1: Fetch product prices using dependency-injected productAPI
const productIds = input.orderItems.map((item) => item.product_id);
const priceMap = await this.productAPI.getPricesByIds(productIds);
// Step 2: Process order items with retrieved prices
const orderItemsWithPrices = input.orderItems.map((item) => ({
...item,
price: priceMap[item.product_id], // Get price from priceMap by product ID
}));
// Validate product existence (optional):
const allProductsExist = orderItemsWithPrices.every((item) => item.price !== undefined);
if (!allProductsExist) {
throw new Error('One or more products not found');
}
// Step 3: Calculate total price using retrieved prices
const totalPrice = orderItemsWithPrices.reduce(
(acc, item) => acc + item.price * item.quantity,
0,
);
// Step 2: Create the order with eager loading
const newOrder = await prisma.order.create({
data: {
user: { connect: { id: user.id } },
status: 'Pending', // Set initial order status
total_price: totalPrice,
OrderItem: {
create: orderItemsWithPrices.map((item) => ({
product: { connect: { id: item.product_id } },
quantity: item.quantity,
price: item.price, // Use the fetched price here
})),
},
payment: {
create: {
user: { connect: { id: user.id } },
payment_method: 'CREDIT_CARD',
exp_month: input.payment.exp_month,
exp_year: input.payment.exp_year,
card_number: input.payment.card_number, // Replace with a secure way to store card details
payment_status: PAYMENT_STATUS.PENDING,
},
},
},
include: { OrderItem: true, payment: true }, // Include associated data for response
});
// Step 3: Return success response with the expected structure
return {
code: 200,
success: true,
message: 'Order created successfully',
order: newOrder,
};
} catch (error) {
console.error('Error creating order:', error);
return {
code: 500,
success: false,
message: 'Failed to create order',
order: null,
};
}
}
}
Mutation resolver itself:
Mutation: {
placeOrder: requireAuthentication(
async (
_parent: unknown,
{ input }: MutationPlaceOrderArgs,
{ dataSources, user }: GQLContext,
) => {
const { orderAPI } = dataSources;
const newOrder = await orderAPI.placeOrder(input, user!);
console.log(newOrder);
return newOrder;
},
),
},
when I console log new Order above this is the output:
{
[0] code: 200,
[0] success: true,
[0] message: 'Order created successfully',
[0] order: {
[0] id: 'a5d581b0-d5c5-4947-b26e-39b3cd55749a',
[0] user_id: 'd300bb1e-ee8a-40b8-a7da-9b5e602e0044',
[0] status: 'Pending',
[0] total_price: 159.92,
[0] payment_id: '6e397b4c-12bc-4fd2-9a18-7276b1970cec',
[0] created_at: 2024-05-15T02:10:55.294Z,
[0] updated_at: 2024-05-15T02:10:55.294Z,
[0] deleted: false,
[0] OrderItem: [ [Object] ],
[0] payment: {
[0] id: '6e397b4c-12bc-4fd2-9a18-7276b1970cec',
[0] user_id: 'd300bb1e-ee8a-40b8-a7da-9b5e602e0044',
[0] payment_method: 'CREDIT_CARD',
[0] exp_month: 12,
[0] exp_year: 28,
[0] card_number: '8888-2232-4443-1111',
[0] payment_status: 'PENDING',
[0] created_at: 2024-05-15T02:10:55.294Z,
[0] updated_at: 2024-05-15T02:10:55.294Z,
[0] deleted: false
[0] }
[0] }
[0] }
Mutation ran from apollo sandbox:
mutation Mutation($input: PlaceOrderInput!) {
placeOrder(input: $input) {
code
success
message
order {
order {
status
total_price
}
payment {
payment_method
card_number
}
orderItems {
quantity
price
id
}
}
}
}
But as you can see I get null back for some fields:
{
"data": {
"placeOrder": {
"code": 200,
"success": true,
"message": "Order created successfully",
"order": {
"order": null,
"payment": {
"payment_method": "CREDIT_CARD",
"card_number": "8888-2232-4443-1111"
},
"orderItems": null
}
}
}
}
Relevant Prisma Models:
model Payment {
id String @id @default(uuid())
user User @relation(fields: [user_id], references: [id])
user_id String // Change type to String
payment_method String // Mock payment method (e.g., "credit card")
exp_month Int? // Optional, month of expiration (e.g., 1 for January)
exp_year Int? // Optional, year of expiration (e.g., 2024)
card_number String? // Optional, masked card number (e.g., "XXXX XXXX XXXX XXXX")
payment_status String @default("PROCESSED") // Mock payment status (e.g., "success", "failure")
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted Boolean @default(false)
Order Order[]
}
model Order {
id String @id @default(uuid())
user User @relation(fields: [user_id], references: [id])
user_id String
status String
total_price Float
payment Payment @relation(fields: [payment_id], references: [id])
payment_id String
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted Boolean @default(false)
OrderItem OrderItem[]
}
model OrderItem {
id String @id @default(uuid())
order Order @relation(fields: [order_id], references: [id])
order_id String // Field will exist in db
product Product @relation(fields: [product_id], references: [id])
product_id String // Field will exist in db
quantity Int
price Float
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted Boolean @default(false)
}
Is my design off? Am I misusing Apollo Server or not using it’s full capabilities?
I don’t think I should have to query for order: {order: {}}...
it should be top level when created. And it’s strange that only payment properly returns.
I can’t seem to figure out how to get any and all fields related to order creation to return in my gql mutation response.
What should I do?