nodejs + sequelize :: include where required false - node.js

i have the following hierarchy of objects:
user
parent (hasOne)
children (hasMany)
once i have a user object, im attempting to load the associated parent object along with some of its children. the following works:
user.getParent({
include: [{
model: Child
}]
}).then(function(parent) {
var children = parent.children;
});
but if i want to selectively load some of the parent's children, like so:
user.getParent({
include: [{
model: Child,
where: { gender: 'FEMALE' }
}]
}).then(function(parent) {
var daughters = parent.daughters;
});
if the parent has one or more daughters, the query works and i get the parent data along with all daughters' data. however, if the parent has only sons, the parent object returned is null..
i want that if the parent has no daughters, the parent object should still be resolved with children = null or children = []..
also, if i wanted to simply load a count of children while loading the parent, how can i go about doing that?
thanks

it turned out to be an issue with my own code.. sequelize fails to load a model if a nested include has a where clause that returns no models.. to ensure it doesnt fail completely, you can set a required: false clause along with the where clause.. this makes sequelize return a blank array instead of failing the load completely..
more info at https://github.com/sequelize/sequelize/issues/4019

required: false + where is inconsistent/buggy in sequelize 6.5.1 SQLite vs PostgreSQL
Edit: it was working fine as of sequelize v6.13.0, so it got fixed somewhere along the way.
This is one of the many many many Sequelize association bugs/inconsistencies that make me want to try and find another ORM at some point. Reported at: https://github.com/sequelize/sequelize/issues/13809
Consider the following example where we want to:
list all tags
in addition, determine if each tag has been assigned to a selected animal
Our where condition on the animal then is a simple "check if the animal is a specific animal", and can only return 0 or 1 entries therefore. But the same would be valid with any other more complex condition.
main.js
#!/usr/bin/env node
const assert = require('assert')
const path = require('path')
const { DataTypes, Sequelize } = require('sequelize')
let sequelize
if (process.argv[2] === 'p') {
sequelize = new Sequelize('tmp', undefined, undefined, {
dialect: 'postgres',
host: '/var/run/postgresql',
})
} else {
sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'tmp.sqlite'
})
}
;(async () => {
const Animal = sequelize.define('Animal', {
species: { type: DataTypes.STRING },
})
const Tag = sequelize.define('Tag', {
name: { type: DataTypes.STRING },
})
Animal.belongsToMany(Tag, { through: 'AnimalTag' })
Tag.belongsToMany(Animal, { through: 'AnimalTag' })
await sequelize.sync({force: true})
const animal0 = await Animal.create({ species: 'bat' })
const animal1 = await Animal.create({ species: 'ant' })
const animal2 = await Animal.create({ species: 'dog' })
const tag0 = await Tag.create({ name: 'mammal' })
const tag1 = await Tag.create({ name: 'flying' })
const tag2 = await Tag.create({ name: 'aquatic' })
await animal0.addTag(tag0)
await animal0.addTag(tag1)
await animal2.addTag(tag1)
const animals = [animal0, animal1, animal2]
for (let animal of animals) {
let rows
rows = await Tag.findAll({
include: [{
model: Animal,
where: { id: animal.id },
// No effect.
//on: { id: animal.id },
through: {
// Same as putting the where outside of through.
//where: { AnimalId: animal.id },
},
required: false,
}],
order: [['name', 'ASC']],
})
console.error(animal.species);
console.error(rows.map(row => { return {
name: row.name,
animals: row.Animals.map(animal => animal.species),
} }))
}
})().finally(() => { return sequelize.close() })
package.json
{
"name": "tmp",
"private": true,
"version": "1.0.0",
"dependencies": {
"pg": "8.5.1",
"pg-hstore": "2.3.3",
"sequelize": "6.5.1",
"sqlite3": "5.0.2"
}
}
Run:
npm install
./main.js p
./main.js
The PotsgreSQL results are as expected:
bat
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [ 'bat' ] },
{ name: 'mammal', animals: [ 'bat' ] }
]
ant
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [] },
{ name: 'mammal', animals: [] }
]
dog
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [] },
{ name: 'mammal', animals: [ 'dog' ] }
]
But the SQLite results are just plain wrong:
bat
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [ 'bat' ] },
{ name: 'mammal', animals: [ 'bat', null ] }
]
ant
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [] },
{ name: 'mammal', animals: [] }
]
dog
[
{ name: 'aquatic', animals: [] },
{ name: 'flying', animals: [] },
{ name: 'mammal', animals: [] }
]
I don't mind the null that much, I can work around that.
But dog not being a mammal, that I cannot work around.
Underlying queries
We can look at the underlying queries to guess what is going on.
PostgreSQL has queries of type:
SELECT
"Tag"."name",
"Animals"."species" AS "Animals.species",
FROM
"Tags" AS "Tag"
LEFT OUTER JOIN (
"AnimalTag" AS "Animals->AnimalTag"
INNER JOIN "Animals" AS "Animals" ON "Animals"."id" = "Animals->AnimalTag"."AnimalId"
) ON "Tag"."id" = "Animals->AnimalTag"."TagId"
AND "Animals"."id" = 1
ORDER BY
"Tag"."name" ASC;
SQLite has queries of type:
SELECT
`Tag`.`name`,
`Animals`.`species` AS `Animals.species`
FROM `Tags` AS `Tag`
LEFT OUTER JOIN `AnimalTag` AS `Animals->AnimalTag`
ON `Tag`.`id` = `Animals->AnimalTag`.`TagId`
LEFT OUTER JOIN `Animals` AS `Animals`
ON `Animals`.`id` = `Animals->AnimalTag`.`AnimalId`
AND `Animals`.`id`= 3
ORDER BY
`Tag`.`name` ASC;
So only the PostgreSQL query is the desired one because of the way it uses the subquery so that the:
ON "Tag"."id" = "Animals->AnimalTag"."TagId"
AND "Animals"."id" = 1
gets applied only to the outer LEFT OUTER JOIN.
The SQLite query produces undesired results, running it manually for each animal we get outputs:
bat
aquatic|
flying|bat
mammal|bat
mammal|
ant
aquatic|
flying|
mammal|
mammal|
dot
aquatic|
flying|
mammal|
mammal|dog
so what seems to happen is that the animal first result is null, then sequelize ignores it on the return, which would explain why we saw:
{ name: 'mammal', animals: [ 'bat', null ] }
for bat but:
{ name: 'mammal', animals: [] }
for dog.
The desired statement would have the AND on the first LEFT OUTER JOIN:
SELECT
`Tag`.`name`,
`Animals`.`species` AS `Animals.species`
FROM `Tags` AS `Tag`
LEFT OUTER JOIN `AnimalTag` AS `Animals->AnimalTag`
ON `Tag`.`id` = `Animals->AnimalTag`.`TagId`
AND `Animals->AnimalTag`.`AnimalId`= 1
LEFT OUTER JOIN `Animals` AS `Animals`
ON `Animals`.`id` = `Animals->AnimalTag`.`AnimalId`
ORDER BY
`Tag`.`name` ASC;
Workaround: super many to many
The best workaround I could come up so far is to use super many to many, this is often a good bet when the problem is that we need greater control about ON conditions:
#!/usr/bin/env node
const assert = require('assert')
const path = require('path')
const { DataTypes, Sequelize } = require('sequelize')
let sequelize
let logging = process.argv[3] === '1'
if (process.argv[2] === 'p') {
sequelize = new Sequelize('tmp', undefined, undefined, {
dialect: 'postgres',
host: '/var/run/postgresql',
logging,
})
} else {
sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'tmp.sqlite',
logging,
})
}
;(async () => {
const AnimalTag = sequelize.define('AnimalTag')
const Animal = sequelize.define('Animal', {
species: { type: DataTypes.STRING },
})
const Tag = sequelize.define('Tag', {
name: { type: DataTypes.STRING },
})
AnimalTag.belongsTo(Animal)
Animal.hasMany(AnimalTag)
AnimalTag.belongsTo(Tag)
Tag.hasMany(AnimalTag)
Animal.belongsToMany(Tag, { through: AnimalTag })
Tag.belongsToMany(Animal, { through: AnimalTag })
await sequelize.sync({force: true})
const animal0 = await Animal.create({ species: 'bat' })
const animal1 = await Animal.create({ species: 'ant' })
const animal2 = await Animal.create({ species: 'dog' })
const tag0 = await Tag.create({ name: 'flying' })
const tag1 = await Tag.create({ name: 'mammal' })
const tag2 = await Tag.create({ name: 'aquatic' })
await animal0.addTag(tag0)
await animal0.addTag(tag1)
await animal2.addTag(tag1)
const animals = [animal0, animal1, animal2]
for (let animal of animals) {
let rows
rows = await Tag.findAll({
include: [{
model: AnimalTag,
where: { AnimalId: animal.id },
required: false,
include: [{
model: Animal,
}]
}],
order: [['name', 'ASC']],
})
console.error(rows.map(row => { return {
name: row.name,
animals: row.AnimalTags.map(animalTag => animalTag.Animal.species)
} }))
console.error();
}
})().finally(() => { return sequelize.close() })
produces the correct output for both PostgreSQL and Sequelize. The produced query is exactly what we wanted:
SELECT
"Tag"."id",
"Tag"."name",
"Tag"."createdAt",
"Tag"."updatedAt",
"AnimalTags"."createdAt" AS "AnimalTags.createdAt",
"AnimalTags"."updatedAt" AS "AnimalTags.updatedAt",
"AnimalTags"."AnimalId" AS "AnimalTags.AnimalId",
"AnimalTags"."TagId" AS "AnimalTags.TagId",
"AnimalTags->Animal"."id" AS "AnimalTags.Animal.id",
"AnimalTags->Animal"."species" AS "AnimalTags.Animal.species",
"AnimalTags->Animal"."createdAt" AS "AnimalTags.Animal.createdAt",
"AnimalTags->Animal"."updatedAt" AS "AnimalTags.Animal.updatedAt"
FROM
"Tags" AS "Tag"
LEFT OUTER JOIN "AnimalTags" AS "AnimalTags" ON "Tag"."id" = "AnimalTags"."TagId"
AND "AnimalTags"."AnimalId" = 3
LEFT OUTER JOIN "Animals" AS "AnimalTags->Animal" ON "AnimalTags"."AnimalId" = "AnimalTags->Animal"."id"
ORDER BY
"Tag"."name" ASC;
Tested on Ubuntu 21.10, PostgreSQL 13.5.

Related

Convert DB response from object to array

I have the following schema:
const mySchema = new mongoose.Schema({
name: String,
subscribers: [ { name: String } ]
})
const myModel = mongoose.model('User', mySchema, 'users')
and I have this code in one of my controllers:
const allOfHisSubscribers = await myModel.findById(req.params.id).select('subscribers')
I want the database response of the await myModel.findByid call to be:
[
{ name: 'x' },
{ name: 'y' },
{ name: 'z' },
]
However, the code above is returning:
{
subscribers: [
{ name: 'x' },
{ name: 'y' },
{ name: 'z' },
]
}
I don't want the JavaScript way of doing it, I know you can use something like Array.prototype.map to get the result, but I am using a middleware, so I can only use the mongoose or MongoDB way of doing it (if possible).
Got it :D, if not, let me know in the comments 🌹

Sequelize Complex Filter | Node.js

i want to filter, but that depend to the user, for example
Data.findAll({
where: {
name: {
[Op.or]: [
{ [Op.like]: ['%samsung%'] },
{ [Op.like]: ['%iphone%'] },
{ [Op.like]: ['%alcatel%']}
]
}
}
}
If the user selects only Samsung, how do I go about filtering only by Samsung?
Assuming req.query.brands stores either a single search string or an array of strings we can build Op.or conditions on the fly:
const brands = [].concat(req.query.brands)
const brandConditions = brands.map(x => ({
[Op.like]: `%${x}%`
})
const foundItems = await Data.findAll({
where: {
name: {
[Op.or]: brandConditions
}
}
}

How do you seed a mongodb database such that the Keystone 5 CMS recognizes the many-to-many relationships?

Let's say I have two objects: Product and Seller
Products can have multiple Sellers.
A single Seller can sell multiple Products.
The goal is to write a seeding script that successfully seeds my MongoDB database such that Keystone.js's CMS recognizes the many-to-many relationship.
Schemas
Product.ts
import { text, relationship } from "#keystone-next/fields";
import { list } from "#keystone-next/keystone/schema";
export const Product = list({
fields: {
name: text({ isRequired: true }),
sellers: relationship({
ref: "Seller.products",
many: true,
}),
},
});
Seller.ts
import { text, relationship } from "#keystone-next/fields";
import { list } from "#keystone-next/keystone/schema";
export const Product = list({
fields: {
name: text({ isRequired: true }),
products: relationship({
ref: "Product.sellers",
many: true,
}),
},
});
KeystoneJS config
My keystone.ts config, shortened for brevity, looks like this:
import { insertSeedData } from "./seed-data"
...
db: {
adapter: "mongoose",
url: databaseURL,
async onConnect(keystone) {
console.log("Connected to the database!");
if (process.argv.includes("--seed-data")) {
await insertSeedData(keystone);
}
},
},
lists: createSchema({
Product,
Seller,
}),
...
Seeding Scripts (these are the files I expect to change)
I have a script that populates the database (seed-data/index.ts):
import { products } from "./data";
import { sellers } from "./data";
export async function insertSeedData(ks: any) {
// setup code
const keystone = ks.keystone || ks;
const adapter = keystone.adapters?.MongooseAdapter || keystone.adapter;
const { mongoose } = adapter;
mongoose.set("debug", true);
// adding products to DB
for (const product of products) {
await mongoose.model("Product").create(product);
}
// adding sellers to DB
for (const seller of sellers) {
await mongoose.model("Seller").create(seller);
}
}
And finally, data.ts looks something like this:
export const products = [
{
name: "apple",
sellers: ["Joe", "Anne", "Duke", "Alicia"],
},
{
name: "orange",
sellers: ["Duke", "Alicia"],
},
...
];
export const sellers = [
{
name: "Joe",
products: ["apple", "banana"],
},
{
name: "Duke",
products: ["apple", "orange", "banana"],
},
...
];
The above setup does not work for a variety of reasons. The most obvious is that the sellers and products attributes of the Product and Seller objects (respectively) should reference objects (ObjectId) and not names (e.g. "apple", "Joe").
I'll post a few attempts below that I thought would work, but did not:
Attempt 1
I figured I'd just give them temporary ids (the id attribute in data.ts below) and then, once MongoDB assigns an ObjectId, I'll use those.
seed-data/index.ts
...
const productIdsMapping = [];
...
// adding products to DB
for (const product of products) {
const productToPutInMongoDB = { name: product.name };
const { _id } = await mongoose.model("Product").create(productToPutInMongoDB);
productIdsMapping.push(_id);
}
// adding sellers to DB (using product IDs created by MongoDB)
for (const seller of sellers) {
const productMongoDBIds = [];
for (const productSeedId of seller.products) {
productMongoDBIds.push(productIdsMapping[productSeedId]);
const sellerToPutInMongoDB = { name: seller.name, products: productMongoDBIds };
await mongoose.model("Seller").create(sellerToPutInMongoDB);
}
...
data.ts
export const products = [
{
id: 0,
name: "apple",
sellers: [0, 1, 2, 3],
},
{
id: 1,
name: "orange",
sellers: [2, 3],
},
...
];
export const sellers = [
{
id: 0
name: "Joe",
products: [0, 2],
},
...
{
id: 2
name: "Duke",
products: [0, 1, 2],
},
...
];
Output (attempt 1):
It just doesn't seem to care about or acknowledge the products attribute.
Mongoose: sellers.insertOne({ _id: ObjectId("$ID"), name: 'Joe', __v: 0}, { session: null })
{
results: {
_id: $ID,
name: 'Joe',
__v: 0
}
}
Attempt 2
I figured maybe I just didn't format it correctly, for some reason, so maybe if I queried the products and shoved them directly into the seller object, that would work.
seed-data/index.ts
...
const productIdsMapping = [];
...
// adding products to DB
for (const product of products) {
const productToPutInMongoDB = { name: product.name };
const { _id } = await mongoose.model("Product").create(productToPutInMongoDB);
productIdsMapping.push(_id);
}
// adding sellers to DB (using product IDs created by MongoDB)
for (const seller of sellers) {
const productMongoDBIds = [];
for (const productSeedId of seller.products) {
productMongoDBIds.push(productIdsMapping[productSeedId]);
}
const sellerToPutInMongoDB = { name: seller.name };
const { _id } = await mongoose.model("Seller").create(sellerToPutInMongoDB);
const resultsToBeConsoleLogged = await mongoose.model("Seller").findByIdAndUpdate(
_id,
{
$push: {
products: productMongoDBIds,
},
},
{ new: true, useFindAndModify: false, upsert: true }
);
}
...
data.ts
Same data.ts file as attempt 1.
Output (attempt 2):
Same thing. No luck on the products attribute appearing.
Mongoose: sellers.insertOne({ _id: ObjectId("$ID"), name: 'Joe', __v: 0}, { session: null })
{
results: {
_id: $ID,
name: 'Joe',
__v: 0
}
}
So, now I'm stuck. I figured attempt 1 would Just Workâ„¢ like this answer:
https://stackoverflow.com/a/52965025
Any thoughts?
I figured out a solution. Here's the background:
When I define the schema, Keystone creates corresponding MongoDB collections. If there is a many-to-many relationship between object A and object B, Keystone will create 3 collections: A, B, and A_relationshipToB_B_relationshipToA.
That 3rd collection is the interface between the two. It's just a collection with pairs of ids from A and B.
Hence, in order to seed my database with a many-to-many relationship that shows up in the Keystone CMS, I have to seed not only A and B, but also the 3rd collection: A_relationshipToB_B_relationshipToA.
Hence, seed-data/index.ts will have some code that inserts into that table:
...
for (const seller of sellers) {
const sellerToAdd = { name: seller.name };
const { _id } = await mongoose.model("Seller").create(sellerToAdd);
// Product_sellers_Seller_products Insertion
for (const productId of seller.products) {
await mongoose
.model("Product_sellers_Seller_products")
.create({
Product_left_id: productIds[productId], // (data.ts id) --> (Mongo ID)
Seller_right_id: _id,
});
}
}
...

Sequelize - Postgres - How to create new object from junction table

I'm trying to use a junction table model to create a new object in Sequelize. I have read through the docs pretty thoroughly and do not believe that this use case is covered, unless I am misunderstanding (which is always a possibility).
Docs:
https://sequelize.org/master/manual/assocs.html
https://sequelize.org/master/class/lib/associations/belongs-to-many.js~BelongsToMany.html
Example code:
import { Model as SQLModel } from 'sequelize';
class CarCompany extends SQLModel {};
class BodyStyle extends SQLModel {};
class Model extends SQLModel {};
// A car company can make multiple types of cars
CarCompany.hasMany(BodyStyle);
// A type of car can be made by multiple companies.
// We have a junction table to represent this relationship
BodyStyle.belongsToMany(CarCompany, { through: CarCompanyBodyStyles });
// Each model has only one type that it fits into and only one company that makes it
Model.belongsTo(CarCompanyBodyStyles);
// Now let's create some example car types
const convertible = new BodyStyle({ name: 'Convertible' });
const suv = new BodyStyle({ name: 'SUV' });
// Now let's create some example car companies
const toyota = new CarCompany({ name: 'Toyota' });
const ferrari = new CarCompany({ name: 'Ferrari' });
// Now let's specify which types of cars are made by each company
const toyotaConvertibles = toyota.addBodyStyle(convertible);
const toyotaSUVs = toyota.addBodyStyle(suv);
const ferrariConvertibles = ferrari.addBodyStyle(convertible);
// Now let's define some specific models
const solara = toyotaConvertibles.createModel({ name: 'Solara' });
const rav4 = toyotaSUVs.createModel({ name: 'RAV-4' });
const spider = ferrariConvertibles.createModel({ name: '488 Spider' });
// Now lets see some nested relational objects that we have created:
const toyotaModels = CarCompany.findByPk(
toyota.id,
{
include: [
{
all: true,
nested: true,
},
],
},
);
const ferrariModels = CarCompany.findByPk(
ferrari.id,
{
include: [
{
all: true,
nested: true,
},
],
},
);
console.log({ ferrariModels, toyotaModels });
What I was hoping to see is something like:
{
ferrariModels: {
bodyStyles: [
{
name: 'Convertible',
models: [
{ name: '488 Spider' },
],
},
],
},
toyotaModels: {
bodyStyles: [
{
name: 'SUV',
models: [
{ name: 'RAV-4' },
],
},
{
name: 'Convertible',
models: [
{ name: 'Solara' },
],
},
],
},
}
But instead I get an error:
TypeError: toyotaConvertibles.createModel is not a function
What am I doing wrong? How am I supposed to go about creating this type of relationship?

Sequelize query with count in inner join

I am trying to convert this query to sequelize query object what is the right wayto do it?
SELECT families.id, count('answers.familyId') FROM families LEFT JOIN
answers on families.id = answers.familyId WHERE answers.isActive=1 AND
answers.answer=1 GROUP BY families.id HAVING COUNT('answers.familyId')>=6
Let's assume that Family is your families sequelize model and Answer is your answers sequelize model, and sequelize is your Sequelize instance
Family.findAll({
attributes: ['*', sequelize.fn('COUNT', sequelize.col('Answers.familyId'))],
include: [
{
model: Answer,
attributes: [],
where: {
isActive: 1,
answer: 1
}
}
],
group: '"Family.id"',
having: sequelize.where(sequelize.fn('COUNT', sequelize.col('Answers.familyId')), '>=', 6)
}).then((families) => {
// result
});
Useful documentation links:
sequelize.fn()
sequelize.where()
sequelize.col()
You need to use get() on the attribute: aliased count column
There are two important gotchas when reading the aggregates out:
the count only shows up on results if you alias it with attributes as shown by Piotr at https://stackoverflow.com/a/42472696/895245 and as shown at How do I select a column using an alias attributes aliasing has the unexpected effect of requiring you to use .get().
as mentioned at: How does group by works in sequelize? the count comes out as a string in PostgreSQL due to bigint shenanigans, and you need parseInt it
Here's a minimal runnable example where we have posts and users who can like posts, and we want to count how:
how many likes each user has
ignoring likes to post2
considering only users that have 0 or 1 likes in total
The following small improvements are made over Piotr's code:
you likely don't want attributes: ['*' because that selects all columns, and therefore generally includes columns that are neither aggregates nor grouped by, leading to indeterminate behavior in some DBMSs and errors in others. You should just specify the GROUP by column instead, in our case the column is name.
using the slightly cleaner Op.lte rather than the literal '<='
Due to required: false, this first version does a LEFT OUTER JOIN + COUNT(column), see also: https://dba.stackexchange.com/questions/174694/how-to-get-a-group-where-the-count-is-zero
sqlite.js
const assert = require('assert');
const { DataTypes, Op, Sequelize } = require('sequelize');
const sequelize = new Sequelize('tmp', undefined, undefined, Object.assign({
dialect: 'sqlite',
storage: 'tmp.sqlite'
}));
;(async () => {
const User = sequelize.define('User', {
name: { type: DataTypes.STRING },
}, {});
const Post = sequelize.define('Post', {
body: { type: DataTypes.STRING },
}, {});
User.belongsToMany(Post, {through: 'UserLikesPost'});
Post.belongsToMany(User, {through: 'UserLikesPost'});
await sequelize.sync({force: true});
const user0 = await User.create({name: 'user0'})
const user1 = await User.create({name: 'user1'})
const user2 = await User.create({name: 'user2'})
const post0 = await Post.create({body: 'post0'})
const post1 = await Post.create({body: 'post1'})
const post2 = await Post.create({body: 'post2'})
// Set likes for each user.
await user0.addPosts([post0, post1])
await user1.addPosts([post0, post2])
let rows = await User.findAll({
attributes: [
'name',
[sequelize.fn('COUNT', sequelize.col('Posts.id')), 'count'],
],
include: [
{
model: Post,
attributes: [],
required: false,
through: {attributes: []},
where: { id: { [Op.ne]: post2.id }},
},
],
group: ['User.name'],
order: [[sequelize.col('count'), 'DESC']],
having: sequelize.where(sequelize.fn('COUNT', sequelize.col('Posts.id')), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows[1].name, 'user2')
assert.strictEqual(parseInt(rows[1].get('count'), 10), 0)
assert.strictEqual(rows.length, 2)
})().finally(() => { return sequelize.close() });
with:
package.json
{
"name": "tmp",
"private": true,
"version": "1.0.0",
"dependencies": {
"pg": "8.5.1",
"pg-hstore": "2.3.3",
"sequelize": "6.5.1",
"sqlite3": "5.0.2"
}
}
and Node v14.17.0.
If we wanted the INNER JOIN version excluding 0 counts, we could just remove the required: false, which makes it be the default true. We can also use do a slightly simpler COUNT(*) since there will be no NULLs now:
let rows = await User.findAll({
attributes: [
'name',
[sequelize.fn('COUNT', '*'), 'count'],
],
include: [
{
model: Post,
attributes: [],
through: {attributes: []},
where: { id: { [Op.ne]: post2.id }},
},
],
group: ['User.name'],
order: [[sequelize.col('count'), 'DESC']],
having: sequelize.where(sequelize.fn('COUNT', '*'), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].get('count'), 10), 1)
assert.strictEqual(rows.length, 1)
PostgreSQL support has been broken for several years due to column X must appear in the GROUP BY clause or be used in an aggregate function
The above code should work for PostgreSQL too, but as mentioned at:
https://github.com/sequelize/sequelize/issues/3256
https://github.com/sequelize/sequelize/issues/5481#issuecomment-964387232
there's a bug and it doesn't. The fact that such glaring bugs have persisted for several years make me doubt if I should really be using this ORM.
The workaround is to use both:
raw: true,
includeIgnoreAttributes: false,
Full working example with the workaround:
#!/usr/bin/env node
const assert = require('assert');
const { DataTypes, Op, Sequelize } = require('sequelize');
const sequelize = new Sequelize('tmp', undefined, undefined, Object.assign({
dialect: 'postgres',
host: '/var/run/postgresql',
}));
;(async () => {
const User = sequelize.define('User', {
name: { type: DataTypes.STRING },
}, {});
const Post = sequelize.define('Post', {
body: { type: DataTypes.STRING },
}, {});
User.belongsToMany(Post, {through: 'UserLikesPost'});
Post.belongsToMany(User, {through: 'UserLikesPost'});
await sequelize.sync({force: true});
const user0 = await User.create({name: 'user0'})
const user1 = await User.create({name: 'user1'})
const user2 = await User.create({name: 'user2'})
const post0 = await Post.create({body: 'post0'})
const post1 = await Post.create({body: 'post1'})
const post2 = await Post.create({body: 'post2'})
// Set likes for each user.
await user0.addPosts([post0, post1])
await user1.addPosts([post0, post2])
let rows = await User.findAll({
attributes: [
'name',
[sequelize.fn('COUNT', '*'), 'count'],
],
raw: true,
includeIgnoreAttributes: false,
include: [
{
model: Post,
where: { id: { [Op.ne]: post2.id }},
},
],
group: ['User.name'],
order: [[sequelize.col('count'), 'DESC']],
having: sequelize.where(sequelize.fn('COUNT', '*'), Op.lte, 1)
})
assert.strictEqual(rows[0].name, 'user1')
assert.strictEqual(parseInt(rows[0].count, 10), 1)
assert.strictEqual(rows.length, 1)
})().finally(() => { return sequelize.close() });
tested on PostgreSQL 13.4, Ubuntu 21.10.
Related
Counting associated entries with Sequelize

Resources