A design wonder in sails.js and waterline models.
I have a "bear" model and a "location" one. They hold a oneToMany association ("bear can have multiple location over time").
bear.js
module.exports = {
attributes: {
location: {
collection: 'location',
via: 'bear'
}
}
};
location.js
module.exports = {
attributes: {
timestamp: {
type: 'datetime',
required: true
},
bear: {
model: 'bear'
}
}
};
I'm trying to make a "oneToOne" association grabbing the last location of a bear (lastLocation), and wondering what's the best solution :
Trying to override ToJSON on bear, fetching last location (doesn't seem to work, because ToJSON is hardly synchronous so I can't make it wait for an asynchronous find)
Creating a real oneToOne relationship and adding a hook AfterCreate on location to update that "lastLocation" association ?
Override blueprint create on location ?
Override blueprint add on bear ?
Any other idea ?
Thanks =)
I think this is your answer here?
Creating a real oneToOne relationship and adding a hook AfterCreate on location to update that "lastLocation" association ?
You should create an additional attribute on bear called lastLocation:{model:'location'}, and automatically update that after each location is created on the associated bear. Their are otherways to do this, but if the last location is something that is read a lot, then this will probably be your best approach.
Now this is a little bit of opinion based as there would be many factors involved in your design to say for sure, but I think you will be fine using this approach.
Here is what I ended up with :
model/Bear.js - last location association
lastLocation: {
model: 'location'
}
model/Location.js - afterCreate hook
afterCreate: function(insertedLocation, callback) {
if(!insertedLocation.bear) {
return callback();
}
Bear.findOneById(insertedLocation.bear, function(err, bear) {
if(err) {
return callback(err);
}
Location.findOneById(bear.lastLocation, function(err, oldLastLocation) {
bear.lastLocation = insertedLocation.id;
bear.save(function(err) {
return callback(err);
});
});
});
}
the if(!insertedLocation.bear) test is the key
I repeated similar code inside afterUpdate hook
afterUpdate: function(updatedLocation, callback) {
if(!updatedLocation.bear) {
return callback();
}
Bear.findOneById(updatedLocation.bear, function(err, bear) {
if(err) {
return callback(err);
}
Location.findOneById(bear.lastLocation, function(err, oldLastLocation) {
bear.lastLocation = updatedLocation.id;
bear.save(function(err) {
return callback(err);
});
});
});
}
adding a test in order to prevent updating bear each time we update lastLocation
One could add some PublishUpdate() logic
Related
In Meteor, one can add additional fields to the root-level of the new user document like so:
// See: https://guide.meteor.com/accounts.html#adding-fields-on-registration
Accounts.onCreateUser((options, user) =>
// Add custom field to user document...
user.customField = "custom data";
return user;
});
On the client, one can retrieve some data about the current user like so:
// { _id: "...", emails: [...] }
Meteor.user()
By default, the customField does not exist on the returned user. How can one retrieve that additional field via the Meteor.user() call such that we get { _id: "...", emails: [...], customField: "..." }? At present, the documentation on publishing custom data appears to suggest publishing an additional collection. This is undesired for reasons of overhead in code and traffic. Can one override the default fields for Meteor.user() calls to provide additional fields?
You have a couple of solutions that you can use to solve this.
Null Publication
Meteor.publish(null, function () {
if (this.userId !== null) {
return Meteor.users.find({ _id: this.userId }, { fields: { customField: 1 } });
} else {
return this.ready();
}
}, { is_auto: true });
This will give you the desired result but will also result in an additional database lookup.. While this is don't by _id and is extremely efficient, I still find this to be an unnecessary overhead.
2.Updating the fields the Meteor publishes for the user by default.
Accounts._defaultPublishFields.projection = { customField: 1, ...Accounts._defaultPublishFields.projection };
This has to be ran outside of any Meteor.startup blocks. If ran within one, this will not work. This method will not result in extra calls to your database and is my preferred method of accomplishing this.
You are actually misunderstanding the documentation. It is not suggesting to populate and publish a separate collection, just a separate publication. That's different. You can have multiple publications/subscriptions that all feed the same collection. So all you need to do is:
Server:
Meteor.publish('my-custom-user-data', function() {
return Meteor.users.find(this.userId, {fields: {customField: 1}});
});
Client:
Meteor.subscribe('my-custom-user-data');
On my server side, I publish a Groups collection. This relies on publishing groups that match another Servers collection.
Meteor.publish('groups', function() {
const servers = Servers.find({}); // simplified code
return Groups.find({serverId: {$in: servers}});
});
A client view then subscribes to that:
self.autorun(() => {
self.subscribe('groups');
});
This generally works fine. The problem comes when I add insert a server in the Servers collections, there is no indication to the publication to update the Groups, therefore this added server's Groups don't publish to the client.
What is the right way to handle this?
The Meteor Guide is always a good starting point for these kind of problems. Go here https://guide.meteor.com/data-loading.html#publishing-relations. TLDR; just add reywood:publish-composite package. :)
Let me know.
A possible straightforward approach here is to create a separate publication for Servers and then make groups Servers-dependent.
// Server
Meteor.publish("servers", function() {
return Servers.find({}, { fields: { _id: 1 } });
});
Meteor.publish("groups", function(serversIds) {
return Groups.find({ serverId: { $in: serversIds } });
});
// Client
self.autorun(() => {
self.subscribe("servers");
});
self.autorun(() => {
const serversIds = Servers.find().map(server => server._id);
self.subscribe("groups", serversIds);
});
I'm trying to set additional info when setting geoFire object in FB.
Something like:
geoFire.set(uniqueId, [latitude, longitude],{'message': message, 'iconUrl': iconURL, lastUpdate: Firebase.ServerValue.TIMESTAMP}).then(function () {
//some stuff
};
I can do it like this and it mostly works (depends on db roundtrip time):
geoFire.set(uniqueId, [latitude, longitude],).then(function () {
firebaseRef.child(uniqueId).update({'message': message, 'iconUrl': iconURL, lastUpdate: Firebase.ServerValue.TIMESTAMP});
})
but my listener triggers too fast and doesn't capture all the data so it renders without icon path.
I had the same problem, didn't want to solve this with a workaround, especially by introducing the delay so kept digging and found this: https://github.com/firebase/geofire-js/issues/40
This basically means we need to restructure the application, where one collection is dedicated to the GeoFire information, another uses the same key to store attached information. Example from the GitHub.
{
geofire: {
"-J9aZZl94Sx6h": { "g": <geohash>, "l": {} }
"-Jhacf97x4S3h": { "g": <geohash>, "l": {} },
...
},
"locations": {
"-J9aZZl94Sx6h": { <location-data> },
"-Jhacf97x4S3h": { <location-data> }
}
}
Env: Titanium 3.1.3, Alloy 1.2.2.
I'm using the following adapter for persistence on the models/collections: https://github.com/viezel/napp.alloy.adapter.restapi
I have an API that has a different URL structure for a collection than it does a single model. Consider the following:
To get a single record: [GET] /files/:id
To get all the files for a user: [GET] /users/:id/files
I have the following schema for files.js:
exports.definition = {
config: {
"URL": "https://my.api.here/files",
//"debug": 1,
"adapter": {
"type": "restapi",
"collection_name": "files",
"idAttribute": "id"
}
},
extendModel: function(Model) {
_.extend(Model.prototype, {});
return Model;
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {
initialize: function(){
this.url = "http://my.api.here/users/"+this.user_id+"/files";
}
});
return Collection;
}
}
What I'm trying to do in the above is override the collection initialize method to change the URL structure for the collection. I then call this accordingly:
var currentUserFiles = Alloy.createCollection("files", {user_id:"12345"});
currentUserFiles.fetch({
success: function(files){
console.log("Woo! Got the user's files!");
console.log(JSON.stringify(files.models));
},
error: function(){
console.log("Nope");
}
});
This doesn't work. The fetch() method just continues to try to call /files. I've tried setting url as a property on the collection after it's created, that also don't work.
Ideally, I'd like to do this for both local instances as well as the singleton version of the collection.
So - the question is: can I utilize a different URL for a collection than I do for a model? Obviously, I don't want to just call /files and sort/filter client-side - that'd be a nightmare with a lot of records. What am I missing here?
It's a bit late but for anyone else that comes across this. I problem is where/how the url is specified for model and collection. The model needs a specific id (eg: primary key) passed into it because the model can only be one object. If you need more than one object, then use the collection. Hope this helps :)
extendModel : function(Model) {
_.extend(Model.prototype, {
url : function() {
return "http://my.api.here/users/"+this.user_id+"/files/"+ FILE_ID
},
});
return Model;
},
extendCollection : function(Collection) {
_.extend(Collection.prototype, {
url : function() {
return "http://my.api.here/users/"+this.user_id+"/files"
},
});
},
As for the new meteor release, I`d like to understand how can I forbid messages with certain words to be added to a collection.
Let's say I'm passing: Messages.insert({message:"Holy ducking smokes", at: new Date()});
What should the code within if (Meteor.is_server) be like so it would block any entries containing "duck"?
Something like this?
Messages.deny({
insert: function(userId, doc) {
if (doc.message.match(/\bduck\b/i)) return true;
return false;
}
}
That will deny the client from inserting the record if the message contains the bounded word "duck". Obviously you could execute other logic there (eg. censoring) if you need to.
One way I could do this was setting a .allow within the Meteor.is_server to test if the value differs from what I`m filtering.
It should look something like this:
if (Meteor.is_server) {
Messages.allow({
insert: function (userId, doc) {
var currentMessage = Messages.findOne({message:doc.message}) ;
if (doc.message == 'duck') { //here i`m filtering stirngs
return false;
} else { return true; }
},
update: function () { (...) },
remove: function () { (...) },
});
}
But I guess using methods would be a better approach to this, as it makes sense to use a single validation rule to both server and client side.