ID photo of Ciro Santilli taken in 2013 right eyeCiro Santilli OurBigBook logoOurBigBook.com  Sponsor 中国独裁统治 China Dictatorship 新疆改造中心、六四事件、法轮功、郝海东、709大抓捕、2015巴拿马文件 邓家贵、低端人口、西藏骚乱
nodejs/sequelize/many_to_many_same_model.js
#!/usr/bin/env node

// https://cirosantilli.com/sequelize-example

const assert = require('assert');
const path = require('path');
const { DataTypes } = require('sequelize');
const common = require('./common')
const sequelize = common.sequelize(__filename, process.argv[2], { define: { timestamps: false } })
;(async () => {

// Create the tables.
const User = sequelize.define('User', {
  username: { type: DataTypes.STRING },
}, {});
User.belongsToMany(User, { through: 'UserFollowUser', as: 'Follows', foreignKey: 'UserId' });
// This is ony needed for the function followedFindAll. "foreignKey" could be removed otherwise.
User.belongsToMany(User, { through: 'UserFollowUser', as: 'Followed', foreignKey: 'FollowId' });
await sequelize.sync({ force: true });

// Create some users.

const user0 = await User.create({ username: 'user0' })
const user1 = await User.create({ username: 'user1' })
const user2 = await User.create({ username: 'user2' })
const user3 = await User.create({ username: 'user3' })

// Make user0 follow user1 and user2
await user0.addFollows([user1, user2])
// Make user2 and user3 follow user0
await user2.addFollow(user0)
await user3.addFollow(user0)

let rows

// Get users followed by user0.
rows = await user0.getFollows({order: [['username', 'ASC']]})
common.assertEqual(rows, [
  { username: 'user1' },
  { username: 'user2' },
])

// Reverse order.
rows = await user0.getFollows({order: [['username', 'DESC']]})
common.assertEqual(rows, [
  { username: 'user2' },
  { username: 'user1' },
])

// Get users followed by user1.
rows = await user1.getFollows({order: [['username', 'ASC']]})
common.assertEqual(rows, [])

// Get users followed by user2.
rows = await user2.getFollows({order: [['username', 'ASC']]})
common.assertEqual(rows, [
  { username: 'user0' },
])

// Get users followed by user3.
rows = await user3.getFollows({order: [['username', 'ASC']]})
common.assertEqual(rows, [
  { username: 'user0' },
])

// Get users followed by an user by username instead of user object using findOne.
// Also get rid of all useless fields from the through table.
// followedFindAll is almost always a better alternative to this.
async function followedFindOne(username, opts={}) {
  return (await User.findOne(Object.assign({
    where: { username },
    attributes: [],
    include: [{
      model: User,
      as: 'Follows',
      through: {attributes: []},
    }],
    order: [['Follows', 'username', 'ASC']]
  }, opts))).Follows
}
rows = await followedFindOne('user0')
common.assertEqual(rows, [
  { username: 'user1'},
  { username: 'user2'},
])
// TODO how to implement limit in this case? We managed it in followedFindAll however.
// Does not work like this because the limit would be applied to the outer findOne.
//rows = await followedFindOne('user0', { limit: 1 })
//common.assertEqual(rows, [
//  { username: 'user1'},
//])
// Reverse order.
rows = await followedFindOne('user0', { order: [['Follows', 'username', 'DESC']] })
common.assertEqual(rows, [
  { username: 'user2'},
  { username: 'user1'},
])

// Same but with findAll. This is the preferred approach in most real world applications.
// Get users followed by an user by username instead of user object using findAll.
// Note that for this to work, we needed to add the extra relation:
// ``
// User.belongsToMany(User, { through: 'UserFollowUser', as: 'Followed', foreignKey: 'FollowId' });
// ``
async function followedFindAll(username, opts={}) {
  return User.findAll(Object.assign({
    include: [{
      model: User,
      as: 'Followed',
      attributes: [],
      through: { attributes: [] },
      where: { username },
    }],
    // Required for limit to work.
    subQuery: false,
    order: [['username', 'ASC']]
  }, opts))
}
rows = await followedFindAll('user0')
common.assertEqual(rows, [
  { username: 'user1'},
  { username: 'user2'},
])
rows = await followedFindAll('user0', { limit: 1 })
common.assertEqual(rows, [
  { username: 'user1'},
])
rows = await followedFindAll('user0', { order: [['username', 'DESC']] })
common.assertEqual(rows, [
  { username: 'user2'},
  { username: 'user1'},
])

// Now the inverse: find users that follow a given user.

// Find users that follow a given user by username.
// TODO Any way with the autogenerated methods? Anyways, this is good enough, almost always what we need.
// * https://github.com/sequelize/sequelize/issues/8263
// * https://stackoverflow.com/questions/27065154/how-to-get-all-children-or-parents-in-a-many-to-many-association-if-one-model-re
async function following(username, opts={}) {
  return User.findAll(
    Object.assign({
      include: [{
        model: User,
        as: 'Follows',
        where: { username },
        attributes: [],
        through: { attributes: [] }
      }],
      // subQuery is mandatory if you want to apply a limit, like in every real query.
      subQuery: false,
      order: [['username', 'ASC']],
    }, opts)
  )
}
common.assertEqual(await following('user0'), [
  { username: 'user2' },
  { username: 'user3' },
])
common.assertEqual(await following('user0', { order: [['username', 'DESC']] }), [
  { username: 'user3' },
  { username: 'user2' },
])
common.assertEqual(await following('user0', { limit: 1 }), [
  { username: 'user2' },
])
common.assertEqual(await following('user0', { limit: 1, order: [['username', 'DESC']] }), [
  { username: 'user3' },
])
common.assertEqual(await following('user1'), [
  { username: 'user0' },
])
common.assertEqual(await following('user2'), [
  { username: 'user0' },
])
common.assertEqual(await following('user3'), [])

// has methods
assert(!await user0.hasFollow(user0))
assert(!await user0.hasFollow(user0.id))
assert( await user0.hasFollow(user1))
assert( await user0.hasFollow(user2))
assert(!await user0.hasFollow(user3))

// Count method
assert.strictEqual(await user0.countFollows(), 2)

})().finally(() => { return sequelize.close() });