I have category that can have child categories
And when I'm doing findAll I want to include all of those nested, but I don't know the depth.
var includeCondition = {
include: [
{
model: models.categories,
as:'subcategory', nested: true
}]
};
models.categories.findAll(includeCondition)
.then(function (categories) {
resolve(categories);
})
.catch(function (err) {
reject(err);
})
});
The result brings me only one level nested include.
[
{
dataValues:{
},
subcategory:{
model:{
dataValues:{
}
// no subcategory here
}
}
}
]
Can I somehow make sequalize include those nested subcategories ?
There are few solutions if found for this
first one is more complicated but will give better performance:
This one is about implementing hierarchical data structure in MySQL
I like the guide here
http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/
The one that is named The Nested Set Model.
The second solution that I actually implemented by myself is recursive expanding, this one uses lots of mysql requests and I believe can be improved, but it's a fast one and works well. The thing is to use for each category function like this
var expandSubcategories = function (category) {
return new promise(function (resolve, reject) {
category.getSubcategories().then(function (subcategories) {
//if has subcategories expand recursively inner subcategories
if (subcategories && subcategories.length > 0) {
var expandPromises = [];
_.each(subcategories, function (subcategory) {
expandPromises.push(expandSubcategories(subcategory));
});
promise.all(expandPromises).then(function (expandedCategories) {
category.subcategories = [];
_.each(expandedCategories, function (expandedCategory) {
category.subcategories.push(expandedCategory);
}, this);
//return self with expanded inner
resolve(category);
});
} else {
//if has no subcategories return self
resolve(category);
}
});
});
};
So it's going through the categories and expanding them recursively.
Maybe this will help someone as well.
This is ihoryam's answer adapted to ES6, using async/await, arrow functions () => and Sequelize ORM to fetch the data, and not using Lodash.
const getSubCategoriesRecursive = async (category) => {
let subCategories = await models.category.findAll({
where: {
parentId: category.id
},
raw : true
});
if (subCategories.length > 0) {
const promises = [];
subCategories.forEach(category => {
promises.push(getSubCategoriesRecursive(category));
});
category['subCategories'] = await Promise.all(promises);
}
else category['subCategories'] = [];
return category;
};
Async functions returning promises, you do not need to precise return new promise(...)
There is a node module which handle it : sequelize-hierarchy
It adds column parentId and hierarchyLevel to your table.
As an example, this is what I did to order employees skills in a tree.
Skills could be "Macro" -> "Excel" -> "Office" -> "Computer"
database.js:
const Sequelize = require('sequelize');
require('sequelize-hierarchy')(Sequelize);
const sequelize = new Sequelize("stackoverflow", null, null, {
dialect: "sqlite",
storage: "database.db"
});
sequelize.sync().then(() => {console.log("Database ready");});
module.exports = sequelize;
skill.js:
module.exports = (sequelize, DataTypes) => {
const Skill = sequelize.define("skill", {
name: DataTypes.STRING,
});
Skill.isHierarchy();
return Skill;
};
Then in your controller:
Skill.findAll().then(skills => {
res.send(skills); // Return a list
});
Skill.findAll({ hierarchy: true }).then(skills => {
res.send(skills); // Return a tree
});
Sequelize currently has no support for common table expressions and recursive CTEs. Adding the ability to include a CTE into the find* family of methods would allow find* to perform recursive queries.
Here is the link for examples.
Common Table Expressions and Recursive Queries
Suppose you have 5 different models A, B, C, D, E and A is associated with B, B with C and so on.
So while fetching data for A you can get the all the nested subcategory hierarchy by using
include: [{ all: true, nested: true }]
Example:
A.findAll(where:{// add conditions}, { include: [{ all: true, nested: true }]});
Related
I am trying to write some code that searches through a bunch of objects in a MongoDB database. I want to pull the objects from the database by ID, then those objects have ID references. The program should be searching for a specific ID through this process, first getting object from id, then ids from the object.
async function objectFinder(ID1, ID2, depth, previousList = []) {
let route = []
if (ID1 == ID2) {
return [ID2]
} else {
previousList.push(ID1)
let obj1 = await findObjectByID(ID1)
let connectedID = obj1.connections.concat(obj1.inclusions) //creates array of both references to object and references from object
let mapPromises = connectedID.map(async (id) => {
return findID(id) //async function
})
let fulfilled = await Promise.allSettled(mapPromises)
let list = fulfilled.map((object) => {
return object.value.main, object.value.included
})
list = list.filter(id => !previousList.includes(id))
for (id of list) {
await objectFinder(id, ID2, depth - 1, previousList).then(result => {
route = [ID1].concat(result)
if (route[route.length - 1] == ID2) {
return route
}})
}
}
if (route[route.length - 1] == ID2) {
return route
}
}
I am not sure how to make it so that my code works like a tree search, with each object and ID being a node.
I didn't look too much into your code as I strongly believe in letting your database do the work for you if possible.
In this case Mongo has the $graphLookup aggregation stage, which allows recursive lookups. here is a quick example on how to use it:
db.collection.aggregate([
{
$match: {
_id: 1,
}
},
{
"$graphLookup": {
"from": "collection",
"startWith": "$inclusions",
"connectFromField": "inclusions",
"connectToField": "_id",
"as": "matches",
}
},
{
//the rest of the pipeline is just to restore the original structure you don't need this
$addFields: {
matches: {
"$concatArrays": [
[
{
_id: "$_id",
inclusions: "$inclusions"
}
],
"$matches"
]
}
}
},
{
$unwind: "$matches"
},
{
"$replaceRoot": {
"newRoot": "$matches"
}
}
])
Mongo Playground
If for whatever reason you want to keep this in code then I would take a look at your for loop:
for (id of list) {
await objectFinder(id, ID2, depth - 1, previousList).then(result => {
route = [ID1].concat(result);
if (route[route.length - 1] == ID2) {
return route;
}
});
}
Just from a quick glance I can tell you're executing this:
route = [ID1].concat(result);
Many times at the same level. Additional I could not understand your bottom return statements, I feel like there might be an issue there.
When i fetch new alerts, i want to check if the ID of the new alert was already recorded. The issue is that that ID is nested inside an array. There's the alertsDetails array, which contains objects and those objects have an _ID filed which is what i want to check. I am not sure how to achieve that. I got the code below but then i have to iterate over the result to check the exists value. Im sure there must be a better way.
const mongoose = require('mongoose');
const { Schema } = mongoose;
const G2AlertsSchema = new Schema(
{
status: { type: String, required: true },
openDate: { type: Date, required: true },
alertType: { type: Array, required: true },
severity: { type: Array, required: true },
locationName: { type: Array, required: true },
history: { type: Array, required: true },
alertDetails: { type: Array, required: false },
assignedTo: { type: Schema.Types.ObjectId, ref: 'user' },
},
{
timestamps: true,
},
);
const G2Alerts = mongoose.model('G2Alert', G2AlertsSchema);
module.exports = G2Alerts;
This is the code i found on mongodb's website. I just want to see if the ID exists only. Basically when i fetch the new alerts i get an array and i iterate over it, i want to check each item's ID against what's inside the Database. If it's there, skip and go to the next. If it's new, then create a new alert and save it.
const exists = await G2Alerts.aggregate([
{
$project: {
exists: {
$in: ['5f0b4f508bda3805754ab343', '$alertDetails._id'],
},
},
},
]);
EDIT: Another thing. I am getting a eslint warning saying i should use array iteration instead of a for loop. The issue is, i need to use await when looking up the Alert ID. If i use, reduce or filter, i can't use await. If i use async inside the reduce or filter function, then it will return promises in or just an empty array.
This below works, based on the answer provided by Tom Slabbaert
const newAlertsData = [];
for (let item of alertData.data.items) {
const exists = await G2Alerts.find({ 'alertDetails._id': `${item._id}` });
if (exists.length === 0) {
newAlertsData.push(item);
}
}
if (newAlertsData.length !== 0) {......
But this does not
const filteredAlerts = alertData.data.items.reduce((filtered, item) => {
const exists = await G2Alerts.find({ 'alertDetails._id': `${item._id}` });
if (exists.length === 0) {
filtered.push(item);
}
return filtered;
}, []);
You're not far off, here is an example using the correct syntax:
const exists = await G2Alerts.findOne({"alertDetails._id": '5f0b4f508bda3805754ab343'}});
if (!exists) {
... do something
}
This can also be achieve using aggregate with a $match stage instead of a $project stage or even better countDocuments which just returns the count instead of the entire object if you do not require it.
One more thing I'd like to add is that make sure alertDetails._id is string type as you're using string in you're $in. otherwise you'll need to cast them to ObjectId type in mongoose like so:
new mongoose.Types.ObjectId('5f0b4f508bda3805754ab343')
And for Mongo:
import {ObjectId} from "mongodb"
...
new ObjectId('5f0b4f508bda3805754ab343')
EDIT
Try something like this?
let ids = alertData.data.items.map(item => item._id.toString());
let existing = await G2Alerts.distinct("alertsDetails._id", {"alertsDetails._id": {$in: ids}});
const filteredAlerts = alertData.data.items.reduce((filtered, item) => {
if (!existing.includes(item._id.toString())) {
return [item].concat(filtered)
}
return filtered;
}, []);
This way you only need to call the db once and not multiple times.
Final code based on the provided answer.
const ids = alertData.data.items.map(item => item._id);
const existing = await G2Alerts.find({ 'alertDetails._id': { $in: ids } }).distinct(
'alertDetails._id',
(err, alerts) => {
if (err) {
res.send(err);
}
return alerts;
},
);
const filteredAlerts = alertData.data.items.reduce((filtered, item) => {
if (!existing.includes(item._id.toString()) && item.openDate > dateLimit) {
return [item].concat(filtered);
}
return filtered;
}, []);
I am expecting the below promise to return customer and blox slot also as part of booking record in second .then(). But addCustomer and addBooking has not executed or yet to be executed.
when I added await in addBooking or addCustomer, it didnt work.
I am not sure where i am missing
const bookingCreated = await Booking.create(data).then((booking) => {
const customers = data.customer_id;
if (data.customer_id !== '') {
customers.forEach((customer) => booking.addCustomer(customer, booking.id));
}
tempBoxSlots.forEach((slot) => boxSlotBooking.addBooking(slot, booking.id));
return booking;
}).then((result) => {
console.log('result');
console.log(result.id);
const boxSlotAttributes = ['id', 'start_time', 'duration'];
const retBooking = Booking.findOne({
where: {
id: result.id
},
include: [{
model: BookingType
},
{
model: BookingSource
},
{
model: Venue
},
{
model: Customer,
as: 'lead'
},
{
model: BoxSlot,
attributes: boxSlotAttributes,
through: {
attributes: []
}
},
{
model: Customer,
attributes: ['id', 'firstname', 'lastname', 'email'],
through: {
attributes: []
}
}
]
});
return retBooking;
}).catch((err) => {
console.log(err);
});
console.log('bookingCreated');
console.log(bookingCreated);
According to the examples given in this sequalize documentation, the methods that are automatically added when you define associations, like add*, return promises.
So instead of the forEach loops with addCustomer and addBooking, you would need to collect those promises (with .map) and pass those to Promise.all:
await Promise.all([...customers.map((customer) => booking.addCustomer(customer, booking.id)),
...tempBoxSlots.map((slot) => boxSlotBooking.addBooking(slot, booking.id))]);
I would also replace all the then calls you have, with await. Go for one pattern: either then chaining, or async/await. But for readability, avoid the mix.
Another remark: the check if (customer_id == '') is a bit odd, because customer_id is an array, and so checking for an non-empty array is better done with if (customer_id.length).
Moreover, there is no reason to exclude an empty array from executing what is in that if block, so you can actually do without that if condition all together.
When at the top of my server-side code, this works fine and the results produced are correct:
var data_playlists = {};
models.Playlist.findAll({
attributes: ['id', 'name']
}).then(function (playlists){
data_playlists['playlists'] = playlists.map(function(playlist){
return playlist.get({plain: true})
});
addsongs(data_playlists, 1);
addsongs(data_playlists, 2);
addsongs(data_playlists, 3);
});
but when it's inside one of my Express methods, it isn't functioning properly; particularly, the addsongs method is not working as it should.
function addsongs(playlist_object, id_entered){
var arraysongs = [];
models.Playlist.findOne({
attributes: ['id'],
where: {
id: id_entered
}
})
.then(function(playlist) {
playlist.getSongs().then(function (thesongs){
for(var k = 0; k < thesongs.length ; k++){
arraysongs.push(thesongs[k].Songs_Playlists.SongId);
}
playlist_object.playlists[(id_entered - 1)]['songs'] = arraysongs;
});
});
}
I cannot for the life of me figure out why it works when the top segment of code is at the top, but doesn't work when inside my app.get() call.
From your code I have conducted that you want to return playlists (id and name) together with their songs (id). First of all your code will not work because the calls of addsongs(data_playlists, id) are run before data_playlists is filled with data by code above it. Moreover, the addsongs function performs asynchronous operations returning Promises, so calling them one by one will not give expected result. I suppose you can do it completely differently.
I suggest you use include attribute of options object that can be passed to findAll() method. include says which association model you also want to return from current query. In this case you want to return playlists together with their songs (M:M relation according to your code), so you need to include Song model in the query.
function getPlaylistsWithSongs() {
return models.Playlist.findAll({
attributes: ['id', 'name'],
include: [
{
model: models.Song,
as: 'Songs', // depends on how you have declare the association between songs and playlists
attributes: ['id'],
through: { attributes: [] } // prevents returning fields from join table
}
]
}).then((playlistsWithSongs) => {
return playlistsWithSongs;
});
}
Example result of getPlaylistsWithSongs result would be (after translating it to JSON e.g. like playlistsWithSongs.toJSON())
[
{
id: 1,
name: 'playlist #1',
Songs: [
{ id: 1 },
{ id: 2 }
]
}
]
Above code returns all playlists (their id and name) with their songs (only their id). Now in your route resolver you can simply call above function to return the result
app.get('/api/playlists', function (request, response) {
response.setHeader("Content-Type", "application/json; charset=UTF-8");
getPlaylistsWithSongs().then(function(playlistsWithSongs){
response.status(200).send(JSON.stringify(playlistsWithSongs));
});
});
EDIT
In order to simply return array of IDs instead array of objects with id (songs), you need to map the result. There is no simple sequelize way to return array of IDs in such a case.
}).then((playlistWithSongs) => {
let jsonPlaylists = playlistsWithSongs.map((singlePlaylist) => {
// return JSON representation of each playlist record
return singlePlaylist.toJSON();
});
jsonPlaylists.forEach((playlist) => {
// at every playlist record we map Songs to array of primitive numbers representing it's IDs
playlist.songs = playlist.Songs.map((song) => {
return song.id;
});
// when we finish we can delete the Songs property because now we have songs instead
delete playlist.Songs;
});
console.log(jsonPlaylists);
// example output: [{ id: 1, name: 'playlist #1', songs: [1, 2, 3] }]
return jsonPlaylists;
});
I'm currently using Dexie.js to store data locally. I have 3 different tables, that are joined with each other by using foreign keys. I managed to setup the schema and insert the corresponding data. However, when I want to retrieve the data, I failed to find an example of how to join different tables.
Here's an example:
var db = new Dexie('my-testing-db');
db.delete().then(function() {
db.version(1).stores({
genres: '++id,name',
albums: '++id,name,year,*tracks',
bands: '++id,name,*albumsId,genreId'
});
db.transaction('rw', db.genres, db.albums, db.bands, function() {
var rock = db.genres.add({
name: 'rock'
}),
jazz = db.genres.add({
name: 'jazz'
});
var justLookAround = db.albums.add({
name: 'Just Look Around',
year: 1992,
tracks: [
'We want the truth', 'Locomotive', 'Shut me out'
]
});
var sickOfItAll = db.bands.add({
name: 'Sick Of it All'
});
justLookAround.then(function(album_id) {
rock.then(function(rock_id) {
sickOfItAll.then(function(band_id) {
db.bands.update(band_id, {
genreId: rock_id,
albumsId: [album_id]
}).then(function(updated) {
});
});
});
});
}).then(function() {
//how to join the tables here????
db.bands.each(function(band) {
console.log(band);
});
});
});
Unfortunately, I arrived here from google looking for actual joins, you know, something of the kind:
db.bands.where(...).equals(..).join(
db.genres.where(...).etc(), 'genreId -> genres.id').then(
function(band, genre) { ... });
This, I think is closer to what the original questioner asked, but based on the the answer provided by #david-fahlander, it seems that this plugin, https://github.com/ignasbernotas/dexie-relationships, might be a bit easier, if you're looking to build a nice object tree.
The readme of the plugin is very similar to your example, so I've copied it verbatim here:
Schema
Note the use of -> which sets the foreign keys.
import Dexie from 'dexie'
import relationships from 'dexie-relationships'
var db = new Dexie('MusicBands', {addons: [relationships]})
db.version(1).stores({
genres: 'id, name',
bands: 'id, name, genreId -> genres.id',
albums: 'id, name, bandId -> bands.id, year'
});
Usage
db.bands
.where('name').startsWithAnyOf('A', 'B') // can be replaced with your custom query
.with({albums: 'albums', genre: 'genreId'}) // makes referred items included
.then(bands => {
// Let's print the result:
bands.forEach (band => {
console.log (`Band Name: ${band.name}`)
console.log (`Genre: ${band.genre.name}`)
console.log (`Albums: ${JSON.stringify(band.albums, null, 4)}`)
});
})
Here's how to join the result. Disclaimer: code not tested!
var all = Dexie.Promise.all;
function joinBands (bandCollection) {
// Start by getting all bands as an array of band objects
return bandCollection.toArray(function(bands) {
// Query related properties:
var genresPromises = bands.map(function (band) {
return db.genres.get(band.genreId || 0);
});
var albumsPromises = bands.map(function (band) {
return db.albums.where('id').anyOf(band.albumsId || []).toArray();
});
// Await genres and albums queries:
return all ([
all(genresPromises),
all(albumsPromises)ยจ
]).then(function (genresAndAlbums) {
// Now we have all foreign keys resolved and
// we can put the results onto the bands array
// before returning it:
bands.forEach(function (band, i) {
band.genre = genresAndAlbums[0][i];
band.albums = genresAndAlbums[1][i];
});
return bands;
});
});
}
// Join all:
joinBands(db.bands.toCollection()).then(function (bands) {
alert ("All bands: " + JSON.stringify(bands, null, 4));
}).catch(function (error) {
alert ("Oops: " + error);
});
// Query and join:
joinBands(db.bands.where('genreId').anyOf([1,5,19]).limit(25)).then(function (bands) {
alert ("Some bands: " + JSON.stringify(bands, null, 4));
}).catch (function (error) {
alert ("Oops: " + error);
});
Preferably call joinBands() from within a transaction to speed the queries up as well as getting a more reliable and atomic result.