I'm learning Javascript and I am stuck using Promises.
I'm trying to make a tree like structure from API documentation, where a $ref key in the JSON is replaced with the API object that resides somewhere else in the file. This needs to happen fairly synchronous where I go through the keys of the API object, when I find a $ref it's looked up and replaced in the JSON.
e.g.
"apiStorageVersion": {
"description": "...",
"properties": {
"apiVersion": {
"description": "...",
"type": "string"
},
"kind": {
"description": "...",
"type": "string"
},
"metadata": {
"$ref": "#/definitions/apiMetaData",
"description": "..."
},
"spec": {
"$ref": "#/definitions/apiSpec",
"description": "..."
},
"status": {
"$ref": "#/definitions/apiStatus",
"description": "..."
}
}
}
I start of with this function that gets a list of API objects that I consider parents in a sense that these are more important objects in the API. Definitions is the content of the file and holds all the API objects.
function fillObjectTree(parents: string[]) {
console.log(parents);
Object.keys(definitions).map(apiTitle => {
// if the apiTitle is part of the parents list than we process it
if (parents.includes(apiTitle)) {
// Read the children and see if any references are part of the parent
let par = readChildren(definitions[apiTitle])
par.then(function (vals) {
// Do something with values
})
}
})
}
Next step is reading the properties of this API object and looking for $ref keys.
function readChildren(definition: { properties: any; }) {
return new Promise((resolve, reject) => {
// Get the properties of the definition
let props = definition.properties;
// Properties are not always present on an object.
if (props) {
Object.keys(props).map(propName => {
Object.keys(props[propName]).map(elem => {
if (elem.includes("$ref")) {
// locationURL = the full url for the reference
let locationURL: string = props[propName][elem];
// returns the needed value for the URL based on the regex
let partialURL: string = locationURL.match('(?<=(\#\/.*\/)).*')[0];
readReference(partialURL).then((body) => {
console.log(body);
delete definition.properties[propName];
definition.properties[propName] = body;
});
}
})
})
resolve(definition);
} else {
resolve(definition);
}
});
}
When a reference is found a second function is called that looks in the current file for this object.
function readReference(apiTitle: string) {
return new Promise((resolve, reject) => {
// Check all the definitions and find a match
Object.keys(definitions).map(apiDef => {
if (apiTitle === apiDef) {
readChildren(definitions[apiDef]).then((body) => {
resolve(body);
})
}
})
})
}
So what's going wrong?
Well the order of operations does not seem to match what I want to happen. The object is not replaced in the JSON and not waited for when executing. I rather not used await or async but keep it to baseline Promises if possible.
readChildren will execute synchronously until it gets to this block:
readReference(partialURL).then((body) => {
console.log(body);
delete definition.properties[propName];
definition.properties[propName] = body;
});
readReference will return a promise, so it gets the promise then schedules whatever is inside the then to happen at some time in then future. Then the function continues, eventually calling resolve(definition); and then falling out of the function. This happens BEFORE whatever is inside the then.
To make resolve(definition); happen after everything else, simply put it in the then block too.
Edit: the above solution doesn't handle the map.
Handling lists of asynchronous results:
const promises = list.map(element => {
return someAsyncFunction(element);
});
Promise.all(promises)
.then(results => {
... do stuff with the results
});
Btw, all of this get flattened out if you use the vastly superior async/await syntax. It becomes much easier to reason about the ordering.
Related
I have a JSON file with some items. I'd like to randomly select one to display using Javascript. Here's the JSON file:
{
"keywords": [
{
"name": "item1",
"property1": "value1",
"property2": "value2",
"property3": "value3"
},
{
"name": "item2",
"property1": "value4",
"property2": "value5",
"property3": "value6"
}
]
}
I fetch the file as follows:
let data;
function fetchJSON() {
fetch('../keyworddata.json')
.then(response => {
if(!response.ok) throw new Error("HTTP status " + response.status);
return response.json();
})
.then(jsonData => {
data = jsonData;
})
.catch(error => {
console.error('Error:', error);
});
}
fetchJSON();
myData = JSON.parse(data);
lengthOfMyData = Object.keys(myData.keywords[0]).length;
console.log(lengthOfMyData);
However, I get the following error:
JSON Parse error: Unexpected identifier "object"
Can you help me? I'd like to find the number of items in the JSON file and then pick a random item from the list.
There's some weirdness going on here: you're using a global variable for data associated with a function, your second then is wrong in terms of what you named things (response.json() parses JSON, it does not return JSON. It returns a normal JS datastructure, leading to the next bit) and you're trying to parse regular JS data as if it's JSON.
So let's fix all of that:
function fetchJSON(filename) {
return fetch(filename)
.then(response => response.json())
.catch(error => {
// Log error, and return an object that the consuming
// code can still work with, but is effectively "empty"
console.error({error});
return { keywords: [] };
});
}
const { keywords } = await fetchJSON(`../keywords.json`);
console.log(keywords.length);
Although it would make far more sense to not make fetchJSON handle the errors, and instead have the calling code have a try/catch so that if the fetch fails for whatever reason, your calling code knows where to branch next.
I'm trying to work around the fact that Datocms doesn't support a where filter in their GraphQL schema. Since there isn't that much data, I figured I could query all of it, and do the find on my end, but ... I'm not succeeding, at least not using "modern" methods.
What I get back when I query all of the data looks like this:
"foo": {
"data": {
"allGiveawayLandingPages": [
{
"lpSection": [
{},
{},
{},
{},
{},
{},
{},
{
"id": "34525949",
"products": [
{
"__typename": "PurchaseCardRecord",
"discountAmount": 50,
"discountAmountPct": null,
"discountEndDate": "2022-11-01T23:00:00+00:00",
"id": "44144096"
},
{
"__typename": "PurchaseCardRecord",
"discountAmount": null,
"discountAmountPct": null,
"discountEndDate": null,
"id": "44144097"
}
]
}
]
}
]
}
}
I need to find the object down in the "products" array by "id". This general question has been asked and answered lots of times, but the only answer I can get to work is from way back in 2013, and it seems to me there aught to be a more modern way to do it.
I'm doing this inside of a try/catch block, which I mention because it seems to be making this hard to debug (I'll come back to this):
export default async function createPaymentIntentHandler(req, res) {
const body = JSON.parse(req.body);
const {
productId,
productType
} = body;
let data;
if ('POST' === req.method) {
try {
switch (productType) {
case 'SeminarRecord':
data = await request({ query: singleSeminarQuery(productId) });
productObjName = 'seminar';
break;
default:
data = await request({ query: singleProductQuery(productId) });
productObjName = 'product';
}
/**
* Here's where I want to do my query / filtering
*/
// ... do more stuff and create Stripe paymentIntent
res.status(200).send({clientSecret: paymentIntent.client_secret})
} catch (error) {
logger.error({error}, 'Create Payment Intent error');
return res.status(400).end(`Create Payment Intent error: ${error.message}`);
}
} else {
res.status(405).end('Method not allowed');
}
}
My first, naive attempt was
const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = foo.data.allGiveawayLandingPages.find((page) => {
return page.lpSection.find((section) => {
return section?.products.find((record) => record.id === parentId)
})
});
logger.debug({card}, 'Got card');
In the abstract, aside from the fact that the above is fairly brittle because it relies on the schema not changing, I'd expect some similar sort of ES6 construction to work. This particular one, however, throws, but not in a particularly useful way:
[08:09:18.690] ERROR: Create Payment Intent error
env: "development"
error: {}
That's what I meant by it being hard to debug — I don't know why the error object is empty. But, in any case, that's when I started searching StackOverflow. The first answer which looked promising was this one, which I implemented as
...
const {
productId,
productType,
parentId
} = body;
...
function findCard(parent, id) {
logger.debug({parent}, 'searching in parent')
for (const item of parent) {
if ('PurchaseCardRecord' === item.__typename && item.id === id) return item;
if (item.children?.length) {
const innerResult = findCard(item.children, id);
if (innerResult) return innerResult;
}
}
}
if ('POST' === req.method) {
try {
...
const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = findCard(foo, parentId);
logger.debug({card}, 'Got card');
This similarly throws unhelpfully, but my guess is it doesn't work because in the structure, not all children are iterables. Then I found this answer, which uses reduce instead of my original attempt at find, so I took a pass at it:
const card = foo.data.allGiveawayLandingPages.reduce((item) => {
item?.lpSection.reduce((section) => {
section?.products.reduce((record) => {
if ('PurchaseCardRecord' === record.__typename && record.id === parentId) return record;
})
})
})
This is actually the closest I've gotten using ES6 functionality. It doesn't throw an error; however, it's also not returning the matching child object, it's returning the first parent object that contains the match (i.e., it's returning the whole "lpSection" object). Also, it has the same brittleness problem of requiring knowledge of the schema. I'm relatively certain something like this is the right way to go, but I'm just not understanding his original construction:
arr.reduce((a, item) => {
if (a) return a;
if (item.id === id) return item;
I've tried to understand the MDN documentation for Array.reduce, but, I don't know, I must be undercaffeinated or something. The syntax is described as
reduce((previousValue, currentValue) => { /* … */ } )
and then several variations on the theme. I thought it would return all the way up the stack in my construction, but it doesn't. I also tried
const card = foo.data.allGiveawayLandingPages.reduce((accumulator, item) => {
return item?.lpSection.reduce((section) => {
return section?.products.reduce((record) => {
if ('PurchaseCardRecord' === record.__typename && record.id === parentId) return record;
})
})
})
but the result was the same. Finally, not understanding what I'm doing, I went back to an older answer that doesn't use the ES6 methods but relies on recursing the object.
...
function filterCards(object) {
if (object.hasOwnProperty('__typename') && object.hasOwnProperty('id') && ('PurchaseCardRecord' === object.__typename && parentId === object.id)) return object;
for (let i=0; i<Object.keys(object).length; i++) {
if (typeof object[Object.keys(object)[i]] == 'object') {
const o = filterCards(object[Object.keys(object)[i]]);
if (o != null) return o;
}
}
return null;
}
if ('POST' === req.method) {
try {
...
const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = filterCards(foo);
logger.debug({card}, 'Got card');
This actually works, but ISTM there should be a more elegant way to solve the problem with modern Javascript. I'm thinking it's some combination of .find, .some, and .reduce. Or maybe just for ... in.
I'll keep poking at this, but if anyone has an elegant/modern answer, I'd appreciate it!
From an API, I'm trying to get data using httpModule. Here is my code
async searchMeaning(form: NgForm) {
const post = {
word: form.value.inputWord,
language: form.value.language
}
console.log(post);
if (post.language && post.word) {
this.output1 = await this.callApi(post); // it displays await has not effect
console.log(this.output1) // undefined.
}
}
callApi(post) {
this.http.get('https://api.dictionaryapi.dev/api/v2/entries/'+post.language+'/'+post.word)
.subscribe((data) => {
console.log(JSON.parse(JSON.stringify(data)));
return data;
}, (error : any) => {
return error
})
}
When I use async and await, it says that await has no effect. An undefined is getting assigned to the variable this.output. How can I make this work?
Also, How can I get access to a variable from the below response array?
[
{
"word": "hello",
"phonetics": [
{
"text": "/həˈloʊ/",
"audio": "https://lex-audio.useremarkable.com/mp3/hello_us_1_rr.mp3"
},
{
"text": "/hɛˈloʊ/",
"audio": "https://lex-audio.useremarkable.com/mp3/hello_us_2_rr.mp3"
}
],
"meanings": [
{
"partOfSpeech": "exclamation",
"definitions": [
{
"definition": "Used as a greeting or to begin a phone conversation.",
"example": "hello there, Katie!"
}
]
},
{
"partOfSpeech": "noun",
"definitions": [
{
"definition": "An utterance of “hello”; a greeting.",
"example": "she was getting polite nods and hellos from people",
"synonyms": [
"greeting",
"welcome",
"salutation",
"saluting",
"hailing",
"address",
"hello",
"hallo"
]
}
]
},
{
"partOfSpeech": "intransitive verb",
"definitions": [
{
"definition": "Say or shout “hello”; greet someone.",
"example": "I pressed the phone button and helloed"
}
]
}
]
} ]
here I need to get the value of the definition variable from the above array. How can I do that?
console image
When I use async and await, it says that await has no effect.
Yes, that's because await only has an effect on Promises (a type native to Javascript). this.callApi(post) returns a Subscription (which is a RxJS type), which isn't the same as a Promise.
In Angular, I'd argue that using Promises is an antipattern (unless required by a third party library). Instead you should use Observables and subscribe to them, which you'll find out later has a tons of advantages in more complex situations. The way you usually do this is by building up observables with pipes as far as possible, and then subscribe when you actually need to do the call, like this:
searchMeaning(form: NgForm) {
const post = {
word: form.value.inputWord,
language: form.value.language
}
console.log(post);
if (post.language && post.word) {
this.callApi(post).subscribe(x => {
this.output1 = x;
console.log(this.output1); // shouldn't be undefined anymore
// if you want to do more to
// affect the state of the component,
// you can do it here
});
// Be cautious, things that you write here will actually execute before the call to the API.
}
}
callApi(post) {
this.http.get('https://api.dictionaryapi.dev/api/v2/entries/'+post.language+'/'+post.word)
.pipe(map(x => {
// if you want to change x in some way
// before returning to the calling method,
// you can do it here
return x;
}));
}
Also, How can I get access to a variable from the below response array?
For example, if you want to access the first definition example, you could do the following: x[0]["meanings"][0]["definitions"][0]["example]. You could also make a type definition to make it even easier to access, but it's probably not worth it if you're just using the dictionary for a few things.
It can be worthwhile to look through the Angular docs on Observables, or at least look at the concrete use cases with calling APIs with HttpClient
You should return an observable from callApi() then you can use await on your function:
callApi(post) {
const sub = new Subject();
this.http.get('https://api.dictionaryapi.dev/api/v2/entries/'+post.language+'/'+post.word)
.subscribe((data) => {
console.log(JSON.parse(JSON.stringify(data)));
sub.next(data)
sub.unsubscribe();
}, (error : any) => {
sub.error(data);
sub.unsubscribe();
})
return sub.asObservable();
}
this.output1 = await this.callApi(post).pipe(first()).toPromise();
If you don't want to use .pipe(first()).toPromise(), then you need to have callApi return a Promise.
Also, depending on what you want callApi to do, you could just return this.http.get() or even this.http.get().pipe(first()).toPromise().
EDIT: Added extra code in the filterEvents snippet for more context.
I'm not quite understanding what's going on with my code. I'm trying to pass an array into an action function inside of my Vuex store. If I return a Promise inside of that action function, then the parameter being passed isn't of type Array and is instead an Object, which results in the reject() error that I have for the Promise.
Here's some code for context:
filterEvents({ commit }, events) {
console.log(Array.isArray(events)); //this ends up false
console.log(events);
return new Promise((resolve, reject) => {
if (!Array.isArray(events)) {
reject("Invalid argument: is not of type Array.");
}
let filtered = events.filter((event) => {
let now = new Date();
let event_stop = new Date(event.stop_time);
if (event_stop >= now || event_stop == null) {
return event;
}
});
resolve(filtered);
});
}
Here's where I call filterEvents; inside of getEvents;
getEvents({ state, commit, dispatch }, searchParams) {
.....
eventful.getEvents(searchParams).then(async (res) => {
.....
console.log(Array.isArray(res.data.events.event)); //this ends up true
console.log(res.data.events.event);
/* where I call it */
await dispatch("filterEvents", res.data.events.event).then((res) => {
.....
});
}).catch((err) => {
.....
});
}
Here's the output from the Chrome developer console. First two outputs are from getEvents and last two are from filterEvents
Would really like an explanation as to why this is the case. I'm going to bet it's something small, but it's 3 a.m. at the moment and my brain can't wrap around why it's not of type Array when passed into filterEvents.
I always try to check the length prop of the array which helps me out in such cases.
...
return new Promise((resolve, reject) => {
if (!Array.isArray(events) && !events.length) {
reject("Invalid argument: is not of type Array.");
}
.....
});
...
I finally understood what my issue was after taking another look at the object that was being logged on the console. I did not know that Vuex actions HAD to have two arguments if you want to pass in a payload into that function. For example, I initially did this
filterEvents(events) {
.....
}
but what I really needed to do was
filterEvents(context, events) {
.....
}
The context argument is the object that allows you to do things such as commit and dispatch. I usually destructure the context object (i.e. { commit, dispatch} ), so I for some reason never thought twice about it. You don't have to destructure the context object to use commit and dispatch; if you don't it would just be like
context.commit('function', payload);
I have an array that looks like this :
"attributes": [{"id": "5dad5242e7038210842ec59c","value": 3},{"id": "5dbade6a824f3e2244b2870b","value": 6},{"id": "5d7c943a5759f91e187520cc","value": 17}]
Each value in the array corresponds to a schema from where I will fetch that data.
Ex : if(value == 1){ fetch data from schemaA}
Based on the data fetched from each schema I will repopulate the array with additional information, so at the end the array would look like this:
"attributes": [{"id": "5dad5242e7038210842ec59c","value": 3, "name": "attributeA"},{"id": "5dbade6a824f3e2244b2870b","value": 6, "name": "attributeF"},{"id": "5d7c943a5759f91e187520cc","value": 17, "name": "attributeQ"}]
So far I have written a function :
exports.fetchAttributes = (attr, callback) => {
try {
switch (attr.value) {
case 3:
this.fetchAttributeC(attr.id, (err, attributeB) => {
callback(err, attributeB);
});
break;
case 6:
this.fetchAttributeF(attr.id, (err, attributeF) => {
callback(err, attributeF);
});
break;
case 7:
this.fetchAttributeQ(attr.id, (err, attributeQ) => {
callback(err, attributeQ);
});
break;
default : console.log("Invalid value");
}
} catch(e){
console.log("Catch in fetchAttributes "+e);
return callback(e, null);
}
}
The above function is being called within another function:
exports.functionAttributes = (attributes, callback) => {
let newAttributes = [];
attributes.forEach((attr) => {
this.fetchAttributes(attr, (err, attributeFull) => {
newAttributes.push(attributeFull);
})
}
//need to pass in callback here, but obviously there is a scope issue
}
I need this array with newAttributes as I have to pass it to the final callback in async waterfall where this function is. Due to the scope issue newAttributes is always empty. I need help finding a better way to achieve this. So I can finally end up with result array as I have shown above. Any help is greatly appreciated.
P.S : I have tried conditional callbacks, promises but I couldn't make it work as I need to pass parameters in each step. Feel free to point out where I am wrong, or if there is a better method of achieving this, I would be happy to learn.
I think a promise list would perfectly solve your problem as long as you structured it correctly.
First: Make fetchAttributes return a promise:
return new Promise(function(resolve, reject) {
...Your try catch using resolve() and reject()
})
Second: Create the Array of your promises
let fetchList= [];
attributes.forEach((attr) => {
fetchList.push(...your function)
}
Third: If you don't care whether one fails, make sure to add in a reflection so the promise.all() always completes -> Check this stack post
Finally Once your promise.all() resolves, you can loop through all of the returned values and push them to the newAttributes variable!!!
If you have trouble with it I can provide more info but that should get you started!