I am currently trying to learn React by developing a meal planner app for myself. Coming from a traditional business programming world I have decided on using MySQL as a backend, and Sequelize/GraphQL as my data interface.
I have a data model that goes as follows:
-Users have Meals
-Meals have the properties of day, type (breakfast, lunch, etc) and "MealItems"
-MealItems can be either a FoodItem or a Recipe
-Recipes are basically a collection of FoodItems
I implemented that idea with this data schema (really quick mockup done in Access): Meal Planner Model
I have managed to code up a Sequelize model that creates the tables and constraints exactly how I want. I used the n:m association example from the official Sequelize documentation to create the MealItems lookup table that should allow for the model to dynamically return either a FoodItem or Recipe, based on the scope ("ItemType"). (But I don't know if I did that part correctly as I can't manage to actually pull the data through anything other than raw SQL queries.)
The full source code for my project can be found here: (the relevant data components are under './src/data') https://github.com/philspins/NourishMe
Sequelize model:
//
// Model definitions
// -----------------------------------------------------------------------------
import DataType from "sequelize";
import Model from "../sequelize";
const FoodItem = Model.define("FoodItem",
{
Name: { type: DataType.STRING(100) },
Quantity: { type: DataType.STRING(32) },
Weight: { type: DataType.INTEGER },
Calories: { type: DataType.INTEGER },
Protein: { type: DataType.DOUBLE },
Carbs: { type: DataType.DOUBLE },
Fat: { type: DataType.DOUBLE },
Fibre: { type: DataType.DOUBLE },
ImageURL: { type: DataType.TEXT }
});
const Recipe = Model.define("Recipe",
{
Name: { type: DataType.STRING(100) },
Instructions: { type: DataType.TEXT },
ImageURL: { type: DataType.TEXT }
});
const Ingredient = Model.define("Ingredient");
const Meal = Model.define("Meal",
{
Day: { type: DataType.DATE }
});
const MealType = Model.define("MealType",
{
Name: { type: DataType.STRING(100) }
});
const MealItem = Model.define("MealItem",
{
id: {type: DataType.INTEGER, primaryKey: true, autoIncrement: true},
ItemType: { type: DataType.STRING(100) },
ItemID: { type: DataType.STRING(100) },
Quantity: { type: DataType.DOUBLE }
},
{
instanceMethods: {
getItem: function() {
return this["get" + this.get("ItemType").substr(0,1).toUpperCase() + this.get("ItemType").substr(1)]();
}
}
});
//
// Recipe and FoodItem relations
// -----------------------------------------------------------------------------
Recipe.FoodItems = Recipe.belongsToMany(FoodItem, {
through: Ingredient,
as: "FoodItems"
});
FoodItem.Recipes = FoodItem.belongsToMany(Recipe, {
through: Ingredient,
as: "Recipes"
});
//
// Meals relationships with Recipe and FoodItem
// -----------------------------------------------------------------------------
Meal.belongsToMany(Recipe, {
through: MealItem,
foreignKey: "ItemID",
constraints: false,
scope: {
ItemType: "Recipe"
}
});
Recipe.belongsToMany(Meal, {
through: MealItem,
foreignKey: "ItemID",
constraints: false,
as: "Recipe"
});
Meal.belongsToMany(FoodItem, {
through: MealItem,
foreignKey: "ItemID",
constraints: false,
scope: {
ItemType: "FoodItem"
}
});
FoodItem.belongsToMany(Meal, {
through: MealItem,
foreignKey: "ItemID",
constraints: false,
as: "FoodItem"
});
//
// Other Meal relationships
// -----------------------------------------------------------------------------
Meal.MealItems = Meal.hasMany(MealItem, {foreignKey: {allowNull: false}, onDelete: "CASCADE"});
Meal.User = User.hasMany(Meal, {foreignKey: {allowNull: false}, onDelete: "CASCADE"});
Meal.MealType = MealType.hasMany(Meal, {foreignKey: {allowNull: false}, onDelete: "CASCADE"});
I have GraphQL types and queries setup to return all data except for Meal. I can not get it to return anything except for the values that are actually in the MealItem table. I was able to link up FoodItem to Recipe no problem, and retrieve a JSON package that has FoodItems embedded in Recipes, but can't figure out how to do the same thing with MealItems. This is the model as I have it working now: [Visualization of GraphQL model as it is][3] But I would like to have Meals be able to have either FoodItems or Recipes embedded in the output rather than MealItems.
And this is my GraphQL code, as I have it working:
import {GraphQLObjectType,
GraphQLList,
GraphQLNonNull,
GraphQLID,
GraphQLString} from "graphql";
import {resolver, attributeFields} from "graphql-sequelize";
import {Meal,
Recipe,
FoodItem as
FoodModel,
MealItem as MealItemModel} from "../models";
const FoodType = new GraphQLObjectType({
name: "FoodItem",
fields: attributeFields(FoodModel),
resolve: resolver(FoodModel)
});
const RecipeType = new GraphQLObjectType({
name: "Recipe",
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
Name: { type: GraphQLString },
Instructions: { type: GraphQLString },
ImageURL: { type: GraphQLString },
Ingredients: {
type: new GraphQLList(FoodType),
resolve: resolver(Recipe.FoodItems) }
}
});
const MealTypeType = new GraphQLObjectType({
name: "MealType",
fields: attributeFields(MealType)
});
const MealItemType = new GraphQLObjectType({
name: "MealItem",
fields: attributeFields(MealItemModel),
resolve: resolver(MealItemModel)
});
const MealType = new GraphQLObjectType({
name: "Meal",
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
Day: { type: DateType },
UserId: { type: GraphQLID },
MealTypeId: { type: GraphQLID },
MealItems: {
type: new GraphQLList(MealItemType),
resolve: resolver(Meal.MealItems)
}
}
});
const Meals = {
type: new GraphQLList(MealType),
resolve: resolver(Meal)
};
const schema = new Schema({
query: new ObjectType({
name: "Root",
fields: {
Meals
}
})
});
What I think I need to do, in order to get the MealType to dynamically return either a FoodType or RecipeType instead of MealItemType, is something along the lines of this. But this is what I can't get to work, and is the cause of this extremely lengthy question.
function resolveMealItemType(value){
if(value.ItemType == "Recipe"){return RecipeType;}else{return FoodType;}
}
const MealItemType = new GraphQLUnionType({
name: "MealItem",
types: [RecipeType, FoodType],
resolveType: resolveMealItemType
});
const MealType = new GraphQLObjectType({
name: "Meal",
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
Day: { type: DateType },
UserId: { type: GraphQLID },
MealTypeId: { type: GraphQLID },
MealItems: {
type: new GraphQLList(MealItemType),
resolve: resolver(Meal.MealItems)
}
}
});
Current query, and output:
{
Meal {
Day
MealTypeId
UserId
MealItems {
ItemType
ItemID
}
}
}
{
"data": {
"Meal": {
"Day": "2017-02-07T16:18:47.000Z",
"MealTypeId": "1",
"UserId": "1",
"MealItems": [
{
"ItemType": "Recipe",
"ItemID": 1
},
{
"ItemType": "FoodItem",
"ItemID": 25
}
]
}
}
}
Desired query, and output:
{
Meal {
Day
MealTypeId
UserId
MealItems {
... on FoodItem {
Name
Quantity
Weight
Calories
Carbs
Protein
Fat
Fibre
}
... on Recipe {
Name
Instructions
Ingredients {
Name
Quantity
Weight
Calories
Carbs
Protein
Fat
Fibre
}
}
}
}
}
{
"data": {
"Meal": {
"Day": "2017-02-07T15:30:10.000Z",
"MealTypeId": "1",
"UserId": "1",
"MealItems": [
{
"Name": "Fish, Halibut, Pacific",
"Quantity": "4 oz uncooked",
"Weight": 113,
"Calories": 124,
"Carbs": 0,
"Protein": 24,
"Fat": 3,
"Fibre": 0
},
{
"Name": "Test Recipe 1",
"Instructions": "Recipe instructions go here...",
"Ingredients": [
{
"Name": "Fish, Halibut, Pacific",
"Quantity": "4 oz uncooked",
"Weight": 113,
"Calories": 124,
"Carbs": 0,
"Protein": 24,
"Fat": 3,
"Fibre": 0
},
{
"Name": "Sardines (herring), canned in olive oil",
"Quantity": "1 can (3.2 oz)",
"Weight": 91,
"Calories": 191,
"Carbs": 0,
"Protein": 23,
"Fat": 11,
"Fibre": 0
}
}
]
}
}
}
Don't set a key resolver
directly inside a GraphQLObjectType
:
...
const FoodType = new GraphQLObjectType({
name: "FoodItem",
fields: attributeFields(FoodModel),
resolve: resolver(FoodModel), // <--- this is wrong, it won't be used
// Only set resolvers inside fields, not on the root of the object
});
...
Then you were right about using the GraphQLUnionType
.
But in the resolve function of the MealType
, you should return a merged array of FoodItems and Recipes and not entries of the MealItems
table. You are saying with the union "I am returning a List of either FoodItem or Recipe", so this is what you should do.
So,
const MealType = new GraphQLObjectType({
...
fields: {
...
MealItems: {
type: new GraphQLList(MealItemType),
resolve: (meal, args, context) => {
return Promise.all([
Meal.getFoodItems(), // pseudo-code
Meal.getRecipes(), // pseudo-code
])
.then(([ foodItems, recipes ]) => foodItems.concat(recipes));
},
},
...
},
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With