vue-apollo mutation trigger unexpected subscription - apollo

I have a smart query in my entry component. It will show items when page loads.
apollo: {
feed: {
query: FEED_QUERY,
result({ data, loading, networkStatus }) {
console.log("Feed success!");
this.feedItems(data.feed); // sync vuex state
},
error(error) {
console.error("Feed error!", error);
},
watchLoading(isLoading, countModifier) {
}
}
}
And I add a standard subscription in created hook, which will refresh items once observing changes.
created: function() {
const _this = this;
const observer = this.$apollo.subscribe({
query: ITEM_SUBSCRIPTION
});
observer.subscribe({
next(data) {
_this.$apollo.queries.feed.refetch();
},
error(error) {
console.error(error);
}
});
},
When I do any mutation like this,
this.$apollo
.mutate({
mutation: ITEM_DELETE_MUTATION,
variables: {
id
}
})
.then(({ data }) => {
console.log("Delete item success!");
})
.catch(error => {
console.error("Delete item fail!");
});
I expect console to log out:
Delete item success!
Observe changes!
Feed success!
but actually it prints
Feed success! <-------- unexpected
Delete item success!
Observe changes!
Feed success!
I don't know how was the first Feed success! message trigged. Any ideas? Thanks in advance.

Well, your result callback is called when a query is loading.
This comes handy when you are designing optimistic UI where you present a fake response from the server to a user and after you get an actual response you update it.
If you want to prevent it just do:
result({data, loading, networkStatus}) {
if (loading) return
console.log('Feed success!')
this.feedItems(data.feed) // sync vuex state
}

Related

What is the best way to mock ember services that use ember-ajax in ember-cli-storybook to post and fetch data?

I'm using Ember CLI Storybook to create a story of a component than internally relies upon services that communicate to the internet, to fetch and post information to the backend. The way I'm doing that is using ember-ajax.
I see how to mock an ember model from this section but wondering if there is a workaround for ember ajax service.
I like to use mswjs.io for mocking remote requests. It uses a service worker so you can still use your network log as if you still used your real API.
I have an example repo here showing how to set it up: https://github.com/NullVoxPopuli/ember-data-resources/
But I'll copy the code, in case I change something.
Now, in tests, you'd want something like this: https://github.com/NullVoxPopuli/ember-data-resources/blob/main/tests/unit/find-record-test.ts#L17
module('findRecord', function (hooks) {
setupMockData(hooks);
But since you're using storybook, you'd instead want the contents of that function. (And without the setup/teardown hooks unique to tests)
https://github.com/NullVoxPopuli/ember-data-resources/blob/main/tests/unit/-mock-data.ts#L22
import { rest, setupWorker } from 'msw';
let worker;
export async function setupMockData() {
if (!worker) {
worker = setupWorker();
await worker.start();
// artificial timeout "just in case" worker takes a bit to boot
await new Promise((resolve) => setTimeout(resolve, 1000));
worker.printHandlers();
}
let data = [
{ id: '1', type: 'blogs', attributes: { name: `name:1` } },
{ id: '2', type: 'blogs', attributes: { name: `name:2` } },
{ id: '3', type: 'blogs', attributes: { name: `name:3` } },
];
worker.use(
rest.get('/blogs', (req, res, ctx) => {
let id = req.url.searchParams.get('q[id]');
if (id) {
let record = data.find((datum) => datum.id === id);
return res(ctx.json({ data: record }));
}
return res(ctx.json({ data }));
}),
rest.get('/blogs/:id', (req, res, ctx) => {
let { id } = req.params;
let record = data.find((datum) => datum.id === id);
if (record) {
return res(ctx.json({ data: record }));
}
return res(
ctx.status(404),
ctx.json({ errors: [{ status: '404', detail: 'Blog not found' }] })
);
})
);
}
Docs for msw: https://mswjs.io/

Using fetch inside an action within my component

I'm curious about how I could implement this, I'd like to not hit this API every time the page loads on the route, but would rather start the call on an action (I suppose this action could go anywhere, but it's currently in a component). I'm getting a server response, but having trouble getting this data inside my component/template. Any ideas? Ignore my self.set property if I'm on the wrong track there....Code below..Thanks!
import Component from '#ember/component';
export default Component.extend({
res: null,
actions: {
searchFlight(term) {
let self = this;
let url = `https://test.api.amadeus.com/v1/shopping/flight-offers?origin=PAR&destination=LON&departureDate=2018-09-25&returnDate=2018-09-28&adults=1&travelClass=BUSINESS&nonStop=true&max=2`;
return fetch(url, {
headers: {
'Content-Type': 'application/vnd.amadeus+json',
'Authorization':'Bearer JO5Wxxxxxxxxx'
}
}).then(function(response) {
self.set('res', response.json());
return response.json();
});
}
}
});
Solved below...
import Component from '#ember/component';
export default Component.extend({
flightResults: null,
actions: {
searchFlight(term) {
let self = this;
let url = `https://test.api.amadeus.com/v1/shopping/flight-offers?origin=PAR&destination=LON&departureDate=2018-09-25&returnDate=2018-09-28&adults=1&travelClass=BUSINESS&nonStop=true&max=2`;
return fetch(url, {
headers: {
'Content-Type': 'application/vnd.amadeus+json',
'Authorization':'Bearer xxxxxxxxxxxxxx'
}
}).then(function(response) {
return response.json();
}).then(flightResults => {
this.set('flightResults', flightResults);
});
}
}
});
You might find ember-concurrency to be useful in this situation. See the example of "Type-ahead search", modified for your example:
const DEBOUNCE_MS = 250;
export default Controller.extend({
flightResults: null;
actions: {
searchFlight(term) {
this.set('flightResults', this.searchRepo(term));
}
},
searchRepo: task(function * (term) {
if (isBlank(term)) { return []; }
// Pause here for DEBOUNCE_MS milliseconds. Because this
// task is `restartable`, if the user starts typing again,
// the current search will be canceled at this point and
// start over from the beginning. This is the
// ember-concurrency way of debouncing a task.
yield timeout(DEBOUNCE_MS);
let url = `https://test.api.amadeus.com/v1/shopping/flight-offers?origin=PAR&destination=LON&departureDate=2018-09-25&returnDate=2018-09-28&adults=1&travelClass=BUSINESS&nonStop=true&max=2`;
// We yield an AJAX request and wait for it to complete. If the task
// is restarted before this request completes, the XHR request
// is aborted (open the inspector and see for yourself :)
let json = yield this.get('getJSON').perform(url);
return json;
}).restartable(),
getJSON: task(function * (url) {
let xhr;
try {
xhr = $.getJSON(url);
let result = yield xhr.promise();
return result;
// NOTE: could also write this as
// return yield xhr;
//
// either way, the important thing is to yield before returning
// so that the `finally` block doesn't run until after the
// promise resolves (or the task is canceled).
} finally {
xhr.abort();
}
}),
});

ember: promise for a controller action that calls a service method

I have an action on my controller that calls a service method. The service method is an ember-data query. I need to return a message contained in that ember-data payload back to the controller (print it to the screen).
I am having a hard time figuring out how to get the controller action (function) to "wait" for the service method to finish.
The controller action:
// controller action
processCoupon() {
// the service method I want to wait for the response for
let messageObject = DS.PromiseObject.create({
promise: this.get('cart').processCoupon()
});
// the message
messageObject.then(response => {
let promo_message = messageObject.get('promo_message');
if (promo_message.message && promo_message.alert) {
if (!promo_message.success) {
// show error message in alert with ember-cli-notifcations
} else {
// show success message in alert with ember-cli-notifcations
}
}
});
},
Method in the service I want to wait for the response for:
// service method syncs cart info (containing promo) with the backend
// promo_message is in the response payload
processCoupon() {
return this.get('store').findRecord('cart', get(this, 'cartObj.id')).then(cart => {
cart.save().then(newCart => {
set(this, 'cartObj', newCart); // sets response to property on service
return newCart.get('promo_message');
});
});
},
the 'response' in the promise is empty, and the MessageObject itself has no content. So I'm doing something wrong here (and it's likely misunderstanding promises).
I messed around with RSVP promises and didn't do well there either. What am I missing, OR is there a better way to do this?
Your service method should return a promise. Then you can use it like this: this.get('cart').process().then((response) => {/*your code working with service's response*/});
You also should be aware that if you use ember data, it will return a model instance, not an original response from your back-end.
And, in order to return promise, you need to wrap your service's method in new Promise((resolve, reject) => {/*asynchronous code here*/});
I've done something similar with Ember.RSVP.Promise() is that what you want?
// controller
myMessage = null,
actions:
cartCoupon() {
let msg = this.get('cart').processCoupon();
msg.then(myDataMessage => this.set('myMessage', myDataMessage);
}
//service
processCoupon() {
return new Ember.RSVP.Promise( resolve => {
let data = this.get('store').findRecord('cart', get(this, 'cartObj.id')).then(cart => {
cart.save().then(newCart => {
set(this, 'cartObj', newCart); // sets response to property on service
return newCart.get('promo_message');
});
});
resolve(data);
});
}

How to continue even if Ember.js model hook doesn't load all promises?

I'm loading a route. Its model hook loads some models. Some are fetch from ember store and some are promises requested through AJAX:
model: function () {
return Em.RSVP.hash({
//the server data might not be loaded if user is offline (application runs using appcache, but it's nice to have)
someServerData: App.DataService.get(),
users: this.store.find('user')
});
}
The App.DataService.get() is defined as:
get: function () {
return new Ember.RSVP.Promise(function(resolve, reject) {
//ajax request here
});
}
Obviously if the request is rejected, the flow is interrupted and I cannot display the page at all.
Is there a way to overcome this?
Ember.RSVP.hashSettled is exactly meant for this purpose.
From tildeio/rsvp.js Github repository:
hashSettled() work exactly like hash(), except that it fulfill with a hash of the constituent promises' result states. Each state object will either indicate fulfillment or rejection, and provide the corresponding value or reason. The states will take one of the following formats:
{ state: 'fulfilled', value: value }
or
{ state: 'rejected', reason: reason }
Here is an example for using it (working JS Bin example):
App.IndexRoute = Ember.Route.extend({
fallbackValues: {
firstProperty: null,
secondProperty: null
},
model: function() {
var fallbackValues = this.get('fallbackValues');
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.RSVP.hashSettled({
firstProperty: Ember.RSVP.Promise.resolve('Resolved data despite error'),
secondProperty: (function() {
var doomedToBeRejected = $.Deferred();
doomedToBeRejected.reject({
error: 'some error message'
});
return doomedToBeRejected.promise();
})()
}).then(function(result) {
var objectToResolve = {};
Ember.keys(result).forEach(function(key) {
objectToResolve[key] = result[key].state === 'fulfilled' ? result[key].value : fallbackValues[key];
});
resolve(objectToResolve);
}).catch(function(error) {
reject(error);
});
});
}
});
fallbackValues can be useful for managing resolved hash's properties' fallback values without using conditions inside the promise function.
Taking into account that Ember.RSVP.hashSettled is not available in my Ember version. I come up with the following solution:
model: function(params) {
var self = this;
return new Em.RSVP.Promise(function(resolve, reject){
// get data from server
App.DataService.get().then(function(serverData) { //if server responds set it to the promise
resolve({
serverData: serverData,
users: self.store.find('user')
});
}, function(reason){ //if not ignore it, and send the rest of the data
resolve({
users: self.store.find('user')
});
});
});
}

Rejecting / Resolving a promise from outside its body

I have a need to reject a promise from outside its body, to handle the case of the user that wanted to cancel the action.
Here, I need to start several uploads at the same time, by calling #start on every queued uploads.
The class that manages the uploads queue then stores all the promises and uses Ember.RSVP.all to handle when all the promises have resolved or one has rejected. This works fine.
Now, I would like to cancel the upload
App.Upload = Ember.Object.extend({
start: function() {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
self.startUpload() // Async upload with jQuery
.then(
function(resp) { resolve(resp) },
function(error) { reject(error) }
);
});
},
cancel: function() {
this.get('currentUpload').cancel() // Works, and cancels the upload
// Would like to reject the promise here
},
startUpload: function() {
return this.set('currentUpload', /* some jqXHR that i build to upload */)
}
});
I have thought of many ways to handle it, but I don't found any method like myPromise.reject(reason).
So what I did, is to store the reject function in the Upload instance and call it from my cancel method, like this :
App.Upload = Ember.Object.extend({
start: function() {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
/* Store it here */
self.set('rejectUpload', reject);
/* ------------- */
self.startUpload() // Async upload with jQuery
.then(
function(resp) { resolve(resp) },
function(error) { reject(error) }
);
});
},
cancel: function() {
this.get('currentUpload').cancel() // Works, and cancels the upload
/* Reject the promise here */
var reject;
if (reject = this.get('rejectUpload')) reject();
/* ----------------------- */
},
startUpload: function() {
return this.set('currentUpload', /* some jqXHR that i build to upload */)
}
});
This sound a bit dirty to me, and I'd like to know if there was a better way to make this.
Thanks for your time !
var deferred = Ember.RSVP.defer();
deferred.resolve("Success!");
deferred.reject("End of the world");
To access the promise (for thening etc)
deferred.promise.then(function(){
console.log('all good');
},function(){
console.log('all bad');
});