Search code examples
ember.jsember-datajson-api

JSONAPI serialize nested hasMany relationships


We're using JSONAPI for this project, but for [reasons] we can't handle its recommended relationship structure in the API, so we're serving and expecting them as nested objects instead, with the following format:

{
  "data":{
    "type":"video",
    "id":"55532284a1f9f909b0d11d73",

    "attributes":{
      "title":"Test",

      "transcriptions":{
        "type": "embedded",

        "data":[
          {
            "type":"transcription",
            "id":"203dee25-4431-42d1-a0ba-b26ea6938e75",

            "attributes":{
              "transcriptText":"Some transcription text here. And another sentence after it.",

              "cuepoints":{
                "type":"embedded",

                "data":[
                  {
                    "type":"cuepoint",
                    "id":"bb6b0434-bdc4-43e4-8010-66bdef5c432a",

                    "attributes":{
                      "text":"Some transcription text here."
                    }
                  },
                  {
                    "type":"cuepoint",
                    "id":"b663ee00-0ebc-4cf4-96fc-04d904bc1baf",

                    "attributes":{
                      "text":"And another sentence after it."
                    }
                  }
                ]
              }
            }
          }
        ]
      }
    }
  }
}

I have the following model structure:

// models/video
export default DS.Model.extend({
  transcriptions: DS.hasMany('transcription')
)};

// models/transcription
export default DS.Model.extend({
  video: DS.belongsTo('video'),
  cuepoints: DS.hasMany('cuepoint')
});

// models/cuepoint
export default DS.Model.extend({
  transcription: DS.belongsTo('transcription')
);

Now, what we want to do is save a video record, and have it serialize the transcriptions and cuepoints it contains. I have the following serializer, and it works absolutely fine for embedding a transcription into a video ie. one level, but I need it to then embed the cuepoints into that too.

export default DS.JSONAPISerializer.extend({
    serializeHasMany: function(record, json, relationship) {
      var hasManyRecords, key;
          key = relationship.key;
          hasManyRecords = Ember.get(record, key);

      if (hasManyRecords) {
        json.attributes[key] = {};

        hasManyRecords.forEach(function(item) {
          json.attributes[key].data = json.attributes[key].data || [];

          json.attributes[key].data.push({
            attributes: item._attributes,
            id: item.get('id'),
            type: item.get('type')
          });
        });
      } else {
        this._super(record, json, relationship);
      }
    }
  });

Inspecting the record, json and relationship properties in the serializeHasMany method, I can't see anything about the nested relationships, so not even sure I'm using the right method.

Any ideas where I'm going wrong with this?


Solution

  • I think I've figured this out. There were a few methods I didn't know existed for looping through relationships, and I needed to write a custom serialize method, instead of just overriding the default serializeHasMany one.

    serialize(record) {
      // Set up the main data structure for the record to be serialized
      var JSON = {
        data: {
          id: record.id,
          type: record.modelName,
          attributes: {}
        }
      };
    
      // Find relationships in the record and serialize them into the JSON.data.attributes object
      JSON.data.attributes = this.serializeRelationships(JSON.data.attributes, record);
    
      // Loop through the record's attributes and insert them into the JSON.data.attributes object
      record.eachAttribute((attr) => {
        JSON.data.attributes[attr] = record.attr(attr);
      });
    
      // Return the fully serialized JSON data
      return JSON;
    },
    
    // Take a parent JSON object and an individual record, loops through any relationships in the record, and creates a JSONAPI resource object
    serializeRelationships(JSON, record) {
      record.eachRelationship((key, relationship) => {
        if (relationship.kind === 'hasMany') {
    
          // Set up the relationship data structure
          JSON[relationship.key] = {
            data: []
          };
    
          // Gran any relationships in the record
          var embeddedRecords = record.hasMany(relationship.key);
    
          // Loop through the relationship's records and build a resource object
          if (embeddedRecords) {
            embeddedRecords.forEach((embeddedRecord) => {
              var obj = {
                id: embeddedRecord.id,
                type: embeddedRecord.modelName,
                attributes: {}
              }
    
              // Recursively check for relationships in the record
              obj.attributes = this.serializeRelationships(obj.attributes, embeddedRecord);
    
              // Loop through the standard attributes and populate the record.data.attributes object
              embeddedRecord.eachAttribute((attr) => {
                obj.attributes[attr] = embeddedRecord.attr(attr);
              });
    
              JSON[relationship.key].data.push(obj);
            });
          }
        }
      });
    
      return JSON;
    }