I'm trying to build a query in sequelize which basically makes a join between two tables: Resource and ContentTag, which are in a manyToMany relationship: In sql the query I'd like to obtain whould look like this:
`SELECT * FROM Resources-Ctag
NATURAL JOIN (SELECT * FROM Resource-Ctag WHERE ContentTag.keyword="A")as ResourceWithTagA
NATURAL JOIN (SELECT * FROM Resource-Ctag WHERE ContentTag.keyword="B")as ResourceWithTagB
WHERE ContentTag.keyword="C"`
Where Resources-Ctag is hypotetically the bridge table which associates resources to contentTags. The objective is to retrieve all resources that contains at least all the three tag together: A, B , C.
Here is the Resource model in sequelize:
import { ContentTag } from "./content_tag";
export class Resource extends Model {
// Default incremental ID
public id!: number;
// Model attributes
public name!: string;
public description!: string;
public format!: string;
public path!: string;
public metadata!: any;
public typologyTagId!: string;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
// Associations
public addContentTag!: HasManyAddAssociationMixin<ContentTag, number>;
public getContentTags!: HasManyGetAssociationsMixin<ContentTag>;
public setContentTags!: HasManySetAssociationsMixin<ContentTag, number>;
public static initialize(sequelize) {
this.init(
{
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: false,
},
description: {
type: DataTypes.STRING,
allowNull: true,
unique: false,
},
format: {
type: DataTypes.STRING,
allowNull: false,
unique: false,
},
path: {
type: DataTypes.STRING,
allowNull: false,
unique: false,
},
metadata: {
type: DataTypes.JSON,
allowNull: true,
unique: false,
},
},
{
sequelize,
timestamps: true,
paranoid: true,
tableName: "resource",
}
);
}
}
And here is the ContentTag Model:
export class ContentTag extends Model {
// Default incremental ID
public id!: string;
// Model attributes
public keyword!: string;
public static initialize(sequelize) {
this.init(
{
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
},
keyword: {
type: DataTypes.STRING,
allowNull: false,
unique: false,
},
},
{
sequelize,
timestamps: true,
createdAt: false,
updatedAt: false,
tableName: "content_tag",
}
);
}
}
I've tried this but it doesnt work:
router.get("/", async (req: any, res: Response) => {
try {
const { typologyTag, timeFrom, timeTo, author } = req.query;
const cTags = JSON.parse(req.query.contentTags)
const where = {};
const include = [
{
model: TypologyTag,
as: "typologyTag",
},
{
model: User,
as: "author",
},
{
model: ContentTag,
as: "contentTags",
},
];
if (typologyTag) {
include[0]["where"] = { keyword: { [Op.eq]: typologyTag } };
};
if (author) {
include[1]["where"] = { displayName: { [Op.eq]: author } };
};
if (timeFrom && timeTo) {
where["createdAt"] = {
[Op.lt]: timeTo,
[Op.gt]: new Date(timeFrom)
};
} else if (timeFrom) {
where["createdAt"] = {
[Op.gt]: new Date(timeFrom)
};
} else if (timeTo) {
where["createdAt"] = {
[Op.lt]: new Date(timeTo)
};
};
if (cTags && cTags.length != 0 && cTags.reduce(
(accumulator, currentValue) => accumulator && currentValue,
true)) {
include[2]["where"] = { keyword: { [Op.eq]: cTags[0] } };
if (cTags[1]) {
include[2]["include"] = [{
model: Resource,
as: "resources",
include: [{
model: ContentTag,
as: "contentTags",
where: { keyword: { [Op.eq]: cTags[1] } },
include: [{
model: Resource,
as: "resources",
include: [{
model: ContentTag,
as: "contentTags",
where: { keyword: { [Op.eq]: cTags[2] } },
}]
}]
}]
}]
}
};
const result = await Resource.findAll({
where: where,
include: include,
});
....
I made such a huge effort thinking about that solution but now I realise it obviously can't work. Now I have no more clue on how to solve that query.
You could fix the self-join but also can consider the alternative SQL like this.
Join ContentTag
where keyword is in "A", "B", "C" to Resource
and count the keyword per Resource.id
where the count should be greater than or equal to 3.
SELECT r.*
FROM Resources r
JOIN (SELECT *
FROM ContentTags
WHERE keyword IN ('A', 'B', 'C')) c ON r.id = c.resource_id
GROUP BY r.id
HAVING COUNT(DISTINCT c.keyword) >= 3;
In Sequelize JS (sorry, I don't write TypeScript).
Resource.findAll({
include: {
attributes: [], // If you need attributes from ContentTag, you need to add aggregation or change to window function.
model: ContentTag,
where: {
keyword: {
[Op.in]: ['A', 'B', 'C']
}
}
},
group: ['Resources.id'],
having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.literal('DISTINCT keyword')), Op.gte, 3)
})