Populate Subdocument in GraphQL

Jenny Yang
The Startup
Published in
5 min readNov 8, 2020

--

Continuing from the last post, I am going to write about populating subdocument in GraphQL.

Context

So, what I have done last time was building two schemas then I nested one of them in an array of another. To give you little more about context about this software, this is a POS software that there are a bunch of products and each payment has an array of products a customer paid.

// models/Transaction.js
import mongoose from 'mongoose';

const Schema = mongoose.Schema;

const itemSchema = new Schema({
name: { type: String },
amount: { type: Number },
});

const transactionSchema = new Schema(
{
price: { type: Number },
method: { type: String, default: 'VISA' },
cardNumber: { type: String },
paidTime: { type: Date, default: new Date() },
items: [itemSchema], // we will change this part
}
);

export const Transaction = mongoose.model('Transaction', transactionSchema);

This time, I would build a completely new model and reference from it that will act as a foreign key.

Overview

What I am going to do is to save ids in an array, rather than store whole subdocument. From something like the left one to the right one.

  1. Create a new model, Product
// models/Product.jsimport mongoose from 'mongoose';const Schema = mongoose.Schema;const productSchema = new Schema({
name: { type: String },
price: { type: Number },
category: { type: String },
});
export const Product = mongoose.model('Product', productSchema);
  1. Reference child from a parent document.
// models/Transaction.jsimport mongoose from 'mongoose';const Schema = mongoose.Schema;const transactionSchema = new Schema(
{
price: { type: Number },
method: { type: String, default: 'VISA' },
cardNumber: { type: String },
paidTime: { type: Date, default: new Date() },
// it used to be items: [itemSchema],
items: [{ type: Schema.Types.ObjectId, ref: 'Product' }],
}
);
export const Transaction = mongoose.model('Transaction', transactionSchema);

Because we no longer need the itemSchema, I deleted them and referenced Product model by its id by specifying the type of property I am going to store in items and its model name as a reference.

{type: id, ref: model name}

TypeDefs

If we changed our model with mongoose, we also need to let typeDefs know the changes too, to make that work in graphQL.

// typeDefs.js// Transaction type and input
type Transaction {
id: ID!
price: Float!
method: String!
cardNumber: String!
paidTime: Date!
items: [Product]
}
input TransactionInput {
price: Float
method: String
cardNumber: String
items: [ID]
}
// Product type
input ProductInput {
name: String
price: Float
category: String
}
type Product {
id: ID!
name: String
price: Float
category: String
}
type Mutation {
createTransaction(TransactionInput: TransactionInput!): Transaction
deleteTransaction(transactionID: ID!): Transaction
// resolver functions for product and item, will explain soon.
createItem(productId: String, transactionId: ID): Transaction
deleteItem(productId: String, transactionId: ID): Transaction
createProduct(ProductInput: ProductInput): Product
deleteProduct(productId: ID!): Product
}

Product Model

Product model will have certain products we have. Imagine you are working in a sandwich shop, and they have two products, and whenever it is ordered, the item’s id will be pushed into the array in a transaction. So what I am going to do is to create some products to be used in items field of the transaction.

Product Resolver

We are going to create a product resolver, and don’t forget to connect the resolver to your apollo server.

// resolvers/product.js
import { Product } from '../models/Product';
export const productResolver = {
Query: { query for product },
Mutation: {
createProduct: async (_, { ProductInput }) => {
try {
const newProduct = await Product.create(ProductInput);
await newProduct.save()
return newProduct
} catch (error) {
console.error(error.message)
}
},
deleteProduct: async (_, { productId }) => {
const deleteProduct = await Product.findByIdAndDelete(productId)
if (deleteProduct) {
console.log(`Product ${productId} deleted.`)
} else {
console.log(`Product ${productId} doesn't exist`)
}
}

I made two product mutation which is equivalent to POST and DELETE.

In createProduct, new product will be created with passing product input then save it.

In deleteProduct, it will find a product to be deleted with its id then be deleted.

We are NOT done yet though. If you try fetching transaction, it will leave items field with an empty array.

Why doesn’t it work?

It is because the transaction doesn’t know what to do with the items field. In typeDefs, we declared items field in Transaction will have a Product object, but only provided resolver with ID!

TypeDefs.js for Transaction input and type

Transaction type

To solve the empty array of item field, we need to let Transaction know what they do with it, precisely letting items field know!

// resolver/transaction.jsexport const transactionResolver = {
Transaction: {
items: async (transaction) => {
return (await transaction.populate('items').execPopulate()).items;
},
},

Mutation: {... some mutation functions ...},
Query: {... some query functions ...},

Transaction here is same as the typeDefs.js, and we are going to specify the items field of it using an async function. This resolver function derived from Apollo server, and the function takes 4 arguments, (parent, args, context, info) . But I am only going to use a parent argument, which returns a value of the resolver for this field’s parent, in this case, transaction, parent of items field.

Specify transaction type in resolver

Now we need to create a resolver to populate items.

// resolver/transaction.js
const transactionResolver = {
... previous code ...
createItem: async (_, { productId, transactionId }) => {
try{
const transaction = await Transaction.findById(transactionId);
await transaction.items.push(productId);
const savedTransaction = await transaction.save();
return savedTransaction;
}catch(error){
console.error(error.message)
}
},

To populate:

  1. find the transaction to populate by its id
  2. push id of product to an items field
  3. then save the change
  4. return save Transaction

So that was how to populate subdocument referenced by its id. I will need to get used to this since it was quite a bit of change I had to make. Thank you for reading, I hope it also helped those of you who are looking to implement a similar schema architecture.

--

--

Jenny Yang
The Startup

Self-taught software engineer | Enthusiasm for programming and computer science