Sequlize combining has many association in single field - graphql

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 ...

Related

GraphQl/Sequalize combine multiple hasMany/BelongsToMany in single list

I'm currently working on a project where I have players playing games against each-other in a 1v1 or 2v2 concept.
So I created the following 2 sequelize classes:
player.js
export class Player extends Model {
static init(sequelize, DataTypes) {
return super.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
gender: DataTypes.STRING,
firstName: DataTypes.STRING,
lastName: DataTypes.STRING
},
{
sequelize
}
);
}
// Associations
static associate() {
this.games1 = this.hasMany(Game, { as: 'player1_team1', foreignKey: 'player1Team1Id' });
this.games2 = this.hasMany(Game, { as: 'player1_team2', foreignKey: 'player1Team2Id' });
this.games3 = this.hasMany(Game, { as: 'player2_team1', foreignKey: 'player2Team1Id' });
this.games4 = this.hasMany(Game, { as: 'player2_team2', foreignKey: 'player2Team2Id' });
}
}
game.js
export class Game extends Model {
static init(sequelize, DataTypes) {
return super.init(
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
createdAt: DataTypes.DATE,
updatedAt: DataTypes.DATE,
playedAt: DataTypes.DATE,
set1_team1: {
type: DataTypes.INTEGER,
allowNull: true
},
set1_team2: {
type: DataTypes.INTEGER,
allowNull: true
},
set2_team1: {
type: DataTypes.INTEGER,
allowNull: true
},
set2_team2: {
type: DataTypes.INTEGER,
allowNull: true
},
set3_team1: {
type: DataTypes.INTEGER,
allowNull: true
},
set3_team2: {
type: DataTypes.INTEGER,
allowNull: true
}
},
{
sequelize
}
);
}
// Associations
static associate() {
this.player1_team1 = this.belongsTo(Player, {
as: 'player1_team1',
foreignKey: 'player1Team1Id'
});
this.player1_team2 = this.belongsTo(Player, {
as: 'player1_team2',
foreignKey: 'player1Team2Id'
});
this.player2_team1 = this.belongsTo(Player, {
as: 'player2_team1',
foreignKey: 'player2Team1Id'
});
this.player2_team2 = this.belongsTo(Player, {
as: 'player2_team2',
foreignKey: 'player2Team2Id'
});
}
}
Then the following scheme for GraphQl
gql`
type Player {
id: ID!
firstName: String!
lastName: String!
games: [Game]
}
type Game {
id: ID!
player1_team1: Player!
player1_team2: Player!
player2_team1: Player!
player2_team2: Player!
}
type Query {
player(id: ID!): Player
}
`
Now I'm trying to have when you query for the Player that you get all the games whether he is player1_team1, player2_team2...
But i'm a bit blocked on figuring out how to do this :/
I tried adding getGames() that combined the 4 arrays to my class, but I didn't find how to call this method
getGames() {
return [...this.games1, ...this.games2, ...this.games3, ...this.games4];
}
I tried searching for a way that you could query with a alias that combines the 4 childs (player1_team1...) but with no success (still new in GraphQl)
Could anyone help me out?
Game is a class. Calling new Game or a static method like Game.create generates an instance of the class. So we have
const Game = require('the location of the model')
const game = new Game()
In this example, static methods will be available on the Game variable because they apply to the class, not an instance of that class. Likewise, non-static methods you define inside the class will be available on instance and not the class (i.e. the game variable).
The hasMany method creates an association between the two models and returns an instance of the HasMany class. By writing this:
this.games1 = this.hasMany(Game, { ... })
you are setting the games1 static property because you are doing this inside a static method. As a result, the property is available on the class, not the instance. Saving the resulting association object to a static property can be helpful, not it's also not necessary. You could just as easily do:
this.hasMany(Game, { ... })
The important bit is that by calling hasMany, you are actually creating a getter on instances of the class. In this case, the 4 getters will be named getPlayer1_team1, getPlayer2_team1, getPlayer1_team2 and getPlayer2_team2 based on the alias you provided (the as parameter).
So you can add a method like:
async getGames() {
const [games1, games2, games3, games4] = await Promise.all([
this.getPlayer1_team1(),
this.getPlayer1_team2(),
this.getPlayer2_team1(),
this.getPlayer2_team2(),
])
return [...this.games1, ...this.games2, ...this.games3, ...this.games4]
}
and then call it from some instance:
const player = await Player.findByPk(1)
const games = await Player.getGames()
If your schema exposes a games field on the Player type, you can just do this in the resolver for the field:
function resolve(parent, args, context, info) {
return parent.getGames()
}
Alternatively...
You can lazy load the associated models and get the games when you fetch the player. This is generally more efficient than fetching the player and then fetching the games. So when fetching the player, you can do something like:
const player = await Player.findByPk(1, {
include: [
{ as: 'player1_team1', model: Game },
{ as: 'player1_team2', model: Game },
{ as: 'player2_team1', model: Game },
{ as: 'player2_team2', model: Game },
]
})
Note: it's important to include the same as value that you used when you defined the association. The resulting player variable will now have 4 properties on it for the associated games (player1_team1, player1_team2, etc.).
Provided you lazy load the associated Games like this when fetching instances of Player, you can now do something like this to resolve your games field in GraphQL:
function resolve(parent, args, context, info) {
return [
...parent.player1_team1,
...parent.player1_team2,
...parent.player2_team1,
...parent.player2_team2,
]
}

Is it possible to do a multi tenancy with Graphql and Sequelize?

I have a rather tricky question about GraphQl and multi-tenancy.
Let's assume there are 3 tables, OWNER, HOUSE and TENANTS. I will describe them in Sequelize and GraphQl pseudo code:
Owner table (has multiple houses and multiple tenants)
const OWNER = sequelize.define('owner', {
ownerId: type: Sequelize.INTEGER,
name: type: Sequelize.STRING
}
OWNER.associate = models => {
models.owner.hasMany(models.house, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
models.owner.hasMany(models.tenant, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
}
House table (belongs to owner and has multiple tenants)
const HOUSE = sequelize.define('house', {
houseId: type: Sequelize.INTEGER,
ownerId: type: Sequelize.INTEGER,
name: type: Sequelize.STRING
}
HOUSE.associate = models => {
models.house.belongsTo(models.owner, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
models.house.hasMany(models.tenant, {foreignKey: {name: 'houseId', field: 'houseId'}})
}
Tenant table (belongs to owner and house)
const TENANT = sequelize.define('tenant', {
tenantId: type: Sequelize.INTEGER,
ownerId: type: Sequelize.INTEGER,
houseId: type: Sequelize.INTEGER,
name: type: Sequelize.STRING
}
TENANT.associate = models => {
models.tenant.belongsTo(models.owner, {foreignKey: {name: 'ownerId', field: 'ownerId'}})
models.tenant.belongsTo(models.house, {foreignKey: {name: 'houseId', field: 'houseId'}})
}
The owner graphql object
const OwnerType = new GraphQLObjectType({
name: 'Owner',
fields: () => ({
ownerId: { type: GraphQLInt },
name: { type: GraphQLString },
houses: {
type: GraphQLList(HouseType),
resolve(owner) {
return owner.getHouse()
}
},
houseById: {
type: HouseType,
args: <args is not defined>
resolve(owner) {
return <???>
}
},
})
})
Here are a few simple GraphQL queries:
ownerById = {
type: OwnerType,
args: {
ownerId: { type: GraphQLInt },
},
resolve(parents, args){
return models.owner.findOne({ where: args })
}
}
houses = {
type: GraphQLList(HouseType),
resolve(parents, args){
return models.house.findAll()
}
}
houseById = {
type: HouseType,
args: {
houseId: { type: GraphQLInt },
},
resolve(parents, args){
return models.house.findOne({ where: args })
}
}
tenants = {
type: GraphQLList(TenantType),
resolve(parents, args){
return models.tenant.findAll()
}
}
These client queries work:
{
ownerById(ownerId: 1) {
ownerId
name
house {
houseId
name
}
}
}
{
houseById(houseId: 2) {
houseId
name
tenant {
tenantId
name
}
}
}
What I need to make multi-tenancy to work is something like that:
{
ownerById(ownerId: 1) {
ownerId
name
houseById(houseId: 2) {
houseId
name
tenant {
tenantId
name
}
}
}
}
Is there a way to archive this or is that out of scope what GraphQl can do?
If yes, how would the graphql object houseById query look like?
Thanks in advance.
Unless I'm missing something, it seems like your resolver for houseById would not be that different from the resolver for the houses field on the same type.
houseById: {
type: HouseType,
args: {
houseId: { type: GraphQLInt },
},
async resolve(owner, { houseId }) {
const houses = await owner.getHouses({ where: { id: houseId } })
return houses[0]
}
},
For a HasMany association, the getter for the target model resolves to an array of instances. So we need to grab that array first and then return just the first item in it, since our field represents a single object and not a list. If you don't want to use async/await, you can also do:
return owner.getHouses({ where: { id: houseId } })
.then(houses => houses[0])
It would also be worth mentioning that this sort of pattern for a schema defies convention. Rather than having a houses field, a houseById field, a houseBySomeOtherArg field, etc., consider exposing a single houses field with one or more arguments like id, name or whatever filter criteria you want to provide. Your field can then just filter the houses based on whatever arguments are passed in, or return all results if no filter arguments were provided.

Dynamic GraphQLObjectType

I'm trying to create a dynamic GraphQLObjectType with graphQl, something like this:
export const Project = (data) => {
return new GraphQLObjectType({
name: 'Project',
fields: () => ({
id: {
type: GraphQLString
},
type: {
type: GraphQLString
},
author: {
type: User,
resolve: (root, args, req) => {
...
}
}
})
})
};
I call this model on my query in this way:
getProjectById: {
type: Project(structure),
args: {
id: { type: GraphQLString }
},
resolve(source, args, req) {
const projectService = new ProjectService(req);
return projectService.getProjectById(args.id)
}
}
the problem is that doing this I get this error:
Schema must contain unique named types but contains multiple types
named "Project"
where is the error? do you have some advice? many thanks
The call Project(structure) in turn calls new GraphQLObjectType({name: 'Project',...}) . If you invoke Project(structure) more than once, you try to declare multiple GraphQLObjectTypes with the same name (which makes no sense).
If you would create/declare GraphQLObjectType dynamically, you have to generate a unique name property. E.g. like this:
// assuming data.name is unique
export const Project = (data) => {
return new GraphQLObjectType({
name: `Project${data.name}`,
...
})
}

GraphQL- conditionally determine the type of a field in a schema

I have the following mongoose schema:
const MessageSchema = new Schema({
author: {
account:{
type:String,
enum:['employee','admin'],
},
id: String,
}
//other fields
})
Then in my graphql-schemas file, I have the following schema types:
const MessageType = new GraphQLObjectType({
name: 'Message',
fields: () => ({
account: {
type: AuthorType,
//resolve method
},
id: {type: GraphQLString},
})
})
const AuthorType= new GraphQLObjectType({
name: 'Author',
fields: () => ({
account: {
type://This will either be AdminType or EmployeeType depending on the value of account in db (employee or admin),
//resolve method code goes here
}
})
})
As indicated in the comments of AuthorType, I need the account field to resolve to Admin or Employee depending on the value of the account field in the database.
How do I conditionally determine the type of a field in a schema on the fly?
Instead of determining the type on the fly, I restructured my code as shown below:
const MessageType = new GraphQLObjectType({
name: 'Message',
fields: () => ({
id:{type:GraphQLString},
author: {
type: AuthorType,
async resolve(parent, args) {
if (parent.author.account === 'guard') {
return await queries.findEmployeeByEmployeeId(parent.author.id).then(guard => {
return {
username: `${guard.first_name} ${guard.last_name}`,
profile_picture: guard.profile_picture
}
})
} else if (parent.author.account === 'admin') {
return {
username: 'Administrator',
profile_picture: 'default.jpg'
}
}
}
},
//other fields
})
})
const AuthorType = new GraphQLObjectType({
name: 'Author',
fields: () => ({
username: {type: GraphQLString},
profile_picture: {type: GraphQLString},
})
})
Since all I need from the AuthorType is the author's username and profile picture, both employee and administrator have these fields, which I pass to AuthorType.
In MessageType, I apply the logic to determine account type in the resolve method of author, then construct custom object out of the logic, to match AuthorType.

How can GraphQL enable an ID based query at sub fields level?

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.

Resources