JS get value of nested key with changing parent name - javascript

I need to get the value of roles in the following example.
const obj ={
id: 1,
"website.com": {
roles: ["SuperUser"]
}
}
const r = obj.hasOwnProperty("roles")
console.log(r)
Its parent objects name ("website.com") can change everytime as Im requesting it from the db. What is the best way to get this variable?
The obj would also be relatively large I just didnt include it in the example.

You could iterate over the object and exclude id. Example:
for (var x in obj) {
if (x !== 'id') {
console.log(obj[x].roles)
}
}
EDIT (to address your question edit):
If the root object has many keys, it would probably make sense to instead either move the domain from a key to a value (for example, domain: 'website.com' and move the roles up (flattening the object); or you could check for a key that looks like a domain using a regex. Example: if (/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/.test(x) rather than if (x !== 'id'). The regex way would probably be brittle.
EDIT 2:
You could use the hasOwnProperty check like this:
let roles
for (let x in obj) {
if (obj[x].hasOwnProperty(roles)) {
roles = obj[x])
}
}

You can get the roles by destructuring the roles property from the value of obj['website.com'].
If you want to do this dynamically, you will need to figure out key has a corresponding object with the property roles. Once you find all valid candidates, you can access the first (or whichever one you want) and then grab its value.
const hasRoles = obj => Object.entries(obj)
.filter(([key, value]) =>
value.hasOwnProperty('roles'));
const obj = {
id: 1,
"website.com": {
roles: ["SuperUser"]
}
}
const [ first ] = hasRoles(obj);
const [ website, { roles } ] = first;
console.log(`website = ${website} | roles = ${roles}`);
Alternatively, for a greedy match:
const hasRolesGreedy = obj => Object.entries(obj)
.find(([key, value]) =>
value.hasOwnProperty('roles'));
const obj = {
id: 1,
"website.com": {
roles: ["SuperUser"]
}
}
const found = hasRolesGreedy(obj);
const [ website, { roles } ] = found;
console.log(`website = ${website} | roles = ${roles}`);

only access to the nested key like this:
let roles = obj["website.com"].roles;
console.log(roles);
That way, you will get an array, then you can iterate or get a value by the index.

Since you don't know what the name is, you need to iterate over all properties and find the first with the roles property:
const testObj ={
id: 1,
"website.com": {
roles: ["SuperUser"]
}
}
const testObj2 = {
id: 2,
"anotherwebsite.com":{
roles: ["AnotherSuperUser"]
}
}
function getRoles(obj){
for(let x in obj){
if(Object.prototype.hasOwnProperty.call(obj, x))
{
if(obj[x].roles){
return obj[x].roles;
}
}
}
return undefined;
}
console.log(getRoles(testObj));
console.log(getRoles(testObj2));

Related

Can destructuring an array be used to map properties of each elements?

Suppose there is an array like this:
const a = [ {p:1}, {p:2}, {p:3} ];
Is it possible to destructure this array in order to obtain p = [1, 2, 3] ?
Because this does not work :
const [ ...{ p } ] = a; // no error, same as const p = a.p;
// p = undefined;
Edit
In response to all the answers saying that I need to use Array.prototype.map, I am aware of this. I was simply wondering if there was a way to map during the destructuring process, and the answer is : no, I need to destructure the array itself, then use map as a separate step.
For example:
const data = {
id: 123,
name: 'John',
attributes: [{ id:300, label:'attrA' }, { id:301, label:'attrB' }]
};
function format(data) {
const { id, name, attributes } = data;
const attr = attributes.map(({ label }) => label);
return { id, name, attr };
}
console.log( format(data) };
// { id:123, name:'John', attr:['attrA', 'attrB'] }
I was simply wondering if there was a way, directly during destructuring, without using map (and, respectfully, without the bloated lodash library), to retrive all label properties into an array of strings.
Honestly I think that what you are looking for doesn't exist, normally you would map the array to create a new array using values from properties. In this specific case it would be like this
const p = a.map(element => element.p)
Of course, there are some packages that have many utilities to help, like Lodash's map function with the 'property' iteratee
you can destructure the first item like this :
const [{ p }] = a;
but for getting all values you need to use .map
and the simplest way might be this :
const val = a.map(({p}) => p)
Here's a generalized solution that groups all properties into arrays, letting you destructure any property:
const group = (array) => array.reduce((acc,obj) => {
for(let [key,val] of Object.entries(obj)){
acc[key] ||= [];
acc[key].push(val)
}
return acc
}, {})
const ar = [ {p:1}, {p:2}, {p:3} ];
const {p} = group(ar)
console.log(p)
const ar2 = [{a:2,b:1},{a:5,b:4}, {c:1}]
const {a,b,c} = group(ar2)
console.log(a,b,c)

Javascript - Access a nested property on an object from an array of strings

I have an object like this
{
metadata: {
correlationId: 'b24e9f21-6977-4553-abc7-416f8ed2da2d',
createdDateTime: '2021-06-15T16:46:24.247Z'
}
}
and I have an array of the properties I wanna access
[metadata, correlationId]
how can I dynamically access the property on the object?
like
keys.forEach((key) => {
object[key][key2] ???
})
it needs to be dynamic since I don't know how deep we need to access the object
Here is a solution without recursion:
const myObj = {
a: {
b: {
c: "I'm the target"
}
}
}
const keys = ['a', 'b', 'c'];
let result = myObj;
for (const key of keys) {
result = result[key];
}
console.log(result);
Or with recursion:
const finder = (obj, keys, index = 0) => {
const result = obj[keys[index++]];
if (!result) {
return obj;
}
return finder(result, keys, index);
}
console.log(finder(myObj, keys));
This is pretty similar to Accessing nested JavaScript objects and arrays by string path, except with one fewer step - you already have the keys you need in the form of an array. .reduce and access the next nested value in each iteration.
const obj = {
metadata: {
correlationId: 'b24e9f21-6977-4553-abc7-416f8ed2da2d',
createdDateTime: '2021-06-15T16:46:24.247Z'
}
};
const keys = ['metadata', 'correlationId'];
const result = keys.reduce((a, key) => a[key], obj);
console.log(result);
This is my idea to solve your problem. Tell me, if is ok for you.
let x = {
metadata: {
correlationId: 'b24e9f21-6977-4553-abc7-416f8ed2da2d',
createdDateTime: '2021-06-15T16:46:24.247Z'
}
}
let fun = x => typeof x === 'string' ? console.log(x) : Object.keys(x).map( y => fun(x[y]));
fun(x);

Assign dynamically nested array of classes

I need to be able to receive data from an external API and map it dynamically to classes. When the data is plain object, a simple Object.assign do the job, but when there's nested objects you need to call Object.assign to all nested objects.
The approach which I used was to create a recursive function, but I stumble in this case where there's a nested array of objects.
Classes
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
}
Function to initialize a class
function create(instance: object, data: any) {
for (const [key, value] of Object.entries(instance)) {
if (Array.isArray(value)) {
for (const element of data[key]) {
// get the type of the element in array dynamically
const newElement = new User();
create(newElement, element)
value.push(newElement);
}
} else if (typeof value === 'object') {
create(value, data[key]);
}
Object.assign(value, data);
}
}
const orgWithError = Object.assign(new Organization(), { admin: { id: 'admin-external' }});
console.log(orgWithError.admin.getFullName()); // orgWithError.admin.getFullName is not a function
const org = new Organization();
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
create(org, data);
// this case works because I manually initialize the user in the create function
// but I need this function to be generic to any class
console.log(org.users[0].getFullName()); // "name surname"
Initially I was trying to first scan the classes and map it and then do the assign, but the problem with the array of object would happen anyway I think.
As far as I understand from your code, what you basically want to do is, given an object, determine, what class it is supposed to represent: Organization, Account or User.
So you need a way to distinguish between different kinds of objects in some way. One option may be to add a type field to the API response, but this will only work if you have access to the API code, which you apparently don't. Another option would be to check if an object has some fields that are unique to the class it represents, like admin for Organization or account for User. But it seems like your API response doesn't always contain all the fields that the class does, so this might also not work.
So why do you need this distinction in the first place? It seems like the only kind of array that your API may send is array of users, so you could just stick to what you have now, anyway there are no other arrays that may show up.
Also a solution that I find more logical is not to depend on Object.assign to just assign all properties somehow by itself, but to do it manually, maybe create a factory function, like I did in the code below. That approach gives you more control, also you can perform some validation in these factory methods, in case you will need it
class Organization {
id = 'org1';
admin = new User();
users: User[] = [];
static fromApiResponse(data: any) {
const org = new Organization()
if(data.id) org.id = data.id
if(data.admin) org.admin = User.fromApiResponse(data.admin)
if(data.users) {
this.users = org.users.map(user => User.fromApiResponse(user))
}
return org
}
}
class User {
id = 'user1';
name = 'name';
account = new Account();
getFullName() {
return `${this.name} surname`;
}
static fromApiResponse(data: any) {
const user = new User()
if(data.id) user.id = data.id
if(data.name) user.name = data.name
if(data.account)
user.account = Account.fromApiResponse(data.account)
return user
}
}
class Account {
id = 'account1';
money = 10;
calculate() {
return 10 * 2;
}
static fromApiResponse(data: any) {
const acc = new Account()
if(data.id) acc.id = data.id
if(data.money) acc.money = data.money
return acc
}
}
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const organization = Organization.fromApiResponse(data)
I can't conceive of a way to do this generically without any configuration. But I can come up with a way to do this using a configuration object that looks like this:
{
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
}
and a pointer to the root node, 'org'.
The keys of this object are simple handles for your type/subtypes. Each one is mapped to an object that has a _ctor property pointing to a constructor function, and a collection of other properties that are the names of members of your object and matching properties of your input. Those then are references to other handles. For an array, the handle is [surrounded by square brackets].
Here's an implementation of this idea:
const create = (root, config) => (data, {_ctor, ...keys} = config [root]) =>
Object.assign (new _ctor (), Object .fromEntries (Object .entries (data) .map (
([k, v]) =>
k in keys
? [k, /^\[.*\]$/ .test (keys [k])
? v .map (o => create (keys [k] .slice (1, -1), config) (o))
: create (keys [k], config) (v)
]
: [k, v]
)))
class Organization {
constructor () { this.id = 'org1'; this.admin = new User(); this.users = [] }
}
class User {
constructor () { this.id = 'user1'; this.name = 'name'; this.account = new Account() }
getFullName () { return `${this.name} surname`}
}
class Account {
constructor () { this.id = 'account1'; this.money = 10 }
calculate () { return 10 * 2 }
}
const createOrganization = create ('org', {
org: { _ctor: Organization, admin: 'usr', users: '[usr]' },
usr: { _ctor: User, account: 'acct' },
acct: { _ctor: Account }
})
const orgWithoutError = createOrganization ({ admin: { id: 'admin-external' }});
console .log (orgWithoutError .admin .getFullName ()) // has the right properties
const data = { id: 'org2', admin: { id: 'admin2' }, users: [ { id: 'user-inside' }]}
const org = createOrganization (data)
console .log (org .users [0] .getFullName ()) // has the right properties
console .log ([
org .constructor .name,
org .admin .constructor.name, // has the correct hierarchy
org .users [0]. account. constructor .name
] .join (', '))
console .log (org) // entire object is correct
.as-console-wrapper {min-height: 100% !important; top: 0}
The main function, create, receives the name of the root node and such a configuration object. It returns a function which takes a plain JS object and hydrates it into your Object structure. Note that it doesn't require you to pre-construct the objects as does your attempt. All the calling of constructors is done internally to the function.
I'm not much of a Typescript user, and I don't have a clue about how to type such a function, or whether TS is even capable of doing so. (I think there's a reasonable chance that it is not.)
There are many ways that this might be expanded, if needed. We might want to allow for property names that vary between your input structure and the object member name, or we might want to allow other collection types besides arrays. If so, we probably would need a somewhat more sophisticated configuration structure, perhaps something like this:
{
org: { _ctor: Organization, admin: {type: 'usr'}, users: {type: Array, itemType: 'usr'} },
usr: { _ctor: User, account: {type: 'acct', renameTo: 'clientAcct'} },
acct: { _ctor: Account }
}
But that's for another day.
It's not clear whether this approach even comes close to meeting your needs, but it was an interesting problem to consider.

Convert all MirageJS ids to integers

MirageJS provides all model ids as strings. Our backend uses integers, which are convenient for sorting and so on. After reading around MirageJS does not support integer IDs out of the box. From the conversations I've read the best solution would be to convert Ids in a serializer.
Output:
{
id: "1",
title: "Some title",
otherValue: "Some other value"
}
But what I want is:
Expected Output:
{
id: 1,
title: "Some title",
otherValue: "Some other value"
}
I really want to convert ALL ids. This would included nested objects, and serialized Ids.
I think you should be able to use a custom IdentityManager for this. Here's a REPL example. (Note: REPL is a work in progress + currently only works on Chrome).
Here's the code:
import { Server, Model } from "miragejs";
class IntegerIDManager {
constructor() {
this.ids = new Set();
this.nextId = 1;
}
// Returns a new unused unique identifier.
fetch() {
let id = this.nextId++;
this.ids.add(id);
return id;
}
// Registers an identifier as used. Must throw if identifier is already used.
set(id) {
if (this.ids.has(id)) {
throw new Error('ID ' + id + 'has already been used.');
}
this.ids.add(id);
}
// Resets all used identifiers to unused.
reset() {
this.ids.clear();
}
}
export default new Server({
identityManagers: {
application: IntegerIDManager,
},
models: {
user: Model,
},
seeds(server) {
server.createList("user", 3);
},
routes() {
this.resource("user");
},
});
When I make a GET request to /users with this server I get integer IDs back.
My solution is to traverse the data and recursively convert all Ids. It's working pretty well.
I have a number of other requirements, like removing the data key and embedding or serializing Ids.
const ApplicationSerializer = Serializer.extend({
root: true,
serialize(resource, request) {
// required to serializedIds
// handle removing root key
const json = Serializer.prototype.serialize.apply(this, arguments)
const root = resource.models
? this.keyForCollection(resource.modelName)
: this.keyForModel(resource.modelName)
const keyedItem = json[root]
// convert single string id to integer
const idToInt = id => Number(id)
// convert array of ids to integers
const idsToInt = ids => ids.map(id => idToInt(id))
// check if the data being passed is a collection or model
const isCollection = data => Array.isArray(data)
// check if data should be traversed
const shouldTraverse = entry =>
Array.isArray(entry) || entry instanceof Object
// check if the entry is an id
const isIdKey = key => key === 'id'
// check for serialized Ids
// don't be stupid and create an array of values with a key like `arachnIds`
const isIdArray = (key, value) =>
key.slice(key.length - 3, key.length) === 'Ids' && Array.isArray(value)
// traverse the passed model and update Ids where required, keeping other entries as is
const traverseModel = model =>
Object.entries(model).reduce(
(a, c) =>
isIdKey(c[0])
? // convert id to int
{ ...a, [c[0]]: idToInt(c[1]) }
: // convert id array to int
isIdArray(c[0], c[1])
? { ...a, [c[0]]: idsToInt(c[1]) }
: // traverse nested entries
shouldTraverse(c[1])
? { ...a, [c[0]]: applyFuncToModels(c[1]) }
: // keep regular entries
{ ...a, [c[0]]: c[1] },
{}
)
// start traversal of data
const applyFuncToModels = data =>
isCollection(data)
? data.map(model =>
// confirm we're working with a model, and not a value
model instance of Object ? traverseModel(model) : model)
: traverseModel(data)
return applyFuncToModels(keyedItem)
}
})
I had to solve this problem as well (fingers crossed that this gets included into the library) and my use case is simpler than the first answer.
function convertIdsToNumbers(o) {
Object.keys(o).forEach((k) => {
const v = o[k]
if (Array.isArray(v) || v instanceof Object) convertIdsToNumbers(v)
if (k === 'id' || /.*Id$/.test(k)) {
o[k] = Number(v)
}
})
}
const ApplicationSerializer = RestSerializer.extend({
root: false,
embed: true,
serialize(object, request) {
let json = Serializer.prototype.serialize.apply(this, arguments)
convertIdsToNumbers(json)
return {
status: request.status,
payload: json,
}
},
})

find and modify deeply nested object in javascript array

I have an array of objects that can be of any length and any depth. I need to be able to find an object by its id and then modify that object within the array. Is there an efficient way to do this with either lodash or pure js?
I thought I could create an array of indexes that led to the object but constructing the expression to access the object with these indexes seems overly complex / unnecessary
edit1; thanks for all yours replies I will try and be more specific. i am currently finding the location of the object I am trying to modify like so. parents is an array of ids for each parent the target object has. ancestors might be a better name for this array. costCenters is the array of objects that contains the object I want to modify. this function recurses and returns an array of indexes that lead to the object I want to modify
var findAncestorsIdxs = function(parents, costCenters, startingIdx, parentsIdxs) {
var idx = startingIdx ? startingIdx : 0;
var pidx = parentsIdxs ? parentsIdxs : [];
_.each(costCenters, function(cc, ccIdx) {
if(cc.id === parents[idx]) {
console.log(pidx);
idx = idx + 1;
pidx.push(ccIdx);
console.log(pidx);
pidx = findAncestorsIdx(parents, costCenters[ccIdx].children, idx, pidx);
}
});
return pidx;
};
Now with this array of indexes how do I target and modify the exact object I want? I have tried this where ancestors is the array of indexes, costCenters is the array with the object to be modified and parent is the new value to be assigned to the target object
var setParentThroughAncestors = function(ancestors, costCenters, parent) {
var ccs = costCenters;
var depth = ancestors.length;
var ancestor = costCenters[ancestors[0]];
for(i = 1; i < depth; i++) {
ancestor = ancestor.children[ancestors[i]];
}
ancestor = parent;
console.log(ccs);
return ccs;
};
this is obviously just returning the unmodified costCenters array so the only other way I can see to target that object is to construct the expression like myObjects[idx1].children[2].grandchildren[3].ggranchildren[4].something = newValue. is that the only way? if so what is the best way to do that?
You can use JSON.stringify for this. It provides a callback for each visited key/value pair (at any depth), with the ability to skip or replace.
The function below returns a function which searches for objects with the specified ID and invokes the specified transform callback on them:
function scan(id, transform) {
return function(obj) {
return JSON.parse(JSON.stringify(obj, function(key, value) {
if (typeof value === 'object' && value !== null && value.id === id) {
return transform(value);
} else {
return value;
}
}));
}
If as the problem is stated, you have an array of objects, and a parallel array of ids in each object whose containing objects are to be modified, and an array of transformation functions, then it's just a matter of wrapping the above as
for (i = 0; i < objects.length; i++) {
scan(ids[i], transforms[i])(objects[i]);
}
Due to restrictions on JSON.stringify, this approach will fail if there are circular references in the object, and omit functions, regexps, and symbol-keyed properties if you care.
See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_native_JSON#The_replacer_parameter for more info.
As Felix Kling said, you can iterate recursively over all objects.
// Overly-complex array
var myArray = {
keyOne: {},
keyTwo: {
myId: {a: '3'}
}
};
var searchId = 'myId', // Your search key
foundValue, // Populated with the searched object
found = false; // Internal flag for iterate()
// Recursive function searching through array
function iterate(haystack) {
if (typeof haystack !== 'object' || haystack === null) return; // type-safety
if (typeof haystack[searchId] !== 'undefined') {
found = true;
foundValue = haystack[searchId];
return;
} else {
for (var i in haystack) {
// avoid circular reference infinite loop & skip inherited properties
if (haystack===haystack[i] || !haystack.hasOwnProperty(i)) continue;
iterate(haystack[i]);
if (found === true) return;
}
}
}
// USAGE / RESULT
iterate(myArray);
console.log(foundValue); // {a: '3'}
foundValue.b = 4; // Updating foundValue also updates myArray
console.log(myArray.keyTwo.myId); // {a: '3', b: 4}
All JS object assignations are passed as reference in JS. See this for a complete tutorial on objects :)
Edit: Thanks #torazaburo for suggestions for a better code.
If each object has property with the same name that stores other nested objects, you can use: https://github.com/dominik791/obj-traverse
findAndModifyFirst() method should solve your problem. The first parameter is a root object, not array, so you should create it at first:
var rootObj = {
name: 'rootObject',
children: [
{
'name': 'child1',
children: [ ... ]
},
{
'name': 'child2',
children: [ ... ]
}
]
};
Then use findAndModifyFirst() method:
findAndModifyFirst(rootObj, 'children', { id: 1 }, replacementObject)
replacementObject is whatever object that should replace the object that has id equal to 1.
You can try it using demo app:
https://dominik791.github.io/obj-traverse-demo/
Here's an example that extensively uses lodash. It enables you to transform a deeply nested value based on its key or its value.
const _ = require("lodash")
const flattenKeys = (obj, path = []) => (!_.isObject(obj) ? { [path.join('.')]: obj } : _.reduce(obj, (cum, next, key) => _.merge(cum, flattenKeys(next, [...path, key])), {}));
const registrations = [{
key: "123",
responses:
{
category: 'first',
},
}]
function jsonTransform (json, conditionFn, modifyFn) {
// transform { responses: { category: 'first' } } to { 'responses.category': 'first' }
const flattenedKeys = Object.keys(flattenKeys(json));
// Easily iterate over the flat json
for(let i = 0; i < flattenedKeys.length; i++) {
const key = flattenedKeys[i];
const value = _.get(json, key)
// Did the condition match the one we passed?
if(conditionFn(key, value)) {
// Replace the value to the new one
_.set(json, key, modifyFn(key, value))
}
}
return json
}
// Let's transform all 'first' values to 'FIRST'
const modifiedCategory = jsonTransform(registrations, (key, value) => value === "first", (key, value) => value = value.toUpperCase())
console.log('modifiedCategory --', modifiedCategory)
// Outputs: modifiedCategory -- [ { key: '123', responses: { category: 'FIRST' } } ]
I needed to modify deeply nested objects too, and found no acceptable tool for that purpose. Then I've made this and pushed it to npm.
https://www.npmjs.com/package/find-and
This small [TypeScript-friendly] lib can help with modifying nested objects in a lodash manner. E.g.,
var findAnd = require("find-and");
const data = {
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Three',
},
],
};
findAnd.changeProps(data, { id: 2 }, { name: 'Foo' });
outputs
{
name: 'One',
description: 'Description',
children: [
{
id: 1,
name: 'Two',
},
{
id: 2,
name: 'Foo',
},
],
}
https://runkit.com/embed/bn2hpyfex60e
Hope this could help someone else.
I wrote this code recently to do exactly this, as my backend is rails and wants keys like:
first_name
and my front end is react, so keys are like:
firstName
And these keys are almost always deeply nested:
user: {
firstName: "Bob",
lastName: "Smith",
email: "bob#email.com"
}
Becomes:
user: {
first_name: "Bob",
last_name: "Smith",
email: "bob#email.com"
}
Here is the code
function snakeCase(camelCase) {
return camelCase.replace(/([A-Z])/g, "_$1").toLowerCase()
}
export function snakeCasedObj(obj) {
return Object.keys(obj).reduce(
(acc, key) => ({
...acc,
[snakeCase(key)]: typeof obj[key] === "object" ? snakeCasedObj(obj[key]) : obj[key],
}), {},
);
}
Feel free to change the transform to whatever makes sense for you!

Categories