Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Breeze create entity from existing one

I've been pounding my head for a few days now.

Imagine you have a car sales management application. You sell different models. Your Car model has 50 properties. Just for the example, let's say that you want to sell Bugatti Veyron. Now, you just received 5 of those cars. So, I log in to my application, create first Bugatti Veyron with specific ID. Then I want to add 2nd one, but there is a problem - I would have to write down all of those properties again! I'd like to have a Copy button and I'd just change serial number, breeze would change ID and voila, two cars in there!

For hack sake, at first I created this solution:

newCar(datacontext.createCar());
newCar().property1(oldCar().property1());
newCar().property2(oldCar().property2());
...

it was ugly, and after I proved I can do it, of course, request for application was to make everything copyable - no way I would do that! There must be a copy somewhere. After digging a lot of things, even trying to change some stuff in breeze itself, I couldn't do something like:

manager.createEntity('Car', oldCar);

Now, latest solution is a bit more viable than first one, but still requires more code than I would want and is not as intuitive as it could be:

        var newObject = {};
        manager.metadataStore._structuralTypeMap[oldCar.entityType.name].dataProperties.forEach(function (singleProperty) {
                if (!singleProperty.isPartOfKey)
                newObject[singleProperty.name] = oldCar[singleProperty.name];
            });
        var newCar = manager.createEntity('Equipment', newObject);

Is there any other "cleaner" way of making a new Entity with exactly the same properties, but of course, different id?

I should mention that Car entity has some ICollections in it, but this hack-ish solution ignores them which could be improved, but currently I handle that myself with a few .forEach loops.

like image 731
Dominictus Avatar asked Dec 15 '22 03:12

Dominictus


2 Answers

We're working on such a thing in the back room. We'll let you know when its ready. No promises or timing.

Meanwhile, I took a crack at it. I decided to leverage the fact that the Breeze EntityManager.exportEntities method knows how to clone an entity. If you read the breeze source code for that method, you know it's tricky.

This is what I came up with (as a civilian, not a Breeze developer):

function cloneItem(item) {
    // export w/o metadata and then parse the exported string.
    var exported = JSON.parse(manager.exportEntities([item], false));
    // extract the entity from the export
    var type = item.entityType;
    var copy = exported.entityGroupMap[type.name].entities[0];
    // remove the entityAspect
    delete copy.entityAspect; 
    // remove the key properties
    type.keyProperties.forEach(function (p) { delete copy[p.name]; });

    // the "copy" provides the initial values for the create
    return manager.createEntity(type, copy);
}

Like yours, it preserves the foreign key properties which means that a reference navigation property to a parent entity will have a value drawn from cache if the source had such a value.

Like yours, the collection navigation properties won't be populated. This method doesn't know how to clone the children. Nor is it self-evident that it should. That's extra credit for you.

Update 15 Dec 2013

Because you asked, I've re-implemented with ability to clone children (collection navigations). I've followed the syntax in you suggested so the usage would be:

cloneItem(something, ['collectionProp1', 'collectionProp2']); 

Note that I'm again relying on Breeze export to do the heavy lifting

Warning: this code is extremely fragile and not generalizable to all models

function cloneItem(item, collectionNames) {
    var manager = item.entityAspect.entityManager;
    // export w/o metadata and then parse the exported string.
    var exported = JSON.parse(manager.exportEntities([item], false));
    // extract the entity from the export
    var type = item.entityType;
    var copy = exported.entityGroupMap[type.name].entities[0];
    // remove the entityAspect (todo: remove complexAspect from nested complex types)
    delete copy.entityAspect;
    // remove the key properties (assumes key is store-generated)
    type.keyProperties.forEach(function (p) { delete copy[p.name]; });

    // the "copy" provides the initial values for the create
    var newItem = manager.createEntity(type, copy);

    if (collectionNames && collectionNames.length) {
        // can only handle parent w/ single PK values
        var parentKeyValue = newItem.entityAspect.getKey().values[0];
        collectionNames.forEach(copyChildren);
    }
    return newItem;

    function copyChildren(navPropName) {
        // todo: add much more error handling
        var navProp = type.getNavigationProperty(navPropName);
        if (navProp.isScalar) return; // only copies collection navigations. Todo: should it throw?

        // This method only copies children (dependent entities), not a related parent
        // Child (dependent) navigations have inverse FK names, not FK names
        var fk = navProp.invForeignKeyNames[0]; // can only handle child w/ single FK value
        if (!fk) return; 

        // Breeze `getProperty` gets values for all model libraries, e.g. both KO and Angular
        var children = item.getProperty(navPropName);
        if (children.length === 0) return;

        // Copy all children
        var childType = navProp.entityType;
        children = JSON.parse(manager.exportEntities(children, false));
        var copies = children.entityGroupMap[childType.name].entities;

        copies.forEach(function(c) {
            delete c.entityAspect;
            // remove key properties (assumes keys are store generated)
            childType.keyProperties.forEach(function (p) { delete c[p.name]; }); 
            // set the FK parent of the copy to the new item's PK               
            c[fk] = parentKeyValue;
            // merely creating them will cause Breeze to add them to the parent
            manager.createEntity(childType, c);
        });
    }
like image 117
Ward Avatar answered Dec 17 '22 17:12

Ward


Took Wards answer and extended it to allow for deep property linking. example usage is

cloneEntity(someEntity, ['collectionProp1.subCollection.another', 'collectionProp2']); 

Note, this is still quite untested and only applicable for certain models.

function cloneEntity(item, collectionNames) {
    var manager = item.entityAspect.entityManager;

    // export w/o metadata and then parse the exported string.
    var exported = JSON.parse(manager.exportEntities([item], false));

    // extract the entity from the export
    var type = item.entityType;
    var copy = exported.entityGroupMap[type.name].entities[0];

    // remove the entityAspect (todo: remove complexAspect from nested complex types)
    delete copy.entityAspect;

    // remove the key properties (assumes key is store-generated)
    type.keyProperties.forEach(function (p) { delete copy[p.name]; });

    // the "copy" provides the initial values for the create
    var newItem = manager.createEntity(type, copy);

    if (collectionNames && collectionNames.length) {
        // can only handle parent w/ single PK values
        var keyValue = newItem.entityAspect.getKey().values[0];
        collectionNames.forEach(function (propertyString) { copyChildren(item, propertyString, keyValue); });
    }
    return newItem;

    function copyChildren(parentItem, navPropString, parentKeyValue) {

        var navPropName;
        // todo: add much more error handling
        var parentType = parentItem.entityType;

        //parse deep properties
        if (navPropString.indexOf('.') >= 0) {
            navPropName = navPropString.substr(0, navPropString.indexOf('.'));
            navPropString = navPropString.substr(navPropString.indexOf('.') + 1);
        } else {
            navPropName = navPropString;
            navPropString = "";
        }

        var navProp = parentType.getNavigationProperty(navPropName);

        if (navProp.isScalar) return; // only copies collection navigations. Todo: should it throw?

        // This method only copies children (dependent entities), not a related parent
        // Child (dependent) navigations have inverse FK names, not FK names
        var fk = navProp.invForeignKeyNames[0]; // can only handle child w/ single FK value
        if (!fk) return;

        // Breeze `getProperty` gets values for all model libraries, e.g. both KO and Angular
        var children = parentItem.getProperty(navPropName);
        if (children.length === 0) return;

        // Copy all children
        var childType = navProp.entityType;
        var copies = JSON.parse(manager.exportEntities(children, false)).entityGroupMap[childType.name].entities;

        copies.forEach(function (c) {

            //Get the original childid for deeper copy
            var originalChildId = c.id;

            delete c.entityAspect;

            // remove key properties (assumes keys are store generated)
            childType.keyProperties.forEach(function (p) { delete c[p.name]; });

            // set the FK parent of the copy to the new item's PK               
            c[fk] = parentKeyValue;

            // merely creating them will cause Breeze to add them to the parent
            var childItem = manager.createEntity(childType, c);

            if (navPropString.length > 0) {
                //Copy children

                var originalChild = $.grep(children, function (a) {
                    return a.id() == originalChildId;
                })[0];

                var childKeyValue = childItem.entityAspect.getKey().values[0];
                copyChildren(originalChild, navPropString, childKeyValue);
            }
        });
    }
};
like image 30
Skyler Todd Avatar answered Dec 17 '22 15:12

Skyler Todd