I have an xml file which has several elements (3rd and 4th level <def> as well as <ex> and <kref> ) that may repeat 1 or more times. When I convert this to json (and evenually yaml with yq), the multiple elements are arrays and understandably the single elments are not. How can I make jq/yq consisently render an element in an array even as a single item.
For brevity, I have included below sample xml and json data with the single elements as well as the desired json output where the single elements would be rendered as an array.
I have also tried converting with cat test.xml | xq '.xdxf.lexicon.ar[].def.def.def=[.def]' , and this seems to be starting to get me somewhere, but the resulting def array is empty. Any help on solving this much appreciated.
original xml:
<xdxf revision="034">
<lexicon>
<ar>
<k xml:lang="dyu">headword2</k>
<def freq="">
<co></co>
<def xml:lang="fr">
<def def-id="63491b7b-5d72-400c-95ee-f2e63f984832">
<gr>
<abbr></abbr>
</gr>
<co></co>
<def def-id="32dc09fd-212f-42da-9f45-7d0fa5258c49">
<deftext>
<dtrn>definition1</dtrn>
</deftext>
<ex type="exm" ex-id="dda450aa-121b-415f-92e4-63366fd78fb5">
<ex_orig></ex_orig>
<ex_tran></ex_tran>
</ex>
<etm></etm>
<categ></categ>
</def>
</def>
</def>
<sr>
<kref type="spv"></kref>
</sr>
</def>
</ar>
</lexicon>
</xdxf>
piped to jq .
{
"xdxf": {
"#revision": "034",
"lexicon": {
"ar": {
"k": {
"#xml:lang": "dyu",
"#text": "headword2"
},
"def": {
"#freq": "",
"co": null,
"def": {
"#xml:lang": "fr",
"def": {
"#def-id": "63491b7b-5d72-400c-95ee-f2e63f984832",
"gr": {
"abbr": null
},
"co": null,
"def": {
"#def-id": "32dc09fd-212f-42da-9f45-7d0fa5258c49",
"deftext": {
"dtrn": "definition1"
},
"ex": {
"#type": "exm",
"#ex-id": "dda450aa-121b-415f-92e4-63366fd78fb5",
"ex_orig": null,
"ex_tran": null
},
"etm": null,
"categ": null
}
}
},
"sr": {
"kref": {
"#type": "spv"
}
}
}
}
}
}
}
Desired output:
{
"xdxf": {
"#revision": "034",
"lexicon": {
"ar": {
"k": {
"#xml:lang": "dyu",
"#text": "headword2"
},
"def": {
"#freq": "",
"co": null,
"def": {
"#xml:lang": "fr",
"def": [
{
"#def-id": "63491b7b-5d72-400c-95ee-f2e63f984832",
"gr": {
"abbr": null
},
"co": null,
"def": [
{
"#def-id": "32dc09fd-212f-42da-9f45-7d0fa5258c49",
"deftext": {
"dtrn": "definition1"
},
"ex": [
{
"#type": "exm",
"#ex-id": "dda450aa-121b-415f-92e4-63366fd78fb5",
"ex_orig": null,
"ex_tran": null
}
],
"etm": null,
"categ": null
}
]
}
]
},
"sr": {
"kref": [
{
"#type": "spv"
}
]
}
}
}
}
}
}
My comment:
So you want every 3'th and 4'th def converted to an array with that object?
OP's comment:
Yes. Hopefully if I can see how that is done I can also see how to do convert ex and kref as well.
.xdxf.lexicon.ar.def.def.def |= [ (.def |= [ . ]) ]
First we select the 3'th def:
.xdxf.lexicon.ar.def.def.def
Then we use an update assignment to change it's value to an array, with the object inside
Normally we'd use |= [ . ] to do that
But since we need to perform the same action on the 4'th def, we apply the same logic inside the array to wrap the 4'th def:
(.def |= [ . ])
The () is used to ensure the |= only applies to that object.
Result JSON, visible in this online demo:
{
"xdxf": {
"#revision": "034",
"lexicon": {
"ar": {
"k": {
"#xml:lang": "dyu",
"#text": "headword2"
},
"def": {
"#freq": "",
"co": null,
"def": {
"#xml:lang": "fr",
"def": [
{
"#def-id": "63491b7b-5d72-400c-95ee-f2e63f984832",
"gr": {
"abbr": null
},
"co": null,
"def": [
{
"#def-id": "32dc09fd-212f-42da-9f45-7d0fa5258c49",
"deftext": {
"dtrn": "definition1"
},
"ex": {
"#type": "exm",
"#ex-id": "dda450aa-121b-415f-92e4-63366fd78fb5",
"ex_orig": null,
"ex_tran": null
},
"etm": null,
"categ": null
}
]
}
]
},
"sr": {
"kref": {
"#type": "spv"
}
}
}
}
}
}
}
Related
This question already has answers here:
How can I access and process nested objects, arrays, or JSON?
(31 answers)
Closed 14 days ago.
Using dot notation to access a JSON response is returning different values than what the API response is saying in my browser
I tried to access a JSON response from an axios request like so:
const response = await axios.get('https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en-US&country=US&allowCountries=US');
console.log(response.data.data.Catalog.searchStore.elements.promotions)
Instead of getting a response something similar to this in JSON:
{
"promotions": {
"promotionalOffers": [
{
"promotionalOffers": [
{
"startDate": "2023-02-02T16:00:00.000Z",
"endDate": "2023-02-09T16:00:00.000Z",
"discountSetting": {
"discountType": "PERCENTAGE",
"discountPercentage": 0
}
}
]
}
],
"upcomingPromotionalOffers": []
}
}
I simply get this from the console log:
undefined
undefined
undefined
undefined
undefined
I might not be using dot notation to access it correctly but I have no idea. You can view the JSON response from the browser here: https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en-US&country=US&allowCountries=US
#1 Getting data from the server by axios.
The elements start [ and end ] is an array data.
(* I copy/paste from Browser to VS code after access URL, saved JSON data)
VS code can expand/collapse data by clicking down arrow.
#2 Filter only promotions
It is one of Key values of array elements
You can filter by Array map()
Simple example for getting title only
const titles = elements.map(item => { return { title : item.title } } )
console.log(JSON.stringify(titles, null, 4))
[
{
"title": "Borderlands 3 Season Pass"
},
{
"title": "City of Gangsters"
},
{
"title": "Recipe for Disaster"
},
{
"title": "Dishonored®: Death of the Outsider™"
},
{
"title": "Dishonored - Definitive Edition"
}
]
So this code will works for getting promotions
const axios = require('axios')
const getPromotions = async (data) => {
try {
const response = await axios.get('https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en-US&country=US&allowCountries=US');
return Promise.resolve(response.data)
} catch (error) {
return Promise.reject(error)
}
};
getPromotions()
.then(result => {
const promotions = result.data.Catalog.searchStore.elements.map(item => { return { promotions : item.promotions } } )
console.log(JSON.stringify(promotions, null, 4))
})
.catch(error => {
console.log(error.message)
});
Result
$ node get-data.js
[
{
"promotions": {
"promotionalOffers": [
{
"promotionalOffers": [
{
"startDate": "2023-01-26T16:00:00.000Z",
"endDate": "2023-02-09T16:00:00.000Z",
"discountSetting": {
"discountType": "PERCENTAGE",
"discountPercentage": 30
}
},
{
"startDate": "2023-01-26T16:00:00.000Z",
"endDate": "2023-02-09T16:00:00.000Z",
"discountSetting": {
"discountType": "PERCENTAGE",
"discountPercentage": 30
}
}
]
}
],
"upcomingPromotionalOffers": []
}
},
{
"promotions": {
"promotionalOffers": [
{
"promotionalOffers": [
{
"startDate": "2023-02-02T16:00:00.000Z",
"endDate": "2023-02-09T16:00:00.000Z",
"discountSetting": {
"discountType": "PERCENTAGE",
"discountPercentage": 0
}
}
]
}
],
"upcomingPromotionalOffers": []
}
},
{
"promotions": {
"promotionalOffers": [],
"upcomingPromotionalOffers": [
{
"promotionalOffers": [
{
"startDate": "2023-02-09T16:00:00.000Z",
"endDate": "2023-02-16T16:00:00.000Z",
"discountSetting": {
"discountType": "PERCENTAGE",
"discountPercentage": 0
}
}
]
}
]
}
},
{
"promotions": {
"promotionalOffers": [
{
"promotionalOffers": [
{
"startDate": "2023-02-02T16:00:00.000Z",
"endDate": "2023-02-09T16:00:00.000Z",
"discountSetting": {
"discountType": "PERCENTAGE",
"discountPercentage": 0
}
}
]
}
],
"upcomingPromotionalOffers": []
}
},
{
"promotions": null
}
]
I have currently done with my code transformation on my api data by its roleId.
However, I need to display another view which will group the data according to which projectId the users are in.
I can simply copy and paste and create another one for projectIds data transformation, however, I feel that my approach may be messy and not easily reusable.
So I would like to ask if there is a better way to do this?
Because i cannot simply swap roleIds in the function to projectIds by putting rolesId or projectIds in a variable to be reused in the function.
Can anyone help me please ?
Code for the api data transformation to dislay in ant design tree data table:
let apiData = [
{
email: "alyssayo#xxx.com",
permissionIds: null,
roleIds: ["raa", "baa", "caa"],
projectIds: ["1aa", "3aa"]
},
{
email: "chiuewww#xxx.com",
permissionIds: null,
roleIds: ["baa", "caa"],
projectIds: ["1aa", "2aa", "3aa"]
},
{
email: "lalaqq#xxx.com",
permissionIds: null,
roleIds: ["caa"],
projectIds: ["1aa"]
},
{
email: "sqssq#xxx.com",
permissionIds: null,
roleIds: [],
projectIds: []
}
];
//Isolate and transform data by roleId
const transData = apiData.reduce((arr, item) => {
let formatted = item.roleIds.map((id) => {
return {
roleIds: id,
children: [{ ...item, roleIds: id }]
};
});
return [...arr, ...formatted];
}, []);
//Group transformed data by roleIds
const findMatch = (arr, roleIds) =>
arr.find((item) => item.roleIds === roleIds);
const groupArray = (originalArr) => {
return Array.isArray(originalArr)
? originalArr.reduce((previousObj, obj) => {
if (findMatch(previousObj, obj.roleIds)) {
findMatch(previousObj, obj.roleIds).children.push(...obj.children);
} else {
previousObj.push(obj);
}
return previousObj;
}, [])
: "Need an array";
};
//Call the group roleId function on transformed data by roleId
const userRoledata = groupArray(transData);
//Add key to parent and children
let key = 1;
userRoledata.forEach((item) => {
item.key = key++;
item.children.forEach((child) => {
child.key = key++;
});
});
setData(userRoledata); //this will be dataSource for table rendering in ant design
What will the data transformed display when used as dataSource in ant design:
If grouped by roleIds:
[
{
"roleIds": "raa",
"children": [
{
"email": "alyssayo#xxx.com",
"permissionIds": null,
"roleIds": "raa",
"projectIds": [
"1aa",
"3aa"
],
"key": 2
}
],
"key": 1
},
{
"roleIds": "baa",
"children": [
{
"email": "alyssayo#xxx.com",
"permissionIds": null,
"roleIds": "baa",
"projectIds": [
"1aa",
"3aa"
],
"key": 4
},
{
"email": "chiuewww#xxx.com",
"permissionIds": null,
"roleIds": "baa",
"projectIds": [
"1aa",
"2aa",
"3aa"
],
"key": 5
}
],
"key": 3
},
{
"roleIds": "caa",
"children": [
{
"email": "alyssayo#xxx.com",
"permissionIds": null,
"roleIds": "caa",
"projectIds": [
"1aa",
"3aa"
],
"key": 7
},
{
"email": "chiuewww#xxx.com",
"permissionIds": null,
"roleIds": "caa",
"projectIds": [
"1aa",
"2aa",
"3aa"
],
"key": 8
},
{
"email": "lalaqq#xxx.com",
"permissionIds": null,
"roleIds": "caa",
"projectIds": [
"1aa"
],
"key": 9
}
],
"key": 6
}
]
If grouped by projectIds:
[
{
"projectIds": "1aa",
"children": [
{
"email": "alyssayo#xxx.com",
"permissionIds": null,
"roleIds": [
"raa",
"baa",
"caa"
],
"projectIds": "1aa",
"key": 2
},
{
"email": "chiuewww#xxx.com",
"permissionIds": null,
"roleIds": [
"baa",
"caa"
],
"projectIds": "1aa",
"key": 3
},
{
"email": "lalaqq#xxx.com",
"permissionIds": null,
"roleIds": [
"caa"
],
"projectIds": "1aa",
"key": 4
}
],
"key": 1
},
{
"projectIds": "3aa",
"children": [
{
"email": "alyssayo#xxx.com",
"permissionIds": null,
"roleIds": [
"raa",
"baa",
"caa"
],
"projectIds": "3aa",
"key": 6
},
{
"email": "chiuewww#xxx.com",
"permissionIds": null,
"roleIds": [
"baa",
"caa"
],
"projectIds": "3aa",
"key": 7
}
],
"key": 5
},
{
"projectIds": "2aa",
"children": [
{
"email": "chiuewww#xxx.com",
"permissionIds": null,
"roleIds": [
"baa",
"caa"
],
"projectIds": "2aa",
"key": 9
}
],
"key": 8
}
]
Define a transform function with 2 parameter. First the apiData, which is the data you want to transform and secondly the transformation_key which is a string of either roleIds or projectIds.
Within this function you first to generate an object with the different roleIds/projectIds as keys and for each key an array of all the items included.
To do so you make use of a reducer and loop over the items
apiData.reduce((obj, item) => {
if (!item[transformation_key]) return obj; // in case item[transformation_key] is null, you can skip the item an just return the item as is.
... // if not, we've to reduce over the array of roleIds/projectIds within the item as well.
}, {}) // {} is the new object (`obj` refers to this within the reducer)
Within each item we also have to loop over all the items in the roleIds/projectIds of that item, so we add a second/inner reducer.
// item[transformation_key] is the array of roleIds/projectIds within your item.
item[transformation_key].reduce((cur, id) => {
// `cur` is actually the same object as the `obj` from the outer reducer.
if (!cur[id]) cur[id] = [] // if the key/id doesn't excist yet on the object, we set it equal to an empty array.
cur[id].push({
...item,
[transformation_key]: id
}) // we push the item to the array (using the spread operator and updating the value for the `transformation_key` within the item.
return cur // you must return the object `cur`.
}, obj) // we pass to `obj` from the outer reducer into the inner reducer.
This will generate an object like
const transformedObject = {
[roleIds/projectIds] : [
... all the children
]
}
next we map the ids to the required output
return Object.keys(transformedObject).map(key => {
return {
[transformation_key]: key,
children: transformedObject[key]
}
})
To summarize
function transform(apiData, transformation_key) {
if (!(transformation_key == 'roleIds' || transformation_key == 'projectIds')) throw new Error("Choose either 'roleIds' or 'projectIds' as a transformation_key")
const transformedObject = apiData
.reduce((obj, item) => {
if (!item[transformation_key]) return obj;
return item[transformation_key].reduce((cur, id) => {
if (!cur[id]) cur[id] = []
cur[id].push({
...item,
[transformation_key]: id
})
return cur
}, obj)
}, {});
return Object.keys(transformedObject).map(key => {
return {
[transformation_key]: key,
children: transformedObject[key]
}
})
}
const transDataByRoleIds = transform(res.data, 'roleIds')
const transDataByProjectIds = transform(res.data, 'projectIds')
node v7.7.1
mongodb: 2.2.33,
mongoose: 4.13.7
Hello all,
i'm having this unexpected behaviour when trying to update a document with multiple pull request based on matching criterias. here is what i mean
my document schma looks like this
{
"_id": "5a1c0c37d1c8b6323860dfd0",
"ID": "1511781786844",
"main": {
"_id": "5a3c37bfc065e86a5c593967",
"plan": [
{
"field1": 1,
"field2": 1,
"_id": "5a3c30dfa479bb4b5887e56e",
"child": []
},
{
"field1": 1,
"field2": 2,
"_id": "5a3c30e1a479bb4b5887e5c",
"child": []
},
{
"field1": 1,
"field2": 3,
"_id": "5a3c37bfc065e86a5c593968",
"child": []
},
{
"field1": 1,
"field2": 4,
"_id": "5a3c37bfc065e86a5c593655",
"child": []
},
{
"field1": 1,
"field2": 5,
"_id": "5a3c30dfa479bb4b5887e56f",
"child": []
},
{
"field1": 1,
"field2": 6,
"_id": "5a3c30e1a479bb4b6887e545",
"child": []
},
{
"field1": 1,
"field2": 7,
"_id": "5a3c37bfc065e86a5c5939658",
"child": []
},
{
"field1": 2,
"field2": 2,
"_id": "5a3c37bfc065e86a5c593963",
"child": []
},
]
},
...
....
}
and this is my code to update the document:
Schema.findOne({ID: data.ID})
.then(function(doc) {
var array = doc.main.plan;
for (i = 0; i < array.length; i++) {
if ( array[i].field1=== 1 )) {
var id = array[i]._id;
console.log('pulling');
doc.pull( { _id: id });
}
}
doc.save().then(function(doc) {
console.log('saving');
// console.log(doc);
if (doc && doc.docID) {
return { success: true };
} else {
return { success: false, error: 'unknownError'}
}
})
}
now the issue is let's say my array has 7 objects that matches the test (array[i].theField === parseInt(updFields.theField)), when i run this and check the logs i see that it will basically pull half of the objects and do a save.
so i would get
pulling
pulling
pulling
pulling
save.
and then i have to run the code for the remaining 3 objects in the array and get
pulling
pulling
saving
so i have to run it a third time to completely clear the array.
need help get this working
thank you
So i created a little workaround by doing a recursive function to pull all with only one click using lodash functions. not pretty but it does the job.
const delObjArray = (doc, cond) => {
const checkField = cond.field;
const checkVal = cond.value;
_.forEach(doc, (value) => {
if (value && value[checkField] === checkVal) {
doc.pull({ _id: value._id });
}
});
const isFound = _.some(doc, { [checkField]: checkVal });
if (isFound) {
delObjArray(doc, cond);
} else {
return true;
}
return true;
};
I'm currently working on a new application in React. This is the first time I'm creating something in React. The application will display our own promotions.
My initial state is as follows:
{
"promotion": {
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"startDateTimeStamp": 1510558814960,
"endDateTimeStamp": 1510558814960,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
}
This is created from my defaultPromotion constant. This constant is stored in a separate file, which I call api.js
export const defaultPromotion = {
name: '',
campaign: '',
url: 'https://',
position: 0,
periods: [
{
startDateTimeStamp: Date.now(),
endDateTimeStamp: Date.now(),
variants: [
{
title: '',
text: '',
image: '',
},
]
},
]
}
In my createPromotion component it's created as followed
let promotionState = api.promotions.defaultPromotion;
this.state = {
promotion: promotionState
};
I can add a new period with the following:
addPromotion() {
let promotion = this.state.promotion;
promotion.periods.push( api.promotions.defaultPromotion.periods[0] );
this.forceUpdate();
}
After that, a new period is added as expected. Suggestions to do this with setState() are very welcome! So, my new state is now:
{
"promotion": {
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
},
{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
}
Now, I want to add a new variant for this promotion period, this is where I'm stuck for 2 days now.
I'm adding a new period as follows:
addVariant( periodKey ) {
const promotion = this.state.promotion;
promotion.periods[periodKey].variants.push(api.promotions.defaultPromotion.periods[0].variants[0]);
this.setState({ promotion: promotion });
}
periodKey is here "1", so, I'm expecting that there will be added a new variant for periods[1], but, it's added to both periods. State is now as follows:
{
"promotion": {
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [
{
"title": "",
"text": "",
"image": ""
},
{
"title": "",
"text": "",
"image": ""
}
]
},
{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [
{
"title": "",
"text": "",
"image": ""
},
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
}
Can someone explain me why this is happening and how I can add a new variant the right way?
Many, many thanks in advance!
UPDATE 1
Based on the answers from bennygenel and Patrick Hübl-Neschkudla, my implementation is now as follows:
Setting the initial state:
constructor(props) {
super(props);
let promotionState = api.promotions.defaultPromotion;
this.state = { ...promotionState };
}
Method:
addVariant( periodKey ) {
this.setState((prevState) => {
const { periods } = prevState;
periods[periodKey].variants.push(
Object.assign({}, { ...periods[periodKey].variants, api.promotions.defaultPromotion.periods[0].variants[0]})
);
return { periods };
});
}
But this still is setting the new variant in all the periods. I've also tried the exact code from Benny, but with the same results. The method is called as
this.props.addVariant( this.props.periodKey );
Even when I call it as:
this.props.addVariant(2);
The same behaviour is happening.
UPDATE 2
I now have rewritten everything to redux, this is so I have access to my promotion in every component the easy way, instead off passing them through certain components. Based on the answer of #mersocarlin, I now have the following reducer cases:
Add period
case PROMOTION_ADD_PERIOD:
const { periods } = { ...state };
periods.push(api.promotions.defaultPromotion.periods[0]);
state = {
...state,
periods: periods
};
break;
Add a period variant
case PROMOTION_ADD_PERIOD_VARIANT :
state = {
...state,
periods: [
...state.periods[action.payload.period],
{
variants: [
...state.periods[action.payload.period].variants,
api.promotions.defaultPromotion.periods[0].variants[0]
]
}
]
};
break;
The following case:
Add a new variant, works, state:
{
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"startDateTimeStamp": 1510599968588,
"endDateTimeStamp": 1510599968588,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
},
{
"startDateTimeStamp": 1510599968594,
"endDateTimeStamp": 1510599968594,
"variants": [
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
After that, adding a new variant, kinda works, well, the variant is added, but I'm losing my 2nd period. State:
{
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [
{
"variants": [
{
"title": "",
"text": "",
"image": ""
},
{
"title": "",
"text": "",
"image": ""
}
]
}
]
}
I think this is a small thing I'm not see'ing. Does someone have the solution for the "PROMOTION_ADD_PERIOD_VARIANT" case?
Update 3
Changed the "PROMOTION_ADD_PERIOD" case as follows:
case PROMOTION_ADD_PERIOD:
state = {
...state,
periods: [
...state.periods,
initialState.periods[0]
]
};
break;
Update 4
Finaly found the solution. See the final code for PROMOTION_ADD_PERIOD_VARIANT below:
state = {
...state,
periods: [
...state.periods.map((item, index) => {
if ( index !== action.payload.period ) {
return item;
}
return {
...item,
variants: [
...item.variants,
initialState.periods[0].variants[0]
]
}
})
]
};
Thank you all so much for your help!!
Rather destruct your state object and avoid mutating it directly. This also happens to be a bad pattern.
Whenever you need to add a new item to the array:
const state = {
arrayProp: [{ prop1: 'prop1', prop2: 'prop2' }]
}
const newItem = {
prop1: 'value1',
prop2: 'value2',
}
const newState = {
...state,
arrayProp: [
...state.arrayProp,
newItem,
]
}
console.log('newState', newState)
Same applies for nested properties within your state:
Redux also uses this very same approach
const state = {
objectProp: {
arrayPropWithinArray: [
{ id: '0', otherProp: 123, yetAnotherProp: 'test' },
{ id: '1', otherProp: 0, yetAnotherProp: '' }
]
}
}
const { objectProp } = state
const index = objectProp.arrayPropWithinArray.findIndex(obj => obj.id === '1')
const newSubItem = {
otherProp: 1,
yetAnotherProp: '2',
}
const newState = {
...state,
objectProp: {
...objectProp,
arrayPropWithinArray: [
...objectProp.arrayPropWithinArray.slice(0, index),
{
...objectProp.arrayPropWithinArray[index],
...newSubItem,
},
...objectProp.arrayPropWithinArray.slice(index + 1),
]
}
}
console.log('newState', newState)
Your specific case (as described in your comment)
const periodKey = '2' // your periodKey var. Get it from the right place, it can be your action for example
const index = state.periods.findIndex(period => period.id === periodKey) // find which index has to be updated
state = {
...state, // propagates current state
periods: [
...state.periods.slice(0, index), // propagates everything before index
{
...state.periods[index],
variants: [
...state.periods[index].variants,
api.promotions.defaultPromotion.periods[0].variants[0],
],
},
...state.periods.slice(0, index + 1) // propagates everything after index
]
}
So, what's happening here is that you have an array with two references to the same object.
Imagine it like this:
myArray[0] = reference to defaultPromotion
myArray[1] = reference to defaultPromotion
That's actually a wonderful example of why immutability concepts got so much attention in the past few years :)
What you'd want to do here is instead of adding the defaultPromotion object to the promotions array, you create a new object with the same props as this object and add it. It would look something like this (depending on your ES version etc.)
promotion.periods.push(
Object.assign({}, api.promotions.defaultPromotion.periods[0])
);
This way, you're creating a new object and pass this to the array instead of a reference to the already existing one.
First suggestion, if you are going to have only one promotion object in your state and not an array, lose the promotion level. this will reduce the complexity of your state. You can use spread syntax to easily set your initial state.
Example
let promotionState = api.promotions.defaultPromotion;
this.state = { ...promotionState };
Above code would end up creating a state like below;
{
"name": "",
"campaign": "",
"url": "https://",
"position": 0,
"periods": [{
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [{
"title": "",
"text": "",
"image": ""
}]
}, {
"startDateTimeStamp": 1510559984421,
"endDateTimeStamp": 1510559984421,
"variants": [{
"title": "",
"text": "",
"image": ""
}]
}]
}
Another suggestion I can make is to use functional setState to reduce possibility to mutate.
Example
addPromotion() {
this.setState((prevState) => {
const { periods } = prevState;
periods.push(api.promotions.defaultPromotion.periods[0]);
return { periods };
});
}
addVariant( periodKey ) {
this.setState((prevState) => {
const { periods } = prevState;
periods[periodKey].variants.push(api.promotions.defaultPromotion.periods[0].variants[0]);
return { periods };
});
}
I am trying to get these data below,
Table relations:
people(one) <---> (many) people_info (one) <---> (many) people_contact
in the following format,
people: {
p_id: 10,
p_price: 3.99,
people_info : [
{
pl_id: 3,
pl_state: 2,
pl_district: 6,
pl_latitude: 6.323434,
pl_longitude: 108.23499,
people_contact: [
{
plc_id: 2
},
{
plc_id: 1
}
]
},
{
pl_id: 2,
pl_state: 7,
pl_district: 12,
pl_latitude: 6.000434,
pl_longitude: 108.9910003,
people_contact: [
{
plc_id: 5
},
{
plc_id: 9
}
]
}
]
}
Currently with these controller codes,
class PeopleController extends Controller
{
public function actionPeople($params){
Yii::$app->response->format = Response::FORMAT_JSON;
....//some other codes//.....
$people= People::find()->select(['p_id', 'p_price'] )->where(['p_id' => $itemId])->one();
$info= PeopleContact::find()->with(['plPeople'])->asArray([])->all();
return array(
'people' => $people,
'info' => $info,
);
}
}
I got these,
"people": {
"p_id": "3",
"p_price": "32.42"
}, "locations": [{
"pl_id": "1",
"pl_people": "3",
"pl_title": "",
"pl_latitude": "6.16438700000000000000",
"pl_longitude": "102.28314649999993000000",
"pl_place": null,
"pl_premise": null,
"pl_street": "1",
"pl_area": "1",
"pl_postcode": "1",
"pl_district": "1",
"pl_state": "3",
"pl_country": 1,
"place": null,
"premise": null,
"street": null,
"area": null,
"postcode": null,
"district": null,
"state": null,
"country": "United Kingdom",
"contacts": [{
"plc_name": "joe",
"plc_phone": "123456",
"plc_email": null
}]
}]
}
How do I achieve it in the format mentioned at the top?
$output;
$people=People::find()->select(['p_id', 'p_price'] )->asArray()->all();
foreach($people as $person) {
$infos = PersonInfo::find()->where(['person_id' => $person->id])->asArray()->all();
foreach($infos as $info) {
$contacts = PersonContact::find()->where(['person_info_id' => $info->id])->asArray()->all();
foreach($contacts as $contact) {
$info['contacts'][] = $contact;
}
$person['info'][] = $info
}
$output['people'][] = $person
}
return $output;
You should loop through and fetch data like this: people > info > contact each next level relying on info fetched from the previous one. Then store it in the format you want such as demonstrated above.
This will output something like:
"people": [{
...
"info": [{
...
"contacts": [{
...
},{
...
}]
}]
},{
...
}]