I'm writing template software for publishing node.js modules to GitHub and NPM. A small snippet of an example template:
{{module.name}}
{{module.description}}
Purpose
{{module.code.purpose}}
I have an object built up with all these properties, ex.
{
"module" : {
"name" : "test",
"description" : "Test module"
...
The problem is that I need to merge this object's sub-objects so the final output looks something like this (for replacing the placeholders in the templates):
{
"module.name" : "test",
"module.description" : "Test module"
...
So I created my own function to do that:
/**
Takes an object like
{
"type" : "meal",
"pizza" : {
"toppings" : ["pepperoni", "cheese"],
"radius" : 6,
"metadata" : {
"id" : 24553
}
}
}
and changes it to
{
"type" : "meal",
"pizza.toppings" : ["pepperoni", "cheese"],
"pizza.radius" : 6,
"pizza.metadata.id" : 244553
}
*/
const mergeObjectsToKeys = object => {
// Loop through the passed in object's properties
return Object.entries(object).reduce((obj, [key, value]) => {
// Check if it is a sub-object or not
if (typeof value === "object") {
// If it is a sub-object, merge its properties with recursion and then put those keys into the master object
const subObject = mergeObjectsToKeys(value);
Object.entries(subObject).forEach(([key2, value2]) => {
obj[key + "." + key2] = value2;
});
} else {
// Otherwise, just put the key into the object to return
obj[key] = value;
}
}, { });
};
Two questions
Is this the correct way to write the software?
If so, is there a built-in function to merge the sub-objects, like shown above?
One built-in function to handle the requirement is Object.assign(); you can use spread element, Object.entries(), .map() to set property names of object which is property of object.
To handler objects where value is not nested object
let p = Object.keys(o).pop();
let res = Object.assign({}, ...Object.entries(o.module).map(([key, prop]) =>
({[`${p}.${key}`]: prop})));
To handle value which is nested object
let o = {
"type": "meal",
"pizza": {
"toppings": ["pepperoni", "cheese"],
"radius": 6,
"metadata": {
"id": 24553
}
}
}
let res = Object.assign({}, ...Object.entries(o).map(([prop, value]) =>
typeof value === "object" && !Array.isArray(value)
? Object.assign({}, ...Object.entries(value)
.map(([key,val]) => ({[`${prop}.${key}`]: key})))
: {[prop]: value})
);
console.log(res);
Related
Suppose I want to create an object with keys as USD and non-USD. Like Below:
let obj = {
USD: {
sourceValue: 100
destinationValue: 10
},
non-USD: {
sourceValue: 10
destinationValue: 100
}
}
Except here, instead of non-USD, I should be able to pass any currency, like SGD or HKD and I should get result of non-USD currency.
so, if I wrote obj[currency].sourceValue and currency=HKD then I should get 10
I don't want to use obj[1].sourceValue as currency value is dynamic.
Also, I don't want to use if(currency!='USD') index=1 and then obj[index].sourceValue
So, my question is, what should I write at place of non-USD while defining object? I checked computation names, but I am not sure, how will I pass long currency array as key name and filter USD out of it?
I don't know of an option in js/ts that would have the key lookup syntax (like obj[currency]) and do the behavior that you want, while keeping the object obj "plain".
But there are options that do what you want with some modifications to the obj and/or with a different call syntax. (The only way that I can think of that would keep the obj completely untouched would require some changes to the currencies then.)
Option 1: add a get function call
const obj1 = {
get: function(currency) {
return this[currency === "USD" ? "USD" : "non-USD"]
},
USD: {
sourceValue: 100,
destinationValue: 10
},
"non-USD": {
sourceValue: 10,
destinationValue: 100
}
}
Or if you cannot change the object's source use Object.assign.
Here you can decide if you want to mutate the original object (Object.assign(target, ...)) or create a new one with Object.assign({}, target, ...)
const addGetter = (target) => Object.assign({}, target, {
get: (currency) => target[currency === "USD" ? "USD" : "non-USD"]
})
const obj1 = addGetter(obj) // <-- pass in the original obj
Usage: obj1.get(currency).sourceValue
Option 2: using a Proxy
Proxy docs, support
Offers the key lookup syntax that you want, but imo, this approach is a bit error-prone, because any access (indexed, by key or property) other than "USD" will return the "non-USD" values. Also the object gets wrapped, which hides object details when logging (console.log(obj)) in some consoles.
const useProxy = (obj) => new Proxy(obj, {
get: (target, currency) => target[currency === "USD" ? "USD" : "non-USD"]
})
const obj2 = useProxy(obj) // <-- pass in the original obj
Usage: obj2[currency].sourceValue
Demo
Check the code comments and the console output
const addGetter = (target) => Object.assign({}, target, {
get: (currency) => target[currency === "USD" ? "USD" : "non-USD"]
})
const useProxy = (obj) => new Proxy(obj, {
get: (target, currency) => target[currency === "USD" ? "USD" : "non-USD"]
})
const obj = {
USD: {
sourceValue: 100,
destinationValue: 10
},
"non-USD": {
sourceValue: 10,
destinationValue: 100
}
}
// Option 1
const obj1 = addGetter(obj)
// Option 2
const obj2 = useProxy(obj)
const c = ["USD", "EUR", "HKD", "non-USD"]
console.log("obj1.get(currency).sourceValue", c.map(currency => currency + " -> " + obj1.get(currency).sourceValue))
console.log("obj2[currency].sourceValue", c.map(currency => currency + " -> " + obj2[currency].sourceValue))
// Option 2 feels a bit error-prone, as any other access will return the fallback value for "non-USD"
console.log("obj2.length", c.map(currency => obj2.length))
console.log("obj2.randomProp", c.map(currency => obj2.randomProp))
You ll actually need to use the if else like condition.
say like this
const key = currency == "USD" ? "USD" : "non-USD";
// and then get the data like this
obj[currency].sourceValue
Input:
{
"mobile": "Mob # Required",
"client": [
undefined,
null,
{
"usergroup": "Required"
},
{
"id": "Required",
"usergroup": "Required"
},
{
"id": "Required",
"usergroup": "Required"
}
]
}
Expected Output:
[
"mobile",
"client.2.usergroup",
"client.3.id",
"client.3.usergroup",
"client.4.id",
"client.4.usergroup"
]
I am using Formiks FieldArray in my project & the field name in error object is not what is expected.
Object.Keys() doesn't work well for such scenario.
You can flatMap the keys of the object. If the current key's value is an object, recursively call getKeys function with the updated prefix. If not, return the current key with the given provided prefix. Use flatMap to get a flattened array of keys instead of nested arrays
const input={mobile:"Mob # Required",client:[{usergroup:"Required"},{id:"Required",usergroup:"Required"},{id:"Required",usergroup:"Required"}]};
const getKeys = (o, prefix = '') =>
Object.keys(o).flatMap(k =>
Object(o[k]) === o[k] ? getKeys(o[k], `${prefix}${k}.`) : [prefix + k]
)
console.log(getKeys(input))
If flatMap is not supported, you can reduce the keys of the object with similar logic
const input={mobile:"Mob # Required",client:[{usergroup:"Required"},{id:"Required",usergroup:"Required"},{id:"Required",usergroup:"Required"}]};
function getKeys(o, prefix = '') {
return Object.keys(o).reduce((acc, k) => {
if (Object(o[k]) === o[k])
acc.push(...getKeys(o[k], `${prefix}${k}.`))
else
acc.push(prefix + k)
return acc;
}, [])
}
console.log(getKeys(input))
I'm sure there are libraries out there to do this for you, but here is what I came up with:
function flattenNestedObject(input, path) {
path = path || [];
return Object.keys(input).reduce(function(arr, key) {
if (input[key] && typeof input[key] === "object") {
return arr.concat(flattenNestedObject(input[key], path.concat(key)));
}
if (typeof input[key] !== 'undefined' && input[key] !== null) {
return arr.concat((path.concat(key).join(".")));
}
return arr;
}, []);
}
https://codesandbox.io/s/sweet-jang-j51dh
The reason Object.keys will not work for you is that it doesn't recursively obtain all keys of an object. Also, in your expected output, you don't want all the keys of an object if it contains nested Arrays or Objects.
Assuming somewhere in NodeJS backend code, we have this object:
{
name : "foo"
secret : "bar"
}
We want to return this object as JSON in response of an HTTP request, but, we don't want it to have the secret, i.e. the return object must be:
{
name : "foo"
}
Assume we are using the latest NodeJS and we can use any 3rd party npm package.
Bonus for default params, for example, what if we want the returned object to always include "age", if it's not present, then set it to 0
{
name : "foo"
age : 0
}
EDIT: To add more context, I am not just trying to remove one particular elements, there could be more unwanted elements:
{
name : "foo"
secret1 : "bar"
secret2 : "bar"
secret3 : "bar"
someSecretThatIdontKnowTheirNameYet : "bar"
}
I just have a prototype, or class, or whatever which says:
{
name: String, required
age: Number, required
}
I am trying to figure what this unknown thing above is. Looking for something like:
cleanedUpObject = clean(rawObject, prototypeOrClassOrSomeOtherThing)
You can use a function, and destructure the object. You can set default values on specific properties. Then you can return a new object with the properties you want.
const formatResponse = ({ name, age = 0 }) => ({
name,
age,
});
const data = {
name : "foo",
secret1 : "bar",
secret2 : "bar",
secret3 : "bar",
someSecretThatIdontKnowTheirNameYet : "bar"
};
console.log(formatResponse(data));
Another option is to reduce the model, and include only the properties that exist in the model. If a property is not found on the object, take the default from the model.
const model = {
name: 'baz',
age: 0
}
const formatResponse = (o) =>
Object.entries(model)
.reduce((r, [k, v]) => {
r[k] = k in o ? o[k] : v;
return r;
}, {});
const data = {
name: "foo",
secret1: "bar",
secret2: "bar",
secret3: "bar",
someSecretThatIdontKnowTheirNameYet: "bar"
};
console.log(formatResponse(data));
Just remove private fields from your data and append default value:
const BLACK_LIST_FIELDS = ["secret"];
const DEFAULT_FIELDS_MAP = {
age: 0,
};
function getResponse(data) {
let safeRes = Object.assign({}, data);
BACK_LIST_FIELDS.forEach((key) => delete safeRes[key]);
Object.keys(DEFAULT_FIELDS_MAP).forEach((key) => {
if (!safeRes[key]) {
safeRes[key] = DEFAULT_FIELDS_MAP[key];
}
});
return safeRes;
}
const data = {
name : "foo",
secret : "bar",
};
console.log(getResponse(data));
Result: { name: 'foo', age: 0 }
best two options I can think of:
toJSON
const o = {
name : "foo",
secret : "bar",
toJSON() {
return {
name: this.name
}
}
}
console.log(JSON.stringify(o));
you can return what you want from toJSON, for more info read https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior
Symbol
const secret = Symbol('secret');
const o = {
name : "foo",
[secret]: "bar"
};
console.log(JSON.stringify(o));
console.log(o[secret]);
for more info on Symbol read https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
Here’s what I understand from your question:
you have an Object that defines the keys and types of your expected result.
You call this prototypeOrClassOrSomeOtherThing. Let’s call this definition, for short (since boh prototype and class have a defined meaning in JS)
you have another Object that contains a certain function result. Let’s call this input.
you want an Object that consists of only those keys and values from input that are defined in definition.
In case input does not contain a key/value that's defined in definition, use a reasonable default value based on the type defined in definition.
Strategy
we’ll write a function defaultsFromDefinition that creates an object with default values as defined by definition
we then call this function to start out with an object of default values, cleanedUpObject
next we replace this object's values with values from input if and only if input has a matching key/value pair
profit
Code
function defaultsFromDefinition(
definition,
defaultsByType = { String: '', Number: 0 })
{
return Object
.entries(definition)
.reduce((result, [key, type]) => ({
...result,
[key]: defaultsByType[type.name]
}), {})
}
function clean(input, definition) {
const cleanedUpObject = defaultsFromDefinition(definition)
return Object
.keys(cleanedUpObject)
.filter(key => input[key] !== undefined)
.reduce((result, key) => ({
...result,
[key]: input[key]
}), cleanedUpObject)
}
// ---- Test ----
const rawObject = {
name : "foo",
secret1 : "bar",
secret2 : "bar",
secret3 : "bar",
someSecretThatIdontKnowTheirNameYet : "bar"
}
const definition = {
name: String,
age: Number
}
const cleanedUpObject = clean(rawObject, definition)
console.log(cleanedUpObject)
I have a basic json question that is giving me headache since a couple of hours, I am trying to dynamically add keys to a Json object using an array of string.
Here is my array of string:
let key = ['session', 'view_state', 'footer', 'config', 'items']
I have another variable which is jsonValue and is my whole json object. I want to end up with one of those options:
jsonValue.session.view_state.footer.config.items
jsonValue['session']['view_state']['footer']['config']['items']
This is my best attempt using a forEach.
forEach(jsonKeys, (el) => {
jsonCollection += jsonCollection !== undefined ? '."' +[el + '"'] : [ '"' + el + '"' ];
})
But I have this result:
undefined"session"."view_state"."footer"."config"."items"
Any help will be appreciated!
To get a value using the keys array
Iterate with Array#reduce, check the type of the current value, if it's an object, return the value of the key, if not return undefined:
const obj = {
"demo": true,
"session": {
"view_state": {
"footer": {
"config": {
"items": [
1,
2
]
}
}
}
}
};
const keys = ['session', 'view_state', 'footer', 'config', 'items'];
const value = keys.reduce((val, key) => val && typeof val === 'object' ?
val[key] : undefind, obj);
console.log(value);
To add a value using the keys array
Use Array#reduceRight to create a chain of objects with the value you want. Use Object#assign to update the original object with the results:
const keys = ['session', 'view_state', 'footer', 'config', 'items'];
const obj = { demo: true };
Object.assign(obj, keys.reduceRight((val, key) => ({ [key]: val }), [1, 2])); // replace [1, 2] with the actual items
console.log(obj);
I am working with a JSON object which can have a property ids at any leaf. I want to traverse the object and find all of the instances of the ids property and store each id in a collection.
Mocked up JSON Object (the ids property could be at much deeper property locations).
{
"id": "b38a683d-3fb6-408f-9ef6-f4b853ed1193",
"foo": {
"ids": [
{
"id": "bd0bf3bd-d6b9-4706-bfcb-9c867e47b881"
},
{
"id": "d1cc529d-d5d2-4460-b2bb-acf24a7c5999"
},
{
"id": "b68d0c8c-548e-472f-9b01-f25d4b199a71"
}
],
"baz": "super"
},
"bar": {
"ids": [
{
"id": "bd0bf3bd-d6b9-4706-bfcb-9c867e47b881"
},
{
"id": "d1cc529d-d5d2-4460-b2bb-acf24a7c5999"
},
{
"id": "b68d0c8c-548e-472f-9b01-f25d4b199a71"
}
]
}
}
I am using the following code to traverse the above JSON.
var jsonFile = require('./file_test.json'); // the above in my local directory
function traverse(obj, ids) {
for (var prop in obj) {
if (typeof obj[prop] == "object" && obj[prop]) {
if (prop == 'ids') {
for (var i = obj[prop].length - 1; i >= 0; i--) {
ids.push(obj[prop][i]._id);
};
}
traverse(obj[prop], ids);
}
}
}
var ids = new Array();
traverse(jsonFile, ids);
console.log('ids', ids);
The above nets the following:
ids
[
'b68d0c8c-548e-472f-9b01-f25d4b199a71',
'd1cc529d-d5d2-4460-b2bb-acf24a7c5999',
'bd0bf3bd-d6b9-4706-bfcb-9c867e47b881',
'b68d0c8c-548e-472f-9b01-f25d4b199a71',
'd1cc529d-d5d2-4460-b2bb-acf24a7c5999',
'bd0bf3bd-d6b9-4706-bfcb-9c867e47b881'
]
While my code works I am not convinced that I am doing this the most efficient or best way. Is there a better way to find all instances of the ids property? Perhaps without passing in an array but returning one? Or setting up for a callback with an ids array?
If the data was actually a JSON string, and not a JavaScript object, you could have something like:
// assuming `json` is the data string
var ids = [];
var data = JSON.parse(json, function(key, value) {
if (key === "id")
ids.push(value);
return value;
});
See reviver on JSON.parse method.
what you have is fine, but this is a little shorter and uses the .map function:
var jsonFile = require('./file_test.json'); // the above in my local directory
function traverse(obj) {
var ids = [];
for (var prop in obj) {
if (typeof obj[prop] == "object" && obj[prop]) {
if (prop == 'ids') {
ids = obj[prop].map(function(elem){
return elem.id;
})
}
ids =ids.concat(traverse(obj[prop]));
}
}
return ids;
}
var ids =traverse(jsonFile);
console.log('ids', ids);
What you're basically trying to do is a tree search of this JSON object, am I right? So if we assume that ids is always a leaf then we do not need to traverse those nodes as we know they are at the leaf and will contain what we want.
Change the if {...} traverse to if {...} else {traverse}
If it is possible to change the data structure of ids to a list of strings instead of a list of objects then you will be able to save the iteration over the array and just merge it onto the ids array passed in, but it depends completely on the context and whether or not you can make this change!
Sorry I'm not of more help!
Assuming ES5 is available natively or via a shim:
function gimmeIds(obj) {
return Object.keys(obj||{})
.reduce(function(ids, key) {
if(key === 'ids') {
return ids.concat(obj[key].map(function(idObj) {
return idObj.id;
}));
}
if(obj[key] && typeof obj[key] == 'object') {
return ids.concat(gimmeIds(obj[key]));
}
return ids;
}, []);
}
Using object-scan this becomes very simple. Note that you can easily specify what is targeted (in this case **.ids[*].id)
// const objectScan = require('object-scan');
const data = { id: 'b38a683d-3fb6-408f-9ef6-f4b853ed1193', foo: { ids: [{ id: 'bd0bf3bd-d6b9-4706-bfcb-9c867e47b881' }, { id: 'd1cc529d-d5d2-4460-b2bb-acf24a7c5999' }, { id: 'b68d0c8c-548e-472f-9b01-f25d4b199a71' }], baz: 'super' }, bar: { ids: [{ id: 'bd0bf3bd-d6b9-4706-bfcb-9c867e47b881' }, { id: 'd1cc529d-d5d2-4460-b2bb-acf24a7c5999' }, { id: 'b68d0c8c-548e-472f-9b01-f25d4b199a71' }] } };
const findIds = (input) => objectScan(['**.ids[*].id'], { rtn: 'value' })(input);
console.log(findIds(data));
/* => [ 'b68d0c8c-548e-472f-9b01-f25d4b199a71',
'd1cc529d-d5d2-4460-b2bb-acf24a7c5999',
'bd0bf3bd-d6b9-4706-bfcb-9c867e47b881',
'b68d0c8c-548e-472f-9b01-f25d4b199a71',
'd1cc529d-d5d2-4460-b2bb-acf24a7c5999',
'bd0bf3bd-d6b9-4706-bfcb-9c867e47b881' ]
*/
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.7.1"></script>
Disclaimer: I'm the author of object-scan