Search code examples
angularjspostgresqlormsails.jsngresource

Angularjs: Many-to-Many with Through Relationship


In my app I have Students who can Enroll in Classes.

An Enrollment records the start and end dates along with a reference to which Class.

The Class has details like a class name and description.

Student (1) - (N) Enrollment (1) - (1) Class

Therefore

Student (1) - (N) Class

This is what I would like the object to look like.

{
  "studentId": 1,
  "name": "Henrietta",
  "enrolledClasses": [
    {
      "enrollmentId": 12,
      "startDate": "2015-08-06T17:43:14.000Z",
      "endDate": null,
      "class": 
        { 
          "classId": 15,
          "name": "Algebra",
          "description": "..."
        }
    },
      "enrollmentId": 13,
      "startDate": "2015-08-06T17:44:14.000Z",
      "endDate": null,
      "class": 
        {
          "classId": 29,
          "name": "Physics",
          "description": "..."
        }
    }
}

But my RESTful API cannot (easily) output the full nested structure of the relationship because it's ORM can't do Many to Many with Through relationships. So I get the student and their multiple enrollments, but only a reference to the class for each, not the details. And I need to show the details, not just an Id.

I could wait for the student object to be available and then use the references to make additional calls to the API for each classId to get details, but am not sure how, or even if, to then integrate it with the student object.

This is what I have so far.

function findOne() {
  vm.student = StudentsResource.get({
    studentId: $stateParams.studentId
  });
};

This is what I get back from my API

{
  "studentId": 1,
  "name": "Henrietta",
  "enrolledClasses": [
    {
      "enrollmentId": 12,
      "startDate": "2015-08-06T17:43:14.000Z",
      "endDate": null,
      "class": 15
    },
      "enrollmentId": 13,
      "startDate": "2015-08-06T17:44:14.000Z",
      "endDate": null,
      "class": 29
    }
}

And for the class details I can them one at a time but haven't figured out how to wait until the student object is available to push them into it. Nor how to properly push them...

{ 
  "classId": 15,
  "name": "Algebra",
  "description": "..."
}

{
  "classId": 29,
  "name": "Physics",
  "description": "..."
}

Question

Is it good practice to combine the student and class(es) details through the enrollments, as one object?

  • If so, whats the most clear way to go about it?
  • If not, what is another effective approach?

Keep in mind that the app will allow changes to the student details and enrollments, but should not allow changes to the details of the class name or description. So if Angular were to send a PUT for the object, it should not try to send the details of any class, only the reference. This would be enforced server side to protect the data, but to avoid errors, the client shouldn't try.

For background, I'm using SailsJS and PostgreSQL for the backend API.


Solution

  • So basically on the Sailsjs side you should override your findOne function in your StudentController.js. This is assuming you are using blueprints.

    // StudentController.js
    module.exports = {
      findOne: function(req, res) {
        Student.findOne(req.param('id')).populate('enrollments').then(function(student){
          var studentClasses = Class.find({
            id: _.pluck(student.enrollments, 'class')
          }).then(function (classes) {
            return classes
          })
          return [student, classes]
        }).spread(function(student, classes){
          var classes = _.indexBy(classes, 'id')
          student.enrollments = _.map(student.enrollments, function(enrollment){
            enrollment.class = classes[enrollment.class]
            return enrollment
          })
          res.json(200, student)
        }).catch(function(err){
          if (err) return res.serverError(err)
        })
      }
    }
    
    // Student.js
    
    module.exports = { 
      attributes: {
        enrollments: {
          collection: 'enrollment',
          via: 'student'
        } 
        // Rest of the attributes..   
      }
    }
    
    // Enrollment.js
    
    module.exports = {
      attributes: {
        student: {
          model: 'student'
        }
        class: {
          model: 'class'
        }
        // Rest of the attributes...
      }
    }
    
    // Class.js
    
    module.exports: {
      attributes: {
        enrollments: {
          collection: 'enrollment',
          via: 'class'
        }
        // Rest of the attrubutes
      }
    }
    

    Explanation:
    1. the _.pluck function using Lo-dash retuns an array of classIds and we find all of the classes that we wanted populated. Then we use the promise chain .spread(), create an array indexed by classid and map the classIds to the actual class instances we wanted populated into the enrollments on the student returned from Student.findOne().
    2. Return the student with enrollments deep populated with the proper class.

    source: Sails.js populate nested associations