There are two popular and similar questions to mine, but the difference is that those only have to worry about deep populating associations for one object, whereas mine is about N objects.
Suppose I have 3 models defined as such (left out some attributes for clarity):
identity: 'room',
attributes: {
LocationId : { type: 'integer',
primaryKey: true,
required: true,
autoIncrement: true },
DisplayName : { type: 'string',
unique: true },
FloorId : { model: 'Floor' }
}
identity: 'floor',
attributes: {
FloorId : { type: 'integer',
primaryKey: true },
FloorName : { type: 'string' },
BuildingId : { model: 'Building' },
rooms: {collection:'room', via:'FloorId'}
}
identity: 'building',
attributes: {
BuildingId : { type: 'integer',
primaryKey: true },
BuildingName : { type: 'string' },
floors: {collection:'floor', via:'BuildingId'}
}
The end goal is to have an array of objects that has this basic structure:
[{
"LocationId": 555,
"DisplayName": 'SomeCoolName',
"Floor" : {
"FloorId": 1337,
"FloorName": '5',
"Building": {
"BuildingId": 4321,
"BuildingName": 'HQ'
}
}
}, {...}]
I've not got far due to not knowing the BlueBird library promises as well as I should:
showWithAssetGeo: function(req, res) {
room.find( { assetCount: { '>': 0 } } )
.populate('FloorId')
.each(function(room){
var Building = Building.find({ id: _.pluck(room.FloorId, 'BuildingId') })
.then(function(Building) {return Building;});
return [room, Building];
})
.spread(function(room, Building) {
//Something to combine it all?
})
.catch (function(err) {
if (err) { res.badRequest('reason' + err); }
}
}
UPDATE: Had to tweak the answer marked below. Here is the final working code.
You need to make sure to execute the find by calling then or exec (each won't do it).
Seems like you're trying to map across all the floors and then bring those promises back to one. Promise.all() is the way to do that.
Try something like the below:
showWithAssetGeo: function(req, res) {
room.find( { assetCount: { '>': 0 } } )
.populate('FloorId')
.then(function(rooms) {
return Promise.all(rooms.map(function(room) {
return Building.findOne({id: room.FloorId.BuildingId})
.then(function(building) {
room.FloorId.building = building;
});
})
})
.then(function(deeplyPopulatedRooms) {
res.json(deeplyPopulatedRooms);
})
.catch(function(error) {
if (err) { res.badRequest('reason' + err); }
});
}
However, it would probably be more performant to pluck all the id's of the possible buildings and do one find for all id's. But the above should work and seems to be consistent with the approach you were taking before.
Related
thx for any help.
Im using at the frontend the apollo-client and at the backend graphql-nexus,prisma2 and graphql-yoga server.
I want to solve the n + 1 problem with #paljs/plugins.
At the frontend I have a query posts like:
query posts{
posts {
id
favoritedBy(where: { id: { equals: $currentUserId } }) {
id
}
author {
id
avatar {
id
}
}
link {
id
}
games {
id
}
tags {
id
}
likes(where: { user: { id: { equals: $currentUserId } } }) {
id
}
}
}
Posts resolver:
import { PrismaSelect } from '#paljs/plugins'
export const posts = queryField('posts', {
type: 'Post',
list: true,
args: {
...
},
resolve: async (_parent, args, { prisma, request }, info) => {
const select = new PrismaSelect(info).value
let opArgs: FindManyPostArgs = {
take: 10,
orderBy: {
[args.orderBy]: 'desc',
},
...select
}
const post = await prisma.post.findMany(opArgs)
//The result I want to return with the "sub-models" like likes, author tags...
console.log(JSON.stringify(post, undefined, 2))
return post
},
})
I logging the queries
const prisma = new PrismaClient({
log: ['query'],
})
My Problem: With PrismaSelect, I have 5 queries more than without and If I check the request-time at the frontend I need 300-400ms longer with PrismaSelect. So what I'm doing wrong?
I saw in the #paljs/plugins doc the select in the context. Maybe that is my mistake. How can I use the select in the context?
Here ist my Context:
import { PrismaClient, PrismaClientOptions } from '#prisma/client'
import { PubSub } from 'graphql-yoga'
import { PrismaDelete, onDeleteArgs } from '#paljs/plugins'
class Prisma extends PrismaClient {
constructor(options?: PrismaClientOptions) {
super(options)
}
async onDelete(args: onDeleteArgs) {
const prismaDelete = new PrismaDelete(this)
await prismaDelete.onDelete(args)
}
}
export const prisma = new PrismaClient({
log: ['query'],
})
export const pubsub = new PubSub()
export interface Context {
prisma: PrismaClient
request: any
pubsub: PubSub
}
export function createContext(request: any): Context {
return { prisma, request, pubsub }
}
You need to know that to use my PrismaSelect plugin you need to remove the nexus-prisma-plugin package and use my Pal.js CLI to create your CRUD and ObjectType for nexus and using #paljs/nexus plugin to add in mackSchema function
import { makeSchema } from '#nexus/schema';
import * as types from './graphql';
import { paljs } from '#paljs/nexus'; // import our plugin
export const schema = makeSchema({
types,
plugins: [paljs()],// here our plugin don't use nexus-prisma-plugin
outputs: {
schema: __dirname + '/generated/schema.graphql',
typegen: __dirname + '/generated/nexus.ts',
},
typegenAutoConfig: {
sources: [
{
source: require.resolve('./context'),
alias: 'Context',
},
],
contextType: 'Context.Context',
},
});
Now add this type to your Context
export interface Context {
prisma: PrismaClient
request: any
pubsub: PubSub
select: any // here our select type
}
export function createContext(request: any): Context {
// our paljs plugin will add select object before resolver
return { prisma, request, pubsub, select: {} }
}
after you add our plugin your query will log like this
extendType({
type: 'Query',
definition(t) {
t.field('findOneUser', {
type: 'User',
nullable: true,
args: {
where: arg({
type: 'UserWhereUniqueInput',
nullable: false,
}),
},
resolve(_, { where }, { prisma, select }) {
// our plugin add select object into context for you
return prisma.user.findOne({
where,
...select,
});
},
});
},
});
Can you please try to use my pal c command to start an example from my list and try your schema and make tests with it
It is working, thx Ahmed your plugin is AWESOME!!!!!
I changed my Post-Object from
const Post = objectType({
name: 'Post',
definition(t) {
t.model.id()
t.model.authorId()
t.model.tags()
t.model.games()
t.model.link()
t.model.report()
t.model.notifications()
t.model.author()
t.model.favoritedBy({
filtering: {
id: true,
},
})
t.model.likes({
filtering: {
user: true,
},
})
}
})
to
const Post = objectType({
name: 'Post',
definition(t) {
t.string('id')
t.field('tags', {
nullable: false,
list: [true],
type: 'Tag',
resolve(parent: any) {
return parent['tags']
},
})
t.field('games', {
list: [true],
type: 'Game',
resolve(parent: any) {
return parent['games']
},
})
t.field('link', {
type: 'Link',
nullable: true,
resolve(parent: any) {
return parent['link']
},
})
t.field('notifications', {
list: [true],
type: 'Notification',
resolve(parent: any) {
return parent['notifications']
},
})
t.field('author', {
nullable: false,
type: 'User',
resolve(parent: any) {
return parent['author']
},
})
t.field('favoritedBy', {
nullable: false,
list: [true],
type: 'User',
args: {
where: 'UserWhereInput',
},
resolve(parent: any) {
return parent['favoritedBy']
},
})
t.field('likes', {
list: [true],
type: 'Like',
args: {
where: 'LikeWhereInput',
},
resolve(parent: any) {
return parent['likes']
},
})
},
})
And I also used the nexus-prisma-plugin and paljs-plugin at the same time
For the project I'm working on a player has multiple games. But these are always needed in a combined way.
So I was wondering on how i could make it so I can place it in my graphql scheme as a signle item called games.
I've did something similar for the fullName but I couldn't find a way to make a combined association
#Table({
timestamps: true
})
export class Player extends Model<Player> {
#Column({ unique: 'compositeIndex' })
firstName: string;
#Column({ unique: 'compositeIndex' })
lastName: string;
#Column({ unique: 'compositeIndex' })
memberId: string;
// Assicioations
#HasMany(() => Game, { as: 'player1-team1', foreignKey: 'player1Team1Id' })
games1?: Game[];
#HasMany(() => Game, { as: 'player1-team2', foreignKey: 'player1Team2Id' })
games2?: Game[];
#HasMany(() => Game, { as: 'player2-team1', foreignKey: 'player2Team1Id' })
games3?: Game[];
#HasMany(() => Game, { as: 'player2-team2', foreignKey: 'player2Team2Id' })
games4?: Game[];
#Column(DataType.VIRTUAL(DataType.STRING, ['firstName', 'lastName']))
get fullName(this: Player): string {
return `${this.firstName || ''} ${this.lastName || ''}`.trim();
}
get games() {
return [...this.games1, ...this.games2, ...this.games3, ...this.games4];
}
}
Current Scheme object
const playerType = new GraphQLObjectType({
name: 'Player',
description: 'A Player',
fields: Object.assign(attributeFields(sequelizeInstance.models.Player), {
games: {
type: new GraphQLList(gameType),
resolve: resolver(/* Some magic here? */),
}
})
});
return new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
player: {
type: playerType,
args: {
id: {
description: 'id of the user',
type: new GraphQLNonNull(GraphQLID)
}
},
resolve: resolver(Player)
}
}
})
});
};
Found a 'solution' to my problem
I've made the "error" that I've made 4 foreign keys. by creating a many to many releation i was able to get everything working as I wanted to.
But I'm still curious on how one would do this ...
If an existing service supporting the following GraphQL queries respectively:
query to a person's bank account:
query {
balance(id: "1") {
checking
saving
}
}
result
{
"data": {
"balance": {
"checking": "800",
"saving": "3000"
}
}
}
query to a person's pending order:
query {
pending_order(id: "1") {
books
tickets
}
}
result
{
"data": {
"pending_order": {
"books": "5",
"tickets": "2"
}
}
}
The source code achieving the above functionality is something like this:
module.exports = new GraphQLObjectType({
name: 'Query',
description: 'Queries individual fields by ID',
fields: () => ({
balance: {
type: BalanceType,
description: 'Get balance',
args: {
id: {
description: 'id of the person',
type: GraphQLString
}
},
resolve: (root, { id }) => getBalance(id)
},
pending_order: {
type: OrderType,
description: 'Get the pending orders',
args: {
id: {
description: 'id of the person',
type: GraphQLString
}
},
resolve: (root, { id }) => getPendingOrders(id)
}
})
});
Now, I want to make my GraphQL service schema support person level schema, i.e.,
query {
person (id: "1") {
balance
pending_order
}
}
and get the following results:
{
"data": {
"balance": {
"checking": "800",
"saving": "3000"
}
"pending_order": {
"books": "5",
"tickets": "2"
}
}
}
How can I re-structure the schema, and how can I reuse the existing query service?
EDIT (after reading Daniel Rearden's answer):
Can we optimize the GraphQL service so that we make service call based upon the query? i.e., if the incoming query is
query {
person (id: "1") {
pending_order
}
}
my actually query becomes
person: {
...
resolve: (root, { id }) => Promise.all([
getBalance(id)
]) => ({ balance})
}
You're going to have to define a separate Person type to wrap the balance and pending_order fields.
module.exports = new GraphQLObjectType({
name: 'Person',
fields: () => ({
balance: {
type: BalanceType,
resolve: ({ id }) => getBalance(id)
},
pending_order: {
type: OrderType,
resolve: ({ id }) => getPendingOrders(id)
}
})
});
And you're going to need to add a new field to your Query type:
person: {
type: PersonType,
args: {
id: {
type: GraphQLString
}
},
// We just need to return an object with the id, the resolvers for
// our Person type fields will do the result
resolve: (root, { id }) => ({ id })
}
There's not much you can do to keep things more DRY and reuse your existing code. If you're looking for a way to reduce boilerplate, I would suggest using graphql-tools.
How can i use a resolve in a param default value, something like
params: {
id: {
type: 'int',
value: ['lists', function(lists) {
return lists[0].id; <====
}],
squash: false,
dynamic: false
}
},
resolve: {
lists: ['MailingListService', function (MailingListService) {
//load the tree data
return MailingListService.getMailingLists().then(function (lists) {
return lists;
});
}],
},
Because parameters have to be processed synchrounously, you cannot use a resolve in a default parameter value.
However, you can use a placeholder value such as null and map it to a resolve value later.
params: {
id: {
type: 'int',
value: null,
squash: false,
dynamic: false
}
},
resolve: {
lists: ['MailingListService', function (MailingListService) {
//load the tree data
return MailingListService.getMailingLists().then(function (lists) {
return lists;
});
}],
listId: ['lists', '$stateParams', function($stateParams) {
return ($stateParams.id === null) lists[0].id : listId;
}],
list: ['MailingListService', 'listId', function (MailingListService, listId) {
return MailingListService.getMailingList(listId);
}],
},
This is the code from official docs of relay, This is for GraphQLAddTodoMutation
const GraphQLAddTodoMutation = mutationWithClientMutationId({
name: 'AddTodo',
inputFields: {
text: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
todoEdge: {
type: GraphQLTodoEdge,
resolve: ({localTodoId}) => {
const todo = getTodo(localTodoId);
return {
cursor: cursorForObjectInConnection(getTodos(), todo),
node: todo,
};
},
},
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
},
mutateAndGetPayload: ({text}) => {
const localTodoId = addTodo(text);
return {localTodoId};
},
});
I think mutateAndGetPayload executes first then outputFields? since it used localTodoId object as parameter, I see localTodoId object returned from mutateAndGetPayload.
and this is the code for relay mutation.please look at the getFatQuery
export default class AddTodoMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on User {
id,
totalCount,
}
`,
};
getMutation() {
return Relay.QL`mutation{addTodo}`;
}
getFatQuery() {
return Relay.QL`
fragment on AddTodoPayload #relay(pattern: true) {
todoEdge,
viewer {
todos,
totalCount,
},
}
`;
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'todos',
edgeName: 'todoEdge',
rangeBehaviors: ({status}) => {
if (status === 'completed') {
return 'ignore';
} else {
return 'append';
}
},
}];
}
getVariables() {
return {
text: this.props.text,
};
}
getOptimisticResponse() {
return {
// FIXME: totalCount gets updated optimistically, but this edge does not
// get added until the server responds
todoEdge: {
node: {
complete: false,
text: this.props.text,
},
},
viewer: {
id: this.props.viewer.id,
totalCount: this.props.viewer.totalCount + 1,
},
};
}
}
I think the todoEdge is from the outputFields from GraphQL? I see a viewer query on it, why does it need to query the viewer? How do I create a getFatQuery? I would really appreciate if someone help me understand this more and about Relay mutation.
mutateAndGetPayload executes then returns the payload to the outputFields
mutationWithClientMutationId
Source-Code
starWarsSchema example
mutationWithClientMutationId
inputFields: defines the input structures for mutation, where the input fields will be wraped with the input values
outputFields: defines the ouptput structure of the fields after the mutation is done which we can view and read
mutateAndGetPayload: this function is the core one to relay mutations, which performs the mutaion logic (such as database operations) and will return the payload to be exposed to output fields of the mutation.
mutateAndGetPayload maps from the input fields to the output fields using the mutation
operation. The first argument it receives is the list of the input parameters, which we can read to perform the mutation action
The object we return from mutateAndGetPayload can be accessed within the output fields
resolve() functions as the first argument.
getFatQuery() is where we represent, using a GraphQL fragment, everything
in our data model that could change as a result of this mutation