Typeorm - Find entries by ManyToMany relation - sql

I am using Nestjs with Typeorm and Mysql, and I can't figure out a good way to filter entries by their many to many relation.
I have these 2 entities:
Group Entity:
#Entity({ name: 'groups' })
export class Group {
#ManyToMany(() => Tag, { eager: true })
#JoinTable()
tags: Tag[];
}
Tag Entity
#Entity({ name: 'tags' })
export class Tag {
#Column()
#Index({ unique: true })
tag?: string;
}
And would like to search all groups that have a tag with a specific text.
ie. all groups that have the tag.tag "sport"
Tried this code:
const args = {
where: [
{
'tags': In([Like(`%sport%`)]),
}
],
relations: ['tags'], // TAGS
take: filter.take,
skip: filter.skip,
order: filter.order
};
return super.findAll(args);
but it doesn't seem to work..
any help would be great!

return find({
where: {
tags: {
tag: Like(`%sport%`),
},
},
relations: ['tags'],
});
Almost, typeorm accepts an ObjectLiteral or keyof typeof Tags from relations like so:
FindConditions<T>: {
where: {
[s: keyof typeof T]: any,
},
}
That's not quite it but that's the general gist. And if the keyof T is a relation then any is replaced with keyof relation pretty much anyway.
This is the full type for findConditions https://github.com/typeorm/typeorm/blob/master/src/find-options/FindConditions.ts

Related

TypeORM - Getting objects of provided id, which is one relation away

I want to get objects from table providing id, which is in relation with table, which is in another relation. It looks like this:
Hand is in relation manyToOne with Action (hand can have only one action),
Action is in relation manyToOne with Situation (action can have only one situation)
I'm trying to make GET request for hands in which I'm providing situationId.
Simplified entities:
#Entity()
export class Hand {
#PrimaryGeneratedColumn()
hand_id: number;
#Column()
hand: string;
#ManyToOne(type => Action, action => action.simplifiedhands, { eager: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' })
action: Action;
}
#Entity()
export class Action {
#PrimaryColumn()
action_id: number;
#ManyToOne(type => Situation, situation => situation.actions, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
#JoinColumn({name: 'situation'})
situation: Situation;
#OneToMany(type => Hand, hand => hand.action)
hands: Hand[];
#OneToMany(type => Hand, hand => hand.action)
hands: Hand[];
}
#Entity()
export class Situation {
#PrimaryColumn()
situation_id: number;
#ManyToOne(type => Strategy, strategy => strategy.situations, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
strategy: Strategy;
#OneToMany(type => Action, action => action.situation)
actions: Action[];
}
What approaches didn't work for me so far (just example variants):
return await this.handsRepository.find({
relations: ["action", "action.situation"],
where: {
"situation": id
}
});
and
return await this.handsRepository.find({
join: {
alias: "hands",
leftJoinAndSelect: {
"action": "hand.action",
"situation": "action.situation"
}
},
where: {
"situation": id
}
});
Generally both 'works' but provide all the records, like there were no where condition.
The keys in the object you assign to where should be members of the entity of the repository, in your case Hand, since situation is a member of action it's not working. I'm surprised you didn't mention any errors.
You can do one of the following (example for postgres)
Using query builder:
return await this.handsRepository.createQueryBuilder(Hand, 'hand')
.leftJoin('hand.action', 'action')
.leftJoin('action.situation', 'situation')
.where('situation.id = :id', { id })
.getMany();
Or, you can try the following (success is not guaranteed):
return await this.handsRepository.find({
relations: ["action", "action.situation"],
where: {
action: {
situation: { id }
}
}
});

select next and previous records in Sequelize based on createdAt

What I want: fetch one record from the database based on its ID (UUID) and retrieve next and previous records too
Associations: posts N:M categories, posts 1:N comments, posts N:1 users, posts N:M tags, posts 1:N post_views
Current solution:
const post = await PostModel.findByPk(id, {
include: [
{ model: CategoryModel, attributes: ['title'] },
{ model: TagModel },
{ model: CommentModel, include: { model: UserModel } },
{ model: PostViewModel },
{ model: UserModel, attributes: ['fullname', 'id', 'avatar'] }
]
});
const nextPost = await PostModel.findOne({
where: { createdAt: {
[Op.gt]: post.createdAt
}}
const prevPost = await PostModel.findOne({
where: { createdAt: {
[Op.lt]: post.createdAt
}}
});
but I think this is not efficient and good.

GraphQL & Sequelize: Users and followers/following

I'm attempting to set up my User GraphQL model to have followers and following attributes to query on. However I'm having trouble setting up the relationship in Sequelize. I'm trying to use a Follower model as a Join Table and setup a BelongsToMany association, but haven't been able to get it working. Can anyone suggest what to do or point out what I'm doing wrong?
I've come up with a temporary solution by manually querying, which you can see in my User.model.ts, but I believe there is a better way to do it using proper configuration.
I'm using typescript wrappers around GraphQL and Sequelize, TypeGraphQL and sequelize-typescript respectively, as well as PostgreSQL.
User.model.ts
// VENDOR
import { ObjectType, Field, ID } from 'type-graphql';
import { Model, Table, Column, PrimaryKey, Unique, IsUUID, HasMany, DefaultScope, AllowNull, DataType, BelongsToMany } from 'sequelize-typescript';
// APP
import Post from '../post/post.types';
import Follower from '../follower/follower.types';
/** User model for GraphQL & Database */
#Table({ timestamps: false, tableName: 'users' }) // tell sequelize to treat class as table model
#DefaultScope(() => ({ include: [{ model: Post.scope(), as: 'posts' }] })) // tell sequelize to include posts in its default queries
#ObjectType() // tell GraphQL to treat class as GraphQL model
export default class User extends Model<User>{
#PrimaryKey
#Unique
#AllowNull(false)
#IsUUID(4)
#Column(DataType.UUID)
#Field(() => ID)
id: string;
#Unique
#AllowNull(false)
#Column
#Field()
ci_username: string;
#AllowNull(false)
#Column
#Field()
username: string;
#AllowNull(false)
#Column
#Field()
first_name: string;
#Column
#Field()
last_name: string;
#Column
#Field({ nullable: true })
profile_picture?: string;
// #BelongsToMany(() => User, { otherKey: 'user_id', as: 'followers', through: () => Follower })
// #Field(() => [User])
// followers: User[];
// MY TEMPORARY SOLUTION USING MANUAL QUERYING
#Field(() => [User])
get followers(): Promise<User[]> {
return Follower.findAll({ where: { user_id: this.id } })
.then(records => records.map(record => record.follower_id))
.then((follower_ids: string[]) => {
return User.findAll({ where: { id: follower_ids }});
})
}
// DOES NOT WORK, BUT I BELIEVE COULD POTENTIALLY LEAD TO BETTER SOLUTION
#BelongsToMany(() => User, { otherKey: 'follower_id', as: 'following', through: () => Follower })
#Field(() => [User])
following: User[];
#HasMany(() => Post)
#Field(() => [Post])
posts: Post[];
}
Follower.model.ts
// VENDOR
import { Model, Table, Column, PrimaryKey, Unique, IsUUID, AllowNull, DataType, Index, ForeignKey, AutoIncrement } from 'sequelize-typescript';
// APP
import User from '../user/user.types';
/** Follower model for Database */
#Table({ timestamps: false, tableName: 'followers' }) // tell sequelize to treat class as table model
export default class Follower extends Model<Follower>{
#PrimaryKey
#AutoIncrement
#Unique
#AllowNull(false)
#Column
id: number;
#AllowNull(false)
#IsUUID(4)
#Index
#ForeignKey(() => User)
#Column(DataType.UUID)
user_id: string;
#AllowNull(false)
#IsUUID(4)
#Index
#ForeignKey(() => User)
#Column(DataType.UUID)
follower_id: string;
}
GraphQL Query
{
users: allUsers {
id
username
first_name
last_name
following {
username
id
}
}
}
GraphQL Response / Error
{
"errors": [
{
"message": "Cannot return null for non-nullable field User.following.",
"locations": [
{
"line": 7,
"column": 5
}
],
"path": [
"users",
0,
"following"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR",
"exception": {
"stacktrace": [
"Error: Cannot return null for non-nullable field User.following.",
" at completeValue (/Users/jsainz237/Projects/trueview/trueview-api/node_modules/graphql/execution/execute.js:560:13)",
" at /Users/jsainz237/Projects/trueview/trueview-api/node_modules/graphql/execution/execute.js:492:16"
]
}
}
}
],
"data": null
}
Any help is appreciated.
You need to write a #FieldResolver manually that will resolve the relation and return proper data.
Another solution is to rely on ORM capabilities and lazy relations - when the returned base entity contains a promise as a field, so when .then() is called, it automatically fetches the relation for the database.

Sequelize and Graphql reverse lookup

I have two Models:
River.associate = models => {
River.hasMany(models.Fish, { as: 'Fishes' });
};
Fish.associate = models => {
Fish.belongsTo(models.River);
};
type River {
id: ID!
name: String!
alternative: String!
geojson: JSON
fishes: [Fish]
}
type Fish {
id: ID!
name: String!
}
How would I findAll Rivers given a list of Fish ID's? Im not sure how the query must look for this for graphql and sequelize?
type Query {
river(id: ID!): River
**rivers(fishIds: ARRAY): River ??????**
fishes: [Fish]
}
Query: {
rivers: (_, { fishIds }) => {
return River.findAll({
where: {
fishes: fishIds
}
});
},
}
You can specify WHERE clauses for each of the models you include. Moreover, doing so will convert the eager load to an inner join (which is what you want), unless you explicitly set the required param to false.
That means you should be able to do something like:
River.findAll({ include:
[
{
model: Fish,
where: { id: fishIds }
},
],
})

GraphQL queries with tables join using Node.js

I am learning GraphQL so I built a little project. Let's say I have 2 models, User and Comment.
const Comment = Model.define('Comment', {
content: {
type: DataType.TEXT,
allowNull: false,
validate: {
notEmpty: true,
},
},
});
const User = Model.define('User', {
name: {
type: DataType.STRING,
allowNull: false,
validate: {
notEmpty: true,
},
},
phone: DataType.STRING,
picture: DataType.STRING,
});
The relations are one-to-many, where a user can have many comments.
I have built the schema like this:
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: {
type: GraphQLString
},
name: {
type: GraphQLString
},
phone: {
type: GraphQLString
},
comments: {
type: new GraphQLList(CommentType),
resolve: user => user.getComments()
}
})
});
And the query:
const user = {
type: UserType,
args: {
id: {
type: new GraphQLNonNull(GraphQLString)
}
},
resolve(_, {id}) => User.findById(id)
};
Executing the query for a user and his comments is done with 1 request, like so:
{
User(id:"1"){
Comments{
content
}
}
}
As I understand, the client will get the results using 1 query, this is the benefit using GraphQL. But the server will execute 2 queries, one for the user and another one for his comments.
My question is, what are the best practices for building the GraphQL schema and types and combining join between tables, so that the server could also execute the query with 1 request?
The concept you are refering to is called batching. There are several libraries out there that offer this. For example:
Dataloader: generic utility maintained by Facebook that provides "a consistent API over various backends and reduce requests to those backends via batching and caching"
join-monster: "A GraphQL-to-SQL query execution layer for batch data fetching."
To anyone using .NET and the GraphQL for .NET package, I have made an extension method that converts the GraphQL Query into Entity Framework Includes.
public static class ResolveFieldContextExtensions
{
public static string GetIncludeString(this ResolveFieldContext<object> source)
{
return string.Join(',', GetIncludePaths(source.FieldAst));
}
private static IEnumerable<Field> GetChildren(IHaveSelectionSet root)
{
return root.SelectionSet.Selections.Cast<Field>()
.Where(x => x.SelectionSet.Selections.Any());
}
private static IEnumerable<string> GetIncludePaths(IHaveSelectionSet root)
{
var q = new Queue<Tuple<string, Field>>();
foreach (var child in GetChildren(root))
q.Enqueue(new Tuple<string, Field>(child.Name.ToPascalCase(), child));
while (q.Any())
{
var node = q.Dequeue();
var children = GetChildren(node.Item2).ToList();
if (children.Any())
{
foreach (var child in children)
q.Enqueue(new Tuple<string, Field>
(node.Item1 + "." + child.Name.ToPascalCase(), child));
}
else
{
yield return node.Item1;
}
}}}
Lets say we have the following query:
query {
getHistory {
id
product {
id
category {
id
subCategory {
id
}
subAnything {
id
}
}
}
}
}
We can create a variable in "resolve" method of the field:
var include = context.GetIncludeString();
which generates the following string:
"Product.Category.SubCategory,Product.Category.SubAnything"
and pass it to Entity Framework:
public Task<TEntity> Get(TKey id, string include)
{
var query = Context.Set<TEntity>();
if (!string.IsNullOrEmpty(include))
{
query = include.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Aggregate(query, (q, p) => q.Include(p));
}
return query.SingleOrDefaultAsync(c => c.Id.Equals(id));
}