Creating a function from strings - javascript

Not sure how 'smart' of an idea this is but I really wanted to experiement with Vue and Javascript and see if I could condense some code a bit (you could say I've done the opposite).
Anways...
I have an data object in vue:
eventOptions: {
eventType: {
data: [],
method: 'getEventTypeList',
service: 'ReportEventService',
},
eventSeverity: {
data: [],
service: 'ReportEventService',
method: 'getSeverityList',
},
eventImpact: {
data: [],
service: 'ReportEventService',
method: 'getBusinessImpactList',
},
eventStatus: {
data: [],
service: 'ReportEventService',
method: 'getEventStatusList',
},
},
And I want to loop through it in the following method and create a function like:
ReportEventService.getEventStatusList() which is referencing an imported javascript file.
async setEventOptions() {
const promises = Object.keys(this.eventOptions).map((key) => {
const { method, service = this.eventOptions[key]
return new Promise(async (resolve, reject) => {
try {
const response = await service[method]();
resolve(response);
} catch (e) {
reject(e);
}
});
});
Promise.all(promises)
.then((responseArray) => {
Object.keys(this.eventOptions).forEach((key, index) => {
this.eventOptions[key]['data'] =
responseArray[index].data;
});
})
.catch((e) => console.log(e));
},
Unfortunately, it's not working.
This line fails:
const callback = service[method]();
Any idea how I can convert two strings to make a function that I can execute? I also understand this undertaking is silly and I can probably just list them out and it would be 10x easier.
I did try:
const func = new Function(`${service}.${method}()`)
The error I get is: TypeError: service[method] is not a function

Object literals can store Object / classes / functions.. etc.
Your currently just storing a string, 'ReportEventService', so when you call the method your doing 'ReportEventService'[method]() and that doesn't make much sense.
But if you store the Object that is ReportEventService instead, you will be calling.. ReportEventService[method]()
IOW: service: ReportEventService,
instead of service: 'ReportEventService',

If you can use any as part of eventOperations, then there is no reason at all to use strings.
Instead make them into callbacks:
eventType: {
data: [],
method: 'getEventTypeList',
service: 'ReportEventService',
},
can become
eventType: {
data: [],
callback: () => ReportEventService.getEventTypeList(),
},
Then you can call it as
const response = await callback();
It you can use a lot more tools to verify your code. At the very least a rename refactoring will work without needing to consider random strings. You can also verify if the method exists or if it is called with the correct number of parameters.
Moreover, you also get more freedom - if you change ReportEventService.getEventTypeList() in the future to require parameters, you can change the callback to () => ReportEventService.getEventTypeList("foo", 42) without having to change the code that consumes it.

Related

Skipping top level of JSON data and retrieving data below it via JavaScript

Via a microservice, I retrieve several packages of JSON data and spit them out onto a Vue.js-driven page. The data looks something like this:
{"data":{"getcompanies":
[
{"id":6,"name":"Arena","address":"12 Baker Street","zip":"15090"},
{"id":7,"name":"McMillan","address":null,"zip":"15090"},
{"id":8,"name":"Ball","address":"342 Farm Road","zip":"15090"}
]
}}
{"data":{"getusers":
[{"id":22,"name":"Fred","address":"Parmesean Street","zip":"15090"},
{"id":24,"name":"George","address":"Loopy Lane","zip":"15090"},
{"id":25,"name":"Lucy","address":"Farm Road","zip":"15090"}]}}
{"data":{"getdevices":
[{"id":2,"name":"device type 1"},
{"id":4,"name":"device type 2"},
{"id":5,"name":"device type 3"}]}}
...and I successfully grab them individually via code like this:
getCompanies() {
this.sendMicroServiceRequest({
method: 'GET',
url: `api/authenticated/function/getcompanies`
})
.then((response) => {
if(response.data) {
this.dataCompanies = response.data.getcompanies
} else {
console.error(response)
}
}).catch(console.error)
}
...with getUsers() and getDevices() looking respectively the same. getCompanies() returns:
[{"id":6,"name":"Arena","address":"12 Baker Street","zip":"15090"},
{"id":7,"name":"McMillan","address":null,"zip":"15090"},
{"id":8,"name":"Ball","address":"342 Farm Road","zip":"15090"}]
...which I relay to the Vue template in a table, and this works just fine and dandy.
But this is obviously going to get unwieldy if I need to add more microservice calls down the road.
What I'm looking for is an elegant way to jump past the response.data.*whatever* and get to those id-records with a re-useable call, but I'm having trouble getting there. response.data[0] doesn't work, and mapping down to the stuff I need either comes back undefined, or in bits of array. And filtering for response.data[0].id to return just the rows with ids keeps coming back undefined.
My last attempt (see below) to access the data does work, but looks like it comes back as individual array elements. I'd rather not - if possible - rebuild an array into a JSON structure. I keep thinking I should be able to just step past the next level regardless of what it's called, and grab whatever is there in one chunk, as if I read response.data.getcompanies directly, but not caring what 'getcompanies' is, or needing to reference it by name:
// the call
this.dataCompanies = this.getFullData('companies')
getFullData(who) {
this.sendMicroServiceRequest({
method: 'GET',
url: 'api/authenticated/function/get' + who,
})
.then((response) => {
if(response) {
// attempt 1 to get chunk below 'getcompanies'
Object.keys(response.data).forEach(function(prop) {
console.log(response.data[prop])
})
// attempt 2
// for (const prop in response.data) {
// console.log(response.data[prop])
// }
let output = response.data[prop] // erroneously thinking this is in one object
return output
} else {
console.error(response)
}
}).catch(console.error)
}
...outputs:
(63) [{…}, {…}, {…}] <-- *there are 63 of these records, I'm just showing the first few...*
0: {"id":6,"name":"Arena","address":"12 Baker Street","zip":"15090"}
1: {"id":7,"name":"McMillan","address":null,"zip":"15090"},
2: {"id":8,"name":"Ball","address":"342 Farm Road","zip":"15090"}...
Oh, and the return above comes back 'undefined' for some reason that eludes me at 3AM. >.<
It's one of those things where I think I am close, but not quite. Any tips, hints, or pokes in the right direction are greatly appreciated.
I feel it's better to be explicit about accessing the object. Seems like the object key is consistent with the name of the microservice function? If so:
getData(functionName) {
return this.sendMicroServiceRequest({
method: 'GET',
url: "api/authenticated/function/" + functionName
})
.then( response => response.data[functionName] )
}
getCompanies(){
this.getData("getcompanies").then(companies => {
this.dataCompanies = companies
})
}
let arrResponse = {data: ['x']};
let objResponse = {data: {getcompanies: 'x'}};
console.log(arrResponse.data[0]);
console.log(Object.values(objResponse.data)[0]);
response.data[0] would work if data was an array. To get the first-and-only element of an object, use Object.values(response.data)[0] instead. Object.values converts an object to an array of its values.
Its counterparts Object.keys and Object.entries likewise return arrays of keys and key-value tuples respectively.
Note, order isn't guaranteed in objects, so this is only predictable in your situation because data has exactly a single key & value. Otherwise, you'd have to iterate the entry tuples and search for the desired entry.
firstValue
Let's begin with a generic function, firstValue. It will get the first value of an object, if present, otherwise it will throw an error -
const x = { something: "foo" }
const y = {}
const firstValue = t =>
{ const v = Object.values(t)
if (v.length)
return v[0]
else
throw Error("empty data")
}
console.log(firstValue(x)) // "foo"
console.log(firstValue(y)) // Error: empty data
getData
Now write a generic getData. We chain our firstValue function on the end, and be careful not to add a console.log or .catch here; that is a choice for the caller to decide -
getData(url) {
return this
.sendMicroServiceRequest({ method: "GET", url })
.then(response => {
if (response.data)
return response.data
else
return Promise.reject(response)
})
.then(firstValue)
}
Now we write getCompanies, getUsers, etc -
getCompanies() {
return getData("api/authenticated/function/getcompanies")
}
getUsers() {
return getData("api/authenticated/function/getusers")
}
//...
async and await
Maybe you could spruce up getData with async and await -
async getData(url) {
const response =
await this.sendMicroServiceRequest({ method: "GET", url })
return response.data
? firstValue(response.data)
: Promise.reject(response)
}
power of generics demonstrated
We might even suggest that these get* functions are no longer needed -
async getAll() {
return {
companies:
await getData("api/authenticated/function/getcompanies"),
users:
await getData("api/authenticated/function/getusers"),
devices:
await getData("api/authenticated/function/getdevices"),
// ...
}
}
Above we used three await getData(...) requests which happen in serial order. Perhaps you want all of these requests to run in parallel. Below we will show how to do that -
async getAll() {
const requests = [
getData("api/authenticated/function/getcompanies"),
getData("api/authenticated/function/getusers"),
getData("api/authenticated/function/getdevices")
]
const [companies, users, devices] = Promise.all(requests)
return { companies, users, devices }
}
error handling
Finally, error handling is reserved for the caller and should not be attempted within our generic functions -
this.getAll()
.then(data => this.render(data)) // some Vue template
.catch(console.error)

React: Realtime rendering Axios return

hope you're well. I've been working on a company list component. I am having a problem with updating it in real time, because the axios call I'm sending to 'getById' after it renders is returning only a promise and not the actual data that it is supposed to and I don't have any idea as to why. So when I push the so called new company that I've just added into the array, which is in state, it is only pushing a promise into the array and not the actual Company. I don't have any idea why this is. What the code is supposed to be doing, is it is supposed to be putting the new company into the database, returning the success result, and then I'm using the item from that to make a fresh get call to the axios DB which is supposed to be returning the information I just entered so that I can then insert it into the same array in state that is within the list that is rendered in the company list. However, as I mentioned, only the promise is coming up for some reason.
At one point I was able to get this working, but I did that by essentially calling, 'componentDidMount' after the promise was pushed into the call back clause of the setState funciton of the push function - which was essentially causing the entire component to re-render. I'm a fairly new coder, but my understanding is is that that is a fairly unconventional way to code something, and contrary to good coding methodologies. I believe I should be able to push it into state, and then have it change automatically. I have attached the relevant code below for you to examine. If you believe you need more please let me know. If someone could please tell me why I am getting this weird promise instead of the proper response object, so that I can then insert that into state, I would greatly appreciate it. I've attached some images below the code snippets that I hope will be helpful in providing an answer. I have also left brief descriptions of what they are.
:
class Companies extends React.Component {
constructor(props) {
super(props);
this.state = {
Companies: [],
formData: { label: "", value: 0 },
};
}
componentDidMount = () => {
this.getListCompanies();
};
getListCompanies = () => {
getAll().then(this.listOfCompaniesSuccess);
};
listOfCompaniesSuccess = (config) => {
let companyList = config.items;
this.setState((prevState) => {
return {
...prevState,
Companies: companyList,
};
});
};
onCompListError = (errResponse) => {
_logger(errResponse);
};
mapCompanies = (Companies) => (
<CompaniesList Companies={Companies} remove={remove} />
);
handleSubmit = (values) => {
if (values.companyName === "PPP") {
this.toastError();
//THIS IS FOR TESTING.
} else {
add(values)
.getById(values.item) //I HAVE TRIED IN TWO DIFFERENT PLACES TO GET THE NEW COMPANY IN. HERE
.then(this.newCompanyPush)
.then(this.toastSuccess)
.catch(this.toastError);
}
};
//THIS CODE RIGHT HERE IS THE CODE CAUSING THE ISSUE.
newCompanyPush = (response) => {
let newCompany = getById(response.item); // THIS IS THE OTHER PLACE I HAVE TRIED.
this.setState((prevState) => {
let newCompanyList = [...prevState.Companies];
newCompanyList.push(newCompany);
return {
Companies: newCompanyList,
};
});
};
toastSuccess = () => {
toast.success("Success", {
closeOnClick: true,
position: "top-right",
});
};
toastError = (number) => {
toast.error(`Error, index is ${number}`, {
closeOnClick: true,
position: "top-center",
});
};
This is the axios call I am using.
const getById = (id) => {
const config = {
method: "GET",
url: companyUrls + id,
withCredentials: true,
crossdomain: true,
headers: { "Content-Type": "application/json" },
};
return axios(config).then(onGlobalSuccess).catch(onGlobalError);
};
After the promise is pushed into the array, this is what it looks like. Which is I guess good news because something is actually rendering in real time.
This is the 'promise' that is being pushed into the array. Please note, when I make the same call in post-man, I get an entirely different response, see below.
This is the outcome I get in postman, when I test the call.

graphql passing dynamic data to mutation

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 use spread properties in Node?

Below is my code snippet and I would like to use additional argument (.i.e search) for connectionArgs.
const connectionArgs = require('graphql-relay').connectionArgs;
users: {
type: UserConnection.connectionType,
args: {
...connectionArgs,
search: {
type: GraphQLString,
},
},
resolve: async (obj, args, { user }) => {
return UserLoader.loadUsers(user, args);
}
},
Above code works fine when using bable, but I am trying to remove babel (since async await is now supported in node 7.6v).
I get Unexpected token for connectionArgs.
Could any one suggest how to use this spread properties here ?

Promise.all vs m.sync

Are there any differences between the two functions?
I recently found a problem while using Promise.all within a controller.
The view would render before the Promise.all completion and I'd get empty variables inside the view.
So if I use one m.request to hit a single api then the view waits for it to complete. If I use many m.request wrapped in Promise.all then it does not wait! Am I doing something wrong?
Is this the correct behaviour? Is m.sync would behave somehow different, it seems to have the same signature as Promise.all?
Thanks.
EDIT
The code with relevant bits
// Does two requests and wraps them in Promise.all
Table.show = (id, load) => {
var header = m.request({
method: "GET",
url: cfg.apiurl(`/tables/${id}`),
});
var body = m.request({
method: "POST",
url: cfg.apiurl(`/tables/${id}`),
data: {
data: load
}
});
return Promise.all([header, body]);
};
// The component
var ShowTable = {
controller, view
};
// Controller function
function controller() {
this.header = m.prop({});
this.records = m.prop([]);
this.pages = m.prop(0);
var load = {
filter: {},
paging: {
number: 1,
size: 10
}
};
var showTable = () => {
Table.show(m.route.param("id"), load).then((res) => {
this.header(res[0].data);
this.records(res[1].data);
this.pages(res[1].meta.pages);
}, (res) => {
popa();
});
};
showTable();
}
// View function
function view(vm) {
return m("div", [
m("p", vm.header()),
m("p", vm.records()),
m("p", vm.pages()),
]);
}
EDIT2
Yes, m.sync works. But Promise.all does not.
A quick look at the documentation landed me to this page. Redraw is done only for mithril's built-in functions, Promise.all is a native javascript function so no auto-redraw. You should either use m.sync or m.startComputation/m.endComputation in your showTable function. You can even return directly [header, body] without Promise.all/m.sync, they are both promises and mithril props so you can use/assign them to your viewmodel and when their value is populated, it should trigger a redraw in the view.
Anyway, if it still doesn't work, don't waste your time, just put m.redraw() in the then of Promise.all.

Categories

Resources