Return object from observable inside another observable - javascript

I try to fetch data from 3 different REST end points. The data model consists of main data and has advanced Array with 2(might be more in the future) Objects. I want to inject an Array with options into each of advanced Objects, based on REST endpoint specified in each of them. Everything works and is returned from Observable as Object, except appended options, that come as Observables.
Simplified data:
{
simple: {
param: "searchQuery",
},
advanced: [
{
param: "food",
optionsModel: {
url: "http://address.com/food",
}
},
{
param: "drinks",
optionsModel: {
url: "http://address.com/drinks",
}
}
]
}
food and drinks have the same structure consisting of Objects with name and id:
{
data: [
{
name: "DR1",
id: 1
},
{
name: "DR2",
id: 1
},
...
]
}
In my data model I don't have options[] array, so I inject it manually. Service:
searchConfiguration$: Observable<SearchConfiguration> = this.http
.get(this._configURL)
.map((config) => {
let configuration = config.json();
let advancedArray = configuration.advanced;
if(advancedArray) {
advancedArray.forEach(advanced => {
advanced.options = [];
advanced.options = this.http.get(advanced.optionsModel.url)
.map((r: Response) => r.json().data
})
}
return configuration;
})
Parent component:
getSearchConfig() {
this.searchConfiguration$ = this._bundlesService.searchConfiguration$;
}
Then I have async pipe in the html to subscribe to Observable. How can I get my options appended to advanced as actual Arrays and not as a stream?
Edit - Solution
Thanks to martin's answer, the solution is to flatten both streams and connect them in the end with forkJoin()
searchConfiguration$: Observable<SearchConfiguration> = this.http
.get(this._configURL)
.map((config) => config.json())
.concatMap((configuration) => {
let observables = configuration.advanced.map((advanced) => {
return this.http.get(advanced.optionsModel.url)
.map((r: Response) => r.json().data)
.concatMap((options) => advanced.options = options)
});
return Observable.forkJoin(observables, (...options) => {
return configuration;
})
});

I didn't test it but I think you could you something as follows:
searchConfiguration$: Observable<SearchConfiguration> = this.http
.get(this._configURL)
.map((config) => config.json())
.concatMap(configuration => {
var observables = configuration.advanced.map(advanced => {
return this.http.get(advanced.optionsModel.url)
.map((r: Response) => r.json().data);
});
return Observable.forkJoin(observables, (...options) => {
configuration.options = options;
return configuration;
});
})

Related

Edit function not saving changes to state data in React

I am trying to provide functionality in my webpage for editing state data.
Here is the state structure
state = {
eventList:[
{
name: "Coachella"
list: [
{
id: 1,
name: "Eminem"
type: "rap"
}
{
id: 2,
name: "Kendrick Lamar"
type: "rap"
}
]
}
]
}
I want to be able to edit the list arrays specifically the id, name, and type properties but my function doesn't seem to edit them? I currently pass data I want to override id name and type with in variable eventData and an id value specifying which row is selected in the table which outputs the state data.
Here is the function code:
editPickEvent = (eventData, id) => {
const eventListNew = this.state.eventList;
eventListNew.map((event) => {
event.list.map((single) => {
if (single.id == id) {
single = eventData;
}
});
});
this.setState({
eventList: eventListNew,
});
};
When I run the code the function doesn't alter the single map variable and I can't seem to pinpoint the reason why. Any help would be great
edit:
Implementing Captain Mhmdrz_A's solution
editPickEvent = (eventData, id) => {
const eventListNew = this.state.eventList.map((event) => {
event.list.map((single) => {
if (single.id == id) {
single = eventData;
}
});
});
this.setState({
eventList: eventListNew,
});
};
I get a new error saying Cannot read property list of undefined in another file that uses the map function to render the state data to the table?
This is the part of the other file causing the error:
render() {
const EventsList = this.props.eventList.map((event) => {
return event.list.map((single) => {
return (
map() return a new array every time, but you are not assigning it to anything;
editPickEvent = (eventData, id) => {
const eventListNew = this.state.eventList.map((event) => {
event.list.forEach((single) => {
if (single.id == id) {
single = eventData;
}
});
return event
});
this.setState({
eventList: eventListNew,
});
};
const editPickEvent = (eventData, id) => {
const updatedEventList = this.state.eventList.map(event => {
const updatedList = event.list.map(single => {
if (single.id === id) {
return eventData;
}
return single;
});
return {...event, list: updatedList};
});
this.setState({
eventList: updatedEventList,
});
}
Example Link: https://codesandbox.io/s/crazy-lake-2q6ez
Note: You may need to add more checks in between for handling cases when values could be null or undefined.
Also, it would be good if you can add something similar to the original data source or an example link.
Turns out primitive values are pass by value in javascript, which I didn't know and why the assignment wasn't working in some of the previous suggested answers. Here is the code that got it working for me:
editEvent = (EventData, id) => {
const eventListNew = this.state.eventList.map((event) => {
const newList = event.list.map((single) => {
return single.id == id ? EventData : single;
});
return { ...event, list: newList };
});
this.setState({
eventList: eventListNew,
});
};

Update a property from a string to object on JavaScript without mutation

When iterating through an object and updating it I feel that I am not doing it right. This is the method I have built to do it and I have doubts about record[header] = {label: record[header]};
Does anybody have a better solution to avoid mutation records array?
setAfterRecords(preview: any): void {
const { columns: headers } = preview;
const { rows: records } = preview;
records.map((record, i) => {
headers.forEach(header => {
record[header] = {
label: record[header],
};
});
return record;
});
this.after = { headers, records };
}
Thank you.
The following should do it. It uses the new-ish object spread operator (... on {}). It's like Object.assign() but in a more concise syntax.
setAfterRecords(preview: any): void {
const { columns: headers, rows: records } = preview;
// Build a new array of records.
const newRecords = records.map(record => {
// Build a new record object, adding in each header.
return headers.reduce((c, header) => {
// This operation shallow-copies record from the initial value or
// previous operation, adding in the header.
return { ...c, [header]: { label: record[header] } }
}, record)
})
this.after = { headers, records: newRecords };
}

tranforming RxJS Observable

I use angularFirestore to query on firebase and I want join data from multiple documents using the DocumentReference.
The first operator map in the pipe return an array of IOrderModelTable, the second operator, i.e, the switchMap iterate over array an for each element use the id contained in each element to query data in other table.
The problem is that in the swithMap I obtain an array of observable due to anidated map operators. How I can obtain an array of IOrderModelTable and then return an observable of this array.
The code is:
getDataTableOperatorsFromDB(): Observable<IOrderModelTable[]> {
const observable = this.tableOperatorsCollectionsRef.snapshotChanges().pipe(
map(actions => {
return actions.map(a => {
const data = a.payload.doc.data() as IOrdersModelDatabase;
const id = a.payload.doc.id;
data.ot = id;
return data;
});
}),
switchMap(data => {
const result = data.map(element => {
return this.afs.collection('Orders/').doc(element.orderNumberReference.id).valueChanges().pipe(map(order => {
return {
otNumber: element.ot,
clientName: '',
clientReference: order.clientReference,
id: element.orderNumberReference,
};
}));
});
// Result must be an IOrderModelTable[] but is a Observable<IOrderModelTable>[]
return of(result);
})
);
You can use to Array operator to transform a stream to an array, but make sure your stream will end.
The trick is to choose the right stream.
For you problem, the natural source would be the list received by your first call. In a schematic way I can put it , you get a list of ids, that you transform into a list of augmented information :
first input ...snapshopChanges():
----[A, B, C]------>
each element is transformed through ...valueChanges():
-------Call A -------------DataA-------->
-------Call B ------------------------DataB----->
-------Call C --------------------DataC----->
Then reduced using toArray() to :
----------------------------------------------[DataA, DataC, DataB]-------->
Code:
getDataTableOperatorsFromDB(): Observable<IOrderModelTable[]> { {
return this.tableOperatorsCollectionsRef.snapshotChanges()
.pipe(
map(actions => {
from(data).pipe(
map(action => {
const data = a.payload.doc.data() as IOrdersModelDatabase;
const id = a.payload.doc.id;
data.ot = id;
return data;
}),
mergeMap(element => {
return this.afs.collection('Orders/').doc(element.orderNumberReference.id).valueChanges().pipe(
map(order => {
return {
otNumber: element.ot,
clientName: '',
clientReference: order.clientReference,
id: element.orderNumberReference,
};
})
);
}),
toArray()
);
})
)
}
Important : I replaced switchMap by mergeMap, otherwise some information could be thrown away.
#madjaoue
You're right, mergeMap is the correct operator in this case because with switchMap for each event emitted the inner observable is destroyed so in the subscribe you only get the final event emitted, i.e, the last row. This observable is long lived, never complete, so also use the operator take with the length of the actions which is the array that contains the list of documents.
Thank you very much for the help. :D
getDataTableOperatorsFromDB(): Observable<IOrderModelTable[]> {
const observable = this.tableOperatorsCollectionsRef.snapshotChanges().pipe(
switchMap(actions => {
return from(actions).pipe(
mergeMap(action => {
console.log(action);
const data = action.payload.doc.data() as IOrdersModelDatabase;
const otNumber = action.payload.doc.id;
return this.afs.collection('Orders/').doc(data.orderNumberReference.id).valueChanges().pipe(map(order => {
return {
otNumber: otNumber,
clientName: '',
clientReference: order.clientReference,
id: data.orderNumberReference,
};
}));
}),
mergeMap(order => {
console.log(order);
return this.afs.collection('Clients/').doc(order.clientReference.id).valueChanges().pipe(map(client => {
return {
otNumber: order.otNumber,
clientName: client.name,
clientReference: order.clientReference,
id: order.id,
};
}));
}),
take(actions.length),
toArray(),
tap(console.log),
);
}),

Promise.resolve() return only one element in nested array

Here is my code:
search(): Promise<MyModel[]> {
const query = {
'action': 'update',
};
return new Promise((resolve, reject) => {
this.api.apiGet(`${API.SEARCH_STUDENT}`, query).then((data) => {
const a = data.items.map(i => i);
const b = data.items.map(i => i);
console.log(a.array1[0].array2.length); // 1
console.log(b.array1[0].array2.length); // 5
resolve(a);
}, (error) => {
reject(error);
});
});
}
MyModel class:
class MyModel {
...
array1: [{
array2: []
}]
}
data.items[0].array1[0].array2 returned by function apiGet contains 5 elements. But if I put a or b into resolve function, it now just keep first element only like the comments in the snippet.
Could anyone show what I miss here?
Firstly I am not sure why you wrap a promise in a promise.. why do you need to do that when
this.api.apiGet returns a promise.
Also, why are you trying to map? I bet if you console.log(data.items) the same data would come back. I think you have just got a little confused with your code. A tidy up of the code should resolve all of this for you.
I would do something like the below, now every time you call search you get all the data back which you can use when you want it.
search(): Promise<MyModel[]> {
const query = {
'action': 'update',
};
return this.api.apiGet(API.SEARCH_STUDENT, query)
.then((data) => data as MyModel[]));
}

normalizr v3 and JSON api

I want to normalise the responses I receive from an API. A typical response could look something like this:
// Get all projects
{data:[
{
id: 1
...
team:{
data: {
id:15
...
}
}
},
{
id:2,
....
},
{
id:3,
...
}
]}
How do I write my schemas so that it removes the 'data' container?
Currently, my schema looks like:
export const project = new schema.Entity('projects', {
team: team, // team omitted
},
{
processStrategy: (value, parent, key) => parent.data
}
)
export const arrayOfProjects = new schema.Array(project)
And I am using it like:
const normalizedProjects = normalize(jsonResponse, arrayOfProjects)
normalizedProjects then looks like this:
{
entities:{
projects:{
undefined:{
0:{
team:{
data:{
id:15,
...
}
}
},
1:{...},
2:{...}.
...
50:{...},
}
}
},
result:[] // length is 0
}
I'm not sure why the list of projects is contained in 'undefined', either?
I also use json_api schema.
How about like this?
const projectsSchema = new schema.Entity('projects', {}, {
processStrategy: processStrategy
});
export const processStrategy = (value, parent, key) => {
const attr = value.attributes;
delete value.attributes;
return { ...value, ...attr };
};
export const fetchProjectsSchema = {
data: [projectsSchema]
}
Each of your entity schema that you want to have the data omitted (or anything else fundamentalyl changed) needs to include a processStrategy that you write to remove or change any data. (see more examples in the tests)

Categories

Resources