I'm using Fetch (Fetch API) in a project and I would like to, for consistence purposes, create a function that receives all the parameters such as method, url and data and creates the correct request, depending if it's a GET or a POST request.
Is it possible, using Fetch, to send a data object that for the GET request, converts data into and string with the parameters and if it is a POST request, it just sends the data object in the body?
It would look like this:
fetch ('/test', {
method: 'GET',
data: {
test: 'test'
}
});
This doubt was inspired by this jQuery ajax behaviour:
$.ajax({
url: '/test',
method: 'GET',
data: {
test: 'test'
}
});
This would produce this request:
'/test/?test=test'
If I pass the data object as normal in the fetch constructor for a
GET request, would it send the request like the example I gave
'/test/?test=test'
If you want to add query string to a fetch request :
From the SPEC
var url = new URL("https://a.com/method"),
params = {a:1, b:2}
Object.keys(params).forEach(key => url.searchParams.append(key, params[key]))
fetch(url)
this will produce a request :
you could either use the Url class:
var url = new URL("/test/")
Object.keys({test: 'test', a: 1}).forEach(key => url.searchParams.append(key, params[key]))
fetch(url);
or parse the string yourself, if you want wider browser support:
var params = {test: 'test', a: 1},
qs = Object.keys(params).reduce(function(_qs, k, i){ return _qs + '&' + k + '=' + params[k]; }, '').substring(1);
console.log(qs)
I'll show you snippets for creating query with and without using URLSearchParams.
The code will be in typescript for the if you're using it. if not, just remove types, it will work in the same way.
Simple solution (URLSearchParams)
/**
* Creates query from given object
* - It doesn't work with deep nesting
* - It doesn't remove empty fields
* #returns `state1=6&state2=horse` without `?`
*/
function createQuery(queryObject?: Record<string | number, unknown> | null): string {
if (queryObject == null) return ""
// Typescript: The `as ...` expression here is ok because `URLSearchParams` will convert non-string by itself
const searchParams = new URLSearchParams(queryObject as Record<string, string>)
return searchParams.toString()
}
Solving problems solution (URLSearchParams)
/**
* Creates query from given object
* - It doesn't work with deep nesting
* - Removes empty fields
* #returns `state1=6&state2=horse` without `?`
*/
function createQuery(queryObject?: Record<string | number, unknown> | null): string {
if (queryObject == null || !Object.keys(queryObject).length) return ""
for (const key in queryObject) {
if (Object.prototype.hasOwnProperty.call(queryObject, key)) {
const value = queryObject[key]
// Use `!value` expression if you want to delete values as `0` (zero) and `""` (empty string) too.
if (value == null) delete queryObject[key]
}
}
const searchParams = new URLSearchParams(queryObject as Record<string, string>)
return searchParams.toString()
}
No URLSearchParams solution
/**
* Creates query from given object
* - Supports prefixes
* - Supports deep nesting
* - Removes empty fields
* #returns `state1=6&state2=horse` without `?`
*/
function createQuery(queryObject?: Record<string | number, unknown> | null, keyPrefix?: string): string {
if (queryObject == null || !Object.keys(queryObject).length) return ""
keyPrefix = keyPrefix ? (keyPrefix + "_") : ""
const queryKeys = Object.keys(queryObject)
const queryArray = queryKeys.map(key => {
const value = queryObject[key]
if (value) {
if (isDictionary(value)) {
return createQuery(value, keyPrefix + key + "_")
}
return keyPrefix + encodeURIComponent(key) + "=" + encodeURIComponent(String(value))
}
return ""
})
return queryArray.filter(Boolean).join("&")
}
isDictionary Helper
I used isDictionary helper here too, you can find it here
Usage
You need to put ? in the beginning of your endpoint plus createQuery
fetch("/test?" + createQuery({ foo: 12, bar: "#user->here", object: { test: "test", bird: { super: { ultra: { mega: { deep: "human" }, shop: 7 } }, multiple: [1, 2, 3] } } }))
Result
foo=12&bar=%40user-%3Ehere&object_test=test&object_bird_super_ultra_mega_deep=human&object_bird_super_ultra_shop=7&object_bird_multiple=1%2C2%2C3
or
foo: 12
bar: #user->here
object_test: test
object_bird_super_ultra_mega_deep: human
object_bird_super_ultra_shop: 7
object_bird_multiple: 1,2,3
Conclusion
We got different snippets you can choose from depending on your goals.
Related
I am using SheetJS in angular to export json as .xlsx file. For reference the json could be as follows:
[{
"ID": "E111",
"Name": "John",
"LastLogin": "2022-02-12"
},
{
"ID": "E112",
"Name": "Jake",
"Score": 22
"LastLogin": "2022-02-12"
}]
Note: The keys to the object are unknown, and can vary. The only known keys are ID and LastLogin.
I am using the following function to export
public exportAsExcelFile(json: any[], excelFileName: string): void {
const worksheet: XLSX.WorkSheet = XLSX.utils.json_to_sheet(json);
console.log('worksheet',worksheet);
const workbook: XLSX.WorkBook = { Sheets: { 'data': worksheet }, SheetNames: ['data'] };
const excelBuffer: any = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
this.saveAsExcelFile(excelBuffer, excelFileName);
}
private saveAsExcelFile(buffer: any, fileName: string): void {
const data: Blob = new Blob([buffer], {
type: EXCEL_TYPE
});
FileSaver.saveAs(data, fileName + '_export_' + new Date().getTime() + EXCEL_EXTENSION);
}
The resulting excel looks like this
I want LastLogin to be the last column no matter the object. Is there a way to achieve this? I am pretty new to this, so any help is appreciated.
The behaviour of SheetJS here is to take the order of column headers for the Excel data from the first row, and then as new object keys are encountered then for the matching row header to be added at the end.
To control this behaviour to get the output formatted the way you want, you can process the input json before calling XLSX.utils.json_to_sheet.
Define this function:
function restructureObjectForSheet(obj) {
// for each object in the array put the keys in a new array
// flatten that array
// there will be duplicate names which can be removed with Set
// turn it back into an array
const uniqKeys = Array.from(new Set(obj.map(o => Object.keys(o)).flat()));
// remove LastLogin from this array
// then put LastLogin at the end of the array
const endKey = "LastLogin";
const rowHeaders = uniqKeys.filter(k => k !== endKey).concat(endKey);
// process the original data into a new array
// first entry will define row headers in Excel sheet
const newData = obj.map(o => {
return rowHeaders.reduce((a, c) => {a[c] = o[c] ; return a}, {});
});
return newData;
}
I've commented the code, but the essential features are:
get an array of all the unique keys across all the objects in your input array (your json variable)
ensure LastLogin is the last element of the array
create one new object per input object and where the original data does not have the property (e.g. Score) then the value is undefined
Now, in your exportAsExcelFile method, just make this adjustment before the 1st line:
const newJson = restructureObjectForSheet(json);
const worksheet: XLSX.WorkSheet = XLSX.utils.json_to_sheet(newJson );
haven't used graphql or mongodb previously. What is the proper way to pass objects for the update mutation?
Since the only other way i see to pass multiple dynamically appearing parameters is to use input type which is appears to be a bit ineffective to me (in terms of how it looks in the code, especially with bigger objects), i just pass the possible values themselves. however in this case i need to dynamically construct updateObject, which again, going to get messy for the bigger models.
for example now i did:
Mutation: {
updateHub: async (_, { id, url, ports, enabled }) => {
const query = {'_id': id};
const updateFields = {
...(url? {url: url} : null),
...(ports? {ports: ports} : null),
...(enabled? {enabled: enabled} : null)
};
const result = await HubStore.findByIdAndUpdate(query, updateFields);
return {
success: !result ? false : true,
message: 'updated',
hub: result
};
}
}
any advise on the better way to handle this?
thanks!
It appears your code could benefit from using ES6 spread syntax -- it would permit you to deal with an arbitrary number of properties from your args object without the need for serial tertiary statements.
Mutation: {
updateHub: async (_, { id, ...restArgs } ) => {
const query = {'_id': id};
const updateFields = { ...restArgs };
const result = await HubStore.findByIdAndUpdate(query, updateFields);
return {
success: !result ? false : true,
message: 'updated',
hub: result
};
}
}
If for some reason you need to explicitly set the undefined properties to null in your object, you could possibly use some a config obj and method like defaults from the lodash library as shown below:
import { defaults } from 'lodash';
const nullFill = { url: null, ports: null, enabled: null }; // include any other properties that may be needed
Mutation: {
updateHub: async (_, { id, ...restArgs } ) => {
const query = {'_id': id};
const updateFields = defaults(restArgs, nullFill);
const result = await HubStore.findByIdAndUpdate(query, updateFields);
return {
success: !result ? false : true,
message: 'updated',
hub: result
};
}
}
Also, FWIW, I would consider placing the dynamic arguments that could be potentially be updated on its own input type, such as HubInput in this case, as suggested in the graphql docs. Below I've shown how this might work with your mutation. Note that because nothing on HubInput is flagged as requird (!) you are able to pass a dynamic collection of properties to update. Also note that if you take this appraoch you will need to properly destructure your args object initially in your mutation, something like { id, input }.
input HubInput {
url: String
ports: // whatever this type is, like [String]
enabled: Boolean
// ...Anything else that might need updating
}
type UpdateHubPayload {
success: Boolean
message: String
hub: Hub // assumes you have defined a type Hub
}
updateHub(id: Int, input: HubInput!): UpdateHubPayload
How to add indexes to array in query string?
I tried send data like this:
axios.get('/myController/myAction', { params: { storeIds: [1,2,3] })
And I got this url:
http://localhost/api/myController/myAction?storeIds[]=1&storeIds[]=2&storeIds[]=3
So, I should to get this url:
http://localhost/api/myController/myAction?storeIds[0]=1&storeIds[1]=2&storeIds[2]=3
What I should add in my params options to get this url?
You can use paramsSerializer and serialize parameters with https://www.npmjs.com/package/qs
axios.get('/myController/myAction', {
params: {
storeIds: [1,2,3]
},
paramsSerializer: params => {
return qs.stringify(params)
}
})
Without having to add more libraries and using ES6 you could write:
axios.get(`/myController/myAction?${[1,2,3].map((n, index) => `storeIds[${index}]=${n}`).join('&')}`);
Thanks so much the answer from Nicu Criste, for my case, the API requires params like this:
params: {
f: {
key: 'abc',
categories: ['a','b','c']
},
per_page: 10
}
Method is GET and this API requires the format is: API?f[key]=abc&f[categories][]=a&f[categories][]=b...
So I assigned the paramsSerializer of axios like this:
config.paramsSerializer = p => {
return qs.stringify(p, {arrayFormat: 'brackets'})
}
Install qs please go to this link
Read more about paramsSerializer in axios document
Edit format of params: Read more at qs stringifying document
In my case, I use ES6 array function.
array element make querystring use reduce function.
Object array also works.
const storeIds = [1,2,3]
axios.get('some url', {
params: {
storeIds: storeIds.reduce((f, s) => `${f},${s}`)
}
})
In my case, I am using someting like this
const params = array.map((v)=>{
return `p=${v}&`
})
Only concat params.join('') to the URL where you get data:
`url_to_get?${params.join('')`
In my back-end in ASP.net I receive this
[FromUri] string [] p
This answer is inspired by #Nicu Criste's answer.
But might be not related to the posted question.
The following code was used to generate the query params with repetitive keys which had been supplied with an object array.
Note: If you are a developer with bundlephobia, use the following approach with care: as with UrlSearchParams support varies on different browsers and platforms.
const queryParams = [{key1: "value1"}, {key2: "value2"}]
axios.get('/myController/myAction', {
params: queryParams,
paramsSerializer: params => {
return params.map((keyValuePair) => new URLSearchParams(keyValuePair)).join("&")
}
})
// request -> /myController/myAction?key1=value1&key2=value2
I rewrote the existing paramSerializer shipped in axios. The following snippet does the same serialization while putting indices between square brackets. I tried qs but it is not compatible with my python connexion backend (for JSON string parameters).
const rcg = axios.create({
baseURL: `${url}/api`,
paramsSerializer: params => {
const parts = [];
const encode = val => {
return encodeURIComponent(val).replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')
.replace(/%20/g, '+')
.replace(/%5B/gi, '[')
.replace(/%5D/gi, ']');
}
const convertPart = (key, val) => {
if (val instanceof Date)
val = val.toISOString()
else if (val instanceof Object)
val = JSON.stringify(val)
parts.push(encode(key) + '=' + encode(val));
}
Object.entries(params).forEach(([key, val]) => {
if (val === null || typeof val === 'undefined')
return
if (Array.isArray(val))
val.forEach((v, i) => convertPart(`${key}[${i}]`, v))
else
convertPart(key, val)
})
return parts.join('&')
}
});
I got using "paramSerializer" a bit confuse. Before looking for the "right way" to use axios with array querystring on Google, I did following and got working:
var options = {};
var params = {};
for(var x=0;x<Products.length;x++){
params[`VariableName[${x}]`] = Products[x].Id;
}
options.params = params;
axios.get(`https://someUrl/`, options)...
It is going to create querystring parameters like:
VariableName[0]=XPTO,VariableName[1]=XPTO2
which the most webservers expected as array format
I know that this approach is not very good and I don't know the downsides it may have, but i tried this and it worked:
before making the request, prepare the params:
let params = '?';
for (let i = 0; i < YOUR_ARRAY.length; i++) { // In this case YOUR_ARRAY == [1, 2, 3]
params += `storeIds=${YOUR_ARRAY[i]}`; // storeIds is your PARAM_NAME
if (i !== YOUR_ARRAY.length - 1) params += '&';
}
And then make the request like so:
axios.get('/myController/myAction' + params)
In React I needed to use axios with a params in array. This was query param:
"fields[0]=username&fields[1]=id&populate[photo][fields][0]=url&populate[job][fields][1]=Job"
to send with axios, for that I installed by CLI
npm install qs Read more about qs
and declared
const qs = require('qs');
after
const query = qs.stringify({
fields: ['username', 'id'],
populate: {
photo: {
fields: ['url']
},
job: {
fields: ['Job']
}
}
}, {
encodeValuesOnly: true
});
and finally I called the axios like this:
axios.create({
baseURL: "http://localhost:1337/api/",
}).get(`/users?${query}`) // this parameter show all data
.then((response) => console.log(response.data))
.catch((err) => {
setError(err);
});
Basically, reading from docs https://axios-http.com/docs/req_config
paramsSerializer is an optional function, which we should use if the default serialization of params done by axios is not as expected. We can use serialization libraries (which I feel is best approach) to serialize in the params in the paramsSerializer function as per our needs.
Let's see an example.Suppose params is like ...
{
params: {
delay: 1,
ar:[1,2,3]
}
}
then you will get queryString like this ?delay=1&ar[]=1&ar[]=2&ar[]=3 when you make the request, but you might want like this
?delay=1&ar[0]=1&ar[1]=2&ar[2]=3 so in order to get query string as per our format. we can use qs https://www.npmjs.com/search?q=qs library and serialize our params in the paramsSerializer function as below
{
method: "GET",
params: {
delay: 1,
ar:[1,2,3]
},
paramsSerializer: (params) => {
return qs.stringify(params,{
encodeValuesOnly: true
});
}
},
This work it for me:
axios.get("/financeiro/listar",{
params: {
periodo: this.filtro.periodo + "",
mostrarApagados: this.filtro.mostrarApagados,
mostrarPagos: this.filtro.mostrarPagos,
categoria: this.filtro.categoria,
conta: this.filtro.conta
}
})
This was better for me:
axios.get('/myController/myAction', {
params: { storeIds: [1,2,3] + ''}
})
In my case, there was already jQuery implemented into my codebase. So I just used the predefined method.
jQuery.param(Object)
I'm using Angular 5 with HttpInterceptors.
I already know I can get each value I want from HttpParams via several methods.
Also - If I want to see all values , I can use the .toString() method
params = new HttpParams()
.set('page', '2')
.set('sort', 'name');
console.log(params.toString()); //Returns page=2&sort=name
But in my case I send json objects as parameters :
{
a:1 , b:[1,2,3] , c:[{...}]
}
I'm using interceptors to log the request parameters , but when I JSON.stringify(req.Params) , I get :
Params={
"updates": null,
"cloneFrom": null,
"encoder": {},
"map": {}
}
Which doesn't expose the values.
I don't want to see the parameters as a regular form post parameters -( it will be very unclear), but as an object as I've sent it.
Question:
How can I extract the parameters from the request object in the interceptor , but as json format :
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
{
JSON.stringify( req.params) // <--- ?? doesn't yield the params.
}
If you don't like method toString() that returns an encoded string, where key-value pairs (separated by =) are separated by &s you can write your own method that will transform data stored in Map in some data you want to use.
For example:
const params = new HttpParams()
.set('page', '2')
.set('sort', 'name');
const paramsArray = params.keys().map(x => ({ [x]: params.get(x) }));
console.log(JSON.stringify(paramsArray));
It's similar to the approach that is used in toString method
https://github.com/angular/angular/blob/master/packages/common/http/src/params.ts#L177-L186
Ng-run Example
What you actually requested was a JSON Object. So this is how you get such an object:
const params = new HttpParams()
.set('page', '2')
.set('sort', 'name');
const paramsObject = params.keys().reduce((object, key) => {
object[key] = params.get(key)
return object
}, {})
console.log(paramsObject)
// And json if you really want
const json = JSON.stringify(paramsObject)
I've got a number of backbone models which have a number of nested sub-models. My solution looks like this:
Models.Base = Backbone.Model.extend ({
relatedModels: {},
/**
* Parses data sent according to the list of related models.
*
* #since Version 1
* #param {Object} response Response
* #return {Object} Parsed data
*/
parse: function (response) {
var key,
embeddedClass,
embeddedData;
for (key in this.relatedModels) {
embeddedClass = this.relatedModels[key];
embeddedData = response[key];
response[key] = new embeddedClass (embeddedData, { parse: true });
}
return response;
}
});
(using stuff gleaned from this post - Nested Models in Backbone.js, how to approach)
This works fine whilst I'm getting stuff from the server:
Models.Individual = Models.Base.extend({
idAttribute: "idInd",
urlRoot: "data/individuals/save",
relatedModels: {
details: Collections.DetailList,
relationships: Collections.RelationshipList
}
});
... but when I try and initialise a model from a plain bit of JSON, for example if I were to do this:
var Ind = new Models.Individual ({
idInd: 1,
name: "Bob Holness",
details: [
{ option: "I'd like an 'e' please, bob" },
{ option: "Can I have a 'p' please, bob" }
],
relationships: []
});
... it doesn't seem to want to parse "details". I'd guess that was because it's not running the Parse function, but anyway - how can I get it to parse the data in both instances?
The easiest way to do it would be to pass parse: true to the constructor, like so:
var Ind = new Models.Individual ({
idInd: 1,
...
}, { parse: true });
If you do this a lot you can override the constructor in your base class and make it pass parse: true every time you create a new model instance:
Models.Base = Backbone.Model.extend({
constructor: function(attributes, options) {
var opts = $.extend({}, options || {});
if (_.isUndefined(opts.parse)) {
opts.parse = true;
}
Backbone.Model.call(this, attributes, opts);
},
...
});