How can I update hasandbelongstomany relations for multiple models at once in strongloop loopback - orm

I have 2 models in a Strongloop Loopback API
Products
Tags
Between those 2 models I have defined a hasAndBelongsToMany-relation.
In my CMS I want a bulk-update functionality for my products, in which I can select many Products, and assign many tags, in one action.
How can I save those easily to my Mysql-DB, without having to iterate over each product, then iterate over each tag, and link those 2 ?
I checked in the docs and found the add and remove functions, but those only connect one model to one relatedModel. Is there already loopback-functionality to do what I want?

I wrote a custom (updated) function along with a helper in a service:
/*
* Add multiple objects to a relationship
* #param {object} origin The object which hold the items
* #param {array} data The new list to be inserted
* #param {string} relation name of the relationship, for instance 'cats'
*/
exports.updateManyRelations = function(origin, data, relation){
//Destroy all old connections
return origin[relation].destroyAll().then(function(response){
//All were deleted and nothing to add
if(!data || data.length == 0){return [];}
//We have a list to go through, do the dirty work!
return addMultipleRelationsToObject(origin[relation], data, []);
}).then(function(newList){
// new items created
return newList
}, function(error){
console.log(error);
});
}
/*
* Helper function to add multiple related objects to a object in a correct promise chain
*/
var addMultipleRelationsToObject = function(objectRelation, list, newList){
if(list && list.length == 0){return Promise.resolve(newList);}
var item = list.pop();
if(!objectRelation.hasOwnProperty("add")){
return Promise.reject("Relationship is wrong for: " + objectRelation);
}
return objectRelation.add(item.id).then(function(newlyAdded){
newList.push(item);
return addMultipleRelationsToObject(objectRelation, list, newList);
});
}

Unfortunately, there is no bulk update yet. See https://github.com/strongloop/loopback/issues/1275

Related

How do I duplicate an object in Verold?

I am trying to duplicate an object every time I click it in Verold. I have attached the Object Picker to the scene and successfully triggered a function which prints to the console.
I've tried this code but I get a Type Error - can't read property of undefined.
var xxx = this.getEntity().clone();
var threeDataxxx = xxx.getThreeData();
threeDataxxx.position.x += Math.random() * 5;
The clone() method is asynchronous (because the same method would be used if you were creating persistent copies of your objects on the server). This function, like many functions in the Verold API, takes an 'options' object as a parameter. In here, you need to specify the 'success' callback method like in the following example. Once you have the clone, you then need to add it to the scene hierarchy using the addChild() method. This will automatically trigger the cloned object to load.
var parent = this.getEntity().getParentObject();
this.getEntity().clone( {
success: function( newEntity ) {
parent.addChild( newEntity );
var position = newEntity.getPosition();
position.x += Math.random() * 10;
newEntity.setPosition( position.x, position.y, position.z );
}
});
The multiple steps are useful because you may want to clone several objects and have them ready to add to the scene at a later time.
And, of course, if you don't require the cloned object to have components or any of the other functionality of a VeroldObject, you can always just get the threeData and then use Three.JS's clone() method.
Hope that helps.

Get newly created Podio-PHP item id from PodioItem:Create

I am trying to create new items in four apps apps via one form -- some the items will become app references. For example, I first create an Entity item, then a Person item and relate the new Entity to the Person via an app reference field in the Person App. Then create a Project and relate both the Person and the Entity to the Project. This was successful until I upgraded to the latest Podio-PHP. In the past, when newly created items were returning item_id as integer then I could pass that into the field as an app reference before the next item was created.
I've been trying to get the item_id as integer so I can do the same, but I keep getting null. Anyway, given the way Podio now returns newly created items as objects, should I be trying this a different way?
$app_id = 1234567;
$fields = new PodioItemFieldCollection(array(
...I am defining external_id and values...
));
$item = new PodioItem(array(
'app' => new PodioApp($app_id),
'fields' => $fields,
));
if( $files ){
$item->files = new PodioCollection($files);
};
$item->save();
$new_id = $item->item_id;
return $new_id;
$new_id is where I'm trying to store the integer so I can pass it along to the next item creation as an app reference.
It's not really handled well in podio-php (it's been handled equally un-well since forever though). If you look at the save method you can see what's going on: https://github.com/podio/podio-php/blob/master/models/PodioItem.php#L58-L72
public function save($options = array()) {
$json_attributes = $this->as_json_without_readonly_fields();
if ($this->id) {
return self::update($this->id, $json_attributes, $options);
}
else {
if ($this->app && $this->app->id) {
return self::create($this->app->id, $json_attributes, $options);
}
else {
throw new PodioMissingRelationshipError('{"error_description":"Item is missing relationship to app"}', null, null);
}
}
}
save() doesn't assign the new item_id to the object itself. This is a bug and it would be good if you created an issue at https://github.com/podio/podio-php/issues so it can be fixed.
For now you can see that save() returns the same as the static create method. So your last three lines needs to be replaced with:
$new_item_placeholder = $item->save();
$item->item_id = $new_item_placeholder->item_id;
return $item->item_id;

HTML5 history API to reduce server requests

I am trying to develop a search filter and making use of the HTML5 history API to reduce the number of requests sent to the server. If the user checks a checkbox to apply a certain filter I am saving that data in the history state, so that when the user unchecks it I am able to load the data back from the history rather than fetching it again from the server.
When the user checks or unchecks a filter I am changing the window URL to match the filter that was set, for instance if the user tries to filter car brands only of a certain category I change the URL like 'cars?filter-brand[]=1'.
But when mutiple filters are applied I have no way of figuring out whether to load the data from the server or to load it from the history.
At the moment I am using the following code.
pushString variable is the new query string that will be created.
var back = [],forward = [];
if(back[back.length-1] === decodeURI(pushString)){ //check last back val against the next URL to be created
back.pop();
forward.push(currentLocation);
history.back();
return true;
}else if(forward[forward.length-1] === decodeURI(pushString)){
forward.pop();
back.push(currentLocation);
history.forward();
return true;
}else{
back.push(currentLocation); //add current win location
}
You can check if your filters are equivalent.
Comparing Objects
This is a simple function that takes two files, and lets you know if they're equivalent (note: not prototype safe for simplicity).
function objEqual(a, b) {
function toStr(o){
var keys = [], values = [];
for (k in o) {
keys.push(k);
values.push(o[k]);
}
keys.sort();
values.sort();
return JSON.stringify(keys)
+ JSON.stringify(values);
}
return toStr(a) === toStr(b);
}
demo
Using the URL
Pass the query part of the URL (window.location.search) to this function. It'll give you an object you can compare to another object using the above function.
function parseURL(url){
var obj = {}, parts = url.split("&");
for (var i=0, part; part = parts[i]; i++) {
var x = part.split("="), k = x[0], v = x[1];
obj[k] = v;
}
return obj;
}
Demo
History API Objects
You can store the objects with the History API.
window.history.pushState(someObject, "", "someURL")
You can get this object using history.state or in a popState handler.
Keeping Track of Things
If you pull out the toStr function from the first section, you can serialize the current filters. You can then store all of the states in an object, and all of the data associated.
When you're pushing a state, you can update your global cache object. This code should be in the handler for the AJAX response.
var key = toStr(parseUrl(location.search));
cache[key] = dataFromTheServer;
Then abstract your AJAX function to check the cache first.
function getFilterResults(filters, callback) {
var cached = cache[toStr(filters)]
if (cached != null) callback(cached);
else doSomeAJAXStuff().then(callback);
}
You can also use localstorage for more persistent caching, however this would require more advanced code, and expiring data.

Doctrine 2 ArrayCollection filter method

Can I filter out results from an arrayCollection in Doctrine 2 while using lazy loading? For example,
// users = ArrayCollection with User entities containing an "active" property
$customer->users->filter('active' => TRUE)->first()
It's unclear for me how the filter method is actually used.
Doctrine now has Criteria which offers a single API for filtering collections with SQL and in PHP, depending on the context.
https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-associations.html#filtering-collections
Update
This will achieve the result in the accepted answer, without getting everything from the database.
use Doctrine\Common\Collections\Criteria;
/**
* #ORM\Entity
*/
class Member {
// ...
public function getCommentsFiltered($ids) {
$criteria = Criteria::create()->where(Criteria::expr()->in("id", $ids));
return $this->getComments()->matching($criteria);
}
}
The Boris Guéry answer's at this post, may help you:
Doctrine 2, query inside entities
$idsToFilter = array(1,2,3,4);
$member->getComments()->filter(
function($entry) use ($idsToFilter) {
return in_array($entry->getId(), $idsToFilter);
}
);
Your use case would be :
$ArrayCollectionOfActiveUsers = $customer->users->filter(function($user) {
return $user->getActive() === TRUE;
});
if you add ->first() you'll get only the first entry returned, which is not what you want.
# Sjwdavies
You need to put () around the variable you pass to USE. You can also shorten as in_array return's a boolean already:
$member->getComments()->filter( function($entry) use ($idsToFilter) {
return in_array($entry->getId(), $idsToFilter);
});
The following code will resolve your need:
//$customer = ArrayCollection of customers;
$customer->getUsers()->filter(
function (User $user) {
return $user->getActive() === true;
}
);
The Collection#filter method really does eager load all members.
Filtering at the SQL level will be added in doctrine 2.3.

Doctrine2 ArrayCollection

Ok, I have a User entity as follows
<?php
class User
{
/**
* #var integer
* #Id
* #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
* #var \Application\Entity\Url[]
* #OneToMany(targetEntity="Url", mappedBy="user", cascade={"persist", "remove"})
*/
protected $urls;
public function __construct()
{
$this->urls = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addUrl($url)
{
// This is where I have a problem
}
}
Now, what I want to do is check if the User has already the $url in the $urls ArrayCollection before persisting the $url.
Now some of the examples I found says we should do something like
if (!$this->getUrls()->contains($url)) {
// add url
}
but this doesn't work as this compares the element values. As the $url doesn't have id value yet, this will always fail and $url will be dublicated.
So I'd really appreciate if someone could explain how I can add an element to the ArrayCollection without persisting it and avoiding the duplication?
Edit
I have managed to achive this via
$p = function ($key, $element) use ($url)
{
if ($element->getUrlHash() == $url->getUrlHash()) {
return true;
} else {
return false;
}
};
But doesn't this still load all urls and then performs the check? I don't think this is efficient as there might be thousands of urls per user.
This is not yet possible in a "domain driven" way, ie. just using objects. You should execute a query to check for the existance:
SELECT count(u.id) FROM User u WHERE ?1 IN u.urls AND u.id = ?2
With Doctrine 2.1 this will be possible using a combination of two new features:
Extra Lazy Collections
#IndexBy for collections, so you would define #OneToMany(targetEntity="Url", indexBy="location")
ExtraLazy Collection Support for index by using ->contains().
Points 1 and 2 are already implemented in Doctrine 2 master, but 3 is still missing.
You should try using the exists method on the collection and manually compare values.