How can I easily copy models and related models in Loopback JS

1.5k views Asked by At

I have a loopback-js API.

In it I have a products-model, which is rather complex, with many related models. (metadata, technical requirements, tags, domains, targetAudience, ...)

I'm writing a CMS, and I want the users to be able to copy the products easily, and then just alter some small things, because many of the data is the same for all those products.

Is there an easy way to do this in Loopback JS? E.g product.saveAs();

The only way I see it is to get the data from a product, then delete the id's and insert the data as a new product in the database, and then do the same for all the related models...

1

There are 1 answers

1
Ernie On

Since I couldn't find an easy answer on the web, I've come up with a mixin that can be implemented with a Model. The mixin defines a duplicate method, which duplicates the model by inspecting the model definition, so it's traversing the relation-tree, to duplicate or link related items as well:

mixin file in common/mixins/duplicate.js

var async = require('async');

function Duplicate(Model){

    Model.duplicate = function (id, cb) {
        var self = this;
        var models = Model.app.models;
        var includeRelations = Object.keys(self.definition.settings.relations);

        self.findById(id, {include: includeRelations}, function(err, fromInstance){
            if(err){
                return cb(err);
            }

            var fromData = JSON.parse(JSON.stringify(fromInstance));
            delete fromData.id;
            self.create(fromData, function(err, newInstance){
                if(err){
                    return cb(err);
                }
                var relations = self.definition.settings.relations;
                var operations = [];
                for(var relationName in relations){
                    var relation = relations[relationName];
                    switch(relation.type){
                        case "hasAndBelongsToMany": //add extra links to relation
                            if(relation.foreignKey == "") relation.foreignKey = "id";
                            for(var i = 0; i < fromData[relationName].length; i++){
                                var relatedItem = fromData[relationName][i];
                                operations.push(async.apply(newInstance[relationName].add, relatedItem[relation.foreignKey]));
                            }
                            break;
                        case "hasMany": //create extra items
                            if(relation.through){
                                //don copy many through relations, add an extra has many on the intermediate
                            } else {
                                // copy ze shit, and recursively check if child relations have to be duplicated
                                for(var i = 0; i < fromData[relationName].length; i++) {
                                    var relatedItem = fromData[relationName][i];

                                    operations.push(async.apply(
                                        function(relation, relatedItem, newInstance, cb2){
                                            try {
                                                models[relation.model].duplicate(relatedItem.id, function(err, duplicatedInstance){
                                                    if(err){
                                                        cb2(err);
                                                    }
                                                    var fk = relation.foreignKey || self.definition.name.substr(0, 1).toLowerCase() + self.definition.name.substr(1) + "Id";
                                                    duplicatedInstance.updateAttribute(fk, newInstance.id , cb2);
                                                });
                                            } catch(err){
                                                cb2(err);
                                            }
                                        },
                                        relation, relatedItem, newInstance));
                                }
                            }
                            break;
                        default: //do nothing
                    }
                }

                if(operations.length > 0){
                    async.parallel(operations, function (err, results) {
                        if (err) cb(err);
                        cb(null, newInstance);
                    });
                } else {
                    cb(null, newInstance);
                }
            });
        })
    }
}

module.exports = Duplicate;

Update your model-config:

{
"_meta": {
  "sources": [
    "loopback/common/models",
    "loopback/server/models",
    "../common/models",
    "./models"
  ],
  "mixins": [
    "loopback/common/mixins",
    "../common/mixins"
  ]
},

Define in model where needed, that you want to use the mixin:

...
"acls": [
    {
       "accessType": "*",
        "principalType": "ROLE",
        "principalId": "$everyone",
        "permission": "DENY"
    }
],
"methods": [],
"mixins": {
  "Duplicate": true
}

Use at own risk

It's far from perfect but for now, it's good enough for what I need. Maybe someone else can use this as well.

Currently it copy's the model data itself, (which includes foreign keys of belongsTo relations, and embedded models), hasMany (recursively) and hasToAndBelongsToMany (not-recursively). If you want hasManyThrough functionality, better add an extra hasmany-relation to the 'through-table', that will be duplicated instead.

Things I might add in the future:

  • check if the mixins is called on a valid Loopback Model
  • add option to specify which relations should be included
  • add has-many-through functionality