I want to implement fetch method by myself with promises with builder pattern, the backend is fine. I tested it with postman and it works fine. I don't know what could be, I tried everything but I don't get any data.
I think the problem is that it doesn't properly transform my data to JSON or something.
Any help will be appreciated.
class Fetch {
constructor(){
this.url = null;
this.result = null;
this.method = null;
this.header = null;
this.body = null;
}
call(){
return new Promise((resolve, reject) => {
fetch(this.url,
{method: this.method, header: this.header, body: this.body})
.then(response => response.json())
.then(data => {
this.result = data;
console.log(data);
resolve(data);
})
})
}
}
class FetchBuilder {
constructor(){
this.customFetch = new Fetch();
}
request(url){
this.flag = true;
this.customFetch.url = url;
return this;
}
method(method){
this.customFetch.method = method;
return this;
}
header(header){
this.customFetch.header = header;
return this;
}
body(body){
if(!this.flag){
this.customFetch.body = JSON.stringify(body);
}
return this;
}
query(obj){
}
send(){
this.customFetch.call();
}
}
const fetchObj = new FetchBuilder();
fetchObj.request('https://node-app-test-picsart.herokuapp.com/signin')
.method('POST')
.header({
'Content-Type': 'application/json;charset=utf-8'
})
.body({
email: 'bro#gmail.com',
password: 'bro'
})
.send()
Header:
Body
Your issue lies in either the this.flag = true or if (!this.flag) { line what causes the body argument passed in the .body() method to be lost. So you're code is doing what you want but just not in the right way, as you are expecting a package to be sent to the endpoint.
So either change the boolean:
...
request(url) {
this.flag = false;
this.customFetch.url = url;
return this;
}
...
Or the condition checking the boolean. The rest of your code works as expected.
...
body(body) {
if (this.flag) {
this.customFetch.body = JSON.stringify(body);
}
return this;
}
...
Run the code below with the network tab in your developer tools opened to see that a request is made with the body you want to send.
class Fetch {
constructor() {
this.url = null;
this.result = null;
this.method = null;
this.header = null;
this.body = null;
}
call() {
return fetch(this.url, {
method: this.method,
header: this.header,
body: this.body
})
.then(response => response.json())
.then(data => {
this.result = data;
console.log(data);
return data;
})
}
}
class FetchBuilder {
constructor() {
this.customFetch = new Fetch();
}
request(url) {
this.flag = true;
this.customFetch.url = url;
return this;
}
method(method) {
this.customFetch.method = method;
return this;
}
header(header) {
this.customFetch.header = header;
return this;
}
body(body) {
if (this.flag) {
this.customFetch.body = JSON.stringify(body);
}
return this;
}
query(obj) {}
send() {
this.customFetch.call();
}
}
const fetchObj = new FetchBuilder();
fetchObj.request('https://node-app-test-picsart.herokuapp.com/signin')
.method('POST')
.header({
'Content-Type': 'application/json;charset=utf-8'
})
.body({
email: 'bro#gmail.com',
password: 'bro'
})
.send()
In your Fetch().call() just return the fetch as it already returns a Promise, wrapping it like this would be an anti-pattern. You could also use the async / await syntax.
Related
I'm not a master in JavaScript. I'm starting use the OOP syntax and I have a small class like this one:
class Scraper {
constructor() {}
question(inputText) {
rl.setPrompt(inputText);
rl.prompt();
return new Promise( (resolve, reject) => {
let answer;
rl.on('line', (input) => {
answer = input;
rl.close();
});
rl.on('close', () => {
resolve(answer);
});
})
}
startFetch(username) {
this.username = String(username);
return axios({
url: `https://www.instagram.com/${this.usernamee}/?__a=1`
}).then( (response) => {
//response.data.graphql.user);
this.user_id = response.data.graphql.user.id;
//totalMedia = response.data.graphql.user.edge_owner_to_timeline_media.count
this.has_next_page = response.data.graphql.user.edge_owner_to_timeline_media.page_info.has_next_page;
response.data.graphql.user.edge_owner_to_timeline_media.edges.map( (item, index) => {
this.processLink(item.node.display_url, index);
});
if( this.has_next_page ){
this.currCursor = response.data.graphql.user.edge_owner_to_timeline_media.page_info.end_cursor;
this.fetchNextPage(this.user_id);
} else {
console.log('Completed');
}
//let nodes = response.data.graphql.user.edge_owner_to_timeline_media.edges.length;
});
}
fetchNextPage(id) {
return axios({
method: 'GET',
baseURL: 'https://www.instagram.com/graphql/query/',
params: {
query_hash: '42323d64886122307be10013ad2dcc44',
variables: {
id: id,
first: "12",
after: this.currCursor
}
}
}).then( (response) => {
//console.log(response.data.data.user.edge_owner_to_timeline_media.edges[0].node)
//totalMedia = response.data.data.user.edge_owner_to_timeline_media.count
response.data.data.user.edge_owner_to_timeline_media.edges.map( (item, index) => {
index++;
this.processLink(item.node.display_url, index);
});
if( response.data.data.user.edge_owner_to_timeline_media.page_info.has_next_page ){
this.currCursor = response.data.graphql.user.edge_owner_to_timeline_media.page_info.end_cursor;
}
});
}
processLink(imageURI, n) {
let filename = path.format({dir: destinationPath, base: `${n}.jpg`});
let file = fs.createWriteStream(filename);
https.get(imageURI, (res) => {
res.pipe(file);
});
}
}
How I can set a variable inside a method and then share it and overwrite if needed inside another method of the same class? The class I'm creating is responsible to scrape an Instagram public profile. After the first request, as suggested to me here I'm calling the fetchNextPage method. To make it works as expected I need to set the currCursor variable and maintain it updated with the new cursors after each request. will the this.user_id be set and called from the method, and the this.currCursor will be updated with new value?
I have the following code.
public async getOrderInforAsync(customerOrderId) {
return new Promise((resolve, reject) => {
this.orderEntryService.getCommissionIncentives(customerOrderId)
.subscribe(
response => {
Object.assign(this.order, response);
this.order.bookingDate = this.order.bookingDate ? new Date(this.order.bookingDate) : null;
this.order.estBillDate = this.order.estBillDate ? new Date(this.order.estBillDate) : null;
this.order.orderSplit.forEach(element => {
element.rcNumberFullName = `${this.order.customerOrderRCNumber}${element.rcNumberSuffix}`;
});
this.initialQuantityAllocated();
this.orderSummary.allocatedEstCommissionPercent = this.calculateTotalOrderPercent();
this.orderSummary.allocatedEstCommissionAmount = this.calculateTotalOrderAmount();
this.highlight = this.checkOrderSummary(this.orderSummary.allocatedEstCommissionPercent, this.orderSummary.allocatedEstCommissionAmount);
this.calculateAllocatedActualPercent();
this.calculateAllocatedActualAmount();
this.onChangeEstSalesPrice();
resolve();
},
error => {
reject();
}
);
});
}
Sometimes the resolve() is called before this.calculateAllocatedActualPercent() and this.calculateAllocatedActualAmount() are done.
So how to make this code run synchronized, it means all functions on this block code had done before resolve() called?
Try this :
public async getOrderInforAsync(customerOrderId) {
return new Promise((resolve, reject) => {
this.orderEntryService.getCommissionIncentives(customerOrderId)
.subscribe(
response => {
Object.assign(this.order, response);
this.order.bookingDate = this.order.bookingDate ? new Date(this.order.bookingDate) : null;
this.order.estBillDate = this.order.estBillDate ? new Date(this.order.estBillDate) : null;
this.order.orderSplit.forEach(element => {
element.rcNumberFullName = `${this.order.customerOrderRCNumber}${element.rcNumberSuffix}`;
});
this.initialQuantityAllocated();
this.orderSummary.allocatedEstCommissionPercent = this.calculateTotalOrderPercent();
this.orderSummary.allocatedEstCommissionAmount = this.calculateTotalOrderAmount();
this.highlight = this.checkOrderSummary(this.orderSummary.allocatedEstCommissionPercent, this.orderSummary.allocatedEstCommissionAmount);
await this.calculateAllocatedActualPercent();
await this.calculateAllocatedActualAmount();
this.onChangeEstSalesPrice();
resolve();
},
error => {
reject();
}
);
});
}
async calculateAllocatedActualPercent(){
return new Promise(resolve,reject){
// call api
if (data)
resolve(data);
else
reject()
}
}
async calculateAllocatedActualAmount(){
return new Promise(resolve,reject){
// call api
if (data)
resolve(data);
else
reject()
}
}
An async function returns a Promise. And to declare a function as async, you need to have an await call inside it.
Change your observable returned from this.orderEntryService.getCommissionIncentives(customerOrderId) .toPromise() and then await it.
Try this:
public async getOrderInforAsync(customerOrderId) {
try {
const response = await this.orderEntryService
.getCommissionIncentives(customerOrderId).toPromise();
this.order = { ...response };
this.order.bookingDate = this.order.bookingDate ? new Date(this.order.bookingDate) : null;
this.order.estBillDate = this.order.estBillDate ? new Date(this.order.estBillDate) : null;
for (let i = 0; i < this.order.orderSplit; i++) {
const element = this.order.orderSplit[i];
element.rcNumberFullName = `${this.order.customerOrderRCNumber}${element.rcNumberSuffix}`
}
this.initialQuantityAllocated();
this.orderSummary.allocatedEstCommissionPercent = this.calculateTotalOrderPercent();
this.orderSummary.allocatedEstCommissionAmount = this.calculateTotalOrderAmount();
this.highlight = this.checkOrderSummary(this.orderSummary.allocatedEstCommissionPercent, this.orderSummary.allocatedEstCommissionAmount);
this.calculateAllocatedActualPercent();
this.calculateAllocatedActualAmount();
this.onChangeEstSalesPrice();
return response;
} catch (error) {
return error;
}
}
I want to cancel the request if there's no token, so I do like this:
instance.interceptors.request.use(config => {
if (!getToken()) {
console.log("interceptors: no access token");
} else {
config.headers.Authorization = "Bearer " + getToken().accessToken;
return config;
}
});
But in negative scenario there's an error TypeError: Cannot read property 'cancelToken' of undefined.
You cannot use the token inside the interceptors but instead throw Cancel
axios.interceptors.response.use(function (response) {
throw new axios.Cancel('Operation canceled by the user.');
}, function (error) {
return Promise.reject(error);
});
Refer to this post:
https://github.com/axios/axios/issues/583
Axios v0.22.0 and higher
As per the documentation, cancellation is now pretty straightforward with the AbortController class
instance.interceptors.request.use(config => {
/* some logic */
const controller = new AbortController();
if (needToCancelRequest) {
controller.abort();
}
return {
...config,
signal: controller.signal
};
});
Browser Compatibility
You might be tempted to do a pretty concise signal: AbortSignal.abort() instead. Please, note that it is much less supported than the solution above. See AbortSignal.abort() vs new AbortController().abort() compatibility.
Axios before v0.22.0
This is a solution taken from the axios issue on github
instance.interceptors.request.use(config => {
/* some logic */
return {
...config,
cancelToken: new CancelToken((cancel) => {
if (needToCancelRequest) {
cancel('Cancel repeated request')
}
})
};
});
So for whatever reason none of these answers worked for me. Here is what did.
axiosInstance.interceptors.request.use(
function (config) {
const controller = new AbortController();
const cfg = {
...config,
signal: controller.signal,
};
controller.abort('We gotta cancel this');
return cfg;
},
function (error) {
return Promise.reject(error);
},
);
Thing I learned from this: AbortController is native to javascript/node.
I have implemented this in this way. I am not sure if this is the best solution, but for my use case is useful.
My idea is not to cancel the last request. I would like to cancel previous requests to the same endpoint, and let the last one to do his job. For that reason I keep track of the request that are being executed.
// I keep track of the current requests that are being executed
const currentExecutingRequests = {};
axios.interceptors.request.use(
(req) => {
let originalRequest = req;
if (currentExecutingRequests[req.url]) {
const source = currentExecutingRequests[req.url];
delete currentExecutingRequests[req.url];
source.cancel();
}
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
originalRequest.cancelToken = source.token;
currentExecutingRequests[req.url] = source;
// here you could add the authorization header to the request
return originalRequest;
},
(err) => {
return Promise.reject(err);
}
);
axios.interceptors.response.use(
(response) => {
if (currentExecutingRequests[response.request.responseURL]) {
// here you clean the request
delete currentExecutingRequests[response.request.responseURL];
}
return response;
},
(error) => {
const { config, response } = error;
const originalRequest = config;
if (axios.isCancel(error)) {
// here you check if this is a cancelled request to drop it silently (without error)
return new Promise(() => {});
}
if (currentExecutingRequests[originalRequest.url]) {
// here you clean the request
delete currentExecutingRequests[originalRequest.url];
}
// here you could check expired token and refresh it if necessary
return Promise.reject(error);
}
);
As of Axios v0.22.0 an AbortSignal is the recommended way to cancel from a request interceptor.
axios.interceptors.request.use(
(requestConfig) => {
/* some logic */
return {
...requestConfig,
signal: AbortSignal.abort()
};
}
},
(error) => {
return Promise.reject(error);
}
);
#Kirill Taletski's answer solve this perfectly, but add one line:
const CancelToken = Axios.CancelToken;
then ,it gonna be like this :
instance.interceptors.request.use(config => {
/* some logic */
const CancelToken = Axios.CancelToken;
return {
...config,
cancelToken: new CancelToken((cancel) => cancel('Cancel repeated request'))
};
});
here is the solution
import axios from 'axios';
const CancelToken = axios.CancelToken;
let cancel;
axios.interceptors.request.use((config) => {
if (cancel) {
cancel(); // cancel request
}
config.cancelToken = new CancelToken(function executor(c)
{
cancel = c;
})
return config
}, function (error) {
return Promise.reject(error)
});
My solution based on https://stackoverflow.com/a/64228288/2051938
axios.ts
const axiosInstance = axios.create({ baseURL: apiBaseUrl });
axiosInstance.interceptors.request.use(
req => {
const originalRequest = req;
const cancelUniqId = (originalRequest.cancelToken as unknown) as string;
if (Object.hasOwnProperty.call(currentExecutingRequests, cancelUniqId)) {
const source = currentExecutingRequests[cancelUniqId];
delete currentExecutingRequests[cancelUniqId];
source.cancel();
}
if (cancelUniqId) {
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
originalRequest.cancelToken = source.token;
currentExecutingRequests[cancelUniqId] = source;
}
return originalRequest;
},
err => {
return Promise.reject(err);
}
);
axiosInstance.interceptors.response.use(
response => {
for (const key of Object.keys(currentExecutingRequests)) {
if (currentExecutingRequests[key].token === response.config.cancelToken) {
delete currentExecutingRequests[key];
break;
}
}
return response;
},
error => {
const { response } = error;
if (axios.isCancel(error)) {
return new Promise(() => {
//
});
}
for (const key of Object.keys(currentExecutingRequests)) {
if (currentExecutingRequests[key].token === response.config.cancelToken) {
delete currentExecutingRequests[key];
break;
}
}
return Promise.reject(error);
}
);
export { axiosInstance };
Usage:
axiosInstance.request({
url: "some/req/path",
method: "POST",
params: {...},
data: {...},
cancelToken: "someUniqRequestID" // <-- IMPORTANT!
})
as a result, all requests with someUniqRequestID token will be cancelled when previous request with SAME cancelToken was not finished before.
This works for me for axios 0.20.0:
const interceptorRequestConfig = (config) => {
if (sourceRequest[config.url]) {
sourceRequest[config.url].cancel('Automatic cancellation')
}
const axiosSource = axios.CancelToken.source()
sourceRequest[config.url] = { cancel: axiosSource.cancel }
config.cancelToken = axiosSource.token
return config
}
credit for the idea: https://stackoverflow.com/a/66701130/8840359
I am calling an API which returns results in pages and I am trying to find an 'elegant' way of retrieving them.
Ideally I want to consume them like this:
let results = api.get();
for await (const page of results) {
// do stuff with page
}
I can active close to this using a generator function like this:
class Results {
constructor(url, token) {
this.url = url;
this.token = token;
}
async *page() {
let url = this.url;
while (true) {
const response = await fetch(url, {
headers: { 'Authorization': 'Bearer ' + this.token }
});
const data = await response.json();
yield data.values;
if (!data.next) return;
url = data.next;
}
}
}
And calling it like:
for await (const page of results.page()) {
// do stuff with page
}
I have tried to do it with a [Symbol.iterator] like this, but cannot get it to work:
[Symbol.iterator]() {
let that = this;
return {
next: async function() {
if (!that.page) {
that.page = that.url;
return {done: true};
}
const response = await fetch(that.page, {
headers: { 'Authorization': 'Bearer ' + that.token }
});
const data = await response.json();
that.page = data.data.next;
return {
value: data,
done: false
}
}
}
}
This issue is I need to get the link to the next page from the current page to determine if there is a next page, but as its a promise i cannot access it in the function.
Any ideas how to get the iterator working?
Following advice here is a working function. [Symbol.asyncIterator] made all the difference. Thanks:
[Symbol.asyncIterator]() {
let that = this;
return {
page: that.url,
token: that.token,
next: async function() {
if (!this.page) {
this.page = that.url;
return {done: true};
}
const response = await fetch(this.page, {
headers: { 'Authorization': 'Bearer ' + this.token }
});
const data = await response.json();
this.page = data.next;
return {
value: data,
done: false
}
}
}
Now that its working ideally I just want to be able to iterate through all results and not know about pages so here is a working solution to this for info:
[Symbol.asyncIterator]() {
let that = this;
return {
page: that.url,
token: that.token,
values: [],
next: async function() {
if (!this.page && this.values.length === 0) {
this.page = that.url;
return {done: true};
}
if (this.values.length > 0) {
return {
value: this.values.pop(),
done: false
}
}
const response = await fetch(this.page, {
headers: { 'Authorization': 'Bearer ' + this.token }
});
const data = await response.json();
this.page = data.next;
this.values = data.values;
if (this.values.length === 0) {
return { done: true }
}
return {
value: this.values.pop(),
done: false
}
}
}
This code can be simplified by using an async generator function like so:
async *[Symbol.asyncIterator]() {
let url = this.url;
const getPage = url =>
fetch(url, this.header)
.then(response => response.json())
.then(data => ({
next: data.next,
values: data.values
}));
while(url) {
const page = await getPage(url);
for (const value of page.values) {
yield value;
}
url = page.next;
}
}
So the full class looks like this:
class Response {
constructor(url, token) {
this.url = url;
this.header = {
headers: {
Authorization: `Bearer ${token}`
}
};
}
async* [Symbol.asyncIterator]() {
let url = this.url;
const getPage = url =>
fetch(url, this.header)
.then(response => response.json())
.then(data => ({
next: data.next,
values: data.values
}));
while (url) {
const page = await getPage(url);
for (const value of page.values) {
yield value;
}
url = page.next;
}
}
}
Which allows you to easily loop through paged API results like this:
for await (const item of response) {
...
}
In my store.js, I have a user_data state, its initial method is fetch_user_data:
export default new Vuex.Store({
state: {
user_data: util.fetch_user_data('username')
...
}
in the util.js:
util.fetch_user_data = function(username){
Lml_http('get', Api_urls.productconfig_common.user_data(username), null, response => {
return response.data // there get the data, in the debugger shows it.
}, error => {
})
}
But when I use the state's user_data, it is undefined.
EDIT-1
I want in the store.js to use the fetch_util's method for fetching data, and commit to the state.
EDIT-2
my lml_http code are bellow:
var lml_http = function (method, url, params, success_cb, error_cb, multipart_formdata=undefined) {
var format_to_form_data = function(data){
let formData = new FormData()
for (let item in data) {
formData.append(item, data[item])
}
return formData
}
var lowercase_method = method.toLowerCase()
var formated_params = params
var header_config = null
if (multipart_formdata) {
header_config = {
headers: {
'Content-Type': 'multipart/form-data'
}
}
formated_params = format_to_form_data(formated_params)
}
if(lowercase_method === "get") {
formated_params = {params: formated_params}
if (!header_config) {
Axios.get(url, formated_params).then(response => {
success_cb(response)
return
}).catch(response => {
error_cb(response)
return
})
} else {
Axios.get(url, format_to_form_data(formated_params), header_config).then(response => {
success_cb(response)
return
}).catch(response => {
error_cb(response)
return
})
}
return
}
else {
if(!header_config) {
Axios[method](url, formated_params).then(response => {
success_cb(response)
}).catch(response => {
error_cb(response)
})
return
}else {
Axios[method](url, formated_params, header_config).then(response => {
success_cb(response)
}).catch( response => {
error_cb(response)
})
return
}
}
}
Just create an action to fetch the user data as follows:
export default new Vuex.Store({
state: {
user_data: null
},
mutations: {
setUserData(state, data) {
state.user_data = data;
}
},
actions: {
fetchUserData({ commit }, username) {
Lml_http(
"get",
Api_urls.productconfig_common.user_data(username),
null,
response => {
commit("setUserData", response.data);
},
error => {}
);
}
}
});
Then dispatch this action in the created() hook of theroot vue instance
//main.js
new Vue({
el: "#app",
store,
render: h => h(App),
created() {
this.$store.dispatch("fetchUserData", "username");
}
});
`
Edit:
Axios returns a Promise by default. So in lml_http return Axios calls as below:
var lml_http = function(
method,
url,
params,
success_cb,
error_cb,
multipart_formdata = undefined
) {
var format_to_form_data = function(data) {
let formData = new FormData();
for (let item in data) {
formData.append(item, data[item]);
}
return formData;
};
var lowercase_method = method.toLowerCase();
var formated_params = params;
var header_config = null;
if (multipart_formdata) {
header_config = {
headers: {
"Content-Type": "multipart/form-data"
}
};
formated_params = format_to_form_data(formated_params);
}
if (lowercase_method === "get") {
formated_params = { params: formated_params };
if (!header_config) {
return Axios.get(url, formated_params)
.then(response => {
success_cb(response);
})
.catch(response => {
error_cb(response);
});
} else {
return Axios.get(url, format_to_form_data(formated_params), header_config)
.then(response => {
success_cb(response);
})
.catch(response => {
error_cb(response);
});
}
} else {
if (!header_config) {
return Axios[method](url, formated_params)
.then(response => {
success_cb(response);
})
.catch(response => {
error_cb(response);
});
} else {
return Axios[method](url, formated_params, header_config)
.then(response => {
success_cb(response);
})
.catch(response => {
error_cb(response);
});
}
}
};
Also add a return statement in utils.js
util.fetch_user_data = function(username){
return Lml_http('get', Api_urls.productconfig_common.user_data(username), null, response => {
return response.data // there get the data, in the debugger shows it.
}, error => {
})
}
Now in your action fetchUserData
fetchUserData({ commit }) {
util.fetch_user_data('username').then((
commit("setUserData", response.data);
});
}
In main.js dispatch the action without any payload
//main.js
new Vue({
el: "#app",
store,
render: h => h(App),
created() {
this.$store.dispatch("fetchUserData", "username");
}
});
Because the fetch api is async, it will return a promise, not the response data. I think you may try something like:
const store = new Vuex.Store({
state: {
user_data: null,
...
};
util.fetch_user_data('username')
.then(user => store.user_data = user);
export default store;