Publishing changes to a single field of a subdocument - javascript

I have a complicated data structure being built by queries on multiple collections and published.
It is working great for the initial creation, and on my local machine all the changes observed are reflected in the client as expected. However, in my staging environment I get the following error from mini-mongo when a change is observed
Uncaught Error: When replacing document, field name may not contain '.'(…)
The publishing code looks like this, where pub is the this from a Meteor.publish and rootObj is a reference to an Object in memory which gets properties modified but never has it's reference destoryed.
function _republish(pub, rootId, rootObj, handles, startup) {
// cleanup handles
if (handles.foo) {
handles.foo.stop();
}
// some query which could depend on rootObj/other calculated values
let cursor = SubColl.find({_id: {$in: bar}});
handles.foo = cursor.observeChanges({
removed(_id) {
rootObj.bar = rootObj.bar.filter(o => o._id !== _id);
pub.changed('foobarbaz', rootId, {bar: rootObj.bar})
},
changed(_id, fields) {
const index = rootObj.bar.findIndex(line => line._id === _id);
const changed = {};
_.each(fields, (value, field) => {
rootObj.bar[index][field] = value;
changed[`bar.${index}.${field}`] = value;
});
pub.changed('foobarbaz', rootId, changed);
},
added(_id, fields) {
rootObj.bar.push(_.extend({}, fields, {_id}));
if (!startup) {
// deeper children stuff
pub.changed('foobarbaz', rootId, {bar: rootObj.bar});
}
}
});
// deeper children stuff
startup = false;
// if startup was true, expect caller to publish this
}
As we can see, the publish works fine when I'm pub.changeding on just bar, but attempting to update a specific subdocument field (e.g. bar.0.prop) results in the inconsistent behaviour
If possible I want to avoid re-publishing the whole of bar as it is huge compared to updating a simple property.
How can I publish the change to a single field of a subdocument?

Related

How to compare two children of Firebase Realtime Database?

Based on the result of data.key === "high_temp_coil", I am printing the data into my webpage with data.val() as shown below:
var deviceRef = app.database().ref('/'+localStorage.getItem('machineid'));
deviceRef.on('child_added', function(data) {
if (data.key === 'high_temp_coil') {
$('#high_temp_coil .value').html(data.val())
}
if (data.key === 'low_temp_coil') {
$('#low_temp_coil .value').html(data.val());
}
}
In my code high_temp_coil represents the high temperature of the coil and low_temp_coil represents the low temperature of the coil, each with their own fields in my database. However, due to a manufacturing issue, sometimes the high temp and low temps are backwards and I need to figure this out before printing the data. This is how I was trying to do that but it doesn't work:
if (data.key === "high_temp_coil"){
let valueh= data.val();
if (data.key === "low_temp_coil"){
let valuel= data.val()
console.log(valueh);
console.log(valuel);
}
}
This is what the data looks like in my database:
{
"MachineNo": {
"water": "value"
"high_temp_coil": "value"
"low_temp_coil": "value"
}
}
When you use a child_added event listener, your callback will be invoked whenever one of the children under that database location changes. Using this, you would need to store high_temp_coil and low_temp_coil in variables outside of the function so that you can compare them properly. Because you store the result in an element, you could pull the current values from there.
Note: In the below snippets I follow the convention of naming the DataSnapshot object as snapshot, reserving data for the plain JavaScript object returned by snapshot.val(). This aids in preventing confusion later on, especially when not using TypeScript.
var deviceRef = app.database().ref('/'+localStorage.getItem('machineid'));
deviceRef.on('child_added', function(snapshot) {
if (snapshot.key === 'high_temp_coil') {
const newHighTempValue = snapshot.val();
const lowTempValue = $('#low_temp_coil .value').html();
if (Number(newHighTempValue) >= Number(lowTempValue)) { // <- assuming the values are numeric and not written as "52.1°C"
// new value is higher than current value
$('#high_temp_coil .value').html(newHighTempValue)
} else {
// new value is lower than current value, swap places
$('#high_temp_coil .value').html(lowTempValue)
$('#low_temp_coil .value').html(newHighTempValue)
}
}
if (snapshot.key === 'low_temp_coil') {
const newLowTempValue = snapshot.val();
const highTempValue = $('#high_temp_coil .value').html();
if (Number(newLowTempValue) < Number(highTempValue)) { // <- assuming the values are numeric and not written as "52.1°C"
// new value is lower than current value
$('#low_temp_coil .value').html(newLowTempValue)
} else {
// new value is higher than current value, swap places
$('#low_temp_coil .value').html(highTempValue)
$('#high_temp_coil .value').html(newLowTempValue)
}
}
}
The main issue with the above code is that the child_added events would fire once on page load, and not get live updates from any sensors updating the data because these changes would fire child_changed events.
However, for your data structure, you can greatly simplify your code by listening to value events instead. These listeners will be fired each time any of the data under that location is updated - including when a machine is created and any changes to the temperatures. Also, because the data is one level higher in the tree, you have access to the latest high_temp_coil and low_temp_coil values right in the snapshot. The trade-off for this listener is that you need to make sure to handle when the data does not exist (snapshot.exists()===false) because child_added listeners would only be invoked when data is created where it is guaranteed to exist.
var deviceRef = app.database().ref('/'+localStorage.getItem('machineid'));
deviceRef.on(
'value',
function(snapshot) {
if (!snapshot.exists()) {
// TODO: handle machine does not exist
console.error(`Machine ${localStorage.getItem('machineid')} does not exist in database`);
return;
}
const data = snapshot.val();
const highTempValue = data.high_temp_coil;
const lowTempValue = data.low_temp_coil;
if (Number(highTempValue) >= Number(lowTempValue)) { // <- assuming the values are numeric and not written as "52.1°C"
$('#high_temp_coil .value').html(highTempValue)
$('#low_temp_coil .value').html(lowTempValue)
} else {
$('#high_temp_coil .value').html(lowTempValue)
$('#low_temp_coil .value').html(highTempValue)
}
},
(error) => {
// TODO: implement better error handling
console.error("Listener was cancelled due to an error:", error);
}
);

sequelize ignore "save()" method on JSON object

I am trying to take benefits of model instance methods, as stated in the doc. So I defined User class as following:
class User extends Model {
addRole(role){
let roles = this. roles;
roles[role] = true;
this.roles = roles;
this.save();
}
removeRole (role) {
let roles = this.roles;
delete roles[role];
this.save();
}
hasRole (role){
return this.roles[role] != null;
}
}
User.init({
// some attributes
,
roles:{
type: DataTypes.JSON,
allowNull: false,
}
}, { sequelize});
I expected to use methods addRole(), removeRole() and hasRole() in any User instance.
The problem that all the methods can't save their changes to database. (Can read only!)
// example
let user = null;
// get the first user to test.
User.findAll()
.then(users =>{
user = users[0];
user.addRole("admin");
console.log(user.roles); // {admin: true}
user.save();
// However the changes don't appear in the database.
});
I had found the answer.
For some reasons, sequelise can't detect the changes of the json object properly. As sequelise is optimised internally to ignore call to model.save() if there is no changes of the model. So, sequelize randomly ignore the save method.
This behavior had no relation with instance method as I believed when I face this problem first time.
To get out of this problem, I had to use :
user.addRole("admin");
user.changed("roles", true); // <<<< look at this;
console.log(user.roles); // {admin: true}
user.save();
Please note that this function will return false when a property from a nested (for example JSON) property was edited manually, you must call changed('key', true) manually in these cases. Writing an entirely new object (eg. deep cloned) will be detected.
Example:
const mdl = await MyModel.findOne();
mdl.myJsonField.a = 1;
console.log(mdl.changed()) => false
mdl.save(); // this will not save anything
mdl.changed('myJsonField', true);
console.log(mdl.changed()) => ['myJsonField']
mdl.save(); // will save
changed method usage

Sequelize: can you use hooks to add a comment to a query?

Heroku recently posted a list of some good tips for postgres. I was most intreged by the Track the Source of Your Queries section. I was curious if this was something that's possible to use with Sequelize. I know that sequelize has hooks, but wasn't sure if hooks could be used to make actual query string adjustments.
I'm curious if it's possible to use a hook or another Sequelize method to append a comment to Sequelize query (without using .raw) to keep track of where the query was called from.
(Appending and prepending to queries would also be helpful for implementing row-level security, specifically set role / reset role)
Edit: Would it be possible to use sequelize.fn() for this?
If you want to just insert a "tag" into the SQL query you could use Sequelize.literal() to pass a literal string to the query generator. Adding this to options.attributes.include will add it, however it will also need an alias so you would have to pass some kind of value as well.
Model.findById(id, {
attributes: {
include: [
[Sequelize.literal('/* your comment */ 1'), 'an_alias'],
],
},
});
This would produce SQL along the lines of
SELECT `model`.`id`, /* your comment */ 1 as `an_alias`
FROM `model` as `model`
WHERE `model`.`id` = ???
I played around with automating this a bit and it probably goes beyond the scope of this answer, but you could modify the Sequelize.Model.prototype before you create a connection using new Sequelize() to tweak the handling of the methods. You would need to do this for all the methods you want to "tag".
// alias findById() so we can call it once we fiddle with the input
Sequelize.Model.prototype.findById_untagged = Sequelize.Model.prototype.findById;
// override the findbyId() method so we can intercept the options.
Sequelize.Model.prototype.findById = function findById(id, options) {
// get the caller somehow (I was having trouble accessing the call stack properly)
const caller = ???;
// you need to make sure it's defined and you aren't overriding settings, etc
options.attributes.include.push([Sequelize.literal('/* your comment */ 1'), 'an_alias']);
// pass it off to the aliased method to continue as normal
return this.findById_untagged(id, options);
}
// create the connection
const connection = new Sequelize(...);
Note: it may not be possible to do this automagically as Sequelize has use strict so the arguments.caller and arguments.callee properties are not accessible.
2nd Note: if you don't care about modifying the Sequelize.Model prototypes you can also abstract your calls to the Sequelize methods and tweak the options there.
function Wrapper(model) {
return {
findById(id, options) {
// do your stuff
return model.findById(id, options);
},
};
}
Wrapper(Model).findById(id, options);
3rd Note: You can also submit a pull request to add this functionality to Sequelize under a new option value, like options.comment, which is added at the end of the query.
This overrides the sequelize.query() method that's internally used by Sequelize for all queries to add a comment showing the location of the query in the code. It also adds the stack trace to errors thrown.
const excludeLineTexts = ['node_modules', 'internal/process', ' anonymous ', 'runMicrotasks', 'Promise.'];
// overwrite the query() method that Sequelize uses internally for all queries so the error shows where in the code the query is from
sequelize.query = function () {
let stack;
const getStack = () => {
if (!stack) {
const o = {};
Error.captureStackTrace(o, sequelize.query);
stack = o.stack;
}
return stack;
};
const lines = getStack().split(/\n/g).slice(1);
const line = lines.find((l) => !excludeLineTexts.some((t) => l.includes(t)));
if (line) {
const methodAndPath = line.replace(/(\s+at (async )?|[^a-z0-9.:/\\\-_ ]|:\d+\)?$)/gi, '');
if (methodAndPath) {
const comment = `/* ${methodAndPath} */`;
if (arguments[0]?.query) {
arguments[0].query = `${comment} ${arguments[0].query}`;
} else {
arguments[0] = `${comment} ${arguments[0]}`;
}
}
}
return Sequelize.prototype.query.apply(this, arguments).catch((err) => {
err.fullStack = getStack();
throw err;
});
};

How to reactively execute code as soon as a client Collection grows?

I'm trying to keep track of an increment of a certain reactive value in Meteor. If the current value has increased by 1 or more, I want something to happen. I do have two problems:
First: I don't know how I can make an if-statement of this function.
Second: I don't know how I can keep track of the increases.
This is the code I have now, using the Mongo.Collection cars (which is from an API):
api = DDP.connect('url');
const currentCars = new Meteor.Collection('cars', api);
const newCars = cars.find().count()
if (Meteor.isClient) {
Template.currentCars.helpers({
carsInCity: function() {
return currentCars.find(
{
location: "city"
}).count();
},
})
}
So there's a current amount of cars in the city. Everytime when there is one more car, I want something to happen in the code. But how on earth can I do that? Maybe by keeping track of when the database has been updated?
A fairly straight-forward solution would be to store the current amount of data in that collection, then run a reactive computation to see if anything changed.
Something like this:
let currentCarsCount = cars.find().count()
Tracker.autorun(function checkForAddedCars() {
// cars.find() is our reactive source
const newCarsCount = cars.find().count()
if(newCarsCount > currentCarsCount) {
currentCarsCount = newCarsCount
// There's new cars, handle them now
// ...
}
})
You may also want to use a template-level autorun so that you don't have to manage stopping checkForAddedCars. You could also store currentCarsCount as a state on the template instance instead of as a hoisted loner.
For example:
Template.currentCars.onCreated(function() {
const templateInstance = this;
// equivalent:
const templateInstance = Template.instance();
templateInstance.currentCarsCount = cars.find().count();
templateInstance.autorun(function checkForAddedCars() {
// cars.find() is our reactive source
const newCarsCount = cars.find().count();
if(newCarsCount > templateInstance.currentCarsCount) {
templateInstance.currentCarsCount = newCarsCount;
// There's new cars, handle them now
// ...
}
});
});
It would also allow you to access currentCarsCount from other places in the template code.

Accessing original field in Parse.com Cloud Code beforeSave function

The ultimate goal is to detect changes between an existing Parse object and the incoming update using the beforeSave function in Cloud Code.
From the Cloud Code log available through parse.com, one can see the input to beforeSave contains a field called original and another one called update.
Cloud Code log:
Input: {"original": { ... }, "update":{...}
I wonder if, and how, we can access the original field in order to detect changing fields before saving.
Note that I've already tried several approaches for solving this without success:
using (object).changedAttributes()
using (object).previousAttributes()
fetching the existing object, before updating it with the new data
Note on request.object.changedAttributes():
returns false when using in beforeSave and afterSave -- see below for more details:
Log for before_save -- summarised for readability:
Input: { original: {units: '10'}, update: {units: '11'} }
Result: Update changed to { units: '11' }
[timestamp] false <--- console.log(request.object.changedAttributes())
Log for corresponding after_save:
[timestamp] false <--- console.log(request.object.changedAttributes())
There is a problem with changedAttributes(). It seems to answer false all the time -- or at least in beforeSave, where it would reasonably be needed. (See here, as well as other similar posts)
Here's a general purpose work-around to do what changedAttributes ought to do.
// use underscore for _.map() since its great to have underscore anyway
// or use JS map if you prefer...
var _ = require('underscore');
function changesOn(object, klass) {
var query = new Parse.Query(klass);
return query.get(object.id).then(function(savedObject) {
return _.map(object.dirtyKeys(), function(key) {
return { oldValue: savedObject.get(key), newValue: object.get(key) }
});
});
}
// my mre beforeSave looks like this
Parse.Cloud.beforeSave("Dummy", function(request, response) {
var object = request.object;
var changedAttributes = object.changedAttributes();
console.log("changed attributes = " + JSON.stringify(changedAttributes)); // null indeed!
changesOn(object, "Dummy").then(function(changes) {
console.log("DIY changed attributes = " + JSON.stringify(changes));
response.success();
}, function(error) {
response.error(error);
});
});
When I change someAttribute (a number column on a Dummy instance) from 32 to 1222 via client code or data browser, the log shows this:
I2015-06-30T20:22:39.886Z]changed attributes = false
I2015-06-30T20:22:39.988Z]DIY changed attributes =
[{"oldValue":32,"newValue":1222}]

Categories