Reputation: 1536
import Student from './api/user/student.model';
import Class from './api/user/class.model';
import User from './api/user/user.model';
import request from 'request';
import async from 'async';
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter;
function seedDb() {
Student.remove({}, (err, data) => {
Class.remove({}, (err, data1) => {
User.remove({}, (err, data2) => {
User.create({
email: '[email protected]',
password: 'admin'
}, (err, data3) => {
User.findOne({
email: '[email protected]'
}, (err, foundUser) => {
foundUser.save((err, done) => {
let url = 'https://gist.githubusercontent.com/relentless-coder/b7d74a9726bff8b281ace936757953e5/raw/6af59b527e07ad3a589625143fb314bad21d4f8e/dummydata.json';
let options = {
url: url,
json: true
}
request(options, (err, res, body) => {
if (!err && body) {
let classId;
async.forEachOf(body, (el, i) => {
console.log(i);
if (i % 4 === 0) {
async.waterfall([(fn) => {
Student.create(el, (err, success) => {
fn(null, success._id);
})
}, (id, fn) => {
console.log('The class creation function is called');
Class.create({name: `Class ${i}`}, (err, newClass)=>{
classId = newClass._id;
newClass.students.push(id);
newClass.save()
fn(null, 'done');
})
}])
} else {
async.waterfall([(fn) => {
Student.create(el, (err, success) => {
fn(null, success._id)
})
}, (id, fn) => {
console.log('Class find function is called and classId', id, classId)
Class.findById(classId, (err, foundClass) => {
console.log(foundClass)
foundClass.students.push(id);
foundClass.save();
fn(null, 'done');
})
}])
}
})
}
});
})
});
})
})
})
})
}
What I am trying to do is that, I have a sample data of 20 students. I have to seed my database with those 20 students distributed among 5 classes with each class containing 4 students.
What's preventing me from achieving this is for some of the iterations, the value of classId
is undefined
.
Can anyone help me with this?
Upvotes: 0
Views: 603
Reputation: 151092
It's not completely clear what you are after without seeing the actual schema and expected results, but I can come to a near facsimile that hopefully you can follow and learn from.
As noted in the comment, there are several wrong things as well as outmoded concepts in what you are presently doing. But as the general objective to parsing the input url content and seeding it to the database then I would instead suggest an approach like this:
const request = require('request-promise-native'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const input = 'https://gist.githubusercontent.com/relentless-coder/b7d74a9726bff8b281ace936757953e5/raw/6af59b527e07ad3a589625143fb314bad21d4f8e/dummydata.json';
const uri = 'mongodb://localhost/school',
options = { useMongoClient: true };
const studentSchema = new Schema({
"student_id": { type: Number, unique: true },
"student_name": String,
"student_email": String,
"neuroticism": Number,
"extraversion": Number,
"openness_to_experience": Number
});
const classSchema = new Schema({
"course_number": { type: Number, unique: true },
"course_name": String,
"teacher_name": String,
"teacher_number": Number,
"students": [{ type: Schema.Types.ObjectId, ref: 'Student' }]
});
const Student = mongoose.model('Student', studentSchema);
const Class = mongoose.model('Class', classSchema);
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
function extractPaths(model) {
return Object.keys(model.schema.paths).filter( p =>
['_id','__v'].indexOf(p) === -1
);
}
(async function() {
try {
console.log('starting');
const conn = await mongoose.connect(uri,options);
console.log('connected');
// Clean models
await Promise.all(
Object.keys(conn.models) // <-- is the same thing
//['Student','Class','User']
.map( m => conn.models[m].remove({}) )
);
console.log('cleaned');
let response = await request({ uri: input, json: true });
for (let res of response) {
let student = extractPaths(Student)
.reduce((acc,curr) => Object.assign(acc,{ [curr]: res[curr] }),{});
log(student);
let sclass = extractPaths(Class).filter(p => p !== 'students')
.reduce((acc,curr) => Object.assign(acc,{ [curr]: res[curr] }),{});
log(sclass);
let fstudent = await Student.findOneAndUpdate(
{ student_id: student.student_id },
student,
{ new: true, upsert: true }
);
let fclass = await Class.findOneAndUpdate(
{ course_number: sclass.course_number },
{
$setOnInsert: sclass,
$addToSet: { 'students': fstudent._id }
},
{ new: true, upsert: true }
);
}
} catch(e) {
console.error(e)
} finally {
mongoose.disconnect()
}
})();
There are a few vastly different concepts in here to take note of, and largely is the use of async/await
syntax, since we can in modern nodejs releases and it makes the code a lot cleaner.
The first other big departure from your present code is in the handling of the .remove()
statements cleaning the data. So instead of chaining callbacks, now we just let Promise.all
tell us when all actions are completed:
// Clean models
await Promise.all(
Object.keys(conn.models) // <-- is the same thing
//['Student','Class','User']
.map( m => conn.models[m].remove({}) )
);
The next big departure is using a regular for of
loop, because again we are going to await
the response from any async operations contained within. We might get a bit fancier and do a similar Promise.all
to run multiple iterations in parallel, but this will do as an example.
Since we await
we then make each call that actually writes to the database separately where we can use the response data to feed to the next call. So the response from creating a Student
can be fed later into Class
.
And the other thing we really change there is both in how we extract the data from the feed to create each object and also how we actually do the updates.
This instead uses .findOneAndUpdate()
and issues "upserts" where we essentially "look for" the current document by a primary key, and where it does not exist we "create" a new one, or otherwise where it is found then we simply "update" with new information.
This is mostly demonstrated with the Class
model where we $addToSet
on the "students"
array with the supplied student _id
value as an "update", and where it would not actually create the same student twice. Both because the Student
would not be duplicated when processing based on their own student_id
value, nor would $addToSet
allow the reference to the Student
to be inserted more than once into the Class
object with the "students"
array.
The final output of the courses
collection with the referenced students for each is then:
{
"_id" : ObjectId("5968597490aa0ed4e5db1c92"),
"course_number" : 101,
"teacher_number" : 539224,
"teacher_name" : "Merideth Merrill",
"course_name" : "Physics 1",
"__v" : 0,
"students" : [
ObjectId("5968597490aa0ed4e5db1c90"),
ObjectId("5968597490aa0ed4e5db1c94"),
ObjectId("5968597490aa0ed4e5db1c97"),
ObjectId("5968597490aa0ed4e5db1c9a"),
ObjectId("5968597490aa0ed4e5db1c9d"),
ObjectId("5968597490aa0ed4e5db1ca0"),
ObjectId("5968597490aa0ed4e5db1ca3"),
ObjectId("5968597490aa0ed4e5db1ca6"),
ObjectId("5968597490aa0ed4e5db1ca9"),
ObjectId("5968597490aa0ed4e5db1cac")
]
}
{
"_id" : ObjectId("5968597490aa0ed4e5db1cb1"),
"course_number" : 102,
"teacher_number" : 539224,
"teacher_name" : "Merideth Merrill",
"course_name" : "AP Physics C",
"__v" : 0,
"students" : [
ObjectId("5968597490aa0ed4e5db1caf"),
ObjectId("5968597490aa0ed4e5db1cb3"),
ObjectId("5968597490aa0ed4e5db1cb6"),
ObjectId("5968597490aa0ed4e5db1cb9"),
ObjectId("5968597490aa0ed4e5db1cbc")
]
}
{
"_id" : ObjectId("5968597590aa0ed4e5db1cc1"),
"course_number" : 103,
"teacher_number" : 731037,
"teacher_name" : "Kelly Boyd",
"course_name" : "English 11",
"__v" : 0,
"students" : [
ObjectId("5968597590aa0ed4e5db1cbf"),
ObjectId("5968597590aa0ed4e5db1cc3"),
ObjectId("5968597590aa0ed4e5db1cc6"),
ObjectId("5968597590aa0ed4e5db1cc9"),
ObjectId("5968597590aa0ed4e5db1ccc")
]
}
And of course all of the Student
entries from the source are included and populated to their own collection as well.
So we generally modernize a few techniques here, and the result is much cleaner code that is simple to follow the logic in, and we also reduce a lot of "back and forth" communication with the database by simply using the most effective methods to actually write and read the data in place. Which is the .findOneAndUpdate()
when processing each item.
Of course it's not 100% of what you are trying to implement, but it should at least show how to do it more effectively in a way that you can follow, and learn from.
If after going through all of that you are still insistent on using async.js, then this listing corrects the usage:
const async = require('async'),
request = require('request'),
mongoose = require('mongoose'),
Schema = mongoose.Schema;
mongoose.Promise = global.Promise;
mongoose.set('debug',true);
const input = 'https://gist.githubusercontent.com/relentless-coder/b7d74a9726bff8b281ace936757953e5/raw/6af59b527e07ad3a589625143fb314bad21d4f8e/dummydata.json';
const uri = 'mongodb://localhost/school',
options = { useMongoClient: true };
const studentSchema = new Schema({
"student_id": { type: Number, unique: true },
"student_name": String,
"student_email": String,
"neuroticism": Number,
"extraversion": Number,
"openness_to_experience": Number
});
const classSchema = new Schema({
"course_number": { type: Number, unique: true },
"course_name": String,
"teacher_name": String,
"teacher_number": Number,
"students": [{ type: Schema.Types.ObjectId, ref: 'Student' }]
});
const Student = mongoose.model('Student', studentSchema);
const Class = mongoose.model('Class', classSchema);
function log(data) {
console.log(JSON.stringify(data,undefined,2))
}
function extractPaths(model) {
return Object.keys(model.schema.paths).filter( p =>
['_id','__v'].indexOf(p) === -1
);
}
async.series(
[
(callback) => mongoose.connect(uri,options,callback),
// Clean data
(callback) =>
async.each(mongoose.models,(model,callback) =>
model.remove({},callback),callback),
(callback) =>
async.waterfall(
[
(callback) => request({ uri: input, json: true },
(err,res) => callback(err,res)),
(response,callback) =>
async.eachSeries(response.body,(res,callback) =>
async.waterfall(
[
(callback) => {
let student = extractPaths(Student)
.reduce((acc,curr) =>
Object.assign(acc,{ [curr]: res[curr] }),
{}
);
log(student);
Student.findOneAndUpdate(
{ student_id: student.student_id },
student,
{ new: true, upsert: true },
callback
);
},
(student,callback) => {
console.log(student);
let sclass = extractPaths(Class)
.filter(p => p !== 'students')
.reduce((acc,curr) =>
Object.assign(acc,{ [curr]: res[curr] }),
{}
);
log(sclass);
Class.findOneAndUpdate(
{ course_number: sclass.course_number },
{
$setOnInsert: sclass,
$addToSet: { 'students': student._id }
},
{ new: true, upsert: true },
callback
);
}
],
callback
),
callback
)
],
callback
)
],
(err) => {
if (err) throw err;
mongoose.disconnect();
}
)
Upvotes: 3
Reputation: 11786
What I am trying to do is that, I have a sample data of 20 students. I have to seed my database with those 20 students distributed among 5 classes with each class containing 4 students.
As Neil suggested, we can eliminate few callbacks and loops. The idea is to separate code based on the functionalities. Following is the approach I took to solve the problem with the available information.
'use strict';
let _ = require('lodash');
const BATCH_COUNT = 4;
function seedDb() {
clearAll().then(() => {
return Promisea.all([
createSuperAdmin(),
fetchStudentDetails()
]);
}).then((result) => {
let user = result[0];
let body = result[1];
return createStudents(body);
}).then((users) => {
let studentsBatch = groupByIds(_.map(users, '_id'), BATCH_COUNT);
return addStudentsToClass(studentsBatch);
}).then(() => {
console.log('Success');
}).catch((err) => {
console.log('err', err.stack);
});
}
function addStudentsToClass(batches) {
let bulk = Class.collection.initializeOrderedBulkOp();
for (let i = 0; i < _.size(batches); i++) {
bulk.insert({
name: `Class ${i}`,
students: batches[i]
});
}
return bulk.execute();
}
function createStudents(users) {
let bulk = Student.collection.initializeOrderedBulkOp();
_.each(users, (user) => {
bulk.insert(user);
});
return bulk.execute();
}
function createSuperAdmin() {
return User.findOneAndUpdate({
email: '[email protected]',
password: 'admin'
}, {}, {
new: true,
upsert: true
});
}
function groupByIds(ids, count) {
let batch = [];
let index = 0;
while (index < ids.length) {
let endIndex = index + count;
batch.push(ids.slice(index, endIndex));
index = endIndex;
}
return batch;
}
function fetchStudentDetails() {
let options = {
url: 'https://data.json', // Your URL
json: true
};
return new Promise((resolve, reject) => {
request(options, (err, res, body) => {
if (err) {
return reject(err);
}
return resolve(body);
});
});
}
function clearAll() {
return Promise.all([
Student.remove({}).exec(),
Class.remove({}).exec(),
User.remove({}).exec()
]);
}
Upvotes: 1