Avoid Code Duplication with GraphQL Cursor based Pagination - javascript

I've been looking all over for an answer to this and I've been banging my head on the wall. I wrote a cursor based pagination example that works well with graphql and the thing is I thought I would do the same thing with authors, that I did with books and the only way I can figure out how to do this is to completely duplicate everything. On the root query there is quite a long chunk of code handling the pagination and I would hate to do that all over for the authors endpoint but I can't seem to find a way to do this while reusing the code
Here is the code
const express = require('express')
const { graphqlHTTP } = require('express-graphql')
const {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLList,
GraphQLInt,
GraphQLNonNull
} = require('graphql')
const {
PageType,
convertNodeToCursor,
convertCursorToNodeId
} = require('./pagination')
const app = express()
const authors = [
{ id: 1, name: "Author 1"},
{ id: 2, name: "Author 2"},
{ id: 3, name: "Author 3"}
]
const books = [
{ id: 1, title: "Book 1", authorId: 1 },
{ id: 2, title: "Book 2", authorId: 1 },
{ id: 3, title: "Book 3", authorId: 1 },
{ id: 4, title: "Book 4", authorId: 2 },
{ id: 5, title: "Book 5", authorId: 2 },
{ id: 6, title: "Book 6", authorId: 2 },
{ id: 7, title: "Book 7", authorId: 3 },
{ id: 8, title: "Book 8", authorId: 3 },
{ id: 9, title: "Book 9", authorId: 3 }
]
const Book = new GraphQLObjectType({
name: 'Book',
description: 'this is a book',
fields: () => ({
id: { type: GraphQLNonNull(GraphQLInt) },
title: { type: GraphQLNonNull(GraphQLString) },
authorId: { type: GraphQLNonNull(GraphQLInt) },
author: {
type: Author,
resolve: ({authorId}) => {
return authors.find(author => author.id === authorId)
}
}
})
})
const Author = new GraphQLObjectType({
name: 'Author',
description: 'this represents the author of a book',
fields: () => ({
id: { type: GraphQLNonNull(GraphQLInt) },
name: { type: GraphQLNonNull(GraphQLString) },
books: {
type: GraphQLList(Book),
resolve: ({id}) => {
return books.filter(book => book.authorId === id)
}
}
})
})
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
description: 'this is the root query',
fields: () => ({
book: {
type: Book,
description: 'a single book',
args: {
id: { type: GraphQLInt }
},
resolve: (_, { id }) => {
return books.find(book => book.id === id)
}
},
author: {
type: Author,
description: 'a single author',
args: {
id: { type: GraphQLInt },
},
resolve: (_, { id }) => {
return authors.find(author => author.id === id)
}
},
books: {
type: PageType(Book),
description: 'a list of books',
args: {
first: { type: GraphQLInt },
afterCursor: { type: GraphQLString }
},
resolve: (_, { first, afterCursor }) => {
let afterIndex = 0
if (typeof afterCursor === 'string') {
let nodeId = convertCursorToNodeId(afterCursor)
let nodeIndex = books.findIndex(book => book.id === nodeId)
if (nodeIndex >= 0) {
afterIndex = nodeIndex + 1
}
}
const slicedData = books.slice(afterIndex, afterIndex + first)
console.log('sliced data: ', slicedData)
const edges = slicedData.map(node => ({
node,
cursor: convertNodeToCursor(node)
}))
let startCursor = null
let endCursor = null
if (edges.length > 0) {
startCursor = convertNodeToCursor(edges[0].node)
endCursor = convertNodeToCursor(edges[edges.length - 1].node)
}
let hasNextPage = books.length > afterIndex + first
return {
totalCount: books.length,
edges,
pageInfo: {
startCursor,
endCursor,
hasNextPage
}
}
}
}
})
})
const schema = new GraphQLSchema({
query: RootQuery
})
app.use('/graphql', graphqlHTTP({
schema,
graphiql: true
}))
app.listen(3000, () => console.log('app running at http://localhost:3000/graphql'))
and I handle the pagination in another file here:
const {
GraphQLString,
GraphQLInt,
GraphQLBoolean,
GraphQLObjectType,
GraphQLList,
} = require('graphql')
const Edge = (itemType) => {
return new GraphQLObjectType({
name: 'EdgeType',
fields: () => ({
node: { type: itemType },
cursor: { type: GraphQLString }
})
})
}
const PageInfo = new GraphQLObjectType({
name: 'PageInfoType',
fields: () => ({
startCursor: { type: GraphQLString },
endCursor: { type: GraphQLString },
hasNextPage: { type: GraphQLBoolean }
})
})
const PageType = (itemType) => {
return new GraphQLObjectType({
name: 'PageType',
fields: () => ({
totalCount: { type: GraphQLInt },
edges: { type: new GraphQLList(Edge(itemType)) },
pageInfo: { type: PageInfo }
})
})
}
const convertNodeToCursor = (node) => {
// Encoding the cursor value to Base 64 as suggested in GraphQL documentation
return Buffer.from((node.id).toString()).toString('base64')
}
const convertCursorToNodeId = (cursor) => {
// Decoding the cursor value from Base 64 to integer
return parseInt(Buffer.from(cursor, 'base64').toString('ascii'))
}
module.exports = {
PageType,
convertNodeToCursor,
convertCursorToNodeId
}
Now if I copy and paste the books endpoint and change it to authors, and change the type to PageType(Author) then I get another error:
Schema must contain uniquely named types but contains multiple types named "PageType".
So this clearly isn't a solution either

You cannot have one EdgeType that contains Authors and another EdgeType that contains Books. Instead, you will need one AuthorEdge and one BookEdge type.
The same holds for the PageType - there can't be two different types with different fields but the same name.
The solution is relatively simple though - if you dynamically generated these types in a function, also name them dynamically:
const Edge = (itemType) => {
return new GraphQLObjectType({
name: itemType.name + 'Edge',
// ^^^^^^^^^^^^^^^^^^^^^^
fields: () => ({
node: { type: itemType },
cursor: { type: GraphQLString }
})
})
}
const PageInfo = new GraphQLObjectType({
name: 'PageInfo',
fields: () => ({
startCursor: { type: GraphQLString },
endCursor: { type: GraphQLString },
hasNextPage: { type: GraphQLBoolean }
})
})
const PageType = (itemType) => {
return new GraphQLObjectType({
name: itemType.name + 'sPage',
// ^^^^^^^^^^^^^^^^^^^^^^^
fields: () => ({
totalCount: { type: GraphQLInt },
edges: { type: new GraphQLList(Edge(itemType)) },
pageInfo: { type: PageInfo }
})
})
}

Related

Write Conflict mongoose transactions

In my application i am trying to build a route for creating orders the flow should go like this:
1- reserve the products for the user
2- empty the user's cart
3- take payment
however if one operation fails the database should return to its original state before updating any documents
I'm using mongodb
this is what i tried:
export default async function order(userId) {
const User = mongoose.model('user');
const Product = mongoose.model('product');
const Order = mongoose.model('order');
const session = await mongoose.startSession();
session.startTransaction();
try {
const user = await User.findById(userId).session(session);
// reserve the product for the user
const results = await Product.bulkWrite(
user.cart.map((item) => ({
updateOne: {
filter: {
_id: item.product,
'combinations.size': item.size,
'combinations.color': item.color,
'combinations.qty': { $gte: item.qty },
},
update: {
$inc: { 'combinations.$.qty': -item.qty },
},
},
})),
{ session }
);
if (results.nModified !== user.cart.length) {
throw new Error('Not all products available, transaction aborted');
}
// empty user's cart
await User.findByIdAndUpdate(userId, { cart: [] }).session(session);
// take payment from the user
const payment = (successed) => {
if (successed) return { id: 'charge id' };
throw new Error('payment failed');
};
const charge = payment(true);
const productsInCart = await Product.find({
_id: { $in: user.cart.map((item) => item.product.toString()) },
});
// create the order
await Order.create([
{
user: userId,
products: user.cart.map((item) => ({
...item,
price: productsInCart.find(
(product) => product._id.toString() === item.product.toString()
).price.curr1,
})),
currency: 'curr1',
status: 'placed',
chargeId: charge.id,
},
]);
await session.commitTransaction();
} catch (err) {
console.log(err);
await session.abortTransaction();
} finally {
await session.endSession();
}
}
however when testing it i get this error
these are my models:
User:
import { Schema, model } from 'mongoose';
const UserSchema = new Schema({
email: String,
cart: [
{
product: { type: Schema.Types.ObjectId, ref: 'product' },
color: String,
size: String,
qty: Number,
},
],
orders: [{ type: Schema.Types.ObjectId, ref: 'order' }],
});
model('user', UserSchema);
Product
import { Schema, model } from 'mongoose';
const ProductSchema = new Schema({
title: String,
price: {
curr1: Number, // price in a region
curr2: Number, // price in another region
},
combinations: [
{
size: String,
color: String,
qty: Number,
},
],
});
model('product', ProductSchema);
Order:
import { Schema, model } from 'mongoose';
const OrderSchema = new Schema({
user: { type: Schema.Types.ObjectId, ref: 'user' },
products: [
{
product: { type: Schema.Types.ObjectId, ref: 'product' },
size: String,
color: String,
qty: Number,
price: Number,
},
],
currency: String,
status: String,
chargeId: String,
});
model('order', OrderSchema);
and my test:
import async from 'async';
import mongoose from 'mongoose';
import order from '../src/order.js';
const User = mongoose.model('user');
const Order = mongoose.model('order');
const Product = mongoose.model('product');
describe('Race condition for orders', async () => {
let products;
let users;
beforeEach(async () => {
products = await Product.insertMany([
{
title: 'test product 1',
price: { curr1: 10, curr2: 10 },
combinations: [
{
size: 'large',
color: 'black',
qty: 1,
},
{
size: 'small',
color: 'red',
qty: 2,
},
],
},
{
title: 'test product 2',
price: { curr1: 10, curr2: 10 },
combinations: [
{
size: 'medium',
color: 'yellow',
qty: 1,
},
],
},
]);
users = await User.insertMany([
{
email: 'test#test.test',
cart: products.map((product) => ({
product: product._id.toString(),
size: product.combinations[0].size,
color: product.combinations[0].color,
qty: 1,
})),
},
{
email: 'test2#test.test',
cart: products.map((product) => ({
product: product._id.toString(),
size: product.combinations[0].size,
color: product.combinations[0].color,
qty: 1,
})),
},
]);
});
it('calls order twice at the same time', async () => {
await new Promise((resolve, reject) =>
async.parallel(
[
async () => {
await order(users[0]._id.toString());
},
async () => {
await order(users[1]._id.toString());
},
],
(err, res) => {
if (err) return reject(err);
resolve(res);
}
)
);
console.log(await Order.find({}));
});
});

GraphQL NodeJS Error: One of the provided types for building the Schema is missing a name

i am learning GraphQL with NodeJS but getting the error.
i have develop a mongoDB and NodeJS GraphQL REST API for Understanding and Learning purpose
Here is my Code :
const graphql = require('graphql');
const user = require('../models/User');
const post = require('../models/Post');
const { GraphQLObjectType, GraphQLID, GraphQLString } = graphql;
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: { type: GraphQLID },
fname: { type: GraphQLString },
lname: { type: GraphQLString },
email: { type: GraphQLString },
posts: {
type: PostType,
resolve(parent, args) {
return 1;
}
}
})
});
const PostType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: { type: GraphQLID },
Userid: { type: GraphQLID },
title: { type: GraphQLString },
date: { type: GraphQLString },
})
});
const RootQueryType = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
user: {
type: UserType,
args: { id: { type: GraphQLID } },
resolve(parent, args) {
return user.findById({ id: args.id });
}
},
post: {
type: PostType,
args: { id: { type: GraphQLID } },
resolve(parent, args) {
return post.findById({ id: args.id });
}
}
}
});
module.exports = new graphql.GraphQLSchema({
query: RootQueryType,
});
Please Help me to find the Solution.
Answer me to resolve this error.

Writing Nested GraphQL Mutations

I am looking for examples of writing nested mutations. I am making a mutation for a recipe object and the schema looks like this:
const RecipeType = new GraphQLObjectType({
name: "Recipe",
fields: () => ({
id: { type: GraphQLID },
name: { type: GraphQLString },
dateCreated: { type: GraphQLString },
authorID: { type: GraphQLID },
prepTime: { type: PrepTimeType },
cookTime: { type: CookTimeType },
ingredients: { type: new GraphQLList(IngredientType) },
steps: { type: new GraphQLList(StepType) }
})
});
const PrepTimeType = new GraphQLObjectType({
name: "PrepTime",
fields: () => ({
quantity: { type: GraphQLFloat },
unit: { type: GraphQLString }
})
});
const CookTimeType = new GraphQLObjectType({
name: "CookTime",
fields: () => ({
quantity: { type: GraphQLFloat },
unit: { type: GraphQLString }
})
});
const IngredientType = new GraphQLObjectType({
name: "Ingredients",
fields: () => ({
name: { type: GraphQLString },
quantity: { type: GraphQLFloat },
unit: { type: GraphQLString }
})
});
const StepType = new GraphQLObjectType({
name: "Ingredients",
fields: () => ({
details: { type: GraphQLString },
estimatedTime: { type: GraphQLFloat },
unit: { type: GraphQLString }
})
});
I am looking to write a mutation that creates an entire object for this item. The mutation looks like the following:
createRecipe: {
type: RecipeType,
args: {
// Required Args
name: { type: new GraphQLNonNull(GraphQLString) },
authorID: { type: new GraphQLNonNull(GraphQLID) },
ingredients: { type: new GraphQLList(IngredientType) },
steps: { type: new GraphQLList(StepType) },
// Not required args
prepTime: { type: PrepTimeType },
cookTime: { type: CookTimeType },
},
resolve(parent, args) {
let recipe = new Recipe({
name: args.name,
dateCreated: new Date().getTime(),
authorID: args.authorID,
ingredients: args.ingredients,
steps: args.steps
});
// Check for optional args and set to recipe if they exist
args.prepTime ? recipe.prepTime = args.prepTime : recipe.prepTime = null;
args.cookTime ? recipe.cookTime = args.cookTime : recipe.cookTime = null;
return recipe.save();
}
}
I am not sure how to create one mutation that creates the entire object. and then updating will be a further challenge. Does anyone have any examples or links to the docs that support this? From what I can tell GraphQL hasn't covered this in a helpful manner.
I am currently getting the following errors:
{
"errors": [
{
"message": "The type of Mutation.createRecipe(ingredients:) must be Input Type but got: [Ingredients]."
},
{
"message": "The type of Mutation.createRecipe(steps:) must be Input Type but got: [Steps]."
},
{
"message": "The type of Mutation.createRecipe(prepTime:) must be Input Type but got: PrepTime."
},
{
"message": "The type of Mutation.createRecipe(cookTime:) must be Input Type but got: CookTime."
}
]
}
Any support would be greatly appreciated.
Cheers,
I figured this out. I needed to create input types for each subdocument. I already had the object types but for the mutations, I had to add new ones. From there I placed it into the mutation as such.
createRecipe: {
type: RecipeType,
args: {
// Required Args
name: { type: new GraphQLNonNull(GraphQLString) },
authorID: { type: new GraphQLNonNull(GraphQLID) },
ingredients: { type: new GraphQLList(IngredientInputType) },
steps: { type: new GraphQLList(StepInputType) },
// Not required args
prepTime: { type: PrepTimeInputType },
cookTime: { type: CookTimeInputType },
},
resolve(parent, args) {
let recipe = new Recipe({
name: args.name,
dateCreated: new Date().getTime(),
authorID: args.authorID,
ingredients: args.ingredients,
steps: args.steps
});
// Check for optional args and set to recipe if they exist
args.prepTime ? recipe.prepTime = args.prepTime : recipe.prepTime = null ;
args.cookTime ? recipe.cookTime = args.cookTime : recipe.cookTime = null ;
return recipe.save();
}
},

How to dynamically set key values in a JS function?

I'm working with the following code:
var list = ['product', 'city', 'village'];
const foobar = new GraphQLObjectType({
name: 'foobarType',
fields: () => ({
product :{ // <== NOTE THAT PRODUCT IS IN THE LIST
type: new GraphQLList(foobarType),
args: {
certainty:{
type: GraphQLFloat,
description: "tester"
}
},
resolve(root,args){
return "foobar"
}
}
})
As you can see I have a list with three items. In the fields function, you can see that I have product.
How can I iterate over the list to dynamically set the fields functions during runtime so the result will be:
var list = ['product', 'city', 'village'];
const foobar = new GraphQLObjectType({
name: 'foobarType',
fields: () => ({
product :{ // <== RESULT FROM LIST
type: new GraphQLList(foobarType),
args: {
certainty:{
type: GraphQLFloat,
description: "tester"
}
},
resolve(root,args){
return "foobar"
}
},
city :{ // <== RESULT FROM LIST
type: new GraphQLList(foobarType),
args: {
certainty:{
type: GraphQLFloat,
description: "tester"
}
},
resolve(root,args){
return "foobar"
}
},
village :{ // <== RESULT FROM LIST
type: new GraphQLList(foobarType),
args: {
certainty:{
type: GraphQLFloat,
description: "tester"
}
},
resolve(root,args){
return "foobar"
}
}
})
})
You can iterate through your list inside the function and add each item with object[item] = value; syntax.
fields: () => {
var fields = {};
list.forEach(item => {
fields[item] = {
type: new GraphQLList(foobarType),
args: {
certainty:{
type: GraphQLFloat,
description: "tester"
}
},
resolve(root,args){
return "foobar"
}
}
});
return fields;
}
This kind of nesting is ugly code, though. I'd prefer if you instead saved fields in a variable before passing it.
const fields = {};
list.forEach(item => {
fields[item] = {
type: new GraphQLList(foobarType),
args: {
certainty:{
type: GraphQLFloat,
description: "tester"
}
},
resolve(root,args){
return "foobar"
}
}
});
const foobar = new GraphQLObjectType({
name: 'foobarType',
fields: () => fields
})
You can use the array's map function and computed property names (bracket) syntax to do this:
fields: () => list.map(item => ({
[item]: {
type: new GraphQLList(foobarType),
args: {
certainty:{
type: GraphQLFloat,
description: "tester"
}
},
resolve(root,args){
return "foobar"
}
}
}))

Building a nested structure in graphql

I'm pretty new to GraphQL and I'm trying to solve a problem that my manager has presented to me.
I have the following data structure made available to me, via a 3rd party API (Which I have zero control over):
[
{
"id": 19,
"date": "2016-10-24T13:59:19",
"date_gmt": "2016-10-24T12:59:19",
"slug: "data",
"provider": {
"name": "data",
"logo": "data",
"destination_url": "data",
"coupon_label": "data",
"coupon_text": "data",
"coupon_code": "data",
"coupon_url": "data",
}
}
]
I need to turn it into a GraphQL schema which looks like the following query:
{
provider(slug: "slug") {
id
date
slug
name
logo
url
coupon {
label
text
code
url
}
}
}
I've managed to sort most of it out with the code below, however, I can't work out how to group the coupon nodes into one.
I am guessing this need to be another custom type? If so this seems inefficient as coupon will never be used outside of the provider type so I wanted to know if there was a more 'best practice' way of doing it that I'm not aware of.
import { GraphQLObjectType, GraphQLInt, GraphQLString } from 'graphql'
const ProviderType = new GraphQLObjectType({
name: 'Provider',
fields: () => ({
id: {
type: GraphQLInt,
description: 'The primary key for the provider'
},
slug: {
type: GraphQLString,
description: 'A unique string for the provider'
},
status: {
type: GraphQLString,
description: 'The the published status of the provider'
},
name: {
type: GraphQLString,
description: 'The name of the provider',
resolve (parent) { return parent.provider.name }
},
logo: {
type: GraphQLString,
description: 'The full url of the provider logo',
resolve (parent) { return parent.provider.logo }
},
url: {
type: GraphQLString,
description: 'The full url of the provider',
resolve (parent) { return parent.provider.destination_url }
},
})
})
export default ProviderType
Update:
I've updated the code to the following but it's still not working, so my assumption must have been incorrect (or I implemented it incorrectly)
const ProviderType = new GraphQLObjectType({
name: 'Provider',
fields: () => ({
id: {
type: GraphQLInt,
description: 'The primary key for the provider'
},
slug: {
type: GraphQLString,
description: 'A unique string for the provider'
},
status: {
type: GraphQLString,
description: 'The the published status of the provider'
},
name: {
type: GraphQLString,
description: 'The name of the provider',
resolve (parent) { return parent.provider.name }
},
logo: {
type: GraphQLString,
description: 'The full url of the provider logo',
resolve (parent) { return parent.provider.logo }
},
url: {
type: GraphQLString,
description: 'The full url of the provider',
resolve (parent) { return parent.provider.destination_url }
},
coupon: {
type: CouponType,
description: 'The coupon information for the provider'
}
})
})
const CouponType = new GraphQLObjectType({
name: 'Coupon',
fields: () => ({
label: {
type: GraphQLString,
description: 'The label for the coupon',
resolve (parent) { return parent.provider.coupon_label }
},
text: {
type: GraphQLString,
description: 'The text for the coupon',
resolve (parent) { return parent.provider.coupon_text }
},
code: {
type: GraphQLString,
description: 'The code for the coupon',
resolve (parent) { return parent.provider.coupon_code }
},
url: {
type: GraphQLString,
description: 'The url for the coupon',
resolve (parent) { return parent.provider.coupon_url }
}
})
})
Your schema is mostly correct, but you need a resolver on your coupon field in provider because it is a nested type. See launchpad example for interactive query https://launchpad.graphql.com/r995kzj5kn
and here is the code. I have removed your descriptions for brevity and added some test data
import {
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
GraphQLInt,
GraphQLList
} from 'graphql'
const data = [
{
"id": 19,
"date": "2016-10-24T13:59:19",
"date_gmt": "2016-10-24T12:59:19",
"slug": "slug",
"provider": {
"name": "provider.name",
"logo": "provider.logo",
"destination_url": "provider.destination_url",
"coupon_label": "provider.coupon_label",
"coupon_text": "provider.coupon_text",
"coupon_code": "provider.coupon_code",
"coupon_url": "provider.coupon_url",
}
},
{
"id": 20,
"date": "2016-10-24T13:59:19",
"date_gmt": "2016-10-24T12:59:19",
"slug": "slugplug",
"provider": {
"name": "provider.name",
"logo": "provider.logo",
"destination_url": "provider.destination_url",
"coupon_label": "provider.coupon_label",
"coupon_text": "provider.coupon_text",
"coupon_code": "provider.coupon_code",
"coupon_url": "provider.coupon_url",
}
}
]
const CouponType = new GraphQLObjectType({
name: 'Coupon',
fields: () => ({
label: {
type: GraphQLString,
resolve (parent) { return parent.provider.coupon_label }
},
text: {
type: GraphQLString,
resolve (parent) { return parent.provider.coupon_text }
},
code: {
type: GraphQLString,
resolve (parent) { return parent.provider.coupon_code }
},
url: {
type: GraphQLString,
resolve (parent) { return parent.provider.coupon_url }
}
})
})
const ProviderType = new GraphQLObjectType({
name: 'Provider',
fields: () => ({
id: { type: GraphQLInt },
date: { type: GraphQLString },
slug: { type: GraphQLString },
status: { type: GraphQLString },
name: {
type: GraphQLString,
resolve (parent) { return parent.provider.name }
},
logo: {
type: GraphQLString,
resolve (parent) { return parent.provider.logo }
},
url: {
type: GraphQLString,
resolve (parent) { return parent.provider.destination_url }
},
coupon: {
type: CouponType,
resolve(parent) {
return parent
}
}
})
})
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
provider: {
type: new GraphQLList(ProviderType),
args: {
slug: { type: GraphQLString }
},
resolve (source, args) {
return args.slug ?
data.filter(({ slug }) => slug === args.slug) :
data
}
}
}
})
const schema = new GraphQLSchema({
query: Query
});
alternately you can just modify the results in the root resolver before sending them down like the following. this would allow you to remove all the resolvers from your types except for coupon on provider which would just return parent.coupon
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
provider: {
type: new GraphQLList(ProviderType),
args: {
slug: { type: GraphQLString }
},
resolve (source, args) {
const filtered = args.slug ?
data.filter(({ slug }) => slug === args.slug) :
data
return filtered.map(doc => {
return {
id: doc.id,
date: doc.date,
slug: doc.slug,
name: doc.provider.name,
logo: doc.provider.logo,
url: doc.provider.coupon_url,
coupon: {
label: doc.provider.coupon_label,
text: doc.provider.coupon_text,
code: doc.provider.coupon_code,
url: doc.provider.coupon_url
}
}
})
}
}
}
})

Categories

Resources