Sequelize query with a where clause on an include of an include

Viewed 1369

I'm struggling to create a query with sequelize.

Some context

I have the following models:

  • A Manifestation can have [0..n] Event
  • An Event belongs to one Manifestation (an Event cannot exist without a Manifestation)
  • A Place can have [0..n] Event
  • An Event belongs to one Place (an Event cannot exist without a Place)
  • A Manifestation can have [1..n] Place
  • A Place can have [0..n] Manifestation

I model the relations as the following:

Manifestation.hasMany(Event, { onDelete: 'CASCADE', hooks: true })
Event.belongsTo(Manifestation)

Place.hasMany(Event, { onDelete: 'CASCADE', hooks: true })
Event.belongsTo(Place)

Manifestation.belongsToMany(Place, { through: 'manifestation_place' })
Place.belongsToMany(Manifestation, { through: 'manifestation_place' })

For me it seems rather correct, but don't hesitate if you have remarks.

The question

I'm trying to query the Place in order to get all Manifestation and Event happening in a given Place. But for the Event ones, I want to include them within their Manifestation even if the Manifestation doesn't happen in the given Place.

Below is the "JSON" structure I'm trying to achieve:

{
  id: 1,
  name: "Place Name",
  address: "Place address",
  latitude: 47.00000,
  longitude: -1.540000,
  manifestations: [
    {
      id: 10,
      title: "Manifestation one",
      placeId: 1,
      events: []
    },
    {
      id: 11,
      title: "Manifestation two",
      placeId: 3,
      events: [
        id: 5,
        title: "3333",
        manifestationId: 11,
        placeId: 1
      ]
    }
  ]
}

So I want to include the Manifestation with id: 11, because one of its Event occurs in the given Place (with id: 1)

Update (04/06/20): For now I rely on javascript to get the expected result

I figured out it would be nice if I posted my current solution before asking.

router.get('/test', async (req, res) => {
  try {
    const placesPromise = place.findAll()
    const manifestationsPromise = manifestation.findAll({
      include: [
        { model: event },
        {
          model: place,
          attributes: ['id'],
        },
      ],
    })

    const [places, untransformedManifestations] = await Promise.all([
      placesPromise,
      manifestationsPromise,
    ])

    const manifestations = untransformedManifestations.map(m => {
      const values = m.toJSON()
      const places = values.places.map(p => p.id)
      return { ...values, places }
    })

    const result = places
      .map(p => {
        const values = p.toJSON()
        const relatedManifestations = manifestations
          .filter(m => {
            const eventsPlaceId = m.events.map(e => e.placeId)
            return (
              m.places.includes(values.id) ||
              eventsPlaceId.includes(values.id)
            )
          })
          .map(m => {
            const filteredEvents = m.events.filter(
              e => e.placeId === values.id
            )
            return { ...m, events: filteredEvents }
          })
        return { ...values, manifestations: relatedManifestations }
      })
      .filter(p => p.manifestations.length)

    return res.status(200).json(result)
  } catch (err) {
    console.log(err)
    return res.status(500).send()
  }
})

But I'm pretty sure I could do that directly with sequelize. Any ideas or recommendations ?

Thanks

3 Answers

This is not optimum. But you can try it out:

const findPlace = (id) => {
    return new Promise(resolve => {
        db.Place.findOne({
            where: {
                id: id
            }
        }).then(place => {
            db.Manefestation.findAll({
                include: [{
                    model: db.Event,
                    where: {
                        placeId: id
                    }
                }]

            }).then(manifestations => {
                const out = Object.assign({}, {
                    id: place.id,
                    name: place.name,
                    address: place.address,
                    latitude: place.latitude,
                    longitude: place.longitude,
                    manifestations: manifestations.reduce((res, manifestation) => {
                        if (manifestation.placeId === place.id || manifestation.Event.length > 0) {
                            res.push({
                                id: manifestation.id,
                                title: manifestation.id,
                                placeId: manifestation.placeId,
                                events: manifestation.Event
                            })
                        }
                        return res;
                    }, [])
                })
            })
            resolve(out);
        })
    })
}

From this, you get all manifestations that assigned to place or have any event that assigns. All included events in the manefestations are assigned to the place.

Edit : You will be able to use the following one too:

const findPlace = (id) => {
    return new Promise(resolve => {
        db.Place.findOne({
            include: [{
                model: db.Manefestation,
                include: [{
                    model: db.Event,
                    where: {
                        placeId: id
                    }
                }]

            }],
            where: {
                id: id
            }
        }).then(place => {
            db.Manefestation.findAll({
                include: [{
                    model: db.Event,
                    where: {
                        placeId: id
                    }
                }],
                where: {
                    placeId: {
                        $not: id
                    }
                }

            }).then(manifestations => {
                place.Manefestation = place.Manefestation.concat(manifestations.filter(m=>m.Event.length>0))
                resolve(place);// or you can rename, reassign keys here
            })
        })
    })
}

Here I take only direct manifestations in the first query. Then, manifestations that not included and concatenate.

I do not know if you figure it out by now. But the solution is provided below. Search with Sequelize could get funny :). You have to include inside another include. If the query gets slow use separate:true.

Place.findAll({
          include: [           
            {
              model: Manifestation,
              attributes: ['id'],
              include: [{ 
              model: Event ,
              attributes: ['id']
               }]
            },
           ],
        })

I tried to complete it in a single query but you will still need JavaScript to be able to get the type of output that you want.

(Note: You need manifestation which is not connected to places but should be included if a event is present of that place. The only SQL way to get that starts by doing a CROSS JOIN between all tables and then filtering out the results which will be a very hefty query)


I came up with this code(tried & executed) which doesn't need you to execute 2 findAll that fetches all data as what you are currently using. Instead it fetched only the data needed for final output in 1 query.

const places = await Place.findAll({
    include: [{
        model: Manifestation,
        // attributes: ['id']
        through: {
            attributes: [], // this helps not get keys/data of join table
        },
    }, {
        model: Event,
        include: [{
            model: Manifestation,
            // attributes: ['id']
        }],
    }
    ],
});
console.log('original output places:', JSON.stringify(places, null, 2));

const result = places.map(p => {
    // destructuring to separate out place, manifestation, event object keys
    const {
        manifestations,
        events,
        ...placeData
    } = p.toJSON();

    // building modified manifestation with events array
    const _manifestations = manifestations.map(m => {
        return ({ ...m, events: [] })
    });

    // going through places->events to push them to respective manifestation events array
    // + add manifestation which is not directly associated to place but event is of that manifestation
    events.map(e => {
        const {
            manifestation: e_manifestation, // renaming variable
            ...eventData
        } = e;
        const mIndex = _manifestations.findIndex(m1 => m1.id === e.manifestationId)
        if (mIndex === -1) { // if manifestation not found add it with the events array
            _manifestations.push({ ...e_manifestation, events: [eventData] });
        } else { // if found push it into events array
            _manifestations[mIndex].events.push(eventData);
        }
    });
    // returning a place object with manifestations array that contains events array
    return ({ ...placeData, manifestations: _manifestations });
})
// filter `.filter(p => p.manifestations.length)` as used in your question
console.log('modified places', JSON.stringify(result, null, 2));
Related