mickhansen / graphql-sequelize

GraphQL & Relay for MySQL & Postgres via Sequelize
MIT License
1.9k stars 172 forks source link

BelongsToMany Through Table values #572

Closed LoganArnett closed 4 years ago

LoganArnett commented 6 years ago

I currently have a pretty basic setup with Users and Projects:

// models/index.js

const Sequelize = require("sequelize");
const ProjectModel = require("./Project");
const UserModel = require("./User");
const UserProjectsModel = require("./UserProjects");
const sequelize = new Sequelize(DB_STRING);

/**
 * Models
 */
const Project = ProjectModel(sequelize, Sequelize);
const User = UserModel(sequelize, Sequelize);
const UserProjects = UserProjectsModel(sequelize, Sequelize);

/**
 * Create Associations/Relationships
 */
User.Projects = User.belongsToMany(Project, { through: UserProjects });
Project.Users = Project.belongsToMany(User, { through: UserProjects });

/**
 * Sync the Database Models
 */
sequelize.sync();

module.exports = { Sequelize, sequelize, Project, User, UserProjects };
// models/User.js

const User = (sequelize, DataTypes) => {
  const UserModel = sequelize.define(
    "user",
    {
      id: {
        type: DataTypes.UUID,
        unique: true,
        defaultValue: DataTypes.UUIDV4,
        allowNull: false,
        primaryKey: true,
        validate: {
          isUUID: 4
        }
      },
      firstName: {
        type: DataTypes.STRING,
        allowNull: false
      },
      lastName: {
        type: DataTypes.STRING,
        allowNull: false
      },
      email: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true
    },
    {
      getterMethods: {
        fullName() {
          return `${this.getDataValue("firstName")} ${this.getDataValue(
            "lastName"
          )}`;
        }
      }
    }
  );
  return UserModel;
};

module.exports = User;
// models/Project.js

const Project = (sequelize, DataTypes) =>
  sequelize.define("project", {
    id: {
      type: DataTypes.UUID,
      unique: true,
      defaultValue: DataTypes.UUIDV4,
      allowNull: false,
      primaryKey: true,
      validate: { isUUID: 4 }
    },
    name: {
      type: DataTypes.STRING,
      unique: true,
      allowNull: false
    }
    status: {
      type: DataTypes.ENUM(
        "concept",
        "active",
        "cancelled"
      ),
      allowNull: false
    }
  });

module.exports = Project;
// models/UserProjects.js

const UserProjects = (sequelize, DataTypes) =>
  sequelize.define("UserProjects", {
    permissions: {
      type: DataTypes.ENUM(
        "admin",
        "guest",
      ),
      allowNull: false
    }
  });

module.exports = UserProjects;

When I run the following code with just sequelize and without using anything attached to graphql or graphql-sequelize I get the expected result of each project having its UserProjects field which is a UserProjects instance:

const models = require("./models");
const Promise = require("bluebird");

return Promise.all([
  models.User.create({
    firstName: "John",
    lastName: "Doe",
    email: "John@Doe.com"
  }),
  models.Project.create({
    name: "Cool Project",
    status: "active"
  })
])
  .spread((user, project) => {
    return user
      .addProject(project, { through: { permissions: "admin" } })
      .then(() => user);
  })
  .then(user => user.getProjects())
  .then(projects => {
    const project = projects[0];
    console.log(project.UserProjects.permissions);
  });

But as soon as I introduce graphql and graphql-sequelize I am getting a UserProjects key on projects that then has an array of UserProjects models and they always only return the userId foreign key and nothing else

Not sure how to go about debugging this, my versions are:

node v9.5.0
npm v5.6.0

"pg": "^7.3.0",
"pg-hstore": "^2.3.2",
"sequelize": "^4.9.0",
"sequelize-cli": "^4.0.0"
mickhansen commented 6 years ago

Potentially related to dataloader, try disabling it via dataloader: false option to resolver/createConnection

carloslenondavis commented 6 years ago

Hi @LoganArnett , can you share those query's and types to see if the resolver working properly?

LoganArnett commented 6 years ago

the query I am trying to run is:

all: {
        type: new GraphQLList(UserType),
        args: {
            limit: { type: GraphQLInt },
            order: { type: GraphQLString }
        },
        resolve: resolver(User, { dataLoader: false }) // just added this from mickhansens recommendation but haven't gotten it to work
    }

// graphql query
{ 
  users { 
  id
    firstName  
    projects {
      id
      name
      userProjects {
        permissions
      }
    }
  }
}
// UserType.js

import GraphQLJSON from 'graphql-type-json';
import {
    GraphQLObjectType,
    GraphQLNonNull,
    GraphQLID,
    GraphQLString,
    GraphQLList
} from 'graphql';
import { resolver } from 'graphql-sequelize';
import { User, UserProjects, Project } from '../models';
import Company from './Company';
import UserProject from './UserProject';

const UserType = new GraphQLObjectType({
    name: 'User',
    description: 'A user',
    fields: {
        id: {
            type: new GraphQLNonNull(GraphQLID),
            description: 'The id of the user.'
        },
        firstName: {
            type: GraphQLString,
            description: 'The first name of the user.'
        },
        lastName: {
            type: GraphQLString,
            description: 'The last name of the user.'
        },
        fullName: {
            type: GraphQLString,
            resolve(obj) {
                return `${obj.firstName} ${obj.lastName}`;
            }
        },
        email: {
            type: GraphQLString,
            description: 'The email of the user.'
        }
        projects: {
            type: new GraphQLList(UserProject),
            resolve: resolver(User.Projects)
        },
        createdAt: {
            type: GraphQLString,
            description: 'The time/date the user was created'
        },
        updatedAt: {
            type: GraphQLString,
            description: 'The time/date the user was updated'
        }
    }
});

export default UserType;
// UserProjectType.js
import {
    GraphQLObjectType,
    GraphQLNonNull,
    GraphQLID,
    GraphQLString,
    GraphQLList
} from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import { resolver } from 'graphql-sequelize';
import { Project, UserProjects } from '../models';
import Company from './Company';
import UserType from './User';
import UserProjectsType from './UserProjects';
import { ProjectStatus } from './shared';

const UserProjectType = new GraphQLObjectType({
    name: 'UserProject',
    description: 'A project',
    fields: {
        id: {
            type: new GraphQLNonNull(GraphQLID),
            description: 'The id of the project.'
        },
        name: {
            type: GraphQLString,
            description: 'The name of the project.'
        },
        status: {
            type: ProjectStatus,
            description: 'The status of the project.'
        },
        userProjects: {
            type: UserProjectsType,
            description: 'Through table info for this project',
            resolve: resolver(Project.UserProjects)
        },
        createdAt: {
            type: GraphQLString,
            description: 'The time/date the project was created'
        },
        updatedAt: {
            type: GraphQLString,
            description: 'The time/date the project was updated'
        }
    }
});

export default UserProjectType;
carloslenondavis commented 6 years ago

@LoganArnett I think is not necessary to add the dataLoader to false because its false by default. Are you try to change the query all resolver List property to true, so the resolver return the same type list. if this doesn't work, i'll think is something related to use instead of type use nestedType. If you see the query are returning a UserType, but this type is not associated to UserProjects. UserProjects is a association model result of User and Project, so your query type should be an associationType. I hope this help you

carloslenondavis commented 6 years ago

@LoganArnett if you agree we can do this via skype so you can share your code. Maybe we can come out with a solution. skype: clenondavis

LoganArnett commented 6 years ago

I unfortunately do not have enough time right now to skype as I will be heading home shortly, explicitly setting the list: true did not change anything I am going to try the nestedType now though

mickhansen commented 6 years ago

@clenondavis That's not completely true, the version that automatically included dataloader called it by default: https://github.com/mickhansen/graphql-sequelize/blob/v5.6.1/src/resolver.js#L21

Dataloader is completely removed in newer versions

carloslenondavis commented 6 years ago

Hi @mickhansen thank youu for your explanation and clarification.

SirWojtek commented 6 years ago

Hello guys, thank you for your posts, they really helped me write a working schema for many-to-many relation. The issue in my case was putting the wrong model in resolver and after injecting an association model everything works fine. If there is anything I can help with (share the code, write tests), I'll glad to do that.

darkrift commented 6 years ago

@SirWojtek mind putting a complete example so I can diagnose what's wrong with my setup ?

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.