Create nested comments array from JSON response - javascript

I would like to use the Hacker News Algolia API (exact query) to find comments, and then create a nested JSON structure from the data. I know I need to use recursion, but I am not too sure how.
This is the structure I would like:
comment1
comment2
comment3
comment4
comment5
In JSON:
[
{
"text": "eee",
"children": [
{
"text": "123",
"childeren": [
{
"text": "rew"
}
]
}
]
},
{
"text": "comment4",
"children": []
},
{
"text": "comment5",
"children": []
}
]
The issue is that the API doesn't return comments in the format above. Returned comments have attribute parent_id which is a reference to their parent comment's objectID. So if you have the following nested objectIDs:
foo
bar
foobar
foobar's parent_id is bar and bar's parent_id is foo. And finally foo's parent_id is the Hacker News post ID, in this case 24120336.
What I have so far [repl]:
import axios from 'axios'
interface Comment {
created_at: string;
author: string;
comment_text: string;
story_id: number;
story_title: string;
story_url: string;
parent_id: number | null;
objectID: string;
}
function getChildren(comment: Comment, allComments: Comment[]) {
const children = allComments.filter(
(c) => String(c.parent_id) === comment.objectID
);
const fullChildren = children.forEach((child) =>
getChildren(child, allComments)
);
return fullChildren;
}
const main = async () => {
const { data } =
await axios.get<{ hits: Comment[] }>(
"https://hn.algolia.com/api/v1/search?tags=comment,story_24120336"
);
data.hits.forEach((comment) => {
// Check if comment is top level
if (String(comment.parent_id) === "24120336") {
console.log(getChildren(comment, data.hits));
}
});
}
main()

forEach is for looping through an Array. Use map to convert an Array to a new Array. Try:
function getChildren(comment: Comment, allComments: Comment[]) {
return {
...comment,
children: allComments
.filter(c => String(c.parent_id) === comment.objectID)
.map(c => getChildren(c, allComments))
};
}
...which will give you the nested array of arrays you want.
However I'd recommend a different approach. Note that for each child you're looping through the entire collection. It's far more efficient to do one pass through the Array to collect the parent/child relationships:
const byParent = new Map<string, Array<Comment>>();
for (const comment of allComments) {
let children = byParent.get(comment.parent_id);
if (!children) {
children = [];
byParent.set(comment.parent_id, children);
}
children.push(comment);
}
Now you can do a byParent.get(comment.objectID) at any time to get child comments and do so recursively when necessary:
function getChildren(comment: Comment) {
return {
...comment,
children: byParent.get(comment.objectID)?.map(getChildren)
};
}

Related

Update ngrx state in nested object Angular

I would be able to update an object nested in another object in my application but i got some problems.
Let's assume that the entity that I want to update is something like this:
{
{
"id": 0,
"name": "Yellow Car",
"details": {
"engine": {},
"ownerInfo": {
"name": "Luke",
"lastName": "Cage",
"email": "l.cage#hisemail.blabla"
},
},
"created": "2018-01-17",
"lastUpdate": "2020-09-03",
}
I can easily update some part of this entity in this way:
let car: Car = {
...car,
...this.form.value
};
let carUpdate: Update<Car> = {
id: car.id,
changes: car
};
this.store.dispatch(carUpdated({carUpdate}));
But in this way I can only update name, created, lastUpdate and I can't update the nested object details. What happens if I try to edit the detail object now? Nothing i wanna happens.
This is the selector:
export const carUpdated = createAction(
"[Edit Car] Car Updated",
props<{carUpdate: Update<Car>}>()
);
The effect:
saveCar$ = createEffect(
() => this.actions$
.pipe(
ofType(CarActions.carUpdated),
concatMap(action => this.carService.editCar(
action.carUpdate.changes,
action.carUpdate.id
))
),
{dispatch: false}
)
The reducer:
on(CarActions.carUpdated, (state, action) =>
adapter.updateOne(action.carUpdate, state)),
The service sends to the backend the right data and it's working good without the state management.
What I am doing now is retrieve the single carObject in this way in the component in the ngOnInit
car$ = this.store.pipe(
select(selectCar(id))
)
and the selector is:
export const selectCar = (id) => createSelector(
selectAllCars,
(cars: any) => cars.filter((car) => {
let filteredCar = car.id == id;
if(filteredCar) {
return car;
}
}).map(car => car)
);
and then after my edit I can use the dispatch to confirm my edit
this.store.dispatch(carUpdated({carUpdate}));
but as I said if I try to update the details object i have this
let car: Car = {
...car, // the entire car object
...this.form.value // only the details
};
let carUpdate: Update<Car> = {
id: car.id,
changes: car //the details are mixed with the car object and not inside the object itself
};
something like this:
{
"id": 0,
"name": "Yellow Car",
"engine": {},
"details": {
},
"ownerInfo": {
"name": "Luke",
"lastName": "Cage",
"email": "l.cage#hisemail.blabla"
},
"created": "2018-01-17",
"lastUpdate": "2020-09-03",
}
Is there an easy way to fix this?
I'll try to provide a general answer which might show techniques on how to update one object with another object.
If in the form, you only have the details as update (update is Partial<Car["details"]>, you can do this:
const update: Partial<Car["details"]> = this.form.value;
const newCar: Car = {
...oldCar,
details: {
...oldCar.details,
...update
}
Then there's the possibility that the update is (partial) Car with partial details:
const update: Partial<Car> & { details: Partial<Car["details"]> } = this.form.value;
const newCar: Car = {
...oldCar,
...update,
details: {
...oldCar.details,
...update.details
}
}
An improbable option is that you have mixed detail and car properties in the update (for whatever reason - you might be doing something wrong). Then you can pick them by hand. Note that this will delete old values if new values are undefined.
const update: Pick<Car, 'name' | 'lastUpdate'> & Pick<Car["details"], 'ownerInfo'> = this.form.value;
const newCar: Car = {
...oldCar,
name: update.name,
lastUpdate: update.lastUpdate,
name: update.name
details: {
...oldCar.details,
ownerInfo: details.ownerInfo
}
}

React extracting a nested json object

How can I extract the 'jobs' object from a nested json list like this:
result:
{
person:
[
{
name: ""
address: ""
jobs: [
{
company:""
},
{
company:""
}
]
}
]
}
Thank you
Write a generic method to extract object properties.
function onExtract(key, data) {
if (isObject(data)) {
for (let item in data) {
if (key === item) {
return data[item];
}
const res = onExtract(key, data[item]);
if (res !== null) return res;
}
}
if (isArray(data)) {
for (let item of data) {
const res = onExtract(key, item);
if (res !== null) return res;
}
}
return null;
}
function isObject(obj) {
return Object.prototype.toString.call(obj) === "[object Object]";
}
function isArray(arr) {
return Object.prototype.toString.call(arr) === "[object Array]";
}
// test
const data = {
person: [
{
name: "",
address: "",
jobs: [
{
company: ""
},
{
company: ""
}
]
}
]
};
console.log(onExtract("jobs", data));
let's say you have a return var that contains this json value
let mappedCompanies = return.person.map(person =>
person.jobs.map(job => job.company)
).flatMap(m => m)
mappedCompanies would contain an array with all the companies names for each one of the registers in "person", all as one array of strings
you can read more about Array.map() here: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Array/map
A dynamic way to query the person[] and find jobs, is to use the javascript map() method.
Here is the code without comments.
const personsJobs = (personName, personAddress) => {
const jobs = result.person.map((el) => {
if (el.name === personName && el.address === personAddress) {
return el.jobs;
} else {
return null;
}
})
.filter((el) => el !== null);
return jobs;
};
console.log(personsJobs("wyatt", "1234 test ln"));
Here is the code with comments to explain how the personsJob function works.
// Blow is an ES6 arrow function with the parameters 'personName' and 'personAddress',
// which represents the person in which you are querying for jobs (using both a persons
// name and address so in the case of persons with the same name, you only find the jobs
// of the person you want).
const personsJobs = (personName, personAddress) => {
// Since 'person' is an array, we can use the 'map' method as stated before, which
// will create a new array (jobs) that will store the jobs a specific person has.
const jobs = result.person.map((el) => {
// el stands for the current position in the person array.
// if el's (the current person) name and address values are equal to that of the
// parameters personName and personAddress, then that persons jobs are added to the jobs // array, however, if el does not satisfy the two parameters, null is added to the jobs
// array.
// The array, upon completion, will look something like this: ["programmer", null, null]
if (el.name === personName && el.address === personAddress) {
return el.jobs;
} else {
return null;
}
})
// Finally, the filter method is called to remove all null values so that you will
// only have the persons job in the jobs array.
// After filtering, the array will look like this: ["programmer"]
.filter((el) => el !== null);
return jobs;
};
// Prints the array of wyatt's jobs
console.log(personsJobs("wyatt", "1234 test ln"));
So, following the conclusion of the function, you will have dynamically found the jobs of a specific person.
you can use flatMap function like:
const jobsData = result.person.flatMap(item => item.jobs);
Here is a flexible solution using object-scan
// const objectScan = require('object-scan');
const data = { person: [{ name: '', address: '', jobs: [{ company: '' }, { company: '' }] }] };
console.log(objectScan(['person[*].jobs'], { reverse: false, rtn: 'value' })(data));
// => [ [ { company: '' }, { company: '' } ] ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#14.0.0"></script>
Disclaimer: I'm the author of object-scan

React setState of for deeply nested value

I’ve got a very deeply nested object in my React state. The aim is to change a value from a child node. The path to what node should be updated is already solved, and I use helper variables to access this path within my setState.
Anyway, I really struggle to do setState within this nested beast. I abstracted this problem in a codepen:
https://codesandbox.io/s/dazzling-villani-ddci9
In this example I want to change the child’s changed property of the child having the id def1234.
As mentioned the path is given: Fixed Path values: Members, skills and variable Path values: Unique Key 1 (coming from const currentGroupKey and both Array position in the data coming from const path
This is my state object:
constructor(props) {
super(props);
this.state = {
group:
{
"Unique Key 1": {
"Members": [
{
"name": "Jack",
"id": "1234",
"skills": [
{
"name": "programming",
"id": "13371234",
"changed": "2019-08-28T19:25:46+02:00"
},
{
"name": "writing",
"id": "abc1234",
"changed": "2019-08-28T19:25:46+02:00"
}
]
},
{
"name": "Black",
"id": "5678",
"skills": [
{
"name": "programming",
"id": "14771234",
"changed": "2019-08-28T19:25:46+02:00"
},
{
"name": "writing",
"id": "def1234",
"changed": "2019-08-28T19:25:46+02:00"
}
]
}
]
}
}
};
}
handleClick = () => {
const currentGroupKey = 'Unique Key 1';
const path = [1, 1];
// full path: [currentGroupKey, 'Members', path[0], 'skills', path[1]]
// result in: { name: "writing", id: "def1234", changed: "2019-08-28T19:25:46+02:00" }
// so far my approach (not working) also its objects only should be [] for arrays
this.setState(prevState => ({
group: {
...prevState.group,
[currentGroupKey]: {
...prevState.group[currentGroupKey],
Members: {
...prevState.group[currentGroupKey].Members,
[path[0]]: {
...prevState.group[currentGroupKey].Members[path[0]],
skills: {
...prevState.group[currentGroupKey].Members[path[0]].skills,
[path[1]]: {
...prevState.group[currentGroupKey].Members[path[0]].skills[
path[1]
],
changed: 'just now',
},
},
},
},
},
},
}));
};
render() {
return (
<div>
<p>{this.state.group}</p>
<button onClick={this.handleClick}>Change Time</button>
</div>
);
}
I would appreciate any help. I’m in struggle for 2 days already :/
Before using new dependencies and having to learn them you could write a helper function to deal with updating deeply nested values.
I use the following helper:
//helper to safely get properties
// get({hi},['hi','doesNotExist'],defaultValue)
const get = (object, path, defaultValue) => {
const recur = (object, path) => {
if (object === undefined) {
return defaultValue;
}
if (path.length === 0) {
return object;
}
return recur(object[path[0]], path.slice(1));
};
return recur(object, path);
};
//returns new state passing get(state,statePath) to modifier
const reduceStatePath = (
state,
statePath,
modifier
) => {
const recur = (result, path) => {
const key = path[0];
if (path.length === 0) {
return modifier(get(state, statePath));
}
return Array.isArray(result)
? result.map((item, index) =>
index === Number(key)
? recur(item, path.slice(1))
: item
)
: {
...result,
[key]: recur(result[key], path.slice(1)),
};
};
const newState = recur(state, statePath);
return get(state, statePath) === get(newState, statePath)
? state
: newState;
};
//use example
const state = {
one: [
{ two: 22 },
{
three: {
four: 22,
},
},
],
};
const newState = reduceStatePath(
state,
//pass state.one[1],three.four to modifier function
['one', 1, 'three', 'four'],
//gets state.one[1].three.four and sets it in the
//new state with the return value
i => i + 1 // add one to state.one[0].three.four
);
console.log('new state', newState.one[1].three.four);
console.log('old state', state.one[1].three.four);
console.log(
'other keys are same:',
state.one[0] === newState.one[0]
);
If you need to update a deeply nested property inside of your state, you could use something like the set function from lodash, for example:
import set from 'lodash/set'
// ...
handleClick = () => {
const currentGroupKey = 'Unique Key';
const path = [1, 1];
let nextState = {...this.state}
// as rightly pointed by #HMR in the comments,
// use an array instead of string interpolation
// for a safer approach
set(
nextState,
["group", currentGroupKey, "Members", path[0], "skills", path[1], "changed"],
"just now"
);
this.setState(nextState)
}
This does the trick, but since set mutates the original object, make sure to make a copy with the object spread technique.
Also, in your CodeSandbox example, you set the group property inside of your state to a string. Make sure you take that JSON string and construct a proper JavaScript object with it so that you can use it in your state.
constructor(props) {
super(props)
this.setState = { group: JSON.parse(myState) }
}
Here's a working example:
CodeSandbox

#Ngrx/store: how to query models with relationships

I an Angular 2 app using Redux (with #ngrx/store), I have modeled the store this way:
{
modelA: {
ids: [1, 2],
entities: { 1: { name: "name modelA 1" },
2: { name: "name modelA 2" }
}
},
modelB: {
ids: [5, 8],
entities: { 5: { name: "name modelB 5" },
8: { name: "name modelA 8" },
9: { name: "name modelA 9" }
}
}
}
Basically, I have 2 types of objects: modelA and modelB. This is ok for now.
But I can't find which is the best way to write a relationship between then, representing something like modelA has many modelB (one-to-many). Can I do something like this?
modelAmodelB: {
entities: {
1: [5],
2: [8, 9]
}
}
This is in the root of the store, it's not a child from 'modelA'.
This might work, but how then would I 'query' the modelB from a specific modelA, using #ngrx/store methods? Because if I write a selector function that reads the global state and returns the partial state from modelAmodelB, I don't have access to the rest of the state when I compose my functions. Example:
compose(getModelAEntities(number[]), getModelAModelBRelations(modelA_id: number), getModelAModelBState() );
I can query this using Observable.combineLast
Observable
.combineLatest(
this.store.select('contentContents'),
this.store.select('contents'),
(relations: any, contents: any) => {
return relations.entities[1].map( id => {
return contents.entities[id]
})
}
).subscribe( data => {
console.log(data);
})
But I don't know if this is right: anytime I change modelA entities object (adding a new one, for example), the subscribe() is called, but the output is the same, because neither modelA entity has changed nor its modelB related objects.
PS: I could do the query this way
export const getModelAModelBs = (id) => {
return state =>
state.select( (s: any) => [s.modelAModelB.entities[id], s.modelB.entities])
.distinctUntilChanged( (prev, next) => {
const new_m = (next[0] || []).map( id => {
return next[1][id];
});
const old_m = (next[0] || []).map( id => {
return prev[1][id];
});
return prev[0] === next[0] && (JSON.stringify(new_m) === JSON.stringify(old_m))
})
.map( ([ids = [], modelBs]) => ids.map( id => modelBs[id]) );
};
//use the selector
this.store.let(getModelAModelBs(1)).subscribe( data => {
console.log(data)
})
But I don't know if this is the best approach.
I was struggling with the same issue and came up with a solution of wrappers.
Please take a look at the ngrx-entity-relationship library: https://www.npmjs.com/package/ngrx-entity-relationship
you can create selectors like that:
export const selectUserWithCompany = entityUser(
entityUserCompany(),
);
and then to use it with the store
this.store.select(selectUserWithCompany, 'userId');
to get
{
id: 'userId',
companyId: 'companyId',
company: {
id: 'companyId',
// ...,
},
// ...,
}

JavaScript Array Comparison Function

I currently have a map of an array of users which all have a unique _id key / value.
user = [{_id: "1", ... }, {_id: "2", ... }, ... ]
I also have two other arrays, one named teams and another named accounts.
teams = [{ _id: "1", members: [{ userId: "2" }, { userId: "4" }, ... ], ... }]
accounts = [{ _id: "1", authorizedUsers: [{ userId: "3"}, ... ], ownerTeamId: "2" }, ... ]
Trying to create two comparison functions which takes the argument of user and outputs numberOfTeams and numberOfAccounts for the corresponding user.
I have attempted the numberOfTeams below but I'm not sure if it's the most optimal.
numberOfTeams(user) {
let count = 0;
teams.forEach(team => {
team.members.forEach(member => {
if (member.userId === user._id) {
count++
}
})
});
return count;
}
With the numberOfAccounts, I'm stuck on how to compare authorizedUsers === user._id OR ownerTeamId === team._id where also members.userId === user.id, and then count++.
It’s probably a good start to write a function to get the teams a user belongs to:
function containsUserId(users, id) {
return users.some(user => user.userId === id);
}
function getUserTeams(user, teams) {
return teams.filter(team =>
containsUserId(team.members, user._id));
}
because then you can write numberOfTeams using it:
numberOfTeams(user) {
return getUserTeams(user, teams).length;
}
then a similar function to get accounts:
function getUserAccounts(user, accounts) {
const userTeamIds = new Set(
getUserTeams(user).map(team => team._id)
);
return accounts.filter(account =>
containsUserId(account.authorizedUsers, user._id) ||
userTeamIds.has(accounts.ownerTeamId));
}
then numberOfAccounts using it:
numberOfAccounts(user) {
return getUserAccounts(user, accounts).length;
}
Essentially: use more functions so you can understand the steps you’re taking to solve your own problem and, in doing so, use those steps more effectively.

Categories

Resources