Enyo Collection Merge Strategy - javascript

Below is the function I have used to fetch more photos from a service provider once clicked on more button:
showMore: function(){
this.$.resultList.collection.fetch({strategy:"merge",rpp:50});
}
This will call the fetch method defined in collection,
fetch: function(opts) {
this.params = {
feature: this.methodType || "popular",
image_size: 3,
sort: "created_at",
rpp: opts && opts.rpp || 25
};
return this.inherited(arguments);
},
This is working fine, but the problem is once more button is clicked, it should fetch another set of 25 photos and append it to my collection, but what actually happening is sometimes, it shows only say 5 photos along with previous photos .
What I understand by "merge" strategy is, if the records received is same as previous records, it will take only those records which are different from previously fetched records and updates the primarykey of the duplicate records.So one reason i am able to figure out is that, may be, it is fetching 25 photos next time, but because most of them are same as before, it is showing only those which are different from the previous fetched photos.
If i go with the "add" strategy, it works fine for one time, i.e, it shows another set of 25 photos appended to the collection, most of them are again same. But if click on more button one more time, no records are being added to the collection.No idea why this is happening.
How should i approach, if i want to fetch only new photos and append it to the collection.

Using the merge strategy is the right approach. Your description of merge is mostly accurate except that it doesn't update the primary key but instead updates the data for existing records with the same primary key.
It's difficult to say why using "add" doesn't always work. If the records don't have a primary key (which is id by default), "add" and "merge" should always add the records to the collection (unless you're using mergeKeys). If they do have a primary key, it's possible that you're trying to add duplicate records which Enyo will complain about and abort. Check your console log.
Without code, the only other suggestion is to set breakpoints and step through enyo.Collection.merge.
Here's an example of fetching records into a collection. If you comment out setting the id, merge and add strategies will always add records. If you comment out the merge strategy, the code will eventually error when requesting more records.
enyo.kind({
name: "ex.MockSource",
kind: "enyo.Source",
fetch: function(rec, opts) {
if(rec instanceof enyo.Model) {
rec.setObject(Faker.Helpers.createCard());
} else if (rec instanceof enyo.Collection) {
var count = opts && opts.count || 25;
var cards = [];
for(var i=0;i<count;i++) {
var card = Faker.Helpers.createCard();
// artificial id to force merges
card.id = enyo.now()%40;
cards.push(card);
}
opts.success(cards);
}
}
});
enyo.store.addSources({
mock: "ex.MockSource"
});
enyo.kind({
name: "ex.App",
kind: "Scroller",
bindings: [
{from: ".data", to: ".$.list.collection"},
{from: ".data.length", to: ".$.count.content", transform: function(v) {
return enyo.format("Displaying %s records", v);
}}
],
components: [
{name: "count"},
{name: "list", kind: "DataRepeater", components: [
{kind: "onyx.Item", components: [
{name: "name"},
{name: "phone"}
], bindings: [
{from: ".model.name", to: ".$.name.content"},
{from: ".model.phone", to: ".$.phone.content"}
]}
]},
{kind: "onyx.Button", content: "More", ontap: "moreTapped"}
],
create: enyo.inherit(function(sup) {
return function() {
sup.apply(this, arguments);
this.set("data", new enyo.Collection({
defaultSource: "mock"
}));
this.fetchRecords();
};
}),
fetchRecords: function() {
this.data.fetch({
count: 5,
strategy: "merge"
});
},
moreTapped: function() {
this.fetchRecords();
}
});
new ex.App().renderInto(document.body);

Related

Mongodb findOneAndUpdate - check if new document added, or just updated

I have the following code which is finding and updating an employee-store record (if it exists, otherwise it creates one).
I have lots of employees in different stores, and they can choose to change store at any point (as long as the location is in America)
Below is my code so far:
employee = await this.employeeStoreModel.findOneAndUpdate(
{ employee: employeeRecord._id, location: "America" },
{
employee: employeeRecord._id,
store: employeeRecord.store,
location: "America",
},
{ new: true, upsert: true }
);
This works correctly, however I am trying to return some messages from this based on what is being updated. It could be any of the following messages:
If it's a completely new employee-store record being added, then return "{{StoreID}} has a new employee - {{EmployeeID}}"
If it's a change of store on an existing employee-store record, then return "{{EmployeeID}} has changed from {{old StoreID}} to {{new StoreID}}"
Is this possible to do? Can anyone guide me on how I could start this?
In the Options field,
rawResult: true
You must add the parameter.
The result will be as in the example below.
{ response:
{ n: 1,
updatedExisting: false,
upserted: 5e6a9e5ec6e44398ae2ac16a },
value:
{ _id: 5e6a9e5ec6e44398ae2ac16a,
name: 'Will Riker',
__v: 0,
age: 29 },
ok: 1 }
Depending on whether there is an update or insert in the updatedExisting field, you can return any message you want.

Vue.js: How to avoid error after updating store?

I am building an app with vue.js and vuex that stores entries of household accounts (i.e. personal expenses and incomes). The entries are filtered by month; a slider can be used to change months. The current code is on Github.
Displaying existing entries works fine. The problem arises when I try to add a new entry. When I add an entry for an existing month, it works fine. But when I add an entry for a new month (i.e. a month for which no other entry exists yet), I get a myterious warning and a myterious error: Unhandled error during execution of scheduler flush. This is likely a Vue internals bug. Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node. Note: When I say month I mean month-year combination.
Any ideas where this error might be coming from?
Here is some more info about the current app. The current application looks like this:
My store looks like this:
const store = createStore({
state() {
return {
categories: [
{
type: "expense",
key: "living",
value: "Wohnen",
},
...
],
entries: [
{
id: 1,
date: "2020-10-15T14:48:00.000Z",
type: "expense",
category: "Lebensmittel",
amount: 15.24,
note: "Edeka",
},
...
],
};
},
getters: {
sorted_entries(state) {
// Returns all entries, sorted by date
...
return sorted_entries;
},
categories(state) {
return state.categories;
},
entriesMonths(state) {
// Return an ordered arrays of all months present among entries
// Ex.:{index: 0, month: "11", year: "2019", displayMonth: "November 2019"}
...
return entriesMonths;
},
},
mutations: {
addEntry(state, entryData) {
const newEntry = {
id: new Date().toISOString(),
date: entryData.date,
type: entryData.type,
category: entryData.category,
amount: entryData.amount,
note: entryData.note,
};
console.log("right before push")
state.entries.push(newEntry);
},
},
actions: {
addEntry(context, entryData) {
context.commit("addEntry", entryData); // Place to store in backend server
},
},
});
In EntryList.vue I get sorted_entries and entriesMonths from the store:
sorted_entries is used to calculate filtered_entries which filters the entries according to what month is currently displayed by the slider (initialization: latest month among entries in store). filtered_entries is displayed as a list
entriesMonths is passed to another component MonthSelector.vue which implements the slider to change months and emits the new month after a change to EntryList.vue so it can update filtered_entries.
New entries are added via NewEntryForm.vue. This component includes a form to ask the user for the new entry data. It then emits this data as an object to its parent AddEntry.vue which in turn sends it to the store. There an action triggers a mutation which adds the entry to the state.
Of cause there are a lot of more details to mention, but I don't know which are relevant to solving this problem. Please ask, if you need more info or have a look at the code (Github).
Many thanks!
The problem is caused by sending the entriesMonths as a property to MonthSelector.vue component,
By that you're violating the purpose of having a store/getters/mutations, you can access the entriesMonths directly from your component, you don't have to pass it as a prop,
So change your MonthSelector.vue as follows:
Remove the property that is passed to the component
In your slideOpts.initialSlide access the entriesMonths directly from the store.
initialSlide: this.$store.getters.entriesMonths.length - 1
*Update: *
Please remove the entriesMonths from the computed properties and put it in data as follows:
data() {
const entriesMonths = this.$store.getters.entriesMonths
return {
entriesMonths,
slideOpts: {
initialSlide: entriesMonths.length - 1, // Start with newest slide/month
speed: 300,
pagination: false,
},
};
},

How to access aggregated collection data in meteor client?

I aggregated some data and published it, but I'm not sure how/where to access the subscribed data. Would I be able to access WeeklyOrders client collection (which is defined as client-only collection i.e WeeklyOrders = new Mongo.Collection(null);)?
Also, I see "self = this;" being used in several examples online and I just used it here, but not sure why. Appreciate anyone explaining that as well.
Here is publish method:
Meteor.publish('customerOrdersByWeek', function(customerId) {
check(customerId, String);
var self = this;
var pipeline = [
{ $match: {customer_id: customerId} },
{ $group: {
_id : { week: { $week: "$_created_at" }, year: { $year: "$_created_at" } },
weekly_order_value: { $sum: "$order_value" }
}
},
{ $project: { week: "$_id.week", year: "$_id:year" } },
{ $limit: 2 }
];
var result = Orders.aggregate(pipeline);
result.forEach(function(wo) {
self.added('WeeklyOrders', objectToHash(wo._id), {year: wo.year, week: wo.week, order_value: wo.weekly_order_value});
});
self.ready();
});
Here is the route:
Router.route('/customers/:_id', {
name: 'customerOrdersByWeek',
waitOn: function() {
return [
Meteor.subscribe('customerOrdersByWeek', this.params._id)
];
},
data: function() { return Customers.findOne(this.params._id); }
});
Here is my template helper:
Template.customerOrdersByWeek.helpers({
ordersByWeek: function() {
return WeeklyOrders.find({});
}
});
You want var self = this (note the var!) so that call to self.added works. See this question for more details. Alternatively you can use the new es6 arrow functions (again see the linked question).
There may be more than one issue where, but in your call to added you are giving a random id. This presents two problems:
If you subscribe N times, you will get N of the same document sent to the client (each with a different id). See this question for more details.
You can't match the document by id on the client.
On the client, you are doing a Customers.findOne(this.params._id) where this.params._id is, I assume, a customer id... but your WeeklyOrders have random ids. Give this a try:
self.added('WeeklyOrders', customerId, {...});
updated answer
You'll need to add a client-only collection as a sort-of mailbox for your publisher to send WeeklyOrders to:
client/collections/weekly-orders.js
WeeklyOrders = new Meteor.Collection('WeeklyOrders');
Also, because you could have multiple docs for the same user, you'll probably need to:
Forget what I said earlier and just use a random id, but never subscribe more that once. This is an easy solution but somewhat brittle.
Use a compound index (combine the customer id + week, or whatever is necessary to make them unique).
Using (2) and adding a customerId field so you can find the docs on the client, results in something like this:
result.forEach(function (wo) {
var id = customerId + wo.year + wo.week;
self.added('WeeklyOrders', id, {
customerId: customerId,
year: wo.year,
week: wo.week,
order_value: wo.weekly_order_value,
});
});
Now on the client you can find all of the WeeklyOrders by customerId via WeeklyOrders.find({customerId: someCustomerId}).
Also note, that instead of using pub/sub you could also just do all of this in a method call. Both are no-reactive. The pub/sub gets you collection semantics (the ability to call find etc.), but it adds the additional complexity of having to deal with ids.

How to improve or avoid find / fetch cycle in meteor's publication?

TL;DR:
Chat is one collection. ChatMess another one that has messages refering to a Chat's _id. How do I get the last messages from a list of chats with the less computation possible ? Here, find / fetch cycle in a loop is way too heavy and long.
I have this publication that is used to return a set of cursor to the user :
The chats sessions he takes part in (from Chat collection)
The last message from each of the chat session referenced in the first cursor (from ChatMess collection)
Currently, the logic is to :
Get the list of chat sessions from the user profile
Find the Chat sessions and loop through it
In the loop, I findOne the last message from this chat session and store its _id in an array. In addition, I store all the other users _ids.
Then, I find the messages which _id match the ones in my array.
Here is my main problem :
Isn't there a way more faster way to get the last messages from each of my chat session ? With that algo, I easily reach the 8000ms of response time, which is a way too heavy computation time, as much of this time is spent to find / fetch the chat messages's _id (cf linked screen from Kadira).
Meteor.publish("publishNewChat", function() {
this.unblock();
// we get a list of chat _id
let chatIdList = _get_all_the_user_chats_ids(this.userId);
if (!chatList)
return ;
// get the chat sessions objects
let chats_cursor = Modules.both.queryGet({
type : 'chat',
method : 'find',
query : { _id: { $in: chatIdList } },
projection : { sort: { _id: 1 }, limit : 1000 }
});
let array_of_fetched_chats = chats_cursor.fetch();
let chat_ids = [];
// and here we loop through the chat documents in order to get the last message that's been attached to each of them
array_of_fetched_chats.forEach(function(e) {
let lastMess = Modules.both.queryGet({
type : 'chatMess',
method : 'findOne',
query : { chatId: e._id },
projection : { sort: { date: -1 } }
});
if (lastMess)
chat_ids.push(lastMess._id);
});
return ([
chats_cursor,
Modules.both.queryGet({
type : 'chatMess',
method : 'find',
query : { _id: { $in: chat_ids } },
projection : { sort: { date: -1 }, limit: 1000 }
})
]);
});
Finally, it also add latence to all my DDP request that follows. I currently use a this.unblock() to avoid that, but I'd prefer not to use it here.
FYI, I have another publish that is updated each time the client change his current active chat session : on the client, routing to a new chat add its _id in a reactive array that update my getChatMess subscription in order to get on the client the messages from every chats the user visited in this since he connected. The goal is obviously to spare the server the sending of every message from every chat session the user have visited in his life.
Unfortunately, I lack ideas to improve that algo without breaking all my chat logic :S. Have you any idea ? How would you do ?
Thanks you.
EDIT: here is a screen from kadira that clearly show the problem :
Have you considered using the reywood/publishComposite package?
With this package you can publish related data in the same method without having to do a bunch of logic to get the correct data published.
The below code should get you started:
Meteor.publishComposite("publishNewChat", function() {
return [{
find:function(){
return Users.find({ _id: this.userId },{fields:{"profile.chat":1}});
},
children:[{
find:function(user){ //this function is passed each user returned from the cursor above.
return UserChats.find({userId:user._id},{fields:{blah:1,blah:1}}); //find the user chats using whatever query
},
children:[
//if there are any children of user chats that you need to publish, do so here...
{
find:function(userchat){
return Chats.find({_id:userchat.chatId})
},
children:[
{
find:function(chat){
return ChatMess.find({chatId:chat._id},{ sort: { date: -1 } });
},
children:[
{
find:function(chatMess){
var uids = _.without(chatMess.participants, this.userId);
return Users.find({_id:{$in:uids}});
}
}
]
}
]
}
]
},
]
}]
This will publish the cursors for all of the documents related to each of the parent documents. It is pretty fast, I use this package on a production platform high traffic and large datasets with no problems. On the client you could then query the documents as normal to get the ones you need to display.
Something like:
Users.findOne({_id:Meteor.userId()});
UserChats.find({userId:Meteor.userId()});
etc...
Here is a solution I developped :
Meteor.publish("publishNewChat", function() {
this.unblock();
let user = Modules.both.queryGet({
type : 'users',
method : 'findOne',
query : { _id: this.userId },
projection : { fields: { "profile.chat": true } }
});
let thisUserschats = tryReach(user, "profile", "chat").value;
if (!thisUserschats)
return ;
thisUserschats = thisUserschats.map(function(e) { return (e.chatId); });
let chats = Modules.both.queryGet({
type : 'chat',
method : 'find',
query : { _id: { $in: thisUserschats } },
projection : { sort : { _id: 1 },
limit : 1000
}
});
let chatArray = chats.fetch(),
uids = cmid = [];
let messages_id_list = [],
i = chatArray.length;
let _parallelQuery = index => {
Meteor.setTimeout(function () {
let tmp = Modules.both.queryGet({
type : 'chatMess',
method : 'find',
query : { chatId: chatArray[index]._id },
projection: { limit: 1, sort: { date: -1 } }
});
tmp.forEach(doc => {
messages_id_list.push((doc && doc._id) ? doc._id : null);
});
}, 1);
}
while (--i >= 0)
_parallelQuery(i);
let cursors = {
chats : chats,
chatMessages : null
}
let interval = Meteor.setInterval(function () {
if (messages_id_list.length === chatArray.length)
{
Meteor.clearInterval(interval);
cursors.chatMessages = Modules.both.queryGet({
type : 'chatMess',
method : 'find',
query : { _id: { $in: messages_id_list } },
projection : { sort: { date: -1 }, limit: 1000 }
});
cursors.chats.observeChanges({
// ...
});
cursors.chatMessages.observeChanges({
// ...
});
self.ready();
self.onStop(() => subHandle.stop(); );
}
}, 10);
});
I used async function with Meteor.setTimeout to parallelize the queries and save an index refering to a chat _id to look for. Then, when a query is finished, I add the last message to an array. With a Meteor.setInterval, I check the array length to know when all the queries are done. Then, as I can't return cursors anymore, I use the Meteor publication low level API to handle the publishing of the documents.
FYI : in a first attempt, I was using 'findOne' in my _parallelQueries, which divided my computation time by 2/3. But then, thanks to a friend, I tried the cursor.foreach() function, which allowed me to divide the computation time by 2 again !
In production, the benchmarks allowed me to go from a 7/8 second response time to an average response time of 1.6 second :)
Hope this will be usefull to you people ! :)

IndexedDB and many-to-many Relationships

How are you all handling many-to-many relationships in IndexedDB?
For example, say I have a Blog object to hold a blog post and a Tag object for a tag/label of the blog post. One Blog can have many Tags and one Tag can be used by many Blogs.
I would create a blog store and tag store (though I'm open to suggestions) to house the two types of objects:
// ...
var blogStore = db.createObjectStore("blog", {keyPath: "blogId", autoIncrement: true});
blogStore.createIndex("title", "title", {unique: true});
var tagStore = db.createObjectStore("tag", {keyPath: "tagId", autoIncrement: true});
tagStore.createIndex("label", "label", {unique: true});
Off hand I can think of two ways to link the two:
have a Blog.tags which would be an array of BlogTag objects which holds blogId and tagId (and would also be in the store for retrieval) or
have a Blog.tags which would be an array of tagIds that could be used to look up the Tags.
The first way seems longer-winded but is how this would be tackled in SQL. Is that just SQL-baggage that I should leave behind?
I suppose a 3rd way would be to have Blog.tags be an array of Tags. This seems simplest but then I couldn't query for Tags or reuse tags across blogs (or could I?).
Has anyone else handled such a situation with indexedDB? If so, what did you end up doing? What were some pitfalls?
I'm working on an IndexedDB-backed JS neural network implementation and faced this very
problem.
We don't have joins in IndexedDB so you're looking at at least two object store hits unless you're doing some sort of memoization/caching.
From experience I've found that a document-oriented style is best with IndexedDB objects (store everything in the same store), but a secondary store is needed to house relations.
Here's what I'm doing.
Say you want to have a local store of actors and movies -- something like IMDB. This and most any many-to-many relationship can be modeled with IndexedDB using two tables: Objects and Relationships.
Here are the two tables. You'd want key lookups* on almost everything. Anything that doesn't say unique can be non-unique.
Objects object store:
type_id*
whatever*..
Relationships object store:
id* (unique, auto-incrementing)
from_type*
to_id*
An actor/movie example would be two records in the Objects table and one in the relationship table:
var actor1 = {
id: 'actor_jonah_goldberg',
display: 'Jonah Goldberg',
};
var actor2 = {
id: 'actor_michael_cera',
display: 'Michael Cera'
};
var movie1 = {
id: 'movie_superbad',
display: 'Superbad',
year: 2007
};
var movie2 = {
id: 'movie_juno',
display: 'Juno',
year: 2007
};
//relationship primary key ids are auto-inc
var relationship1 = {
from_id: 'actor_jonah_goldberg',
to_id: 'movie_superbad'
}
var relationship2 = {
from_id: 'actor_michael_cera',
to_id: 'movie_superbad'
}
var relationship3 = {
from_id: 'actor_michael_cera',
to_id: 'movie_juno'
}
Psuedo-code for getting Michael Cera's movies:
IndexedDBApp( { 'store': 'relationships', 'index': 'from_id', 'key': 'actor_michael_cera', 'on_success': function( row ) {...} );
// Would return movie_superbad and movie_juno rows on_success
Psuedo-code for getting all movies from a given year:
IndexedDBApp( { 'store': 'objects', 'index': 'year', 'key': 2007, 'on_success': function( row ) {...} );
// Would return movie_superbad and movie_juno rows on_success
Psuedo-code for getting a movie's actors:
IndexedDBApp( { 'store': 'relationships', 'index': 'to_id', 'key': 'movie_superbad', 'on_success': function( row ) {...} );
// Would return actor_jonah_goldberg and actor_michael_cera on_success
Psuedo-code for getting all actors:
IndexedDBApp( { 'store': 'relationships', 'index': 'id', 'cursor_begin': 'actor_a', 'cursor_end': 'actor_z', 'on_success': function( row ) {...} );
// Would return actor_jonah_goldberg and actor_michael_cera on_success

Categories